프록시 패턴 이해하기: Java에서 정적 및 동적 프록시의 이유와 방법

발행: (2025년 12월 29일 오후 07:49 GMT+9)
13 min read
원문: Dev.to

Source: Dev.to

Introduction

이전 글에서는 Spring의 @Transactional 어노테이션에 대해 이야기하고, Spring AOP와 그 뒤에서 조용히 작동하는 동적 프록시 덕분에 어떻게 마법을 부리는지 살펴보았습니다.

그때 문득 생각이 들었습니다: 여기서 멈출 필요가 있을까? 프록시는 Spring에서 언제든지 사용되니, 그 작동 방식을 깊이 파고들어 보는 건 어떨까요?

그래서 오늘은 동적 프록시의 힘을 풀어내는 일련의 글을 시작하려 합니다! 동적 프록시는 깔끔한 코드를 작성하기 위한 비밀 무기와 같습니다. 반복되는 보일러플레이트 코드를 한 번만 작성하고, 간단한 어노테이션만 붙이면 필요한 곳 어디든 기능을 뿌릴 수 있습니다. 마치 슈퍼 파워를 만들고 이를 코드베이스 전체에 배포하는 것과 같죠.

우리는 먼저 Gang of Four의 유명한 책 *Design Patterns*에 나오는 고전적인 Proxy 패턴부터 살펴볼 것입니다. 이 패턴과 Spring 같은 프레임워크가 매일 사용하는 동적 프록시 사이의 연관성을 연결해 보겠습니다. 모두가 같은 이해를 갖도록, 먼저 간단한 정적 프록시를 직접 만들어 보겠습니다.

그리고 학습의 최종 목표인 캡스톤 프로젝트로, Spring의 @Transactional과 동일한 기능을 흉내 내는 @MyTransactional 어노테이션을 직접 구현해 볼 것입니다.

프록시에 전혀 익숙하지 않든, ByteBuddy 같은 고급 도구를 활용하고 싶든, 편안히 자리를 잡고 함께 해 주세요! 이 시리즈가 여러분에게 친절하고 실용적인 가이드가 되길 바라며, 마지막엔 동적 프록시에 대해 한층 더 깊이 이해하게 될 것입니다.

Source:

프록시 패턴 시작하기

프록시는 클라이언트가 메서드를 호출하는 순간과 실제 객체(Real Subject)가 그 메서드를 실행하는 순간 사이에 제어 레이어를 추가하고 싶을 때 사용합니다. 핵심은 프록시 패턴이 다른 객체를 대표하는 객체를 제공한다는 점입니다.

조금 추상적으로 들릴 수 있지만, 예시를 통해 차근차근 살펴보겠습니다.

  1. Client가 서비스를 사용하고 싶어합니다. 여기서는 이 서비스를 Subject라고 부릅니다.
  2. Subject는 인터페이스이며, 실제 작업은 그 인터페이스를 구현한 Real Subject 클래스가 수행합니다.
  3. Proxy가 Client와 Real Subject 사이에 끼어 있습니다. Client가 Subject의 메서드를 호출하면, Proxy가 Real Subject에 도달하기 전에 그 호출을 가로챕니다. 이를 통해 Proxy는 요청을 전달하기 혹은 결과를 받은 에 추가 작업을 수행할 수 있습니다.

프록시가 할 수 있는 일

Use‑case (사용 사례)Example (예시)
Access Control (접근 제어, 보안 담당)“잠깐, 이 호출을 할 권한이 있나요?”
Lazy Initialization (지연 초기화, 게으름)“필요할 때까지 무거운 객체(예: 대용량 파일이나 DB 연결)를 만들지 않을게요.”
Remote Invocation (원격 호출, 메신저 역할)“실제 객체가 다른 머신에 있나요? 네트워크 통신을 제가 대신 처리합니다.”
Cross‑Cutting Concerns (공통 관심사 처리, 번거로운 작업)“로깅, 캐싱, 트랜잭션 시작 등을 자동으로 처리해서 메인 객체는 깔끔하게 유지됩니다.”

이 모든 것이 아름다운 이유는? Real Subject는 비즈니스 로직에만 집중할 수 있고, 프록시는 반복적이지만 중요한 작업을 대신 처리합니다. 마치 모든 준비와 정리를 담당해 주는 전담 비서가 있는 것과 같습니다!

Source:

정적 프록시 만들기

자, 이제 프록시가 무엇인지 배웠으니, 자바로 직접 만들어 봅시다!

1️⃣ Subject 정의 (계약)

interface Subject {
    void execute();
}

2️⃣ RealSubject 구현 (실제 작업)

public class RealSubject implements Subject {

    @Override
    public void execute() {
        System.out.println("Performing an expensive operation.");
        // ... an operation
    }
}

3️⃣ Proxy 생성 (중개자)

public class SubjectProxy implements Subject {

    private final RealSubject realSubject = new RealSubject();

    @Override
    public void execute() {
        System.out.println("Proxy intercepting RealSubject's operation.");
        // logging the method call
        // ... any extra work
        realSubject.execute();   // delegate to the real subject
    }
}

4️⃣ Client 코드에서 프록시 사용

public class Client {

    private final Subject subject = new SubjectProxy();

    public void call() {
        subject.execute();
    }
}

보시다시피, 프록시는 실제 메서드가 호출되기 이나 에 자체 기능을 자연스럽게 추가할 수 있습니다. 몇 줄만으로 실제 객체를 건드리지 않고도 관리, 보안, 모니터링 등을 수행할 수 있는 도우미를 만들었습니다!

수동으로 수행할 때의 문제

