Spring

[SpringBoot] 빈 후처리기 (BeanPostProcessor) 정리

꾸준함. 2021. 12. 21. 00:15

개요

기존에 ProxyFactory에 대해 간단히 알아봤습니다. (https://jaimemin.tistory.com/2026)

이제 학습한 프록시를 컴포넌트 스캔한 빈에 적용하기 위해서는 실제 객체를 스프링 컨테이너에 빈으로 등록하지 않고 부가 기능이 적용된 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 합니다.

이를 위해서 등장한 개념이 빈 후처리기이며 이번 게시글에서는 빈 후처리기와 스프링에서 제공하는 빈 후처리기에 대해 간단히 알아보겠습니다.

 

1. 빈 후처리기

빈 후처리기는 스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶을 때 사용하는 기능입니다.

빈 후처리기는 객체를 조작할 수도 있고, 완전히 다른 객체로 교체하는 것도 가능합니다.

 

https://stackoverflow.com/questions/29743320/how-exactly-does-the-spring-beanpostprocessor-work

 

빈 후처리기를 통한 빈 등록 과정은 아래와 같습니다.

  • 스프링 빈 대상이 되는 객체를 생성
  • 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달
  • 전달된 스프링 빈 객체를 빈 후처리기가 조작 혹은 다른 객체로 교체
  • 전달된 빈을 그대로 반환하면 해당 빈이 등록되고 교체하면 다른 객체가 빈 저장소에 등록

 

1.1 빈 후처리기 예제

빈 후처리기를 구현하기 위해서는 스프링에서 제공하는 BeanPostProcessor 인터페이스를 구현해준 뒤 스프링 빈으로 등록해주면 됩니다.


postProcessBeforeInitialization 메서드는 객체 생성 이후에 @PostConstruct 어노테이션과 같이 초기화가 발생하기 전에 호출되는 포스트 프로세서이며 postProcessAfterInitialization 메서드는 객체 생성 이후에 @PostConstruct와 같이 초기화가 발생한 다음에 호출되는 포스트 프로세서입니다.

두 메서드 모두 default 키워드가 있기 때문에 필요한 메서드만 오버라이딩해주면 되며 이번 예제에서는 빈 객체를 등록한 뒤 교체를 하기 때문에 아래 예제에서는 postProcessAfterInitialization 메서드만 오버라이드 합니다.


 

예제의 동작 흐름은 간단합니다.

1. Before 객체를 등록하는데

2. BeanPostProcessor를 구현한 BeforeToAfterPostProcessor가 Before 객체인 것을 인지하고 After 객체로 교체합니다.

2.1 만약 Before 객체가 아니라면 교체하지 않고 그대로 등록합니다.

3. 최종적으로 "beanBefore"라는 스프링 빈 이름에 Before 객체 대신 After 객체가 등록된 것을 확인 가능한 예제입니다.

 

* 현재 예제에서는 빈 객체를 또 다른 빈 객체로 교체했지만 빈 객체를 프록시로 교체하는 것 또한 가능합니다.

* 따라서, 개요에서 설명했던 한계를 빈 후처리기를 통해 극복 가능합니다.

 

2. 빈 후처리기에 포인트 컷 적용

기존에 프록시 팩토리를 정리할 때 포인트 컷에 대해서도 알아봤고(https://jaimemin.tistory.com/2026) 포인트 컷이 클래스, 메서드 단위의 필터링 기능을 가지고 있기 때문에 프록시 적용 대상 여부를 정밀하게 설정할 수 있다는 것을 알 수 있었습니다.

 

따라서 결과적으로 포인트 컷은 아래의 두 곳에 사용이 됩니다.

1. 프록시 적용 대상 여부를 체크한 뒤 필요한 곳에만 프록시 적용 (BeanPostProcessor에서 추가된 내용)

2. 프록시의 어떤 메서드가 호출되었을 때 어드바이스 적용 여부 판단

 

3. 스프링이 제공하는 빈 후처리기

스프링에서 제공하는 빈 후처리기를 사용하기 위해서는 우선 아래의 라이브러리를 gradle 혹은 pom.xml에 추가해줘야 합니다.

implementation 'org.springframework.boot:spring-boot-starter-aop'

위 라이브러리를 추가해주면 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록해주고 그 과정에서 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동으로 등록이 됩니다.

해당 빈 후처리기는 자동으로 프록시를 생성해주며 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해줍니다.

따라서, 개발자는 Advisor 즉, Pointcut과 Advice를 구현하여 Pointcut으로 어떤 스프링 빈에 프록시를 적용해야할지 필터링을 하고 Advice로 부가기능을 적용하면 됩니다. (Advisor 구현 뒤 Configuration 내 Bean으로 등록)

 

https://www.fatalerrors.org/a/spring-source-code-6-enableaspectjautoproxy.html

 

자동 프록시 생성기의 작동 과정은 아래와 같습니다.

  • 스프링이 스프링 빈 대상이 되는 객체를 생성
  • 생성된 객체를 빈 저장소에 등록하기 전에 빈 후처리에 전달
  • 빈 후처리가 스프링 컨테이너 내에 있는 모든 Advisor 조회
  • 조회한 Advisor에 포함되어 있는 포인트 컷을 통해 해당 객체가 프록시를 적용할 대상인지 판단
    • 클래스 정보와 해당 객체의 모든 메서드명을 Pointcut에 하나하나 매칭 해본 뒤 조건이 하나라도 만족하면 프록시 적용 대상이 됨
  • 프록시 적용 대상에 대해 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록
    • 프록시 적용 대상이 아니라면 원본 객체를 스프링 빈으로 등록
  • 반환된 객체를 스프링 빈으로 등록

 

위 과정에서 포인트 컷이 중요 역할을 하므로 다시 복습하자면 Pointcut은 아래 2가지 케이스에 사용됩니다.

1. 생성 단계에서 프록시 적용 여부 판단

  • 자동 프록시 생성기가 Pointcut을 사용해 해당 빈의 프록시 생성여부 판단
  • 클래스명, 메서드명 조건을 모두 비교하고 한 가지라도 충족하면 프록시 생성 (or)

2. 사용 단계에서 어드바이스 적용 여부 판단

  • 프록시가 호출되었을 때 부가 기능인 어드바이스 적용 여부를 판단

 

* 프록시를 모든 곳에 생성하는 것은 자원 낭비이므로 조건에 맞게 Pointcut을 적절하게 구현 필요

 

3.1 필터링하고 싶은 메서드명이 스프링에서 제공하는 라이브러리 내 메서드명과 겹칠 경우 처리 방법

3번에서 다룬 내용을 제가 직접 적용하고 싶어 "request"로 시작하는 메서드명에 대해 필터링을 하여 빈 후처리기를 통해 프록시를 생성하도록 처리했습니다.

그런데 이렇게 하면 스프링 내 DispatcherServlet 또한 프록시가 생성되므로 아래와 같은 에러 문구와 함께 문제가 발생했습니다.

The bean 'dispatcherServlet' could not be injected because it is a JDK dynamic proxy

따라서, 개발자가 구현한 객체에 대해서만 프록시를 생성하기 위해서는 특정 패키지 내에 있는 객체에 대해서만 프록시를 생성하도록 설정을 해야 하며 이는 AspectJExpressionPointcut을 통해 적용 가능합니다.


AspectJExpressionPointcut을 적용하기 위해서는 AspectJ가 제공하는 포인트 컷 표현식을 사용해야 하는데 간단하게 알아보자면 아래와 같습니다.

  • *: 모든 반환 타입
  • <패키지명>..: 해당 패키지와 하위 패키지
  • *(..): * 모든 메서드 이름, (..) 모든 파라미터

 

참고

인프런 스프링 핵심 원리 - 고급편 (김영한 강사님)

반응형