김영한의 스프링 핵심 원리(고급편) - 프록시 팩토리

2025. 2. 22. 00:45·김영한의 스프링 핵심 원리 - 고급편

Question

  • 프록시 팩토리는 어떠한 문제를 해결하는가
  • 프록시 팩토리의 플로우는?
  • Advice는 어떻게 JDK 동적 프록시와 CGLIB을 동시에 처리가 가능한가
  • 어드바이스, 포인트컷, 어드바이저에 대해 설명하시오
  • 하나의 타겟에 여러 어드바이저를 적용하는 방법에 대해 설명하시오
  • 프록시 팩토리의 한계는?

 

프록시 팩토리

  • 이전 글에서 인터페이스가 있는 경우에는 JDK 동적 프록시, 그렇지 않은 경우에는 CGLIB을 적용했음.
  • 이는 같은 기능을 하지만 인터페이스 여부에 따라 다른 구현을 해야 하므로 번거롭고 중복 코드 문제가 발생
  • 프시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있으면 CGLIB을 사용하도록 제공

 

프록시 팩토리의 플로우

  • Client의 요청이 오면 프록시 팩토리는 인터페이스 여부에 따라 JDK 동적 프록시를 사용할지 CGLIB을 사용할지 결정
  • 그리고 그 결과를 클라이언트에 반환 함

 

Advice

  • JDK 동적 프록시는 Invocation을 사용하고 CGLIB은 MethodInterceptor를 사용하는데 이 차이를 프록시 팩토리는 어떻게 극복할까
  • 프록시 팩토리는 Client의 요청을 받으면 인터페이스 유무에 따라 jdk proxy, cglib proxy을 각각 구현한 Handler로 요청이 가게 함
  • 그러면 각 InvocationHandler, MethodInterceptor 모두 결국에는 Advice를 호출하게 만들어, 개발자는 Advice만 만들어 주면 됨
  • Advice란 프록시에 적용하는 "부가 기능" 로직을 의미
  • 이는 이전에 InvocationHandler, MethodInterceptor의 개념과 유사함

 

Advice 구현 방법

@Slf4j
public class TimeAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("TimeAdvice 실행");
        long startTime = System.currentTimeMillis();

        Object result = invocation.proceed();

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

        log.info("TimeAdvice 종료 resultTime={}ms", resultTime);
        return result;
    }
}
  • 구현해야 하는게 CGLIB에서 사용하는 MethodInterceptor와 이름이 같으니 주의
  • MethodInterceptor -> Intertceptor -> Advice 이런식으로 구조가 되어 있음
  • 기존 InvoactionHandler, MethodInterceptor와 다르게 target과 args가 없는데, 이는 파라미터에 있는 invocation에 이미 들어가 있고 이는 프록시 팩토리로 프록시를 생성해주는 앞 단계에서 이미 target 정보를 파라미터로 전달함
  • invocation에 모든 정보가 들어있기 때문에 여기서는 단순히 invocation.proceed()만 수행해주면 됨

 

프록시 팩토리로 프록시 생성 방법

@Slf4j
public class ProxyFactoryTest {

    @Test
    @DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
    void interfaceProxy() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        proxyFactory.addAdvice(new TimeAdvice());

        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

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

        proxy.save();

        assertThat(AopUtils.isAopProxy(proxy)).isTrue();
        assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
        assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
    }
}
  • new ProxyFactory(target)으로 프록시 팩토리를 생성할 때 target을 넣어 줌
  • 프록시 팩토리에 addAdvice로 앞서 만든 advice를 넣어 줌
  • 프록시 팩토리가 만들어지면 getProxy를 통해 프록시를 가져올 수 있음.
  • 프록시의 타겟이 Interface였기 때문에 자동으로 Cglib이 아닌 Jdk 동적 프록시가 만들어 졌기에 isJdkDynamicProxy는 true고 isCglibProxy는 false
  • 만약 Interface라도 CGLIB으로 만들고 싶으면 proxyFactory.setProxyTargetClass(true); 설정을 해주면 됨

 

