프로그래밍/알고리즘&자료구조&패턴

디자인패턴 - 프록시 패턴

lazy_web_devloper 2025. 8. 18. 10:52
728x90
반응형

JAVA로 구현하는 알고리즘과 디자인 패턴 공부: 프록시 패턴 (Proxy)

1. 프록시 패턴이란? 💳

개념: 어떤 객체에 대한 접근을 제어하기 위한 대리인(Proxy)이나 대리자 역할을 하는 객체를 제공하는 패턴입니다. 프록시는 실제 객체와 동일한 인터페이스를 가지므로, 클라이언트는 실제 객체를 사용하는지 프록시 객체를 사용하는지 모른 채 상호작용할 수 있습니다.

가장 쉬운 예는 신용카드입니다. 신용카드는 여러분의 은행 계좌(실제 객체)에 대한 프록시입니다. 여러분은 식당에서 현금(실제 객체)을 직접 꺼내지 않고 신용카드(프록시)로 결제합니다. 이 과정에서 카드는 결제 한도 확인(접근 제어), 포인트 적립(부가 기능) 등의 역할을 수행합니다.

2. 왜 사용할까요?

프록시는 어떤 목적으로 '대신'하느냐에 따라 다양하게 활용됩니다.

  • 가상 프록시 (Virtual Proxy): 생성하는 데 비용이 많이 드는 객체의 생성을 실제 필요할 때까지 지연시킵니다. (예: 고해상도 이미지 로딩)
  • 보호 프록시 (Protection Proxy): 특정 클라이언트만 실제 객체에 접근할 수 있도록 접근을 제어합니다. (예: 관리자 권한 확인)
  • 원격 프록시 (Remote Proxy): 다른 주소 공간에 있는 원격 객체에 대한 로컬 대리자를 제공합니다.
  • 로깅/캐싱 프록시: 실제 객체에 대한 요청 전후에 로그를 남기거나, 요청 결과를 캐싱하여 성능을 향상시킵니다.

3. Java로 구현하는 방법

시나리오: 고해상도 이미지를 화면에 표시하는 기능을 만들어 보겠습니다. 이미지는 용량이 매우 커서 로딩하는 데 시간이 오래 걸립니다. 따라서 이미지가 실제로 화면에 표시될 때까지 로딩을 지연시키는 가상 프록시를 구현해 보겠습니다.

1단계: 공통(Subject) 인터페이스 정의

실제 객체와 프록시 객체가 모두 구현해야 할 공통 인터페이스입니다.

Java
 
// 실제 이미지와 이미지 프록시가 모두 구현할 공통 인터페이스
public interface Image {
    void display();
}

2단계: 실제 객체(Real Subject) 클래스 구현

많은 리소스를 사용하는 무거운 실제 객체입니다.

Java
 
// 실제 고해상도 이미지. 생성 비용이 매우 비싸다.
public class RealImage implements Image {
    private String fileName;

    // 생성자에서 디스크로부터 이미지를 로딩하는 무거운 작업을 수행
    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println(fileName + " 이미지를 디스크에서 로딩하는 중...");
        // (시간이 오래 걸리는 작업이라고 가정)
        try {
            Thread.sleep(2000); // 2초 딜레이
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void display() {
        System.out.println(fileName + " 이미지를 화면에 표시합니다.");
    }
}

3단계: 프록시(Proxy) 클래스 구현

이 패턴의 핵심입니다. 프록시는 실제 객체와 동일한 인터페이스를 구현하고, 내부에 실제 객체에 대한 참조를 가집니다. 그리고 실제 객체는 display() 메서드가 처음 호출될 때 생성됩니다.

Java
 
public class ImageProxy implements Image {
    private RealImage realImage; // 실제 객체에 대한 참조
    private String fileName;

    public ImageProxy(String fileName) {
        this.fileName = fileName;
    }

    // display() 메서드가 호출될 때까지 실제 객체 생성을 미룬다.
    @Override
    public void display() {
        // 실제 객체가 아직 생성되지 않았다면 새로 생성한다.
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }
        // 생성된 실제 객체에 요청을 위임한다.
        realImage.display();
    }
}

