김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술

2025. 2. 21. 23:14·김영한의 스프링 핵심 원리 - 고급편

Question

  • JDK 동적 프록시가 동적으로 프록시를 만드는 것은 어떠한 문제를 해결할 수 있는가?
  • 동적 프록시의 동작 순서는?
  • JDK 동적 프록시의 한계는?
  • CGLIB이란?
  • CGLIB의 동작 순서는?
  • CGLIB의 제약은?
  • 인터페이스 유무에 따라 JDK 동적 프록시와 CGLIB을 사용하는 단점을 보완하는 방법은?

 

 

JDK 동적 프록시

  • 기존 단순 프록시를 적용할 때는 적용 대상만큼 프록시 클래스를 만들어야 했음
  • 적용 대상이 100개면 프록시를 100개 만들어야 함
  • 코드는 거의 같은데 대상만 달라도 프록시를 새로 만들어야 하는 문제
  • 동적 프록시는 개발자가 직접 프록시 클래스를 만들지 않아도 런타임에 자동으로 만들어 주는 기술

 

JDK 동적 프록시 구현 방법

public interface AInterface {
    void call();
}

public class AImpl implements AInterface {
    @Override
    public void call() {
        System.out.println("AImpl 호출");
    }
}

public interface BInterface {
    void call();
}

public class BImpl implements BInterface {
    @Override
    public void call() {
        System.out.println("BImpl 호출");
    }
}
  • JDK 동적 프록시를 구현하려면 단순 구현체 클래스면 안되고 인터페이스가 있어야만 함
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
    private final Object target;
    
    public TimeInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();
        
        // 실제 메서드 호출
        Object result = method.invoke(target, args);
        
        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;
        
        log.info("TimeProxy 종료 resultTime={}ms", resultTime);
        return result;
    }
}
  • invoke 함수에 원하는 코드를 구현
  • method.invoke 부분이 실제 메서드 호출 부분이며 안에 target과 인자가 들어감
@Slf4j
public class JdkDynamicProxyTest {

    @Test
    void dynamicA() {
        AInterface target = new AImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);

        AInterface proxy = (AInterface) Proxy.newProxyInstance(
                AInterface.class.getClassLoader(),
                new Class[]{AInterface.class},
                handler);

        proxy.call();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }

    @Test
    void dynamicB() {
        BInterface target = new BImpl();
        TimeInvocationHandler handler = new TimeInvocationHandler(target);

        BInterface proxy = (BInterface) Proxy.newProxyInstance(
                BInterface.class.getClassLoader(),
                new Class[]{BInterface.class},
                handler);

        proxy.call();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }
}
  • AInterface와 BInterface는 서로 다른 클래스인데 같은 프록시(TimeInvocationHandler)를 사용
  • Proxy를 생성을 위한 인자만 직접 넣어주면 됨

 

JDK 동적 프록시 실행 순서

  • 클라이언트는 JDK의 동적 프록시의 call() 실행
  • JDK 동적 프록시는 InvocationHandler.invoke()를 호출하고 TimeInvocationHandler가 구현체이므로 TimeInvocationHandler.invoke()가 호출 됨
  • TimeInvocationHandler가 내부 로직을 수행하고, method.invoke(target, args)를 호출해서 target인 실제 객체(AImpl)을 호출
  • AImpl 인스턴스의 call()이 실행 됨
  • AImpl 인스턴스의 call() 실행이 끝나면 TimeInvocationHandler로 응답이 돌아오고 시간 로그를 출력하고 결과를 반환

 

JDK 동적 프록시 적용 후 변경 점

  • InvocationHandler가 Interface의 구현체 Proxy를 직접 만들어 줌
  • 기존에는 각각 클래스를 만들어 줘야 했는데 InvocationHandler 하위에 하나만 만들어 주면 알아서 프록시 클래스를 생성해줌

 

JDK 동적 프록시를 로그트레이스 예제에 적용

@Slf4j
public class LogTraceBasicHandler implements InvocationHandler {
    
    private final Object target;
    private final LogTrace logTrace;
    
    public LogTraceBasicHandler(Object target, LogTrace logTrace) {
        this.target = target;
        this.logTrace = logTrace;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TraceStatus status = null;
        try {
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message);
            
            // 실제 메서드 호출
            Object result = method.invoke(target, args);
            
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            if (status != null) {
                logTrace.exception(status, e);
            }
            throw e;
        }
    }
}


@Configuration
public class DynamicProxyBasicConfig {
    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(
                OrderControllerV1.class.getClassLoader(),
                new Class[]{OrderControllerV1.class},
                new LogTraceBasicHandler(orderController, logTrace)
        );
        return proxy;
    }

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(
                OrderServiceV1.class.getClassLoader(),
                new Class[]{OrderServiceV1.class},
                new LogTraceBasicHandler(orderService, logTrace)
        );
        return proxy;
    }
    ...
}
  • InvocationHandler를 구현하는 LogTraceBasicHandler에 LogTrace 코드를 넣어 구현
  • Config에서 빈을 리턴할 때 JDK 동적 프록시를 리턴

 