포인트컷, 어드바이스, 어드바이저

  • 포인트컷( Pointcut )
    • 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직
    • 주로 클래스와 메서드 이름으로 필터링
    • 이름 그대로 어느 포인트에 적용할지 딱 컷해서 지정
  • 어드바이스( Advice )
    • 이전에 본 것 처럼 프록시가 호출하는 부가 기능
    • 단순하게 프록시 로직이라고 보면 됨
  • 어드바이저( Advisor )
    • 하나의 포인트컷과 하나의 어드바이스
    • 어드바이저 = 포인트컷1 + 어드바이스1
    • 어드바이저(조언자)는 어디(포인트컷)에 어떤 조언(어드바이스)를 할 것이냐
  • 이는 역할과 책임을 명확하게 분리하는 방법

 

 

어드바이저 사용 방법

    @Test
    void advisorTest1() {
        // 실제 대상 객체
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();  // 어드바이스가 적용됩니다.
        proxy.find();  // 어드바이스가 적용됩니다.
    }
  • 어드바이저는 PointCut + Advice이기 때문에 Advisor를 생성할때는 포인트컷과 어드바이스를 넣어 줘야 함
  • Pointcut.TRUE로 세팅하면 모두에 적용한다는 의미
  • 이전의 코드에서는 proxyFactory.addAdvice(new TimeAdvice()) 이런식으로 사용했었는데, 단순히 Advice만 넣으면 Pointcut.TRUE가 적용된 어드바이저가 사용되는 것

 

커스텀 포인트컷

@Slf4j
public static class MyPointcut implements Pointcut {
    @Override
    public ClassFilter getClassFilter() {
        return ClassFilter.TRUE;
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        return new MyMethodMatcher();
    }
}

@Slf4j
public static class MyMethodMatcher implements MethodMatcher {
    private String matchName = "save";

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        boolean result = method.getName().equals(matchName);
        log.info("포인트컷 호출 method={} targetClass={}", method.getName(), targetClass);
        log.info("포인트컷 결과 result={}", result);
        return result;
    }

    @Override
    public boolean isRuntime() {
        return false;
    }

    @Override
    public boolean matches(Method method, Class<?> targetClass, Object... args) {
        throw new UnsupportedOperationException();
    }
}
  • 메서드 이름이 save면 true가 나오도록 포인트컷을 사용
  • 실제 포인트컷을 직접 구현해서 사용할일은 거의 없음
  • 사용을 한다면 스프링이 제공하는 포인트컷을 사용하면 됨
  • 위와 같은 커스텀 포인트컷을 만들면 new DefaultPointcutAdvisor(new MyPointcut(), new TimeAdvice()) 와 같이 사용 가능

 

스프링이 제공하는 포인트컷

@Test
@DisplayName("스프링이 제공하는 포인트컷")
void advisorTest3() {
    ServiceImpl target = new ServiceImpl();
    ProxyFactory proxyFactory = new ProxyFactory(target);
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedNames("save");
    DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
    proxyFactory.addAdvisor(advisor);
    ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
    proxy.save();
    proxy.find();
}
  • NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut(); 으로 pointcut 생성
  • pointcut.setMappedNames("save"); 으로 매칭할 method name을 입력
  • DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice()) 으로 pointcut 을 Advisor 생성시 넣어 줌
  • 스프링이 제공하는 포인트 컷
    • NameMatchMethodPointcut: 메서드 이름을 기반으로 매칭합니다. 내부적으로 PatternMatchUtils를 사용합니다. 예) *xxx*와 같은 패턴을 허용합니다.
    • JdkRegexpMethodPointcut: JDK 정규 표현식을 기반으로 포인트컷을 매칭합니다.
    • TruePointcut: 항상 참을 반환합니다.
    • AnnotationMatchingPointcut: 애노테이션을 기반으로 매칭합니다.
    • AspectJExpressionPointcut: AspectJ 표현식을 기반으로 매칭합니다.
      • 사실상 실무에서는 AspectJ 표현식 기반을 거의 사용하며 이는 추후 AOP 부분에서 자세히 설명

 

 

