5 분 소요

 인프런 스프링 핵심 원리 - 고급편을 학습하고 정리한 내용 입니다.

빈 후처리기 - 적용

빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보자.

이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.

더 나아가서 설정 파일에 있는 수 많은 프록시 생성 코드도 한번에 제거할 수 있다.

코드로 만들어 보자.

해당 위치에 만든다.

PackageLogTraceProxyPostProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Slf4j  
public class PackageLogTraceProxyPostProcessor implements BeanPostProcessor {  
  
    private final String basePackage;  
    private final Advisor advisor;  
  
    public PackageLogTraceProxyPostProcessor(String basePackage, Advisor advisor) {  
        this.basePackage = basePackage;  
        this.advisor = advisor;  
    }  
  
    @Override  
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {  
        log.info("param beanName={}, bean={}", beanName, bean.getClass());  
  
        // 프록시 적용 대상 여부 체크  
        // 프록시 적용 대상이 아니면 원본을 그대로 진행  
        String packageName = bean.getClass().getPackageName();  
        if (!packageName.startsWith(basePackage))  
            return bean;  
  
        //프록시 대상이면 프록시를 만들어서 반환  
        ProxyFactory proxyFactory = new ProxyFactory(bean);  
        proxyFactory.addAdvisor(advisor);  
  
        Object proxy = proxyFactory.getProxy();  
        log.info("create proxy: target={} proxy={}", bean.getClass(), proxy.getClass());  
        return proxy;  
  
    }  
}
  • PackageLogTraceProxyPostProcessor는 원본 객체를 프록시 객체로 변환하는 역할을 함. 이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 advisor가 필요하기 때문에 이 부분은 외부에서 주입받도록 했다.
  • 모든 스프링 빈들에 프록시를 적용할 필요는 없다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. hello.proxy.app과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.
  • 프록시 적용 대상의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환한다. 따라서 스프링 컨테이너원본 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 원본 객체는 스프링 빈으로 등록되지 않는다.

이제 이 후처리기를 등록해보자.

BeanPostProcessorConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j  
@Configuration  
@Import({AppV1Config.class, AppV2Config.class})  
public class BeanPostProcessorConfig {  
  
    @Bean  
    public PackageLogTraceProxyPostProcessor logTraceProxyPostProcessor(LogTrace logTrace) {  
        return new PackageLogTraceProxyPostProcessor("hello.proxy.app", getAdvisor(logTrace));  
    }  
  
    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);  
  
    }  
}
  • @Import({AppV1Config.class, AppV2Config.class}) : V3 는 컴포넌트 스캔으로 자동으로 스프링 빈에 등록되어 있지만 V1, V2는 수동으로 등록했기 때문에 따로 등록해줘야 한다.
    • ProxyApplication에서 등록해도 되지만 편의 상 여기서 등록
  • @Bean logTraceProxyPostProcessor() : 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다. 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다.
    • 여기에서 프록시를 적용할 패키지 정보(hello.proxy.app)와 어드바이저(getAdvisor(logTrace))를 넘겨준다.
  • 이제 프록시를 생성하는 코드가 설정 파일에는 필요 없다. 순수한 빈 등록만 고민하면 된다. 프록시를 생성하고 프록시를 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.

마지막으로 이 config를 등록하면 준비 끝이다.

ProxyApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//@Import(AppV1Config.class)  
//@Import({AppV1Config.class, AppV2Config.class})  
//@Import(InterfaceProxyConfig.class)  
//@Import(ConcreteProxyConfig.class)  
//@Import(DynamicProxyBasicConfig.class)  
//@Import(DynamicProxyFilterConfig.class)  
//@Import(ProxyFactoryConfigV1.class)  
//@Import(ProxyFactoryConfigV2.class)  
@Import(BeanPostProcessorConfig.class)  
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의  
public class ProxyApplication {  
  
    public static void main(String[] args) {  
       SpringApplication.run(ProxyApplication.class, args);  
    }  
  
    @Bean  
    public LogTrace logTrace() {  
       return new ThreadLocalLogTrace();  
    }  
}