Our SubjectProxy works great for a simple example, but imagine you have dozens of services, each with dozens of methods. Writing a static proxy for every interface quickly becomes:

  • Tedious – a lot of boilerplate code. → 지루함 – 많은 보일러플레이트 코드.
  • Error‑prone – easy to forget to delegate a method or to keep the proxy in sync with the interface. → 오류 발생 가능 – 메서드 위임을 잊거나 프록시와 인터페이스를 동기화하는 것을 놓치기 쉽다.
  • Hard to maintain – any change to the interface forces you to update every proxy. → 유지 보수 어려움 – 인터페이스가 변경될 때마다 모든 프록시를 업데이트해야 한다.

That’s why frameworks like Spring generate dynamic proxies at runtime. They let you write the cross‑cutting concern once (e.g., logging, transaction management) and automatically apply it to any bean that matches a pointcut, without manually writing a proxy class for each interface.
→ 그래서 Spring 같은 프레임워크는 런타임에 동적 프록시를 생성한다. 교차 관심사(예: 로깅, 트랜잭션 관리)를 한 번만 작성하면 포인트컷에 매칭되는 모든 빈에 자동으로 적용되며, 각 인터페이스마다 프록시 클래스를 수동으로 작성할 필요가 없다.

The N+1 Class Problem

Imagine you’re working on a massive enterprise application with hundreds of services. If you wanted to add logging or transaction management to every single one of them using a static approach, you’d have to write a separate proxy class for every service interface.

That’s a lot of boilerplate! It’s tedious, error‑prone, and—let’s be honest—not very “engineer‑y.” This is what we call the N+1 Class Problem: for every business class you write, you’re forced to write a corresponding proxy class.

There has to be a better way, right?
→ 수백 개의 서비스가 있는 대규모 엔터프라이즈 애플리케이션을 작업하고 있다고 상상해 보라. 정적 방식으로 각각에 로깅이나 트랜잭션 관리를 추가하려면 모든 서비스 인터페이스마다 별도의 프록시 클래스를 작성해야 한다.

그것은 엄청난 보일러플레이트다! 지루하고 오류가 발생하기 쉬우며—솔직히 말해서—그다지 “엔지니어답게” 보이지 않는다. 이것을 N+1 클래스 문제라고 부른다: 비즈니스 클래스를 하나 만들 때마다 대응되는 프록시 클래스를 작성해야 한다.

더 나은 방법이 있겠지, 그렇지?

동적 프록시 소개: 자동 중개자

정적 프록시가 만나는 사람마다 맞춤 계약서를 손수 작성하는 것이라면, 동적 프록시는 필요할 때마다 스스로 작성되는 스마트 템플릿과 같습니다.

핵심 아이디어는 간단합니다: SubjectProxy.java 같은 클래스를 직접 작성하는 대신, 실행 시간에 Java Virtual Machine (JVM)에 다음과 같이 지시합니다.

“이 인터페이스와 같은 객체가 필요해, 그런데 누군가 메서드를 호출하면 내가 만든 단일 Handler 클래스로 그 호출을 전달해줘.”

간단한 예시

// 모든 메서드 호출을 모든 인터페이스에 대해 처리하는 단일 "Handler"
InvocationHandler handler = (proxy, method, args) -> {
    System.out.println("Dynamic Proxy intercepting: " + method.getName());
    return method.invoke(realSubject, args);
};

// 마법: 실행 중에 프록시 인스턴스 생성
Subject dynamicProxy = (Subject) Proxy.newProxyInstance(
    Subject.class.getClassLoader(),
    new Class[] { Subject.class },
    handler
);

dynamicProxy.execute(); // 이 호출은 우리의 핸들러에 의해 가로채집니다!

Note: 이 코드가 낯설게 느껴져도 걱정하지 마세요. 시리즈 후반부에서 JDK 동적 프록시에 대해 더 자세히 살펴볼 예정입니다. 핵심은 SubjectProxy 라는 클래스를 직접 작성하지 않았고, 프로그램이 실행되는 동안 자동으로 생성했다는 점입니다. 만약 100개의 서로 다른 인터페이스가 있다 하더라도, 같은 로직이 모두를 처리합니다—더 이상 N+1 문제는 없습니다.

요약 및 다음 단계

우리는 고전적인 디자인 패턴에서 정적 프록시를 이용한 “수동 작업” 현실로 옮겨갔으며, Java가 이러한 중개자를 동적으로 생성해 보일러플레이트 코드에 빠지는 일을 방지할 수 있다는 점을 살펴보았습니다.

하지만 이것이 게으른 개발자를 위한 깔끔한 트릭일까요? 전혀 그렇지 않습니다.

다음 글에서는 “Hello World” 예제를 벗어나 실제 적용 사례를 살펴볼 예정입니다. Java 생태계의 거대 프레임워크인 Spring, Hibernate, Mockito가 동적 프록시를 활용해 우리가 매일 사용하는 기능들을 어떻게 구현하는지 깊이 파고들겠습니다. 구체적으로 다음을 살펴볼 것입니다:

  • @Transactional 애노테이션이 프록시를 통해 내부적으로 어떻게 동작하는지.
  • Hibernate가 데이터의 지연 로딩을 어떻게 관리하는지.
  • Mockito가 메서드 호출을 시뮬레이션하고 모의 데이터를 반환하는 방식.

Part 2를 기대해 주세요—흥미진진한 내용이 기다리고 있습니다!

Back to Blog

관련 글

더 보기 »

Java 소개

인간이 작성한 Java 코드는 어떻게 기계에서 실행될까요? 프로그래밍 언어를 기계어로 변환하기 위해 JDK → JRE → JVM → JIT 라는 번역 시스템을 사용합니다.