4단계: 클라이언트 코드에서 사용하기

클라이언트는 Image 인터페이스만 알면 되고, 자신이 프록시를 사용하는지 실제 객체를 사용하는지 모릅니다.

Java
 
public class ImageViewer {
    public static void main(String[] args) {
        System.out.println("프로그램 시작...");

        // ImageProxy 객체를 생성. 이 시점에는 아직 이미지가 로딩되지 않는다.
        Image image1 = new ImageProxy("vacation_photo_1.jpg");
        Image image2 = new ImageProxy("family_photo_2.jpg");

        System.out.println("이미지 객체는 생성되었지만 아직 로딩은 안됨.");
        System.out.println("---");

        // display()를 호출하는 순간, 프록시가 실제 RealImage 객체를 생성하고 로딩한다.
        System.out.println("첫 번째 이미지 표시 요청:");
        image1.display();
        System.out.println("---");

        // 이미 한 번 로딩되었으므로, 다시 display()를 호출하면 바로 표시된다.
        System.out.println("첫 번째 이미지 다시 표시 요청:");
        image1.display();
        System.out.println("---");

        System.out.println("두 번째 이미지 표시 요청:");
        image2.display();
    }
}

4. 프록시 패턴과 스프링 (Spring AOP)

프록시 패턴은 **스프링의 관점 지향 프로그래밍(AOP)**을 구현하는 핵심 기술입니다.

스프링에서 트랜잭션(@Transactional)이나 보안, 로깅 같은 부가 기능(Aspect)을 메서드에 적용하면, 스프링은 컴파일 시점이 아닌 런타임에 해당 객체에 대한 프록시 객체를 동적으로 생성합니다.

클라이언트가 원본 객체의 메서드를 호출하면, 이 호출은 스프링이 생성한 프록시 객체를 먼저 거치게 됩니다. 프록시는 부가 기능(예: 트랜잭션 시작)을 먼저 실행하고, 그 다음에 원본 객체의 실제 메서드를 호출한 뒤, 다시 부가 기능(예: 트랜잭션 커밋/롤백)을 실행하는 방식으로 동작합니다.

이 모든 과정이 프록시를 통해 이루어지므로, 개발자는 핵심 비즈니스 로직에만 집중할 수 있고 부가 기능 코드를 분리하여 관리할 수 있습니다.


5. 문제 제시 및 답변 💡

문제: 특정 서비스의 메서드는 '관리자(ADMIN)' 역할을 가진 사용자만 호출할 수 있도록 접근을 제어하고 싶습니다. AdminService 라는 인터페이스와 그 구현체인 RealAdminService가 있다고 가정할 때, **보호 프록시(Protection Proxy)**를 사용하여 이 기능을 어떻게 구현할 수 있을까요?

답변:

AdminServiceProxy 라는 프록시 클래스를 만들면 됩니다.

  1. AdminServiceProxy 클래스는 원본과 동일하게 **AdminService 인터페이스를 구현(implements)**합니다.
  2. 이 클래스는 내부에 RealAdminService 객체에 대한 참조와, 현재 사용자의 User 객체를 가집니다.
  3. AdminService의 메서드(예: deleteUser())를 오버라이딩합니다.
  4. 오버라이딩한 deleteUser() 메서드 내부에서, 먼저 사용자의 역할(role)을 확인합니다.
    • 역할이 'ADMIN'이면, 내부의 RealAdminService 객체에 요청을 그대로 위임하여 deleteUser()를 실행합니다.
    • 역할이 'ADMIN'이 아니면, 예외(Exception)를 발생시키거나 접근 거부 메시지를 반환합니다.

이렇게 하면, 클라이언트는 권한 확인 로직을 신경 쓸 필요 없이 AdminService를 사용할 수 있고, 핵심 서비스 로직과 권한 검사 로직을 깔끔하게 분리할 수 있습니다.

728x90
반응형