Proxy Pattern 정리

테사벨로그·2025년 11월 14일

Design Pattern

목록 보기
16/19

1. 왜 Proxy Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 직접 접근으로 인한 문제
public class ImageViewer {
    public void display() {
        // 매우 큰 이미지를 직접 로드
        HighResolutionImage image = new HighResolutionImage("large_image.jpg");
        image.display();  // 이미지가 필요없어도 무조건 로딩됨
    }
}

// ❌ 원격 객체에 직접 접근
public class GumballMonitor {
    GumballMachine machine;  // 다른 서버에 있는 객체에 직접 접근 불가능
    
    public void report() {
        // 네트워크 통신이 필요한데 일반 객체처럼 접근할 수 없음
        System.out.println(machine.getCount());
    }
}

문제점:

  • 무거운 객체를 매번 생성하면 성능 저하
  • 원격 객체에 직접 접근할 수 없음
  • 객체 접근 제어가 필요한 경우 처리 불가능
  • 네트워크 통신이 필요한 객체를 로컬 객체처럼 사용 불가능

2. Subject Interface VS Proxy VS RealSubject

1. Subject Interface

  • "RealSubject와 Proxy가 동일한 인터페이스를 제공하세요"
  • 클라이언트가 실제 객체인지 프록시인지 구분하지 못하게 함
  • "can-do" 관계 (동일한 작업을 수행할 수 있음)
public interface Image {
    void display();
    int getWidth();
    int getHeight();
}

왜 Interface인가?

  • Proxy와 RealSubject가 동일한 인터페이스를 구현
  • 클라이언트는 둘을 구분하지 않고 사용 가능
  • 다양한 타입의 프록시 구현 가능 (Remote, Virtual, Protection)

2. Proxy

  • "RealSubject에 대한 접근을 제어하는 대리인"
  • RealSubject에 대한 참조를 유지
  • 실제 작업은 RealSubject에게 위임
public class ImageProxy implements Image {
    private String filename;
    private RealImage realImage;  // 실제 객체 참조
    
    public ImageProxy(String filename) {
        this.filename = filename;
    }
    
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename);  // 필요할 때만 생성
        }
        realImage.display();
    }
}

역할:

  • RealSubject 생성 시점 제어
  • 네트워크 통신 처리
  • 접근 권한 검사
  • 추가 기능 제공 (캐싱, 로깅 등)

3. RealSubject

  • "실제 작업을 수행하는 진짜 객체"
  • 비용이 많이 드는 작업 수행
public class RealImage implements Image {
    private String filename;
    private byte[] imageData;
    
    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();  // 실제로 파일을 로드
    }
    
    private void loadFromDisk() {
        System.out.println("Loading " + filename);
        // 무거운 작업 수행
    }
    
    public void display() {
        System.out.println("Displaying " + filename);
    }
}

3. Proxy Pattern의 주요 유형

1. Virtual Proxy (가상 프록시)

  • 생성 비용이 큰 객체를 lazy initialization
  • 실제로 필요할 때까지 객체 생성을 지연

2. Remote Proxy (원격 프록시)

  • 다른 주소 공간에 있는 객체의 로컬 대리인
  • 네트워크 통신을 숨겨줌 (RMI Stub이 이에 해당)

3. Protection Proxy (보호 프록시)

  • 접근 권한에 따라 객체 접근을 제어
  • 클라이언트가 적절한 권한을 가졌는지 확인

4. Proxy Pattern 핵심 구조

      Subject
         ↑
    ┌────┴────┐
    │         │
  Proxy  RealSubject
    │
    └──────→ subject (RealSubject 참조)

- Client는 Subject 인터페이스를 통해서만 접근
- Proxy는 RealSubject와 동일한 인터페이스 구현
- Proxy가 RealSubject에 대한 접근을 제어

5. 예시 코드

Virtual Proxy 예제

Step 1: Subject 인터페이스 정의

public interface Image {
    void display();
}

Step 2: RealSubject 구현

public class RealImage implements Image {
    private String filename;
    
    public RealImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }
    
    private void loadFromDisk() {
        System.out.println("Loading " + filename + " from disk...");
        // 시간이 오래 걸리는 작업 시뮬레이션
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void display() {
        System.out.println("Displaying " + filename);
    }
}

Step 3: Proxy 구현

public class ImageProxy implements Image {
    private String filename;
    private RealImage realImage;  // 실제 이미지 참조
    
    public ImageProxy(String filename) {
        this.filename = filename;
        // 실제 이미지는 아직 생성하지 않음!
    }
    
    @Override
    public void display() {
        // 실제로 필요할 때만 이미지 생성 (Lazy Initialization)
        if (realImage == null) {
            System.out.println("Creating real image...");
            realImage = new RealImage(filename);
        }
        realImage.display();
    }
}

Step 4: 클라이언트 코드

public class ProxyPatternDemo {
    public static void main(String[] args) {
        System.out.println("=== Virtual Proxy Demo ===\n");
        
        // Proxy 객체 생성 - 빠름!
        Image image1 = new ImageProxy("photo1.jpg");
        Image image2 = new ImageProxy("photo2.jpg");
        System.out.println("Proxy objects created (fast!)\n");
        
        // 첫 번째 display - 실제 이미지 로드됨 (느림)
        System.out.println("First call to display():");
        image1.display();
        
        System.out.println("\nSecond call to display():");
        // 두 번째 display - 이미 로드된 이미지 사용 (빠름)
        image1.display();
        
        System.out.println("\nDisplaying second image:");
        image2.display();
    }
}

출력 결과

=== Virtual Proxy Demo ===

Proxy objects created (fast!)

First call to display():
Creating real image...
Loading photo1.jpg from disk...
Displaying photo1.jpg