실행 로그를 보면

엄청나게 많은 인스턴스들이 빈 후처리기를 거쳐서 스프링 빈으로 등록되는 걸 볼 수 있다.

우리가 hello.proxy.app에 제한을 걸어 놓은 이유가 저거다. 저거 다 프록시 객체로 바꾸면 동작 안 할 수도 있다.

hello.proxy.app에 등록한 bean들만 정리해서 보면

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#v1 애플리케이션 프록시 생성 - JDK 동적 프록시
create proxy: target=v1.OrderRepositoryV1Impl proxy=class com.sun.proxy.$Proxy50
create proxy: target=v1.OrderServiceV1Impl proxy=class com.sun.proxy.$Proxy51
create proxy: target=v1.OrderControllerV1Impl proxy=class com.sun.proxy.$Proxy52

#v2 애플리케이션 프록시 생성 - CGLIB
create proxy: target=v2.OrderRepositoryV2 proxy=v2.OrderRepositoryV2$
$EnhancerBySpringCGLIB$$x4
create proxy: target=v2.OrderServiceV2 proxy=v2.OrderServiceV2$
$EnhancerBySpringCGLIB$$x5
create proxy: target=v2.OrderControllerV2 proxy=v2.OrderControllerV2$
$EnhancerBySpringCGLIB$$x6

#v3 애플리케이션 프록시 생성 - CGLIB
create proxy: target=v3.OrderRepositoryV3 proxy=3.OrderRepositoryV3$
$EnhancerBySpringCGLIB$$x1
create proxy: target=v3.orderServiceV3 proxy=3.OrderServiceV3$
$EnhancerBySpringCGLIB$$x2
create proxy: target=v3.orderControllerV3 proxy=3.orderControllerV3$
$EnhancerBySpringCGLIB$$x3

인터페이스는 JDK 동적프록시, 구체 클래스는 CGLIB가 적용되는 것을 알 수 있다.

v1, v2, v3 모두 다 로그가 잘 남는다.

프록시 적용 대상 여부 체크

  • 애플리케이션을 실행해서 로그를 확인해보면 알겠지만, 우리가 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다.
    그래서 어떤 빈을 프록시로 만들 것인지 기준이 필요하다.
    여기서는 간단히 basePackage를 사용해서 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시로 만든다.
  • 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다. 따라서 모든 객체를 프록시로 만들 경우 오류가 발생한다.

빈 후처리기 - 정리

이전에 보았던 문제들이 빈 후처리기를 통해 어떻게 해결되었는지 정리해보자.

문제 1 - 너무 많은 설정

프록시를 직접 스프링 빈으로 등록하는 ProxyFactoryConfigV1과 같은 설정 파일은 프록시 관련 설정이 지나치게 많다는 문제가 있다.

예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 프록시 설정 코드가 들어가야 한다.

무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다.

스프링 빈을 편리하게 등록하려고 컴포넌트 스캔까지 사용하는데, 이렇게 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 했다.

문제 2 - 컴포넌트 스캔

애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능했다.

왜냐하면 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.

좀 더 풀어서 설명하자면, 지금까지 학습한 방식으로 프록시를 적용하려면, 원본 객체를 스프링 컨테이너에 빈으로 등록 하는 것이 아니라 ProxyFactoryConfigV1에서 한 것 처럼, 프록시를 원본 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다.

그런데 컴포넌트 스캔은 원본 객체를 스프링 빈으로 자동으로 등록하기 때문에 프록시 적용이 불가능하다.

문제 해결

빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있었다. 그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있었다.

덕분에 애플리케이션에 수 많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다. 그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용된다.

하지만 개발자의 욕심은 끝이 없다.
스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.

중요
프록시의 적용 대상 여부를 여기서는 간단히 패키지를 기준으로 설정했다. 그런데 잘 생각해보면 포인트컷을 사용하면 더 깔끔할 것 같다.
포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.
참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다.
다음에 학습하겠지만 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.

결과적으로 포인트컷은 다음 두 곳에 사용된다.
1. 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
2. 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)

댓글남기기