자바 언어·플랫폼 용어 사전
JVMjava.lang.reflect.Proxy · InvocationHandler · CGLIB · $Proxy0

동적 프록시

인터페이스만 주면 그 구현체를 실행 중에 만들어 낸다. method.invoke 앞뒤에 코드를 넣는 것이 전부다.

인터페이스만 주면 그 구현체를 실행 중에 만들어 내는 JDK 표준 기능.

interface OrderService { void save(Order o); }

OrderService proxy = (OrderService) Proxy.newProxyInstance(
    OrderService.class.getClassLoader(),
    new Class<?>[]{ OrderService.class },
    (p, method, args) -> {
        long t0 = System.nanoTime();
        try {
            return method.invoke(realTarget, args);      // 진짜 대상 호출
        } finally {
            System.out.printf("%s %.2fms%n", method.getName(),
                              (System.nanoTime() - t0) / 1e6);
        }
    });

JVM 이 $Proxy0 같은 이름의 클래스를 그 자리에서 만들어 로딩한다. 호출자는 그냥 OrderService 를 쓰는 줄 안다.

method.invoke 앞뒤에 코드를 넣는 것, 그것이 동적 프록시가 하는 일의 전부다. 스프링의 @Transactional·@Cacheable, MyBatis 매퍼 인터페이스, RMI 스텁 — 전부 그 앞뒤에 무엇을 넣느냐만 다르다.

두 가지 방식

JDK 동적 프록시CGLIB · ByteBuddy
방식인터페이스를 구현한 클래스를 생성대상 클래스를 상속한 서브클래스를 생성
필요 조건인터페이스가 있어야 한다없어도 된다
못 하는 것클래스만 있으면 불가final 클래스·메서드, private 메서드
생성 클래스명$Proxy0Foo$$SpringCGLIB$$0

코틀린 클래스가 기본 final 이라 프록시를 못 만드는 것도 이 표의 오른쪽 칸 때문이다. all-open 플러그인이 필요한 이유가 여기 있다.

밖에서 들어오는 호출만 가로챈다

public void placeAll(List<Order> orders) {
    for (Order o : orders) save(o);   // this.save() — 프록시를 안 거친다
}

호출자가 프록시를 거쳐 placeAll() 에 들어온 순간 this 는 이미 진짜 객체다. 그 안에서 자기 메서드를 부르면 프록시를 다시 탈 이유가 없다.

이것은 버그가 아니라 원리적 한계다. 프록시는 객체 '바깥' 에 서 있으므로 객체 '안' 에서 일어나는 호출을 볼 방법이 없다. 부가 기능이 조용히 사라지는 이 현상은 설계로 푼다 — 대상 메서드를 다른 객체로 분리하거나, 자기 프록시를 주입받아 부른다.

네트워크 프록시와 무엇이 다른가

발상은 같다 — 진짜 대상 앞에 서서 요청을 가로채고, 호출자는 진짜인 줄 안다. 가로채는 대상이 다를 뿐이다.

  • 동적 프록시 — 메서드 호출을 가로챈다. 같은 JVM 안, 힙 위의 객체다
  • 리버스 프록시 — 네트워크 요청을 가로챈다. 별도 프로세스이고 다른 머신일 수도 있다

성능

프록시 한 겹은 메서드 호출 하나가 더 붙는 정도다. 다만 method.invoke 는 리플렉션 호출이라 직접 호출보다 비싸고, 프록시가 여러 겹 쌓이면 스택이 깊어져 스택트레이스가 읽기 어려워진다. 그래서 최근에는 빌드 시점에 코드를 생성해 프록시를 대신하는 방식도 늘고 있다.

면접 함정

  • "프록시는 스프링 기능" → JDK 표준(java.lang.reflect.Proxy)이다. 스프링은 그 위에 얹었다.
  • "자기 호출이 안 걸리는 건 스프링 버그" → 프록시의 원리적 한계다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — JVM 실행 흐름 — 내 코드가 도는 과정