프로그래밍/디자인패턴

프록시 패턴(Proxy Pattern)

Jinwookoh 2025. 12. 7. 17:43

우리는 살면서 직접 하기 부담스러운 일을 "대리인"에게 맡기곤 합니다. 법적인 문제는 변호사에게, 부동산 거래는 공인중개사에게 맡기죠.

소프트웨어에서도 마찬가지입니다. 어떤 객체에 직접 접근하는 것이 보안상 위험하거나, 메모리 비용이 너무 클 때 **가짜 객체(Proxy)**를 앞에 세워서 제어하는 방식을 프록시 패턴이라고 합니다.

오늘은 프록시 패턴이 사용 목적에 따라 어떻게 나뉘는지, 그리고 Spring 개발자라면 반드시 알아야 할 Dynamic Proxy까지 정리해 봅니다.

Shutterstock
 

1. 기본 개념 및 구조

프록시의 핵심은 **"실제 객체와 같은 인터페이스를 구현한다"**는 점입니다. 그래서 클라이언트는 자신이 프록시와 대화하는지, 실제 객체와 대화하는지 모릅니다.

공통 인터페이스

Java
public interface Image {
    void display();
}

2. 종류 1: 가상 프록시 (Virtual Proxy) - "지연 로딩"

가장 많이 쓰이는 방식입니다. 실제 객체 생성 비용이 너무 비쌀 때, 진짜 필요할 때까지 생성을 미루는(Lazy Loading) 용도입니다.

상황

고화질 이미지(100MB)를 로딩해야 하는데, 사용자가 스크롤을 내리기 전까지는 굳이 메모리에 올릴 필요가 없을 때.

코드 예시

Java
public class ProxyImage implements Image {
    private RealImage realImage; // 실제 객체 (처음엔 null)
    private String fileName;

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

    @Override
    public void display() {
        // 실제 요청이 들어왔을 때, 그때서야 진짜 객체를 생성 (Lazy Loading)
        if (realImage == null) {
            realImage = new RealImage(fileName);
        }
        realImage.display(); // 작업 위임
    }
}

JPA의 지연 로딩: Spring Data JPA에서 FetchType.LAZY를 쓰면, 실제 엔티티 대신 하이버네이트가 만든 프록시 객체가 조회되는 것이 정확히 이 원리입니다.


3. 종류 2: 보호 프록시 (Protection Proxy) - "보안 및 검증"

클라이언트가 실제 객체에 접근할 권한이 있는지 검사하는 용도입니다.

상황

인사 정보 데이터는 '관리자(ADMIN)'만 수정할 수 있고, 일반 직원은 조회만 가능하게 하고 싶을 때.

코드 예시

Java
public class EmployeeProxy implements EmployeeService {
    private RealEmployeeService realService;
    private String userRole;

    public EmployeeProxy(String userRole) {
        this.userRole = userRole;
        this.realService = new RealEmployeeService();
    }

    @Override
    public void modifyInfo() {
        if (!"ADMIN".equals(userRole)) {
            throw new SecurityException("권한이 없습니다.");
        }
        realService.modifyInfo(); // 권한 있는 경우에만 통과
    }
}

4. 종류 3: 동적 프록시 (Dynamic Proxy) - "프레임워크의 마법"

위의 1, 2번 방식은 단점이 있습니다. 프록시 클래스(ProxyImage, EmployeeProxy)를 개발자가 일일이 직접 만들어야 한다는 점입니다. 만약 인터페이스의 메서드가 100개라면? 100개를 다 구현해야 합니다.

이를 해결하기 위해, 런타임(실행 중)에 프록시 클래스를 자동으로 만들어주는 방식이 동적 프록시입니다. Spring AOP의 핵심 기술입니다.

JDK Dynamic Proxy 예시 (Java 기본 제공)

Java
// InvocationHandler: 프록시가 할 일을 정의
public class LogHandler implements InvocationHandler {
    private Object target; // 실제 객체

    public LogHandler(Object target) { this.target = target; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("로그: " + method.getName() + " 실행 시작"); // 부가 기능
        
        Object result = method.invoke(target, args); // 실제 객체 실행
        
        System.out.println("로그: " + method.getName() + " 실행 종료");
        return result;
    }
}

// 사용 (가짜 객체 자동 생성)
Image proxyInstance = (Image) Proxy.newProxyInstance(
    Image.class.getClassLoader(),
    new Class[] { Image.class },
    new LogHandler(new RealImage("test.jpg"))
);
proxyInstance.display(); 

Spring의 @Transactional: 메서드에 이 어노테이션을 붙이면, 스프링은 해당 클래스의 동적 프록시를 몰래 만들어서 앞뒤로 Transaction.begin()과 commit() 코드를 심어넣습니다.


요약: 어떤 프록시를 언제 만나는가?

종류 목적 대표적인 예시
Virtual Proxy 메모리 절약 (지연 로딩) JPA FetchType.LAZY, 이미지 미리보기
Protection Proxy 접근 제어 (보안) 자바의 보안 관리자, 인가(Authorization) 필터
Remote Proxy 원격 객체 제어 RMI, gRPC 스텁 (로컬 객체처럼 쓰지만 실제론 서버 호출)
Dynamic Proxy 프록시 자동 생성 Spring AOP, @Transactional, Mockito(Mock 객체)

결론

"이거 프록시 패턴 써야겠는데?"라고 직접 코드로 Proxy 클래스를 만드는 일은 실무에서 드뭅니다. (보통 가상 프록시나 보호 프록시 정도만 직접 구현합니다.)

하지만 여러분이 사용하는 Spring 프레임워크와 JPA, Mockito 라이브러리가 내부적으로 이 패턴(특히 동적 프록시)으로 도배되어 있다는 사실을 이해하는 것은 디버깅과 성능 최적화에 매우 중요합니다.