"헤더퍼스트 디자인패턴" + "면접을 위한 CS 전공 지식 노트"에 개인적인 의견과 생각들을 추가하여 작성한 포스팅이므로 틀린 내용이 있을 수 있습니다. (있다면 지적 부탁 드립니다.)
📕 목차
1. Proxy Pattern
2. Remote Proxy : Monitoring
3. Protection & Dynamic Proxy
1. Proxy Pattern
📌 프록시 패턴
- 다른 객체의 대리인을 두어 로직의 흐름을 제어하는 행동 패턴
- Client가 대상 객체(Subject)의 메서드를 직접 실행하지 않고, Proxy 객체의 메서드에서 추가적인 로직을 처리한 뒤 접근하게 한다.
- 대상 클래스가 민감한 정보를 가지고 있거나, 원본 객체의 불변성을 유지하면서 생성 비용이 비싼 혹은 추가적인 기능을 가미하고 싶을 때 사용한다.
- 프록시 패턴에는 다양한 변종이 존재하고, 접근 제어 방법을 다르게 제공한다.
- 원격 프록시를 사용해서 원격 객체로의 접근을 제어할 수 있다.
- 가상 프록시(virtual proxy)를 사용해서 생성이 힘든 자원의 접근을 제어할 수 있다.
- 보호 프록시(protection proxy)를 사용해서 접근 권한이 필요한 자원으로의 접근을 제어할 수 있다.
- 프록시 패턴으로 얻을 수 있는 이점
- 보안(Security): Client의 접근 권한을 Proxy가 우선적으로 확인할 수 있다.
- 캐싱(Caching): 대상 객체를 실행하기 전에 데이터가 캐시에 있다면, 바로 응답으로 돌려준다.
- 데이터 유효성 검사(Data Validation): 대상으로 전달하기 전에 유효성을 검증할 수 있다.
- 지연 초기화(Lazy Initalization): 대상의 생성 비용이 비싸다면, 필요로 할 때까지 연기할 수 있다.
- 로깅(Logging): 메서드 호출과 상대 매개 변수를 Intercept하고 기록할 수 있다.
- 원격 객체(Remote Objects): 원격 위치에 있는 객체를 가져와서 Local처럼 보이게 할 수 있다.
📌 프록시 패턴 구조
- Client
- Subject Interface를 이용하여 Proxy 객체를 생성해서 이용한다.
- Subject
- Proxy와 RealSubject를 하나로 묶는 Interface (다형성)
- Proxy와 RealSubject 모두 Subject interface를 구현해야 한다.
- Client는 Proxy와 RealSubject의 차이를 구분할 필요 없다.
- RealSubject
- 원본 대상 객체
- 실제 Client의 작업을 수행하는 객체
- Proxy
- 대상 객체(RealSubject)의 대리인 역할
- 대상 객체를 합성(Composition)하여 Reference로 참조한다.
- RealSubject와 똑같은 인터페이스(Subject)를 구현하고 있으므로, 대상 객체가 들어갈 자리라면 언제든지 Proxy를 대신 넣을 수 있다.
- 대상 객체와 같은 이름의 메서드를 호출하면서, 별도의 접근 통제 혹은 지연 생성 등의 별도의 로직을 추가해 접근 제어가 가능하다.
- 단, Proxy는 흐름 제어만 할 뿐 결과값을 조작하거나 변경시켜서는 안 된다.
📌 프록시 패턴 종류
• 기본형 프록시 (Normal Proxy)
• 가상 프록시 (Virtual Proxy)
• 보호 프록시 (Protection Proxy)
• 로깅 프록시 (Logging Proxy)
• 원격 프록시 (Remote Proxy)
• 캐싱 프록시 (Caching Proxy)
• 직렬화 프록시 (Serialization Proxy)
🟡 기본형 프록시 (Normal Proxy)
interface Subject {
void request();
}
class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject.request()");
}
}
class NormalProxy implements Subject {
private RealSubject realSubject;
public NormalProxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
realSubject.request();
System.out.println("Proxy.request()");
}
}
public class Client {
public static void main(String[] args) {
Subject subject = new NormalProxy(new RealSubject());
subject.request();
}
}
RealSubject.request()
Proxy.request()
- Class Diagram을 충실히 따른다면 누구나 만들 수 있는 형태의 Proxy Pattern
- Proxy는 RealSubject의 메서드를 호출한 후에 print 명령어 하나를 추가해서 보여준다.
🟡 가상 프록시 (Virtual Proxy)
class VirtualProxy implements Subject {
private RealSubject realSubject;
VirtualProxy() {
}
@Override
public void request() {
if (realSubject == null) {
realSubject = new RealSubject();
}
realSubject.request();
System.out.println("VirtualProxy.request()");
}
}
public class VirtualProxyClient {
public static void main(String[] args) {
Subject subject = new VirtualProxy();
subject.request();
}
}
- 실제로 대상 객체가 사용될 때, 지연 초기화 전략을 사용한 방식
- 사용 빈도 수 대비 생성 비용이 비싼 대상 객체의 생성에 사용되는 방식 (애초에 지연 생성 전략 의도가 그거라..)
🟡 보호 프록시 (Protection Proxy)
class ProtectionProxy implements Subject {
private final RealSubject realSubject;
private boolean isAuth = false;
ProtectionProxy(RealSubject realSubject, boolean isAuth) {
this.realSubject = realSubject;
this.isAuth = isAuth;
}
@Override
public void request() {
if (isAuth) {
realSubject.request();
} else {
System.out.println("You don't have permission to access this resource.");
}
}
}
public class ProtectionProxyClient {
public static void main(String[] args) {
Subject subject = new ProtectionProxy(new RealSubject(), false);
subject.request();
}
}
- Proxy가 Subject 자원 접근 제어 (Authorization)
- Client 자격 증명이 성공한 경우에만 RealSubject에 요청을 전달할 수 있다.
🟡 로깅 프록시 (Logging Proxy)
class LoggingProxy implements Subject {
private RealSubject realSubject;
LoggingProxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
@Override
public void request() {
System.out.println("RealSubject 접근 감지");
realSubject.request();
System.out.println("RealSubject 실행 완료");
System.out.println(LocalDateTime.now());
}
}
public class LoggingProxyClient {
public static void main(String[] args) {
Subject subject = new LoggingProxy(new RealSubject());
subject.request();
}
}
- Normal Proxy와 같으나, 대상 객체에 대한 Log를 관리하는 경우 사용
- 별 거 아닌 거 같아 보이지만, 다른 디자인 패턴들과 적용하면 무궁무진한 활용성을 보여줄 수 있다.
🟡 원격 프록시 (Remote Proxy)
interface MyRemote extends Remote {
String sayHello() throws RemoteException;
}
class MyMonitor {
private final MyRemote service;
MyMonitor(MyRemote service) {
this.service = service;
}
public void report() {
try {
System.out.println(service.sayHello());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
public class RemoteProxyClient {
public static void main(String[] args) {
String url = "rmi://어쩌구저쩌구.com/RemoteHello";
try {
MyRemote service = (MyRemote) Naming.lookup(url);
MyMonitor monitor = new MyMonitor(service);
monitor.report();
} catch (NotBoundException | MalformedURLException | RemoteException e) {
e.printStackTrace();
}
}
}
- Proxy는 Local에 존재하고, 대상 객체는 Remote Server에 존재하는 경우 (Network 통신)
- Proxy 객체가 Network 작업을 수행한 후, 불필요한 작업을 처리하고 결과값만 반환
- Client는 Local의 Proxy만 사용하므로, 원격인지 로컬인지 신경쓰지 않아도 된다.
✒️ 스텁(Stub)과 스켈레톤(Skeleton)
일반적인 분산 개체 애플리케이션은 공통적으로 아래 목적을 추구한다.
- 덩치가 큰 프로그램(개체)은 Server가 가지고 있는다.
- 프로그램 실행은 Server가 담당한다.
- Client는 해당 프로그램을 조작하는 데 필요한 최소한의 코드만 전달한다.
여기서 3번째 목적을 수행하기 위한 코드가 Stub(여기선 Proxy)과 Skeleton이다.
- 스텁(Stub)
- 그 자체로 어떠한 기능을 수행하지는 못 한다. (비지니스 로직을 담고 있지는 않다.)
- 원래 개체가 무엇인지 알아낼 수 있는 참조 역할을 한다.
- 대상 객체의 기능 조작을 위임받은 대리자(Proxy) 역할만 수행한다.
- 스켈레톤(Skeleton)
- Client로 부터 받은 Stream을 분석하여 어떤 메서드가 호출되었는 지 파악한다.
- Server에 실제 비지니스 로직이 담긴 대상 객체의 메서드를 호출한다.
- 결과값을 Stub으로 반환한다.
🟡 용어 정리
- lookup: Client가 조작하고자 하는 Subject를 포함하는 Remote Server에 접속하여, 해당 Subject를 탐색
- marshalling: Network로 파라미터들을 전달하기 위한 작업
- unmarshalling: Network로 전달받은 파라미터들을 원래대로 복원하는 작업
🟡 캐싱 프록시 (Caching Proxy)
interface UserService {
List<String> getUsers(String id);
int getAccessCount();
}
class UserServiceImpl implements UserService {
private final Map<String, List<String>> users = Map.of(
"id1", List.of("user1", "user2", "user3"),
"id2", List.of("user4", "user5", "user6"),
"id3", List.of("user7", "user8", "user9")
);
private int count;
@Override
public List<String> getUsers(String id) {
++count;
return List.of("user1", "user2", "user3");
}
@Override
public int getAccessCount() {
return count;
}
}
class CachingProxy implements UserService {
private final UserService userService;
private ConcurrentMap<String, List<String>> cachedUsers;
private final Object writeLock = new Object();
CachingProxy(UserService userService) {
this.userService = userService;
this.cachedUsers = new ConcurrentHashMap<>();
}
@Override
public List<String> getUsers(String id) {
if (!cachedUsers.containsKey(id)) {
synchronized (writeLock) {
if (!cachedUsers.containsKey(id)) {
cachedUsers.put(id, userService.getUsers(id));
}
}
}
return cachedUsers.get(id);
}
@Override
public int getAccessCount() {
return userService.getAccessCount();
}
}
public class CachingProxyClient {
public static void main(String[] args) {
UserService cachingProxy = new CachingProxy(new UserServiceImpl());
System.out.println(cachingProxy.getUsers("id1"));
System.out.println(cachingProxy.getUsers("id2"));
System.out.println(cachingProxy.getUsers("id3"));
System.out.println(cachingProxy.getUsers("id1"));
System.out.println("Access count: " + cachingProxy.getAccessCount());
}
}
- 데이터가 큰 경우, 결과를 캐싱해두었다가 재사용한다.
- 값이 변경될 수도 있으므로 TTL을 알맞게 설정해서, cache의 수명 주기를 관리해주는 것이 중요하다.
🟡 직렬화 프록시 (Serialization Proxy)
- 실제 객체를 숨기고 Proxy를 통해 직렬화/역직렬화를 수행하는 패턴
📌 프록시 패턴 특징
- 사용 시기
- 기존 특정 객체를 수정할 수 없는 상황일 때
- 접근을 제어하거나 기능을 추가하고 싶은 경우
- 기존 객체 동작 수정 없이 초기화 지연, 접근 제어, 로깅, 캐싱 등을 적용하고 싶을 때
- 장점
- 개방 폐쇄 원칙(OCP) 준수
- 단일 책임 원칙(SRP) 준수
- Client는 행위에 대한 결과를 받는 것만 알면 되고, 내부적으로 부수작업(네트워크 통신)을 수행할 수 있다.
- 단점
- 유지보수해야 할 Proxy 객체 수 증가 → Dynamic Proxy 기법으로 해결 가능
- Proxy 클래스 자체에서 자원을 많이 사용하면 응답이 늦어질 수 있다.
📌 백엔드 프록시 서버
- Proxy Server: Server와 Client 사이에서 Client가 자신을 통해 다른 Network Service에 간접적으로 접속할 수 있도록 해주는 컴퓨터 시스템 혹은 응용 프로그램
- 가장 유명한 프록시 서버로 Ngnix가 있다.
- 익명 사용자가 직접적으로 Server에 접근하는 것을 차단할 수 있다.
- 실제 Server port를 숨기고, 정적 자원을 gzip 압축하거나 메인 서버 앞단에서 Logging 작업 수행 가능
- DDOS 공격 방어나 HTTPS 구축에도 사용된다.
- 짧은 시간 동안 네트워크에 많은 요청을 보내 Network를 마비시키는 DDOS 공격에서 의심스러운 트래픽(가령 사용자가 접속하는 것이 아닌 시스템을 통해 오는 트래픽)을 자동으로 차단한다.
- 인증서 기반을 통해 HTTPS를 구축하거나, CloudFlare같은 CDN 서비스를 사용하면 보다 편리하게 적용 가능하다.
* CDN(Content Delivery Network): 각 사용자가 인터넷에 접속하는 곳과 지리적으로 가까운 곳에서 Contents를 Caching 혹은 배포하는 Server Network
📌 프론트엔드 프록시 서버
- CORS(Cross-Origin Resource Sharing) 에러를 해결할 수 있다.
- CORS: 서버가 웹 브라우저에서 Resource를 로드할 때 다른 오리진을 통해 로드하지 못 하게 하는 HTTP 헤더 기반 메커니즘
- Origin: protocol과 Hostname, port의 조합 (ex. https://example.com:8080/hello에서 밑줄 친 부분)
- 프론트(127.0.0.1:3000)와 백엔드(127.0.0.1:8080)의 Origin이 달라서 CORS 에러가 날 때, 프론트단의 Proxy Server가 프론트 서버에서 요청되는 Origin을 127.0.0.1:3000으로 바꿔버릴 수 있다.
2. Remote Proxy : Monitoring
📌 디자인 패턴 없이 구현하기
class Machine {
String location;
Integer count;
Integer state;
Machine(String location, Integer count, Integer state) {
this.location = location;
this.count = count;
this.state = state;
}
public void getLocation() {
System.out.println(location);
}
public void getCount() {
System.out.println(count);
}
public void getState() {
System.out.println(state);
}
}
class LegacyMonitor {
private final Machine machine;
LegacyMonitor(Machine machine) {
this.machine = machine;
}
public void report() {
System.out.println("Location: " + machine.location);
System.out.println("Count: " + machine.count);
System.out.println("State: " + machine.state);
}
}
public class NonProxyMonitorClient {
public static void main(String[] args) {
Machine machine = new Machine("서울", 10, 1);
LegacyMonitor monitor = new LegacyMonitor(machine);
monitor.report();
}
}
- Monitor 객체는 Machine 객체를 직접 참조하여 메서드를 사용한다.
- Machine 객체가 Local에 존재한다면 이 정도로도 충분하다.
- 하지만 Machine이 Remote Server에 존재한다면, Remote Proxy 패턴으로 변형해야 한다.
- Proxy가 저수준의 작업(ex. Network 통신)을 처리한다.
- Client는 기존의 Local에서 사용하던 방식과 차이를 느낄 수 없다.
- 하지만 기존의 방식대로 다른 Heap에 들어있는 Instance reference를 가져올 수는 없으므로 주의해야 한다.
📌 Server 측 Remote Service를 만드는 4단계
1️⃣ Remote Interface 정의
클라이언트가 원격으로 호출할 메서드 정의 단계.
Stub과 실제 Service에서 해당 인터페이스를 구현해야 한다.
interface MyRemote extends Remote {
String sayHello() throws java.rmi.RemoteException;
}
- Marker interface인 Remote를 확장하는 interface 정의
- 모든 메서드에서 RemoteException을 던지도록 선언
- 검사 예외를 발생시켜, 반드시 호출자가 예외를 컨트롤하도록 만들어야 한다.
- 모든 parameter와 return value는 원시 형식(primitive) 또는 Serializable 형식이어야 한다.
2️⃣ Service Implementation
실제 작업을 처리하는 대상 클래스
class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
private static final long serialVersionUID = 1L;
protected MyRemoteImpl() throws RemoteException {
}
@Override
public String sayHello() {
return "Server says, 'Hey'";
}
}
- Client가 호출할 인터페이스 메서드를 구현
- UnicastRemoteObject를 확장해서, Superclass에서 제공하는 기능을 사용한다.
- Serializable을 구현하고 있기 때문에 serialVersionUID 필드를 정의한다. (아무 값이나 상관없다.)
- UnicastRemoteObject 생성자가 RemoteException을 던지므로, 똑같이 생성자 클래스에서 예외를 발생시킨다.
try {
MyRemote service = new MyRemoteImpl();
Naming.rebind("RemoteHello", service);
} catch (Exception e) {
e.printStackTrace();
}
- Remote Client가 Remote Service를 사용할 수 있도록 RMI registry에 등록한다.
- Naming.rebind()로 등록된 객체는 Registry에 Stub만 등록한다. (Client가 stub만 필요하니까)
3️⃣ RMI registry 실행
Client가 rmiregistry로부터 Proxy(stub)를 받을 수 있도록 RMI가 실행되고 있어야 한다.
% rmiregistry
4️⃣ Remote Service 실행
Service를 구현한 클래스에서 instance를 생성하고, 해당 instance를 RMI registy에 등록한다.
public static void main(String[] args) {
try {
MyRemote service = new MyRemoteImpl();
Naming.rebind("RemoteHello", service);
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
}
}
% java MyRemoteImpl
- Remote Service 구현 클래스의 main() 메서드로도 실행 가능하다.
- Naming의 rebind 정적 메서드 활용
- key의 값에 service를 rmiregistry에 결합한다.
- Client에서 RMI registry로 Service를 검색할 때도 지정된 key로 검색한다.
- 여기까지 하면 stub 객체가 RMI registry에 등록된다.
📌 Client 측 호출 코드
public class MyRemoteClient {
public static void main(String[] args) {
new MyRemoteClient().go();
}
public void go() {
try {
MyRemote service = (MyRemote) java.rmi.Naming.lookup("rmi://127.0.0.1/RemoteHello");
String s = service.sayHello();
System.out.println(s);
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
}
}
}
- Client에서 RMI Registry를 lookup한다.
- rmiregistry에서 stub 객체를 반환한다. (RMI가 stub을 자동 역직렬화한다.)
- Client는 stub의 method를 호출하게 된다. (Client는 stub이 진짜 service 객체라고 인지하게 된다.)
📌 디자인 패턴 적용해서 Remote Service 재구현
1️⃣ Client에서 호출할 수 있는 메서드가 정의된 Interface 정의
public interface MachineRemote extends Remote {
int getCount() throws RemoteException;
String getLocation() throws RemoteException;
State getState() throws RemoteException;
}
public interface State extends Serializable {
void insertCoin();
void ejectCoin();
void turnCrank();
void dispense();
}
- MachineRemote의 모든 메서드는 반환 값이 원시 형식 또는 Serializable이어야 한다.
- 또한 모든 메서드는 RemoteException을 던질 수 있다.
2️⃣ 구체 클래스 구현
public class NoQuarterState implements State {
private static final long serialVersionUID = 2L;
transient Machine machine;
...
}
- 모든 State 객체는 Machine 메서드 호출을 위해 직접적으로 참조하고 있을 확률이 높다.
- 따라서 transient 키워드를 추가해서 직렬화하지 않음을 선언한다.
public class Machine extends UnicastRemoteObject implements MachineRemote {
@Serial
private static final long serialVersionUID = 1L;
private String location;
private int count;
private State state;
public Machine(String location, int count) throws RemoteException {
this.location = location;
this.count = count;
}
...
}
- 여기까지 정의했다면 rmiregistry에 등록한다.
3️⃣ Client Proxy 클래스
public class Monitor {
MachineRemote machineRemote;
public Monitor(MachineRemote machineRemote) {
this.machineRemote = machineRemote;
}
public void report() {
try {
System.out.println("뽑기 기계");
System.out.println("현재 재고: " + machineRemote.getCount() + "개");
System.out.println("현재 위치: " + machineRemote.getLocation());
System.out.println("현재 상태: " + machineRemote.getState());
} catch (Exception e) {
e.printStackTrace();
}
}
}
- MachineRemote를 구현하는 대신, Remote Server에서 정한 Remote interface를 composition한다.
- 내부적으로 네트워크 통신에 대한 예외는 Proxy가 적절히 컨트롤해준다.
4️⃣ Client 호출
public class Client {
public static void main(String[] args) {
String location = "rmi://127.0.0.1/DesignMachine";
try {
MachineRemote service = (MachineRemote) Naming.lookup(location);
Monitor monitor = new Monitor(service);
monitor.report();
} catch (Exception e) {
System.out.println("Exception: " + e.getMessage());
}
}
}
- Client는 monitoring할 location과 method 실행 시 반환값만 신경쓰면 된다.
- 내부적으로 동작하는 Network 통신 등에 대해서는 Proxy가 적절히 처리해준다.
3. Protection & Dynamic Proxy
📌 Dynamic Proxy
- 기존 Proxy pattern은 대상 원본 클래스 수와 일대일 대응되는 Proxy Class를 만들어야 했다.
- Dynamic Proxy: JVM에서 compile 시점이 아닌, runtime 시점에 Proxy Class를 만들어주는 것
- Java의 reflection API를 응용한 기법
- java.lang.reflect.Proxy 패키지의 API를 이용해 동적으로 Proxy instance를 만들어 등록한다.
- 기존 Proxy(Stub) 클래스를 지우고, InvocationHandler 인터페이스를 구현한 Proxy Handler 전용 함수형 클래스를 구현한다.
🟡 java.lang.reflect.Proxy의 newProxyInstance() 메서드
public class Proxy implements java.io.Serializable {
@java.io.Serial
private static final long serialVersionUID = -2222568056686623797L;
...
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {
...
}
...
}
- ClassLoader
- Proxy class를 생성할 로더
- Proxy 객체가 구현할 Interface에 Class Loader를 받아오는 것이 일반적이다.
- Class<?>[] interfaces
- Proxy Class가 구현하고자 하는 Interface 목록
- InvocationHandler
- Proxy method(invoke)가 호출되었을 때 실행되는 Handler method
🟡 InvocationHandler
💡 Dynamic Proxy의 method 호출 시, 이를 낚아채서 대신 실행되는 메서드
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
...
}
- Object proxy: Proxy 객체
- Method method: 호출한 메서드 정보
- Object[] args: 메서드에 전달된 매개변수
📌 Class Diagram
- Java의 Proxy 클래스가 Subject Interface 전체를 구현한다.
- 필요한 코드를 직접 구현하지 않으면서, Proxy 클래스에게 무슨 일을 해야하는 지 알려주어야 한다.
- InvocationHandler에서 Proxy에 호출되는 모든 method에 응답하도록 구현한다.
📌 Design
1️⃣ Person Entity
public interface Person {
String getName();
void setName(String name);
String getGender();
void setGender(String gender);
String getInterests();
void setInterests(String interests);
int getGeekRating();
void setGeekRating(int rating);
}
public class PersonImpl implements Person {
String name;
String gender;
String interests;
int rating;
int ratingCount = 0;
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public String getGender() {
return gender;
}
@Override
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String getInterests() {
return interests;
}
@Override
public void setInterests(String interests) {
this.interests = interests;
}
@Override
public int getGeekRating() {
return ratingCount == 0 ? 0 : rating / ratingCount;
}
@Override
public void setGeekRating(int rating) {
this.rating += rating;
++ratingCount;
}
}
- 전체 회원 정보를 관리하기 위한 Entity 클래스
- 만약 '나'가 아닌 다른 사람의 정보를 함부로 수정할 수 있으면 안 된다. → Protection
- 그렇지만 다른 사람의 정보를 조회하는 것은 누구나 할 수 있어야 한다.
2️⃣ 2개의 InvocationHandler 구현
- Proxy 클래스와 객체 생성은 Java가 처리한다.
- Proxy의 행동을 구현해주는 handler 필요
3️⃣ Dynamic Proxy 생성 코드 만들기
- Proxy 클래스를 생성하고, instance를 만드는 코드
4️⃣ 적절한 Proxy로 Person 감싸기
- Person 객체를 사용하는 객체가 자신(owner)인지, 타인(non-owner)인지 구분해야 한다.
📌 2개의 InvocationHandler 구현
// InvocationHandler 인터페이스를 구현해야 한다.
public class OwnerInvocationHandler implements InvocationHandler {
Person person; // Subject 객체 composition
public OwnerInvocationHandler(Person person) {
this.person = person;
}
// invoke 메소드는 Proxy 객체의 모든 메소드 호출을 처리한다. (proxy method 호출될 때마다 호출된다.)
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (method.getName().startsWith("get")) { // get 메소드는 호출할 수 있다.
return method.invoke(person, args);
} else if (method.getName().equals("setGeekRating")) { // setGeekRating 메소드는 호출할 수 없다.
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) { // set 메소드는 호출할 수 있다.
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null; // 다른 메서드가 호출된다면 null을 반환한다.
}
}
public class NonOwnerInvocationHandler implements InvocationHandler {
Person person;
public NonOwnerInvocationHandler(Person person) {
this.person = person;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (method.getName().startsWith("get")) {
return method.invoke(person, args);
} else if (method.getName().equals("setGeekRating")) {
return method.invoke(person, args);
} else if (method.getName().startsWith("set")) {
throw new IllegalAccessException();
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
- Owner는 자신의 rating을 수정할 수 없지만, 그 외의 정보는 수정 가능하다.
- Non-Owner의 정보는 rating을 수정할 수는 있지만, 그 외의 정보는 수정할 수 없다.
- Owner, Non-Owner 구분 없이 모든 사용자의 정보를 조회할 수 있다.
🟡 대략적인 동작 방식
- Proxy의 proxy.setGreekRating(9); 호출
- Proxy에서 InvocationHandler의 invoke(Object proxy, Method method, Object[] args) 호출
- Object proxy ← proxy
- Method method ← setGreekRationg()
- Object[] args ← 9
- Handler에서 주어진 요청을 어떻게 처리할 지 결정 retrun method.invoke(person, args)
- 실제 Subject를 참조한다.
- 단, 인터페이스 기반으로 프록시를 동적으로 만들기 때문에 타입은 반드시 인터페이스를 파라미터로 사용해야 한다.
📌 Dynamic Proxy 생성 코드 만들기
Person getOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person));
}
Person getNonOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new NonOwnerInvocationHandler(person));
}
- Subject를 인자로 받아서 Proxy를 return 한다
- 로더와 인터페이스들을 넘기는 건 그냥 메서드로 쉽게 처리 가능
- Proxy에서 구현해야 하는 Interface를 같이 전달하면 끝난다.
📌 적절한 Proxy로 Person 감싸기
public class MatchMaking {
public static void main(String[] args) {
MatchMaking test = new MatchMaking();
test.drive();
}
public void drive() {
Person 김철수 = getPersonInstance("김철수", "남자", "게임", 10); // 김철수 등록
Person ownerProxy = getOwnerProxy(김철수); // 김철수의 소유자 프록시 생성
System.out.println("Name is " + ownerProxy.getName()); // getter 메서드는 누구나 호출 가능
ownerProxy.setInterests("Bowling, Go"); // setter 메서드는 소유자만 호출 가능
System.out.println("Interests set from owner proxy");
try {
ownerProxy.setGeekRating(10); // setGeekRating 메서드는 비소유자만 호출 가능 => 예외 발생
} catch (Exception e) {
System.out.println("Can't set rating from owner proxy");
}
System.out.println("Rating is " + ownerProxy.getGeekRating());
Person nonOwnerProxy = getNonOwnerProxy(김철수); // 김철수의 비소유자 프록시 생성
System.out.println("Name is " + nonOwnerProxy.getName()); // getter 메서드는 누구나 호출 가능
try {
nonOwnerProxy.setInterests("Bowling, Go"); // setter 메서드는 소유자만 호출 가능 => 예외 발생
} catch (Exception e) {
System.out.println("Can't set interests from non owner proxy");
}
nonOwnerProxy.setGeekRating(3); // setGeekRating 메서드는 비소유자만 호출 가능
System.out.println("Rating set from non owner proxy");
System.out.println("Rating is " + nonOwnerProxy.getGeekRating());
}
...
}
Name is 김철수
Interests set from owner proxy
Can't set rating from owner proxy
Rating is 10
Name is 김철수
Can't set interests from non owner proxy
Rating set from non owner proxy
Rating is 6
짜잔, 이렇게 하면 더 이상 Subject와 일대일 대응하는 Proxy를 생성하지 않아도 된다.
관점이 행위로 옮겨졌으므로, 다른 동작이 필요한 경우의 Proxy 동작에 대해서만 구현해주면 해결된다.