SW
[개발서적] 헤드퍼스트 디자인 패턴 Ch11. 프록시 패턴 본문
1. 모니터링 코드 만들기
CEO는 원격에 있는 모든 뽑기 기계를 모니터링 하기를 원합니다.
뽑기 기계 코드의 알맹이의 개수를 알려주는 메소드와 기계의 현재 상태를 알려주는 메소드는 현재 존재합니다.
여기서, 모든 뽑기 기계의 재고와 현재 상태를 알려 주는 기능을 추가하려고 합니다.
우선 GumballMachine 클래스에 뽑기 기계의 현재 위치를 알려 주는 기능을 추가해봅니다.
public class GumballMachine {
String location; // 기계의 위치
int count; // 알맹이의 개수
String state; // 기계의 현재 상태
// 생성자: 기계의 위치와 재고를 초기화
public GumballMachine(String location, int count) {
this.location = location;
this.count = count;
this.state = "대기 중"; // 초기 상태 설정
}
// 기계의 위치를 반환하는 메소드
public String getLocation() {
return location;
}
// 재고 수를 반환하는 메소드 (이미 구현된 것)
public int getCount() {
return count;
}
// 상태를 반환하는 메소드 (이미 구현된 것)
public String getState() {
return state;
}
}
뽑기 기계의 위치, 재고, 현재 상태를 가져와서 보고서를 출력해주는 GumballMonitor 클래스를 만들어봅니다.
public class GumballMonitor {
GumballMachine machine; // 모니터링할 GumballMachine 객체
// 생성자: GumballMachine 객체를 전달받아 초기화
public GumballMonitor(GumballMachine machine) {
this.machine = machine;
}
// 보고서 출력 메소드
public void report() {
System.out.println("뽑기 기계 위치: " + machine.getLocation());
System.out.println("현재 재고: " + machine.getCount() + " 개");
System.out.println("현재 상태: " + machine.getState());
}
}
모니터링 테스트를 해봅니다.
public class GumballMachineTestDrive {
public static void main(String[] args) {
int count = 0;
// 입력 값 유효성 확인
if (args.length < 2) {
System.out.println("GumballMachine <location> <inventory>");
System.exit(1); // 인수가 부족할 경우 프로그램 종료
}
// 명령줄 인수에서 위치와 초기 재고 개수 가져오기
String location = args[0]; // 기계 위치
count = Integer.parseInt(args[1]); // 초기 재고 수
// GumballMachine 객체 생성
GumballMachine gumballMachine = new GumballMachine(location, count);
// GumballMonitor 객체 생성
GumballMonitor monitor = new GumballMonitor(gumballMachine);
// 보고서 출력
monitor.report();
}
}
하지만, 요구사항은 원격 모니터링입니다.
현재 GumballMonitor는 로컬에서만 Gumball Machine을 모니터링할 수 있습니다.
원격 프록시를 통해 해결이 가능합니다.
- 프록시 개념: 프록시는 진짜 객체처럼 동작하지만, 실제 객체에 접근하기 전 이를 제어하거나 간접적으로 접근하게 만드는 디자인 패턴입니다.
- 원격 프록시(Remote Proxy): 원격 서버에 있는 객체에 접근하기 위해, 원격 통신을 관리하고 제어할 수 있는 대리 객체입니다.
- 역할: 로컬 객체처럼 보이지만 실제로는 원격 서버에 있는 진짜 객체와 통신하여 데이터를 주고받습니다.
- 동작: GumballMonitor는 진짜 Gumball Machine 대신 원격 프록시 객체에 접근합니다. 프록시는 원격으로 실제 기계의 상태와 재고를 조회하고, 이를 모니터에 전달합니다.
2. 원격 프록시의 역할
원격 프록시는 원격 객체의 로컬 대변자 역할을 합니다.
원격 객체 = 자바 가상 머신의 힙에 살고 있는 객체(다른 주소 공간에서 돌아가고 있는 객체)
로컬 대변자 = 어떤 메소드를 호출하면, 다른 원격 객체에게 그 메소드 호출을 전달해 주는 객체
클라이언트 객체(GumballMonitor)는 원격 객체의 메소드 호출을 하는 것처럼 행동합니다.
하지만 실제로는 로컬 힙에 들어있는 'Proxy' 객체의 메소드를 호출하고 있죠.
네트워크 통신과 관련된 저수준 작업은 이 'Proxy' 객체에서 처리해줍니다.
3. 모니터링 코드에 원격 프록시 추가하기
다른 JVM에 들어있는 객체의 메소드를 호출하는 프록시를 만들어봅시다.
아래와 같은 방법으로는 다른 힙에 들어있는 객체 레퍼런스를 가져올 수는 없습니다.
Duck d = <다른 힙에 있는 객체>
변수 d가 어떤 객체를 참조하든, 그 객체는 선언문이 있는 코드와 같은 힙 공간에 있어야 합니다.
여기서 이제 Java의 원격 메소드 호출(RMI, Remote Method Invocation)이 쓰입니다.
RMI를 사용하면 원격 JVM에 있는 객체를 찾아서 그 메소드를 호출할 수 있습니다.
4. 원격 메소드의 기초
- 클라이언트 객체: 메소드를 호출하는 원본 객체.
- 클라이언트 보조 객체(Proxy): 원격 메소드 호출을 포장하고 전달하는 프록시 객체.
- 서비스 보조 객체(Remote Skeleton): 클라이언트의 호출 정보를 해석하고, 서비스 객체의 메소드를 호출하는 역할.
- 서비스 객체: 실제 비즈니스 로직을 수행하고, 작업을 처리하는 원본 객체.
1. Client → [Client Proxy] : doBigThing()
2. [Client Proxy] → (네트워크) → [Remote Skeleton] : doBigThing() 호출 정보 전달
3. [Remote Skeleton] → [Service Object] : 메소드 해석 후, 서비스 객체 메소드 호출
4. [Service Object] : 비즈니스 로직 처리 후, 결과 반환
5. [Remote Skeleton] → (네트워크) → [Client Proxy] : 메소드 호출 결과 전달
6. [Client Proxy] → Client : 메소드 결과 리턴
5. 자바 RMI(Remote Method Invocation)로 원격 서비스 만들기
RMI는 클라이언트와 서비스 보조 객체를 만들어 줍니다. 보조 객체에는 원격 서비스와 똑같은 메소드가 들어있습니다.
클라이언트는 로컬 JVM에 있는 메소드를 호출하듯 원격 메소드(진짜 서비스 객체에 있는 메소드)를 호출할 수 있습니다.
또한 클라이언트가 원격 객체를 찾아서 접근할 때 쓸 수 있는 룩업(lookup) 서비스도 RMI에서 제공합니다.
RMI와 로컬 메소드의 차이점 : RMI는 클라이언트 보조 객체가 네트워크로 호출을 전송해야 하므로 네트워킹 및 입출력 기능이 반드시 필요합니다.
RMI(Remote Method Invocation)를 사용한 원격 서비스 구현 4단계
1. 원격 인터페이스 만들기 (Remote Interface)
- 목표: 클라이언트가 원격 객체에서 호출할 메소드를 정의하는 인터페이스를 생성합니다.
- 이 인터페이스는 java.rmi.Remote 인터페이스를 상속하고, 예외 처리용으로 RemoteException을 선언해야 합니다.
- 클라이언트와 서버 모두 이 인터페이스를 사용하여 통신하게 됩니다.
- 이 인터페이스는 원격 객체의 메소드 호출 방식을 클라이언트와 서버 간에 표준화해 줍니다.
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface MyService extends Remote {
// 클라이언트가 원격으로 호출할 메소드 정의
public String getServiceMessage() throws RemoteException;
}
2. 서비스 구현 클래스 만들기 (Service Implementation)
- 목표: 원격 인터페이스(MyService)를 구현한 서비스 클래스를 작성하여, 원격 메소드의 실제 동작을 정의합니다.
- 이 클래스는 UnicastRemoteObject를 상속받고, 원격 메소드를 구현해야 합니다.
- 실제 작업을 수행하는 메소드의 구체적인 로직을 이 클래스에 작성합니다.
- 예를 들어, Gumball Machine의 getState(), getCount() 같은 메소드를 정의할 수 있습니다.
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// MyService 인터페이스 구현 클래스
public class MyServiceImpl extends UnicastRemoteObject implements MyService {
// 생성자: RemoteException을 던져야 함. 어떤 클래스가 생성될 때 그 슈퍼클래스의 생성자도 반드시
// 호출되므로 슈퍼클래스 생성자가 어떤 예외를 던진다면 서브 클래스의 생성자도 그 예외를 선언해야 함.
public MyServiceImpl() throws RemoteException {
super();
}
// 원격으로 호출할 메소드 구현
@Override
public String getServiceMessage() throws RemoteException {
return "원격 서비스에 접근 성공!";
}
}
3. RMI 레지스트리 실행하기 (rmiregistry)
- 목표: RMI 레지스트리는 원격 객체를 클라이언트가 찾을 수 있도록 "전화번호부" 역할을 합니다.
- 레지스트리에는 각 원격 객체의 레퍼런스가 등록되어 있으며, 클라이언트는 이 레지스트리를 통해 객체를 찾고 접근할 수 있습니다.
- 레지스트리를 실행하면, RMI 시스템이 원격 객체의 프록시(Stub)를 생성하고 이를 레지스트리에 등록합니다.
- RMI 레지스트리는 별도의 터미널 창에서 다음 명령어를 사용해 실행합니다.
# RMI 레지스트리 실행 명령어
rmiregistry
4. 원격 서비스 실행 및 등록하기
- 목표: 원격 객체의 인스턴스를 생성하고, 이를 RMI 레지스트리에 등록합니다.
- 이 과정에서 서비스 객체의 이름을 RMI 레지스트리에 바인딩하여, 클라이언트가 원격 객체에 접근할 수 있게 합니다.
- 스텁(Stub)과 스켈레톤(Skeleton)은 RMI 시스템이 자동으로 생성하므로, 개발자가 직접 작성할 필요가 없습니다.
- 클라이언트는 레지스트리에서 이 이름을 사용하여 원격 객체의 레퍼런스를 가져와 메소드를 호출하게 됩니다.
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class MyServiceServer {
public static void main(String[] args) {
try {
// RMI 레지스트리 시작
LocateRegistry.createRegistry(1099);
// 원격 서비스 객체 생성
MyService service = new MyServiceImpl();
// RMI 레지스트리에 원격 객체를 바인딩
Naming.rebind("rmi://localhost/MyService", service);
System.out.println("원격 서비스가 시작되었습니다.");
} catch (RemoteException | java.net.MalformedURLException e) {
e.printStackTrace();
}
}
}
클라이언트가 RMI 레지스트리를 사용하여 원격 객체의 스텁(Stub) 객체를 가져와 메소드를 호출하는 과정
import java.rmi.Naming;
public class MyServiceClient {
public static void main(String[] args) {
new MyServiceClient().go();
}
// 원격 객체 호출을 수행하는 메소드
public void go() {
try {
// 1. RMI 레지스트리에서 원격 객체 조회 (Object 타입이므로 명시적 캐스팅 필요)
MyService service = (MyService) Naming.lookup("rmi://localhost/MyService");
// 2. 스텁 객체의 메소드 호출 (원격 메소드 실행)
String response = service.getServiceMessage();
// 3. 결과 출력
System.out.println("원격 서비스의 응답: " + response);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
6. 뽑기 기계용 원격 프록시
이제 뽑기 기계용 원격 프록시를 만들어보겠습니다.
원격 프록시를 쓸 수 있도록 가장 먼저 GumballMachine 클래스를 클라이언트로부터 전달된 원격 요청을 처리하도록 바꿉니다.
즉, 서비스를 구현한 클래스를 만들어야 합니다.
1. 원격 인터페이스 정의 (GumballMachineRemote)
GumballMachine의 원격 메소드를 정의하는 인터페이스를 생성합니다. 이 인터페이스는 Remote 인터페이스를 상속받고, 원격 호출할 수 있는 메소드들을 포함합니다. 각 메소드는 RemoteException을 던져야 하며, 리턴 타입은 직렬화 가능해야 합니다.
import java.rmi.Remote;
import java.rmi.RemoteException;
// GumballMachine의 원격 인터페이스
public interface GumballMachineRemote extends Remote {
public int getCount() throws RemoteException; // 알맹이의 개수를 반환
public String getLocation() throws RemoteException; // 기계의 위치 반환
public State getState() throws RemoteException; // 기계의 현재 상태 반환
}
2. 직렬화 가능한 State 인터페이스
GumballMachine의 메소드 중 getState()는 State 객체를 반환합니다. State 클래스가 원격 호출에서 사용되려면 Serializable 인터페이스를 구현해야 합니다. 따라서 State 인터페이스를 다음과 같이 변경합니다.
import java.io.Serializable;
// State 인터페이스에 Serializable 추가
public interface State extends Serializable {
public void insertQuarter();
public void ejectQuarter();
public void turnCrank();
public void dispense();
}
이렇게 State가 직렬화 가능해지면, RMI 시스템이 State 객체를 네트워크를 통해 안전하게 전송할 수 있습니다.
3. GumballMachine 클래스에서 원격 인터페이스 구현
이제 기존 GumballMachine 클래스를 원격 서비스 객체로 변경하여 GumballMachineRemote 인터페이스를 구현합니다. 또한, UnicastRemoteObject를 상속받아 RMI 시스템에서 사용할 수 있는 원격 객체로 설정합니다.
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// GumballMachine 클래스가 GumballMachineRemote를 구현하도록 변경
public class GumballMachine extends UnicastRemoteObject implements GumballMachineRemote {
String location;
int count;
State state;
// 생성자: RemoteException을 던져야 함
public GumballMachine(String location, int count) throws RemoteException {
this.location = location;
this.count = count;
// 초기 상태 설정 (State 객체는 직렬화 가능해야 함)
this.state = new NoQuarterState(this); // 예: NoQuarterState는 State의 직렬화 가능 클래스
}
// 원격 메소드 구현
@Override
public int getCount() throws RemoteException {
return count;
}
@Override
public String getLocation() throws RemoteException {
return location;
}
@Override
public State getState() throws RemoteException {
return state;
}
// 기타 GumballMachine 클래스의 메소드들...
}
이렇게 변경하면, GumballMachine 클래스가 원격 서비스로 동작하며, RMI 시스템에서 클라이언트가 호출할 수 있는 원격 메소드를 제공합니다.
4. GumballMachine 객체를 RMI 레지스트리에 등록
GumballMachine을 RMI 레지스트리에 등록하고, 클라이언트가 접근할 수 있도록 설정합니다. 기존 MyServiceServer 코드와 유사한 방식으로 RMI 레지스트리를 초기화하고, GumballMachine 객체를 등록할 수 있습니다.
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class GumballMachineServer {
public static void main(String[] args) {
try {
// 1. RMI 레지스트리 시작
LocateRegistry.createRegistry(1099);
// 2. 원격 GumballMachine 객체 생성
GumballMachine gumballMachine = new GumballMachine("Seattle", 100);
// 3. RMI 레지스트리에 원격 GumballMachine 객체 바인딩
Naming.rebind("rmi://localhost/GumballMachine", gumballMachine);
System.out.println("Gumball Machine 원격 서비스가 시작되었습니다.");
} catch (RemoteException | java.net.MalformedURLException e) {
e.printStackTrace();
}
}
}
5. 클라이언트 코드 (GumballMonitor)
이제 클라이언트가 RMI 레지스트리에서 GumballMachineRemote 객체를 가져와서 원격 메소드를 호출할 수 있도록 클라이언트 코드를 작성합니다.
import java.rmi.Naming;
public class GumballMonitorTest {
public static void main(String[] args) {
try {
// RMI 레지스트리에서 GumballMachine 원격 객체 가져오기
GumballMachineRemote machine = (GumballMachineRemote) Naming.lookup("rmi://localhost/GumballMachine");
// 원격 메소드 호출
System.out.println("Gumball Machine 위치: " + machine.getLocation());
System.out.println("현재 재고: " + machine.getCount());
System.out.println("현재 상태: " + machine.getState());
} catch (Exception e) {
e.printStackTrace();
}
}
}
요약
- GumballMachineRemote 원격 인터페이스 생성.
- State 클래스 직렬화 가능하도록 수정.
- GumballMachine 클래스를 원격 서비스로 변환.
- RMI 레지스트리에 등록하여 원격 서비스 설정.
- 클라이언트 코드(GumballMonitorTest)를 통해 원격 메소드 호출.
이렇게 GumballMachine을 원격 서비스로 변환하여 클라이언트가 네트워크를 통해 접근할 수 있게 만들 수 있습니다.
7. 새로운 모니터링 기능 테스트
처음에 CEO가 만들고 싶어했던, 수많은 뽑기 기계를 모니터링 할 수 있는 코드를 작성해봅니다.
import java.rmi.Naming;
public class GumballMonitorTestDrive {
public static void main(String[] args) {
// 1. 여러 대의 Gumball Machine이 위치한 RMI URL 배열
String[] location = {
"rmi://santafe.mightygumball.com/gumballmachine",
"rmi://boulder.mightygumball.com/gumballmachine",
"rmi://austin.mightygumball.com/gumballmachine"
};
// 2. GumballMonitor 배열 생성 (각 Gumball Machine을 모니터링할 객체)
GumballMonitor[] monitor = new GumballMonitor[location.length];
// 3. RMI 레지스트리에서 각 원격 GumballMachine 객체를 가져와 모니터링 객체 생성
for (int i = 0; i < location.length; i++) {
try {
// 3-1. RMI 레지스트리에서 GumballMachine 원격 객체 가져오기
GumballMachineRemote machine = (GumballMachineRemote) Naming.lookup(location[i]);
// 3-2. 각 GumballMachineRemote 객체에 대한 GumballMonitor 생성
monitor[i] = new GumballMonitor(machine);
System.out.println("모니터링할 Gumball Machine: " + location[i]);
} catch (Exception e) {
e.printStackTrace();
}
}
// 4. 각 Gumball Machine의 보고서 출력
for (int i = 0; i < monitor.length; i++) {
monitor[i].report();
}
}
}
8. 프록시 패턴의 정의
- 정의: 프록시 패턴은 특정 객체에 대한 접근을 제어하는 대리 객체를 제공하는 패턴입니다. 프록시는 실제 객체에 대한 대리인 역할을 하며, 클라이언트가 이 대리자를 통해 원래 객체에 접근할 수 있게 합니다.
- 목적: 실제 객체의 직접 접근을 제한하거나, 특정 상황에서 추가적인 기능을 제공하기 위해 사용됩니다. 클라이언트가 직접 접근하지 않고, 프록시를 통해 간접적으로 접근함으로써 접근 권한을 제어하고, 부가적인 로직을 추가하거나, 네트워크 호출 등을 투명하게 처리할 수 있습니다. => 다양한 목적으로 쓰인다.
프록시 패턴의 주요 변형
프록시 패턴에는 여러 변형이 있으며, 각 변형은 특정한 접근 제어 방법을 사용하여 프록시 객체의 역할을 다르게 정의합니다. 다음은 대표적인 프록시 패턴의 변형입니다:
- 원격 프록시(Remote Proxy):
- 정의: 원격 객체에 대한 접근을 제어하는 프록시입니다. 원격 프록시는 네트워크 통신을 통해 원격 서버에 있는 객체와 클라이언트 사이의 프록시 역할을 합니다.
- 예: GumballMachineRemote를 사용하여 원격 Gumball Machine 객체와 클라이언트 간의 통신을 관리.
- 가상 프록시(Virtual Proxy):
- 정의: 가상 프록시는 생성 비용이 높은 객체를 필요할 때만 생성하도록 제어합니다. 객체의 지연 초기화(Lazy Initialization)를 수행하며, 클라이언트가 요청할 때 실제 객체를 생성하여 접근을 제어합니다.
- 예: 이미지 로딩이 오래 걸리는 상황에서, 이미지를 보여줘야 할 때만 실제 이미지 객체를 생성하도록 가상 프록시를 사용.
- 보호 프록시(Protection Proxy):
- 정의: 보호 프록시는 접근 권한을 제어하여 특정 클라이언트만 실제 객체에 접근할 수 있도록 합니다. 보안 검사나 권한 관리가 필요할 때 사용됩니다.
- 예: 사용자 권한에 따라 읽기/쓰기 권한을 다르게 설정하여, 권한이 없는 사용자가 데이터에 접근할 수 없도록 보호.
- Subject: 공통 인터페이스로, 실제 객체와 프록시가 같은 메소드를 가지고 있어야 합니다.
- RealSubject: 실제 동작을 수행하는 진짜 객체로, 프록시를 통해 접근하는 대상 객체입니다.
- Proxy: Subject 인터페이스를 구현한 대리자 객체로, RealSubject와 동일한 메소드를 제공합니다. 프록시는 클라이언트의 요청을 받아 이를 실제 객체에게 전달하고, 결과를 다시 클라이언트에게 반환합니다.
9. 원격 프록시와 가상 프록시 비교하기
원격 프록시
원격 프록시는 다른 JVM에 들어있는 객체의 대리인에 해당하는 로컬 객체입니다.
가상 프록시
가상 프록시는 생성하는 데 많은 비용이 드는 객체를 대신합니다.
진짜 객체가 필요한 상황이 오기 전까지 객체의 생성을 미루는 기능을 제공합니다.
객체 생성 전이나 객체 생성 도중에 객체를 대신하기도 합니다. 객체 생성이 끝나면 그냥 RealSubject에 직접 요청을 전달합니다.
가상 프록시를 사용하는 프레임워크 예
- React와 Vue의 Lazy Loading:
- 동적 import를 통해 필요한 컴포넌트만 로드하여, 초기 렌더링을 최적화합니다.
- IntersectionObserver API:
- 이미지나 비디오의 지연 로딩을 통해 사용자가 볼 때만 로드하여, 네트워크 트래픽을 최적화합니다.
10. 가상 프록시로 앨범 커버 뷰어 만들기
가상 프록시(Virtual Proxy)를 사용하여 앨범 커버 뷰어를 만드는 방식은, 네트워크로부터 이미지를 로드하는 동안 사용자에게 로딩 상태를 표시하고, 실제 이미지가 로드될 때 이를 화면에 보여줌으로써 사용자 경험을 개선하는 디자인 패턴입니다.
- Icon (인터페이스):
- paintIcon(), getIconWidth(), getIconHeight() 등의 메소드를 정의하여 이미지를 화면에 그리는 표준 인터페이스 역할을 합니다.
import java.awt.Component;
import java.awt.Graphics;
// 공통 인터페이스: 아이콘을 그리기 위한 표준 메소드 정의
public interface Icon {
void paintIcon(Component c, Graphics g, int x, int y);
int getIconWidth();
int getIconHeight();
}
- RealImageIcon (구현 클래스):
- 실제로 이미지를 로드하고, 화면에 이미지를 표시하는 객체입니다.
- Icon 인터페이스를 구현하여 실제 이미지 데이터를 화면에 렌더링합니다.
import java.awt.*;
import javax.swing.*;
import java.net.*;
public class RealImageIcon implements Icon {
private ImageIcon imageIcon; // 실제 이미지 아이콘
private URL imageUrl;
// 생성자: 네트워크 URL에서 이미지를 가져옴
public RealImageIcon(URL url) {
this.imageUrl = url;
try {
this.imageIcon = new ImageIcon(imageUrl); // 실제 이미지 로딩
} catch (Exception e) {
e.printStackTrace();
}
}
// 이미지를 그리는 메소드
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
if (imageIcon != null) {
imageIcon.paintIcon(c, g, x, y);
}
}
@Override
public int getIconWidth() {
return (imageIcon != null) ? imageIcon.getIconWidth() : 0;
}
@Override
public int getIconHeight() {
return (imageIcon != null) ? imageIcon.getIconHeight() : 0;
}
}
- ImageProxy (가상 프록시):
- Icon 인터페이스를 구현하지만, 실제 이미지가 로드되기 전까지 대체 메시지를 표시합니다.
- 이미지가 로드되면 RealImageIcon 객체로 모든 작업을 위임하여 실제 이미지를 화면에 표시합니다.
import java.awt.*;
import javax.swing.*;
import java.net.*;
public class ImageProxy implements Icon {
private RealImageIcon realImageIcon; // 실제 이미지 객체
private URL imageUrl;
private boolean retrieving = false; // 이미지 로드 상태
public ImageProxy(URL url) {
this.imageUrl = url;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
if (realImageIcon != null) {
realImageIcon.paintIcon(c, g, x, y); // 실제 이미지가 있으면 그리기
} else {
g.drawString("앨범 커버를 불러오는 중입니다... 잠시만 기다려 주세요.", x + 50, y + 50);
if (!retrieving) {
retrieving = true;
new Thread(() -> { // 비동기 로딩
realImageIcon = new RealImageIcon(imageUrl);
c.repaint(); // 이미지 로딩 후 화면 갱신
}).start();
}
}
}
@Override
public int getIconWidth() {
return (realImageIcon != null) ? realImageIcon.getIconWidth() : 800; // 기본 너비 설정
}
@Override
public int getIconHeight() {
return (realImageIcon != null) ? realImageIcon.getIconHeight() : 600; // 기본 높이 설정
}
}
11. 보호 프록시 만들기
동적 프록시(Dynamic Proxy)는 자바의 java.lang.reflect 패키지를 사용하여 런타임에 생성되는 프록시 객체입니다. 자바의 동적 프록시를 사용하면 인터페이스 기반으로 프록시를 생성하고, 메소드 호출을 제어하거나 특정 로직을 동적으로 추가할 수 있습니다. 이번에는 동적 프록시를 사용하여 보호 프록시(Protection Proxy)를 만들어 보겠습니다.
보호 프록시는 주로 권한 제어를 통해 클라이언트가 특정 메소드에 접근하지 못하게 막는 역할을 수행합니다. 예를 들어, 사용자가 자신의 정보를 수정할 수는 있지만, 다른 사용자의 정보를 수정할 수는 없도록 제한하는 로직을 포함할 수 있습니다.
- 인터페이스(Subject):
- 프록시와 실제 객체가 공통적으로 구현하는 인터페이스입니다.
- java.lang.reflect.Proxy는 반드시 인터페이스를 기반으로 프록시를 생성하기 때문에, 프록시로 사용할 객체는 인터페이스가 필요합니다.
- 실제 객체(Real Subject):
- 보호 프록시를 통해 접근을 제어할 실제 비즈니스 로직을 구현한 클래스입니다.
- 프록시 객체(Proxy):
- java.lang.reflect.Proxy와 InvocationHandler를 사용하여 동적으로 생성됩니다.
- InvocationHandler를 구현하여, 모든 메소드 호출을 가로채고 권한을 확인한 후 실제 객체로 메소드를 전달하거나, 접근이 허용되지 않은 경우 예외를 발생시킵니다.
- InvocationHandler:
- 모든 메소드 호출을 가로채는 역할을 하며, 메소드 호출 시 접근 제어 로직을 추가하여 권한이 있는지 확인합니다.
- invoke() 메소드를 통해 모든 메소드 호출을 처리합니다.
12. 객체마을 데이팅 서비스
객체마을의 데이팅 서비스에서는 Person 인터페이스를 사용하여 사용자의 이름, 성별, 관심사 및 괴짜 지수(Geek Rating)를 설정하고 조회할 수 있습니다. 이 인터페이스를 구현한 PersonImpl 클래스를 통해 사용자의 데이터를 저장하고, get 및 set 메소드로 데이터를 조작할 수 있습니다.
그러나 여기서 보호 프록시(Protection Proxy)를 사용하여 권한에 따라 접근을 제어하고, 사용자가 자신의 프로필을 수정할 때와 다른 사용자의 프로필을 평가할 때 동작을 다르게 설정할 수 있습니다. 예를 들어, 사용자가 자신의 프로필을 수정할 수는 있지만, 다른 사용자의 프로필을 수정할 수는 없도록 제한할 수 있습니다.
이제 객체마을 데이팅 서비스의 보호 프록시를 사용하여 사용자의 접근 권한을 제어하는 방법을 구현해 보겠습니다.
서비스 시나리오
- 데이팅 서비스에는 두 가지 유형의 사용자가 있습니다:
- 소유자(Owner): 자신의 정보를 수정하고, 이름, 성별, 관심사, 괴짜 지수를 설정할 수 있습니다.
- 게스트(Guest): 다른 사람의 프로필을 볼 수 있지만, 평가(괴짜 지수 설정)만 가능합니다. 이름, 성별, 관심사는 변경할 수 없습니다.
1. 인터페이스 설계
Person 인터페이스
Person 인터페이스는 사용자의 정보를 설정하고 가져오는 메소드를 정의합니다.
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);
}
PersonImpl 클래스
Person 인터페이스를 구현한 구체 클래스입니다. 사용자 정보를 저장하고 관리하는 역할을 수행합니다.
public class PersonImpl implements Person {
private String name;
private String gender;
private String interests;
private int rating;
private 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() {
if (ratingCount == 0) return 0;
return (rating / ratingCount);
}
@Override
public void setGeekRating(int rating) {
this.rating += rating;
ratingCount++;
}
}
2. 보호 프록시 설계
InvocationHandler 구현
두 가지 접근 제어 방식이 필요합니다:
- OwnerInvocationHandler: 소유자가 자신의 프로필을 전체 수정할 수 있습니다.
- NonOwnerInvocationHandler: 게스트는 프로필 평가(괴짜 지수 설정)만 가능하고, 다른 필드는 수정할 수 없습니다.
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;
}
}
public class NonOwnerInvocationHandler implements InvocationHandler {
private Person person;
public NonOwnerInvocationHandler(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")) {
return method.invoke(person, args);
} else if (method.getName().startsWith("set")) {
throw new IllegalAccessException("게스트는 프로필을 수정할 수 없습니다.");
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
프록시 생성기
Proxy 클래스를 사용하여, Person 객체의 프록시 객체를 생성합니다. OwnerInvocationHandler와 NonOwnerInvocationHandler에 따라 소유자와 게스트 권한을 다르게 설정합니다.
public class ProxyFactory {
public static Person getOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person)
);
}
public static Person getNonOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new NonOwnerInvocationHandler(person)
);
}
}
3. 테스트 코드
소유자와 게스트의 권한을 테스트하여, 보호 프록시가 권한에 따라 동작하는지 확인합니다.
public class DatingServiceTest {
public static void main(String[] args) {
Person joe = new PersonImpl();
joe.setName("Joe Javabean");
joe.setGender("Male");
joe.setInterests("Programming, Tennis, Movies");
// 1. 소유자 프록시 생성
Person ownerProxy = ProxyFactory.getOwnerProxy(joe);
System.out.println("이름: " + ownerProxy.getName());
ownerProxy.setInterests("Programming and playing games");
System.out.println("관심사: " + ownerProxy.getInterests());
try {
ownerProxy.setGeekRating(10); // 소유자가 자신의 괴짜 지수를 설정하려고 하면 예외 발생
} catch (Exception e) {
System.out.println(e.getMessage());
}
// 2. 게스트 프록시 생성
Person nonOwnerProxy = ProxyFactory.getNonOwnerProxy(joe);
System.out.println("\n게스트 권한 테스트:");
System.out.println("이름: " + nonOwnerProxy.getName());
try {
nonOwnerProxy.setInterests("Watching movies"); // 게스트가 관심사를 설정하려고 하면 예외 발생
} catch (Exception e) {
System.out.println(e.getMessage());
}
nonOwnerProxy.setGeekRating(5); // 게스트는 괴짜 지수를 설정할 수 있음
System.out.println("괴짜 지수: " + nonOwnerProxy.getGeekRating());
}
}
결과
- 소유자:
- 프로필 정보(이름, 성별, 관심사)를 수정할 수 있지만, 괴짜 지수 설정은 불가능.
- 게스트:
- 프로필 정보는 읽기만 가능하며, 괴짜 지수 설정만 가능.
이렇게 구현하면 보호 프록시(Protection Proxy)를 통해 접근 권한을 제어하고, 사용자 권한에 따라 메소드 접근을 제한할 수 있습니다. 이 패턴을 통해 사용자의 잘못된 접근을 방지하고, 안정적인 시스템 제어를 할 수 있습니다.
'개발서적 > 헤드퍼스트 디자인패턴' 카테고리의 다른 글
[개발서적] 헤드퍼스트 디자인 패턴 Ch12. 복합 패턴 (0) | 2024.10.24 |
---|---|
[개발서적] 헤드퍼스트 디자인 패턴 Ch10. 상태 패턴 (0) | 2024.09.22 |
[개발서적] 헤드퍼스트 디자인 패턴 Ch9. 반복자 패턴과 컴포지트 패턴 (0) | 2024.09.04 |
[개발서적] 헤드퍼스트 디자인 패턴 Ch8. 템플릿 메소드 패턴 (0) | 2024.08.30 |
[개발서적] 헤드퍼스트 디자인 패턴 Ch7. 어댑터 패턴과 퍼사드 패턴 (1) | 2024.08.29 |