특정 객체로의 접근을 제어하는 대리인(특정 객체를 대변하는 객체)를 제공
구성요소
Subject
- RealSubject와 Proxy의 인터페이스를 제공
- 두 객체가 동일한 인터페이스를 구현해서 RealSubject가 들어갈 자리에 Proxy도 들어갈 수 있다.
RealSubject
- 실제로 작업을 처리하는 객체
Proxy
- RealSubject의 대변인 역할을 하며 해당 객체로의 접근을 제어
- RealSubject의 레퍼런스가 들어있으며, 인스턴스를 생성하거나 제거하는 역할도 수행함
책에 소개된 종류
- 원격 프록시: 원격 객체로의 접근을 제공
- 가상 프록시: 생성하기 힘든 자원으로의 접근을 제공 (e.g 생성 비용이 비싼 객체)
- 보호 프록시: 접근 권한이 필요한 자원으로의 접근을 제어
그 외 다양한 프록시 패턴의 변형이 존재한다.
- 방화벽 프록시
- 네트워크 자원으로의 접근을 제어하여 허락되지 않은 클라이언트로부터 보호
- 스마트 레퍼런스 프록시
- 주제가 참조될 때마다 추가 행동을 제공
- 캐싱 프록시
- 비용이 많이 드는 작업의 결과를 임시로 저장
- 여러 클라이언트에서 결과를 공유하게 함으로써 계산 시간과 네트워크 지연을 줄여 주는 효과도 있음
- e.g. 웹 서버 프록시, 컨텐츠 관리 및 퍼블리싱 시스템
- 동기화 프록시
- 여러 스레드에서 주제에 접근할 때 안전하게 작업을 처리할 수 있도록 해
- e.g. 분z산 환경에서 일련의 객체로의 동기화된 접근을 제어해주는 자바 스페이스
- 복잡도 숨김 프록시
- 복잡한 클래스의 집합으로의 접근을 제어하고, 복잡도를 숨겨줌
- 지연-복사 프록시
- 클라이언트에서 필요로 할 때 까지 객체가 복사되는 것을 지연
공통점: 클라이언트가 실제 객체의 메소드를 호출하면 그 호출을 중간에 가로챈다는 것
차이점: 여러 프록시 패턴들의 차이점은 접근을 제어하는 방법이 다르다는 것
원격 프록시
클라리언트 힙
- 클라이언트 객체: 로컬 객체(클라이언트 보조 객체)의 메소드 호출
- 클라이언트 보조 객체: 서버에 연락, 메소드 호출에 관한 정보(메소드 이름, 인자 등)를 전달 후 서버로부터의 리턴되는 정보를 기다림
서버 힙
- 서비스 보조 객체: Socket 연결로 클라이언트 보조객체로부터 요청을 받고, 호출 정보를 해석해서 서비스 객체에 메소드 호출. 리턴되는 값을 소켓의 출력 스트림으로 전송
- 서비스 객체: 서비스 보조 객체로부터의 메소드 호출을 받아서 로직 수행 및 결과 리턴
클라이언트 객체 → 클라이언트 보조 객체 → 서비스 보조 객체 → 서비스 객체 → 서비스 보조 객체 → 클라이언트 보조 객체 → 클라이언트 객체
(보조 객체들은 각각 in-bound, out-bound adapter 의 역할인 듯)
가상 프록시
생성하는 데 많은 비용이 드는 객체를 대신하는 객체
진짜 객체가 필요한 상황이 오기 전까지 객체의 생성을 미루거나, 객체 생성 전이나 도중에 대신하는 기능을 제공
실제 객체가 생성되었다면 RealSubject에 요청을 전달
예시: 앨범 커버 뷰어 만들기
화면에 이미지를 표시하는 ImageProxy를 생성
- paintIcon() 메서드가 호출되면 ImageProxy에서 이미지를 가져오고, ImageIcon 객체를 생성하는 스레드를 시작
- ImageIcon 객체가 생성되었으면 해당 객체에서 이미지를 불러옴
- 그렇지 않으면 ImageProxy에서 안내 문구를 반환
보호 프록시
접근 권한을 바탕으로 접근을 제어
이때 하나 이상의 인터페이스를 구현하고, 지정한 클래스에 메서드 호출을 전달하는 프록시 클래스를 생성하는 동적 프록시를 활용할 수 있다. 실제 클래스는 실행 중에 생성되므로 이를 동적 프록시라고 함
예시: 데이팅 앱 - 개인 정보는 본인만 수정, 본인 평가는 타인만 수정 가능
public class OwnerInvocationHandler implements InvocationHandler {
Person person;
public OwnerInvocationHandler(Person person) {
this.person = person;
}
// 프록시 메서드가 호출될 때 마다 핸들러의 invoke 메서드를 호출한다.
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("setHotOrNotRating")) {
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
public class NonOwnerInvocationHandler implements InvocationHandler {
Person person;
public NonOwnerInvocationHandler(Person person) {
this.person = person;
}
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("setHotOrNotRating")) {
return method.invoke(person, args);
} else if (method.getName().startsWith("set")) {
throw new IllegalAccessException();
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
다른 패턴과의 차이점
- 데코레이터: 다른 객체를 감싸서 새로운 행동을 추가
- 퍼사드: 여러 객체를 감싸서 인터페이스를 단순하게 만듦
- 어댑터: 다른 객체를 감싸서 다른 인터페이스로 제공
- 프록시: 다른 객체를 감싸서 접근을 제어한다
참고) 자바 RMI 내부 구현
- 원격 인터페이스 만들기
- 서비스 구현 클래스 만들기
- RMI 레지스트리 실행
- 원격 서비스 실행 & 서비스 인스턴스를 RMI 레지스트리에 등록 (스텁만 등록됨 → 서비스 보조 객체)
이후 클라이언트는 서비스가 돌아가고 있는 시스템의 호스트 이름 또는 IP 주소를 통해 룩업 과정을 실행해 스텁을 가져옴
e.g. MyRemote service = (MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello")
추가 정리
책에 나온 동적 프록시는 JDK 동적 프록시 방법.
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[] args : 메서드를 호출할 때 전달한 인수
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
@Test
void dynamicA() {
AInterface target = new AImpl();
// 동적 프록시에 적용할 핸들러 로직
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
proxy.call();
}
또 다른 동적 프록시 방법: CGLIB
package org.springframework.cglib.proxy;
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 메서드를 호출하면서 전달된 인수
- proxy : 메서드 호출에 사용
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = proxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
- CGLIB는 성능상 MethodProxy proxy 를 사용하는 것을 권장
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class); // 구체 클래스를 상속 받아서 프록시를 생성
enhancer.setCallback(new TimeMethodInterceptor(target)); // 콜백으로 메서드 인터셉터를 등록
ConcreteService proxy = (ConcreteService) enhancer.create();
proxy.call();
}
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 체크해야 한다.
- CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- CGLIB에서는 예외가 발생한다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
- CGLIB에서는 프록시 로직이 동작하지 않는다.
- RealSubject와 Proxy의 인터페이스를 제공
- 두 객체가 동일한 인터페이스를 구현해서 RealSubject가 들어갈 자리에 Proxy도 들어갈 수 있다.
- CGLIB에서는 프록시 로직이 동작하지 않는다.
'Read Book > 헤드퍼스트 디자인 패턴' 카테고리의 다른 글
13장. 실전 디자인 패턴 (0) | 2023.07.02 |
---|---|
12장. 복합 패턴 (0) | 2023.07.02 |
10장. 상태 패턴 (0) | 2023.07.02 |
9장. 반복자 패턴과 컴포지트 패턴 (0) | 2023.07.02 |
8장. 템플릿 메서드 패턴 (0) | 2023.05.14 |