하나의 타겟에 여러 어드바이저 적용

  • 기존에 사용하던 코드를 활용해서 Proxy를 2개 만들어서 target은 proxy1로 감싸고, proxy1은 proxy2로 감싸면 하나의 target에 두 개의 어드바이저를 적용 가능
  • 만약 10개의 어드바이저를 적용하려면 10개의 프록시를 생성해야하는 문제 발생

 

하나의 타겟에 여러 어드바이저 적용 개선 방법

@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest2() {
    //proxy -> advisor2 -> advisor1 -> target
    DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
    DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
    ServiceInterface target = new ServiceImpl();
    ProxyFactory proxyFactory1 = new ProxyFactory(target);
    proxyFactory1.addAdvisor(advisor2);
    proxyFactory1.addAdvisor(advisor1);
    ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
    //실행
    proxy.save();
}
  • 프록시 팩토리에 addAdvisor로 여러 어드바이저 추가 가능
  • 이 때 넣어준 순서대로 동작
  • 이 부분에서 중요한 점은 추후에 나올 AOP에서 AOP 적용 수 만큼 프록시가 생성된다는 착각을 방지하기 위해
  • 이 앞전의 예제처럼 여러 프록시를 만들어서 어드바이저를 적용하는 방법도 존재
  • 하지만, 스프링 AOP는 target 마다 하나의 프록시만 생성하는 방법을 사용

 

프록시 팩토리를 LogTrace에 적용

@Slf4j
public class LogTraceAdvice implements MethodInterceptor {
    private final LogTrace logTrace;

    public LogTraceAdvice(LogTrace logTrace) {
        this.logTrace = logTrace;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        TraceStatus status = null;

        try {
            Method method = invocation.getMethod();
            String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
            status = logTrace.begin(message);

            // 로직 호출
            Object result = invocation.proceed();
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}
  • Advice를 생성
@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
    @Bean
    public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
        OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
        OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
        return proxy;
    }

    @Bean
    public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
        OrderRepositoryV2 orderRepository = new OrderRepositoryV2();
        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
        return proxy;
    }

    private Advisor getAdvisor(LogTrace logTrace) {
        // pointcut
        NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
        pointcut.setMappedNames("request*", "order*", "save*");

        // advice
        LogTraceAdvice advice = new LogTraceAdvice(logTrace);

        // advisor = pointcut + advice
        return new DefaultPointcutAdvisor(pointcut, advice);
    }
}
  • Advice와 Pointcut를 적용한 Advisor로 프록시 팩토리를 만들어 프록시 적용
  • 이는 적용하고자 하는 타겟이 인터페이스든 단순 구현체든 상관이 없음

 

프록시 팩토리의 한계

  • 위 Configuration 코드를 보면 설정 파일이 너무 복잡함
  • 프록시 적용을 위한 스프링 빈이 100개면 100개 다 코드를 작성해 줘야 함
  • 이는 심지어 중복된 코드임
  • 요즘엔 컴포넌트 스캔을 사용하는데, 프록시 팩토리를 적용하라면 수동 등록을 해야 함
  • 이 문제를 뒤에서 나올 빈 후처리기가 개선할 수 있음

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

김영한의 스프링 핵심 원리(고급편) - @Aspect, AOP  (0) 2025.02.24
김영한의 스프링 핵심 원리(고급편) - 빈 후처리기  (0) 2025.02.22
김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술  (0) 2025.02.21
김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴  (0) 2025.02.21
김영한의 스프링 핵심 원리(고급편) - 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴  (0) 2025.02.20
'김영한의 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
  • 김영한의 스프링 핵심 원리(고급편) - @Aspect, AOP
  • 김영한의 스프링 핵심 원리(고급편) - 빈 후처리기
  • 김영한의 스프링 핵심 원리(고급편) - 동적 프록시 기술
  • 김영한의 스프링 핵심 원리(고급편) - 프록시 패턴, 데코레이터 패턴
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)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

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

    • 최근 글

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

    티스토리툴바