오봉이와 함께하는 개발 블로그

스프링 핵심 원리 - 고급편 > 빈 후처리기 적용, 정리 본문

BE/Spring

스프링 핵심 원리 - 고급편 > 빈 후처리기 적용, 정리

오봉봉이 2024. 10. 7. 23:02
728x90

스프링 핵심 원리 - 고급편 > 빈 후처리기 적용

빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록하자.
이렇게 하면 수동은 물론, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.
더 나아가서 설정 파일에 있는 수 많은 프록시 생성 코드도 한번에 제거할 수 있다.

image
@Slf4j
public class PackageLogTracePostProcessor implements BeanPostProcessor {
  private final String basePackage;
  private final Advisor advisor;

  public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
    this.basePackage = basePackage;
    this.advisor = advisor;
  }

  @Override
  public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    log.info("param Bean Name = {}, 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과 관련된 부분에만 적용
    • 다른 패키지의 객체는 원본 그대로 사용
  • 프록시 적용 대상의 반환 값을 보면 원본 객체 대신 프록시 객체를 반환
    • 따라서 스프링 컨테이너에 원복 객체 대신 프록시 객체가 스프링 빈으로 등록
    • 원본 객체는 스프링 빈으로 등록되지 않음
@Slf4j
@Configuration
@Import({AppConfigV1.class, AppConfigV2.class})
public class BeanPostProcessorConfig {

  @Bean
  public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
    return new PackageLogTracePostProcessor("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);
    //advisor = pointcut + advice
    return new DefaultPointcutAdvisor(pointcut, advice);
  }
}
  • @Import({AppV1Config.class, AppV2Config.class})
    • V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되지만, V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작
    • ProxyApplication에서 등록해도 되지만 편의상 여기에 등록
  • @Bean logTraceProxyPostProcessor()
    • 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록
    • 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작
    • 여기에 프록시를 적용할 패키지 정보(hello.proxy.app)와 어드바이저(getAdvisor(logTrace))를 넘겨준다.
  • 이제 프록시를 생성하는 코드가 설정 파일에는 필요 없다.
    • 순수한 빈 등록만 고민하면 된다.
    • 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
@Import(BeanPostProcessorConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(ProxyApplication.class, args);
    }

    @Bean
    public LogTrace logTrace() {
        return new ThreadLocalLogTrace();
    }

}

설정을 등록하고 실행하면 로그가 쭉~ 뜰텐데
중요한 부분만 남기고 하나씩 살펴보자.
실행 로그에 create proxy를 검색으로 검색하면 V1 ~ V3까지 프록시가 생성된 로그를 확인할 수 있다.

  • V1: 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
  • V2: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
  • V3: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.

컴포넌트 스캔에도 적용
여기서 중요한 포인트는 빈 후처리기 덕분에 v1, v2와 같이 수동으로 등록한 빈 뿐만 아니라 컴포넌트 스캔을 통해 등록한 v3 빈들도 프록시를 적용할 수 있다는 점이다.

실행

각 링크를 들어가서 로그를 보면 다 적용된 것을 알 수 있다.

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

스프링 핵심 원리 - 고급편 > 빈 후처리기 정리

문제 1 - 너무 많은 설정

프록시를 직접 스프링 빈으로 등록하는 ProxyFactoryConfigV1, ProxyFactoryConfigV2 와 같은 설정 파일은 프록시 관련 설정이 지나치게 많다는 문제가 있다.
예를 들어 스프링 빈이 100개가 있다면 100개의 설정 코드가 들어가야 한다.... 난 자신이 없다.
스프링 빈을 편하게 등록하려고 컴포넌트 스캔을 사용하는데 설정 지옥에 빠지면 곤란하다.

문제 2 - 컴포넌트 스캔

애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능했다.
왜냐하면 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.
좀 더 풀어서 설명하자면, 지금까지 학습한 방식으로 프록시를 적용하려면, 원본 객체를 스프링 컨테이너에 빈으로 등록 하는 것이 아니라 ProxyFactoryConfigV1에서 한 것 처럼, 프록시를 원본 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다.
그런데 컴포넌트 스캔은 원본 객체를 스프링 빈으로 자동으로 등록하기 때문에 프록시 적용이 불가능하다.

문제 해결

빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다.
그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다.
덕분에 애플리케이션에 수 많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다.
그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용된다.

스프링 빈 후처리기

스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.

중요

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

결과적으로 포인트컷은 다음 두 곳에 사용된다.

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

출처: 김영한 지식공유자의 스프링 핵심 원리 고급편

728x90
Comments