JDK 동적 프록시의 한계

  • JDK 동적 프록시는 인터페이스가 한계
  • 인터페이스가 없이 동적 프록시를 적용하려면 CGLIB이라는 바이트 코드 조작 특별 라이브러리 사용해야 함

 

CGLIB

  • CGLIB: Code Generator Library
  • 바이트 코드를 조작해 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
  • 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들 수 있음
  • 원래는 외부 라이브러리인데, 스프링이 내부 소스에 포함시켜서 별도 라이브러리 추가 하지 않아도 사용 가능
  • JDK 동적 프록시에서는 InvocationHandler를 사용했는데 CGLIB에서는 MethodInterceptor를 사용

 

CGLIB 사용 방법

@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
    private final Object target;

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

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        log.info("TimeProxy 실행");
        long startTime = System.currentTimeMillis();

        // 실제 메서드 호출
        Object result = proxy.invoke(target, args);

        long endTime = System.currentTimeMillis();
        long resultTime = endTime - startTime;

        log.info("TimeProxy 종료 resultTime={}ms", resultTime);
        return result;
    }
}
  • 사용 방법이 JDK 동적 프록시와 거의 똑같음

 

CGLIB의 동작 순서

  • CGLIB을 사용하면 실제 구현 클래스인 ConcreteService를 상속하는 가짜 동적 프록시 클래스를 생성
  • 따라서, Client는 ConcreteService를 상속하는 가짜 프록시 클래스를 호출
  • 가짜 프록시 클래스는 MethodInterceptor를 호출
  • MethodInterceptor는 우리가 원하는 코드를 호출한 이후에 실제 타겟을 호출

 

CGLIB의 제약

  • 인터페이스가 아닌 클래스 기반 상속을 사용하는 방법은 몇 가지 제약이 있음
  • 부모 클래스의 생성자를 체크해야 함 -> CGLIB은 자식 클래스를 동적으로 생성하기 때문에 기본 생성자 필요
  • 클래스에 final 키워드가 붙으면 상속이 불가능
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없음

 

JDK 동적 프록시와 CGLIB을 사용하는 것의 문제

  • 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고 인터페이스가 없는 경우에는 CGLIB을 적용해야 함
  • 그러면 같은 코드를 사용하는데도 두 번의 작업을 해야 하는 문제가 생김
  • 이러한 문제를 뒤에 프록시 팩토리해서 해결 가능

'김영한의 스프링 핵심 원리 - 고급편' 카테고리의 다른 글

김영한의 스프링 핵심 원리(고급편) - 빈 후처리기  (0) 2025.02.22
김영한의 스프링 핵심 원리(고급편) - 프록시 팩토리  (0) 2025.02.22
김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴  (0) 2025.02.21
김영한의 스프링 핵심 원리(고급편) - 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴  (0) 2025.02.20
김영한의 스프링 핵심 원리(고급편) - (Log Trace, ThreadLocal)  (0) 2025.02.18
'김영한의 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
  • 김영한의 스프링 핵심 원리(고급편) - 빈 후처리기
  • 김영한의 스프링 핵심 원리(고급편) - 프록시 팩토리
  • 김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴
  • 김영한의 스프링 핵심 원리(고급편) - 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴
5jyan5
5jyan5
  • 5jyan5
    jyan
    5jyan5
  • 전체
    오늘
    어제
    • 분류 전체보기 (242)
      • 김영한의 스프링 핵심 원리(기본편) (8)
      • 김영한의 스프링 핵심 원리 - 고급편 (11)
      • 김영한의 스프링 MVC 1편 (1)
      • 김영한의 스프링 DB 1편 (3)
      • 김영한의 스프링 MVC 2편 (3)
      • 김영한의 ORM 표준 JPA 프로그래밍(기본편) (9)
      • 김영한의 스프링 부트와 JPA 활용2 (2)
      • 김영한의 실전 자바 - 중급 1편 (1)
      • 김영한의 실전 자바 - 고급 1편 (9)
      • 김영한의 실전 자바 - 고급 2편 (9)
      • Readable Code: 읽기 좋은 코드를 작성.. (2)
      • 김영한의 실전 자바 - 고급 3편 (9)
      • CKA (118)
      • 개발 (37)
      • 경제 (4)
      • 리뷰 (1)
      • 정보 (2)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      @within
      requset scope
      버퍼
      jpq
      고급
      프록시
      WAS
      cglib
      reentarantlock
      양방향 맵핑
      @discriminatorvalue
      jdk 동적 프록시
      @discriminatorcolumn
      Thread
      @args
      단방향 맵핑
      락
      typequery
      김영한
      빈 후처리기
      프록시 팩토리
      스레드
      JPQL
      gesingleresult
      자바
      페치 조인
      조회 성능 최적화
      log trace
      hibernate5module
      Target
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.2
    5jyan5
    김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술
    상단으로

    티스토리툴바