Second call to display():
Displaying photo1.jpg

Displaying second image:
Creating real image...
Loading photo2.jpg from disk...
Displaying photo2.jpg

Remote Proxy 예제 (RMI)

Step 1: Remote Interface 정의

import java.rmi.*;

public interface GumballMachineRemote extends Remote {
    int getCount() throws RemoteException;
    String getLocation() throws RemoteException;
    String getState() throws RemoteException;
}

Step 2: Remote Implementation

import java.rmi.*;
import java.rmi.server.*;

public class GumballMachine extends UnicastRemoteObject 
                           implements GumballMachineRemote {
    String location;
    int count;
    String state;
    
    public GumballMachine(String location, int count) throws RemoteException {
        this.location = location;
        this.count = count;
        this.state = "No Quarter";
    }
    
    public int getCount() {
        return count;
    }
    
    public String getLocation() {
        return location;
    }
    
    public String getState() {
        return state;
    }
}

Step 3: Server 등록

import java.rmi.*;

public class GumballMachineServer {
    public static void main(String[] args) {
        try {
            GumballMachine machine = new GumballMachine("Seoul", 100);
            
            // RMI 레지스트리에 등록
            Naming.rebind("//localhost/gumballmachine", machine);
            
            System.out.println("Gumball Machine Server ready");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Step 4: Client (Monitor)

import java.rmi.*;

public class GumballMonitor {
    GumballMachineRemote machine;  // 프록시 역할
    
    public GumballMonitor(GumballMachineRemote machine) {
        this.machine = machine;
    }
    
    public void report() {
        try {
            System.out.println("Gumball Machine: " + machine.getLocation());
            System.out.println("Current inventory: " + machine.getCount() + " gumballs");
            System.out.println("Current state: " + machine.getState());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        try {
            // RMI 레지스트리에서 프록시(Stub) 획득
            GumballMachineRemote machine = 
                (GumballMachineRemote) Naming.lookup("//localhost/gumballmachine");
            
            GumballMonitor monitor = new GumballMonitor(machine);
            monitor.report();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Protection Proxy 예제

// Subject 인터페이스
public interface Document {
    void view();
    void edit();
    void delete();
}

// RealSubject
public class SecureDocument implements Document {
    private String content;
    
    public SecureDocument(String content) {
        this.content = content;
    }
    
    public void view() {
        System.out.println("Viewing: " + content);
    }
    
    public void edit() {
        System.out.println("Editing document...");
    }
    
    public void delete() {
        System.out.println("Deleting document...");
    }
}

// Protection Proxy
public class DocumentProxy implements Document {
    private SecureDocument document;
    private String userRole;  // "admin", "editor", "viewer"
    
    public DocumentProxy(String content, String userRole) {
        this.document = new SecureDocument(content);
        this.userRole = userRole;
    }
    
    public void view() {
        // 모든 사용자가 볼 수 있음
        document.view();
    }
    
    public void edit() {
        // Editor와 Admin만 편집 가능
        if (userRole.equals("admin") || userRole.equals("editor")) {
            document.edit();
        } else {
            System.out.println("Access denied: You don't have permission to edit");
        }
    }
    
    public void delete() {
        // Admin만 삭제 가능
        if (userRole.equals("admin")) {
            document.delete();
        } else {
            System.out.println("Access denied: Only admin can delete");
        }
    }
}

// Client
public class ProtectionProxyDemo {
    public static void main(String[] args) {
        // Viewer 권한
        Document doc1 = new DocumentProxy("Confidential Report", "viewer");
        doc1.view();    // ✅ 성공
        doc1.edit();    // ❌ 실패
        doc1.delete();  // ❌ 실패
        
        System.out.println();
        
        // Admin 권한
        Document doc2 = new DocumentProxy("Confidential Report", "admin");
        doc2.view();    // ✅ 성공
        doc2.edit();    // ✅ 성공
        doc2.delete();  // ✅ 성공
    }
}

6. 핵심 정리

Proxy Pattern의 구성

요소역할왜 이렇게?
SubjectProxy와 RealSubject의 공통 인터페이스클라이언트가 둘을 구분 없이 사용하도록
ProxyRealSubject에 대한 접근 제어생성 지연, 원격 접근, 권한 검사 등
RealSubject실제 작업 수행비용이 큰 실제 객체

Proxy 유형별 사용 시기

유형사용 시기예시
Virtual Proxy생성 비용이 큰 객체를 지연 로딩이미지, 대용량 문서
Remote Proxy원격 객체에 로컬처럼 접근RMI, 웹 서비스
Protection Proxy접근 권한 제어 필요문서 보안, 리소스 접근 제어

언제 사용하는가?

  • 객체 생성 비용이 클 때 - Virtual Proxy로 lazy initialization
  • 원격 객체 접근 - Remote Proxy로 네트워크 통신 숨김
  • 접근 제어 필요 - Protection Proxy로 권한 관리
  • 추가 기능 필요 - 로깅, 캐싱, 참조 카운팅 등

핵심 원칙

  • 대리인 역할: Proxy는 RealSubject의 대리인
  • 동일한 인터페이스: 클라이언트는 Proxy인지 RealSubject인지 모름
  • 접근 제어: Proxy가 RealSubject에 대한 접근을 제어
  • 투명성: 클라이언트 코드 변경 없이 Proxy 추가 가능

Proxy VS 다른 패턴

패턴인터페이스목적
Adapter다른 인터페이스 제공호환되지 않는 인터페이스 연결
Decorator동일 인터페이스 + 기능 추가객체에 동적으로 기능 추가
Proxy동일 인터페이스접근 제어
profile
다들 응원합니다.

0개의 댓글