스프링 핵심 원리 - 고급편 - 섹션6. 스프링이 지원하는 프록시
CS/김영한 스프링 강의

스프링 핵심 원리 - 고급편 - 섹션6. 스프링이 지원하는 프록시

전 섹션에서의 문제는 인터페이스가 있을때랑 없을때랑 적용 방법이 달랐다.

이런걸 추상화해서 통일해준게 스프링. 프록시 팩토리를 만들어줬다.

 

 

예시코드를 작성해보자.

 

이 MethodInterceptor의 맨 상위 부모가 Advice다. 이미 invocation 안에 어떤 클래스를 타켓으로 하는지 생성할 때 저장해놓았기 때문에 그냥 proceed()만 하면 된다.

 

 

감싸지길 원하는 실제 비즈니스 클래스를 타켓으로 하고 프록시 팩토리 안에 넣어준 뒤 프록시 받아오면 된다. 프록시 팩토리로 만들어진 프록시 클래스인지와 이게 동적으로 만들었는지 정적으로 만들어졌는지 조차도 알 수 있다.

 

구체 클래스로도 만들어보자. 그리고 인터페이스가 있어도 구체클래스처럼 CGLIB를 만들도록 설정할 수도 있다.

 

 

 

참고로 스프링 부트에선 AOP를 적용할 때 기본값이 proxyTargetClass=true라서 CGLIB를 사용해서 생성하는게 기본값이다. 이런 이유는 나중에 설명한다.

 

 

이것도 덩치가 슬슬 커지다보니 개념을 나눠서 생각하게 된다. 그게 포인트컷, 어드바이스, 어드바이저 3개로 나누었다.

 

 

포인트컷은 언제 사용할지고, 어드바이스는 추가기능. 이 두개를 합친걸 어드바이저. 핸들러 작성하면서 봤던건데, 필터로 어느거에 부가기능을 사용할지 거르고 적용했다. 여기서 포인트컷이 필터고 밑에 붙여주는게 어드바이스라고 보면 된다. 그리고 이런 경우가 많아서 개념적으로 역할과 책임을 나눈 것.

 

어드바이저를 사용하는 예시 테스트 코드를 만들자.

addAdvisor로 어드바이저를 추가해서 등록한다. 사실 원래는 프록시 팩토리에 어드바이저를 추가했어야 하는데 아깐 그냥 addAdvice로 어드바이스만 추가했다. 그래도 된 이유는 만약 addAdvice로 추가한 경우 pointcut은 무조건 True인 것으로 자동으로 같이 들어가서 추가된 것. 즉 그냥 편의를 위해 만들어준 거다.

 

이제 항상 참이도록 해봤으니, 직접 포인트컷을 만들어보자. 실제로 만들 일은 없으나 학습을 위해..

물론 포인트컷을 만들지 않고 전처럼 어드바이스하는 메소드 안에 if문으로 필터를 만들어도 된다. 근데 그럼 코드 안에 굳어지고 재사용이 힘드니까. 그리고 그럴거면 포인트컷 왜쓰는데

    @Test
    @DisplayName("직접 만든 포인트컷")
    void advisorTest2() {
        ServiceInterface target = new ServiceImpl();
        ProxyFactory proxyFactory = new ProxyFactory(target);
        DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(
                new MyPointcut(),
                new TimeAdvice()
        );
        proxyFactory.addAdvisor(advisor);
        ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();

        proxy.save();
        proxy.find();
    }

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

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

    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, targetClass);
            log.info("포인트컷 결과 result={}", result);
            return result;
        }

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

        @Override
        public boolean matches(Method method, Class<?> targetClass, Object... args) {
            return true;
        }
    }

 

포인트컷 조건을 통과할 때만 어드바이스 함수가 실행되고 아닐 땐 안 실행된걸 알 수 있다.

 

 

스프링에서 제공하는걸 쓰자. 사실 이것만 쓰게된다.

 

스프링에서 제공해주는 함수들이 있는데, 위 함수들이 대표적인거고, 특히 이 중에서도 AspectJExpressionPointcut를 제일 많이 쓴다고 한다. 이 함수 사용법은 나중에 알아보고 지금은 포인트컷이 뭔지에 집중하자.

 

여러 어드바이저를 함께 쓸 때

 

적용하고 싶은 조건과 기능만큼 프록시를 새로 만들어 마치 체인처럼 이어붙인다.

이게 틀린건 아니지만, 하나의 프록시가 여러개의 어드바이저도 받을 수 있다. 당연히 성능 및 가독성 면에서도 이게 더 이득이다.

 

 

이런 이점때문에 스프링에서 프록시를 쓸 때마다 새로운 프록시가 만들어지는게 아니라 프록시는 하나인데 여러 어드바이저가 생성되어 출력이 나오는 것. 헷갈리기 쉽다.

 

 

이제 실제로 적용해보자. 설정만 빡세다.

@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {

    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderController);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
        return proxy;
    }

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        ProxyFactory factory = new ProxyFactory(orderService);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
        log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
        return proxy;
    }

    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();

        ProxyFactory factory = new ProxyFactory(orderRepository);
        factory.addAdvisor(getAdvisor(logTrace));
        OrderRepositoryV1 proxy = (OrderRepositoryV1) 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);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

}

 

 

 

 

인터페이스가 없는 v2에도 적용해보자.

@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);
        return new DefaultPointcutAdvisor(pointcut, advice);
    }

}

 

 

쌩 클래스기 때문에 CGLIB를 사용한걸 볼 수 있다.

 

 

아직까지 남아있는 치명적인 2가지 단점이 있는데, 하나는 스프링에 등록하는 설정 코드를 보면 알겠지만 사실 안에 중복되는 내용도 많고, 이렇게 등록하는 방식때문에 v3처럼 컴포넌트 스캔을 통한 방식에선 사용할 수 없다는 것.

 

그래서 이 두가지 문제들을 처리할 빈 후처리기를 다음 세션에서 배운다.