새로운 요구사항 & 문제 제시
기존 코드(뽑기 기계)에 알맹이의 개수와 기계의 현재 상태를 알려주는 기능을 추가한다.
각각의 메서드는 이미 존재하는 상태라, 출력용 메서드만 만들면 되는 상황.
다음과 같이 뽑기 기계 코드에 getter 를 추가하고, 이를 통해 출력문을 완성해주는 Monitor 클래스를 추가하여 간단하게 요구사항을 만족시킬 수 있다.
그런데 만약, 네트워크를 통해 원격으로 모니터링을 해야 한다는 요구사항이 추가된다면 어떻게 대응할 수 있을까?
이때는, 원격 프록시를 사용할 수 있다. 원격 프록시는 원격 객체(다른 JVM의 객체)에게 그 메소드 호출을 전달해주는 역할을 수행한다.
다른 JVM의 객체 레퍼런스를 어떻게 가져올 수 있을까?
아래의 클라이언트 보조 객체와 서비스 보조 객체가 다른 JVM과의 연결을 담당하게 되고, 클라이언트와 서버 객체는 각각 자신의 JVM 내부의 객체와만 통신한다고 느끼게 된다. 실제로는 보조 객체가 다른 JVM 과의 연결을 수행한다.
자바 RMI의 개요
1. 원격 인터페이스 만들기
원격 인터페이스는 클라이언트가 원격으로 호출한 메서드를 정의한다. 실제로 서버의 서비스도 이 인터페이스를 구현해야 한다.
2. 서비스 구현 클래스 만들기
1에서 만들었던 원격 인터페이스를 구현하는 서비스 클래스를 만든다.
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
private static final long serialVersionUID = 1L;
}
원격 서비스를 원격 클라이언트에서 사용할 수 있게 만들기 위해서는 서비스를 RMI 레지스트리에 등록해야 한다.
try {
MyRemote service = new MyRemoteImpl();
} catch (Exception ex) {}
3. 터미널에서 rmiregistry 실행
4. 다른 터미널에서 원격 서비스 실행
클라이언트에서 실제로 서비스의 스텁을 가져오는 방식
// RemoteHello는 서비스를 등록할 때 사용한 이름
MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
이렇게 얻은 MyRemote 객체를 클라이언트에서 사용하면, 서버의 메서드가 호출된다!
GumballMachine 을 원격 서비스로 바꾸기
1. GumballMachine의 원격 인터페이스 만들기
2. 인터페이스의 모든 리턴 형식을 직렬화할 수 있는지 확인하기 (적절히 trasient 사용)
public interface State extends Serializable {
public void insertQuarter();
public void xx();
public void yy();
public void zz();
}
3. 구상 클래스에서 인터페이스 구현하기
4. GumballMonitor 에서 기존 GumballMachine 대신 GumballMachineRemote 사용하기
모니터링 기능 테스트
public class GumballMonitorTestDrive {
public static void main(String[] args) {
String[] location = {
"rmi://santafe.mightygumball.com/gumballmachine",
"rmi://boulder.mightygumball.com/gumballmachine",
"rmi://austin.mightygumball.com/gumballmachine"
};
GumballMonitor[] monitor = new GumballMonitor[location.length];
// 각 위치에 대한 GumballMachineRemote 객체를 Lookup한 뒤,
// 모니터 배열에 GumballMonitor 인스턴스를 저장
for (int i = 0; i < location.length; i++) {
try {
GumballMachineRemote machine =
(GumballMachineRemote) Naming.lookup(location[i]);
monitor[i] = new GumballMonitor(machine);
System.out.println(monitor[i]);
} catch (Exception e) {
e.printStackTrace();
}
}
// 각 모니터 인스턴스에 대해 report() 메서드 호출
for (int i = 0; i < monitor.length; i++) {
monitor[i].report();
}
}
}
위와 같이 호출하고 실행해보면, 아래와 같이 조회가 되는 것을 확인할 수 있다.
실제로 리턴 값이 전달되는 과정
프록시 패턴의 정의
특정 객체로의 접근을 제어하는 대리인(특정 객체를 대변)을 제공
RealSubject, Proxy 모두 Subject 를 구현하고 실제 작업은 RealSubject에서, 접근 제어는 Proxy에서 수행한다.
위에서 살펴본 '원격 프록시' 외에 '가상 프록시' 도 있다.
생성에 비용이 많이 드는 RealSubject 앞에 존재하면서 실제로 객체가 필요한 상황이 오기전까지 객체의 생성을 미룬다. JPA의 프록시가 이에 해당하는 듯!
어김없이 등장하는 킹받는 토크쇼
결론은, 프록시와 데코레이터는 구조는 비슷하지만 용도가 다르다! 데코레이터는 동작을 추가하려는 목적이고 프록시는 접근을 제어하려는 목적이 크다(그 안에서 동작이 추가될 수도 있지만 주 목적은 그게 아니다).
자바의 동적 프록시 (reflection) 활용하기
자신의 괴짜 지수를 직접 조작할 수 없고, 다른 사람들의 개인 정보도 수정할 수 없도록 Person 인터페이스를 개선한다.
1. InvocationHandler 만들기
InvocationHandler는 프록시의 행동을 구현한다. 자바에서 알아서 처리해주기 때문에 어떤 할 일을 해야하는지만 지정해주면 된다.
프록시의 특정메서드가 호출되면 자바에서 InvocationHandler를 아래와 같이 호출해준다.
그러면 InvocationHandler에서는RealSubject에게 상황에 따라 요청을 전달하게 되는데, 아래의 invoke 메서드에서와 같은 방법으로 결정이 된다.
import java.lang.reflect.*;
public class OwnerInvocationHandler implements InvocationHandler {
private Person person;
public OwnerInvocationHandler(Person person) {
this.person = person;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException {
try {
if (method.getName().startsWith("get")) {
return method.invoke(person, args);
} else if (method.getName().equals("setGeekRating")) {
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
2. 동적 프록시 생성 코드 만들기, 3. 적절한 프록시로 Person 객체 감싸기
프록시 클래스를 생성하고, 그 인스턴스를 만드는 코드가 필요하다.
public Person getOwnerProxy(Person person) {
// Person 객체(진짜 주체)를 인자로 받아서 프록시를 리턴
return (Person) Proxy.newProxyInstance(
// person의 클래스 로더를 인자로 전달
person.getClass().getClassLoader(),
// 프록시에서 구현해야 하는 인터페이스 배열을 인자로 전달
person.getClass().getInterfaces(),
// 호출 핸들러로 OwnerInvocationHandler를 사용하고,
// 생성자 인자로 person을 전달해 실제 주체를 다룸
new OwnerInvocationHandler(person)
);
}
public class MatchMakingTestDrive {
// 인스턴스 변수 선언
public static void main(String[] args) {
// main 메서드는 프로그램 실행의 시작점이며
// MatchMakingTestDrive 객체를 생성 후 drive() 메서드 호출
MatchMakingTestDrive test = new MatchMakingTestDrive();
test.drive();
}
public MatchMakingTestDrive() {
// 생성자에서 데이터베이스를 초기화 (예: 미팅 서비스 회원 정보 세팅)
initializeDatabase();
}
public void drive() {
// 인물 정보를 데이터베이스로부터 가져옴
Person kim = getPersonFromDatabase("김자바");
// 본인(Owner) 프록시를 생성하고, getName() 메서드 호출
Person ownerProxy = getOwnerProxy(kim);
System.out.println("이름: " + ownerProxy.getName());
// 세터 메서드 호출 → 관심 사항 등록
ownerProxy.setInterests("블링, 바둑");
System.out.println("본인 프록시에 관심 사항을 등록합니다.");
// 괴짜 지수(Geek Rating) 설정 시도
try {
ownerProxy.setGeekRating(10);
} catch (Exception e) {
System.out.println("본인 프록시에는 괴짜 지수를 매길 수 없습니다.");
}
// 설정된 괴짜 지수 출력
System.out.println("괴짜 지수: " + ownerProxy.getGeekRating());
// 타인(Non-Owner) 프록시를 생성하고, getName() 메서드 호출
Person nonOwnerProxy = getNonOwnerProxy(kim);
System.out.println("이름: " + nonOwnerProxy.getName());
// 세터 메서드 호출 시도 → 타인 프록시에 관심 사항 등록
try {
nonOwnerProxy.setInterests("블링, 바둑");
} catch (Exception e) {
System.out.println("타인 프록시에는 관심 사항을 등록할 수 없습니다.");
}
// 타인 프록시에 괴짜 지수 설정
nonOwnerProxy.setGeekRating(3);
System.out.println("타인 프록시에 괴짜 지수를 매깁니다.");
System.out.println("괴짜 지수: " + nonOwnerProxy.getGeekRating());
}
// getOwnerProxy, getNonOwnerProxy, getPersonFromDatabase 등
// 필요한 메서드들은 아래와 같이 구현한다고 가정
private Person getPersonFromDatabase(String name) {
// 실제 데이터베이스에서 Person 객체를 가져오는 로직
return null;
}
private void initializeDatabase() {
// 데이터베이스 초기화 로직
}
private Person getOwnerProxy(Person person) {
// Owner 프록시 생성 로직
return null;
}
private Person getNonOwnerProxy(Person person) {
// Non-Owner 프록시 생성 로직
return null;
}
}
실행 예제
그 밖의 프록시
- 방화벽(Firewall) 프록시
- 네트워크 접근을 통제해 보안과 트래픽 관리를 담당하는 프록시임. 외부에서 내부 자원에 직접 접근하지 못하도록 규칙(허용/차단)을 설정하고, 필터링과 로깅 등을 수행함.
- 스마트 레퍼런스(Smart Reference) 프록시
- 원본 객체에 대한 추가 기능(예: 객체 사용 횟수 기록, 리소스 정리, 지연 초기화 등)을 제공하는 프록시임. 실제 객체에 접근하는 시점에 부가 로직을 삽입해 객체를 더 안전하고 효율적으로 사용할 수 있게 해줌.
- 캐싱(Caching) 프록시
- 자주 요청되는 데이터를 미리 저장해두고, 동일한 요청이 반복될 때 원본 객체를 다시 호출하지 않고 캐시에서 결과를 반환함. 네트워크나 디스크 I/O 비용을 줄여 성능 향상에 기여함.
- 동기화(Synchronization) 프록시
- 멀티스레드 환경에서 여러 스레드가 동시에 원본 객체에 접근해도 문제가 없도록, 접근 과정을 동기화(스레드 안전) 처리해주는 프록시임. 임계 구역 설정이나 락(lock) 처리 같은 로직을 내부적으로 수행함.
- 복잡도 숨김(Complexity Hiding) 프록시
- 원본 객체를 사용하는 클라이언트가 복잡한 내부 구조나 구현 세부 사항을 몰라도 되도록, 간단한 인터페이스로 추상화해주는 역할을 함.
- 예를 들어, 여러 라이브러리나 복잡한 설정이 필요한 객체를 프록시가 한꺼번에 초기화·관리하고, 클라이언트는 단순 메서드 호출만 하게끔 만듦.
- 지연 복사(원본 지연 로딩, Copy-on-write) 프록시
- 대용량 객체(예: 대규모 컬렉션, 이미지, 파일 등)를 복사해야 할 때, 쓰기 작업(변경)이 실제로 발생하기 전까지는 복사를 지연함으로써 성능과 메모리 사용을 최적화함.
- 읽기 전용으로만 접근할 때는 동일한 참조를 공유하고, 수정이 필요하면 그 시점에 복사를 수행함.
'스터디' 카테고리의 다른 글
[헤드퍼스트 디자인패턴] 10. 상태 패턴 (0) | 2025.01.15 |
---|---|
헤드퍼스트 디자인패턴 7. 어뎁터 & 퍼사드 패턴 (0) | 2024.12.27 |
헤드퍼스트 디자인패턴 5. 싱글턴 패턴 (0) | 2024.12.11 |
헤드퍼스트 디자인패턴 4. 팩토리 패턴 (0) | 2024.12.04 |
헤드퍼스트 디자인패턴 3. 데코레이터 패턴 (0) | 2024.10.22 |
백엔드 개발을 공부하고 있습니다.