[ 김영한 스프링 핵심 원리 - 고급편 #7 ] 빈 후처리기 (1)

김수호·2023년 11월 21일
0
post-thumbnail

이번 섹션에서는 [빈 후처리기]에 대해서 알아보자.

👉 목차는 다음과 같다.

1) 빈 후처리기 - 소개
2) 빈 후처리기 - 예제 코드1
3) 빈 후처리기 - 예제 코드2
4) 빈 후처리기 - 적용
5) 빈 후처리기 - 정리

6) 스프링이 제공하는 빈 후처리기1
7) 스프링이 제공하는 빈 후처리기2
8) 하나의 프록시, 여러 Advisor 적용
9) 정리

이번 섹션은 1) ~ 5), 6) ~ 9) 로 나눠서 포스팅하고자 한다.

바로 하나씩 확인해보자.


1) 빈 후처리기 - 소개

@Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 그리고 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.

  • 참고)

 

빈 후처리기 - BeanPostProcessor

  • 스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.
  • 빈 포스트 프로세서( BeanPostProcessor )는 번역하면 빈 후처리기인데, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.

빈 후처리기 기능

  • 빈 후처리기의 기능은 막강하다. 객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.

빈 후처리기 과정

  • 참고)
    • 빈 등록 과정을 빈 후처리기와 함께 살펴보자.
      • 1) 생성: 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
      • 2) 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
      • 3) 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
      • 4) 등록: 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.

 

다른 객체로 바꿔치는 빈 후처리기

  • 참고)
    • ex) 1) 스프링 빈 대상이 되는 객체(A)를 생성한다. 2) 그러면 스프링은 생성된 객체를 빈 저장소에 등록하기 전에, (빈 후처리기가 있으면) 빈 후처리기에 전달(빈 이름, 빈 객체)한다. 3) 빈 후처리기에서는 전달된 A객체를 조작하거나 B객체로 바꿔치기해서 반환할 수 있다. 4) 그러면 B객체가 반환되어 빈 저장소에는 B객체가 등록된다.

2) 빈 후처리기 - 예제 코드1

일반적인 스프링 빈 등록 과정

빈 후처리기를 학습하기 전에 먼저 일반적인 스프링 빈 등록 과정을 코드로 작성해보자.

  • 참고)

👉 코드로 작성해보자.

  • BasicTest 생성: test > java > hello > proxy > postprocessor 패키지 내부에 BasicTest 클래스를 생성하자.
    • new AnnotationConfigApplicationContext(BasicConfig.class) : 스프링 컨테이너를 생성하면서 BasicConfig.class 를 넘겨주었다. BasicConfig.class 설정 파일은 스프링 빈으로 등록된다.

✔️ 코드 설명

  • 등록
    • beanA 라는 이름으로 A 객체를 스프링 빈으로 등록했다.
  • 조회
    • A a = applicationContext.getBean("beanA", A.class)
      • beanA 라는 이름으로 A 타입의 스프링 빈을 찾을 수 있다.
    • applicationContext.getBean(B.class)
      • B 타입의 객체는 스프링 빈으로 등록한 적이 없기 때문에 스프링 컨테이너에서 찾을 수 없다. (NoSuchBeanDefinitionException 발생)

3) 빈 후처리기 - 예제 코드2

빈 후처리기 적용

이번에는 빈 후처리기를 통해서 A객체를 B객체로 바꿔치기 해보자.

  • 참고)
    • 빈 후처리기에서 A객체를 B객체로 바꿔치기 해보자. 그러면 최종적으로 스프링 컨테이너 내부 빈 저장소에는 빈 이름은 beanA이고, 빈 객체는 B객체가 저장된다.
  • 참고) BeanPostProcessor 인터페이스 - 스프링 제공
    • 빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
    • postProcessBeforeInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.
    • postProcessAfterInitialization : 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.

 

👉 코드로 적용해보자.

  • BeanPostProcessorTest 생성: test > java > hello > proxy > postprocessor 패키지 내부에 BeanPostProcessorTest 클래스를 생성하자.
  • 실행해보자.

✔️ 코드 설명

AToBPostProcessor

  • 빈 후처리기이다. 인터페이스인 BeanPostProcessor 를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.
  • 이 빈 후처리기는 A객체를 새로운 B객체로 바꿔치기 한다. 파라미터로 넘어오는 빈( bean ) 객체가 A 의 인스턴스이면 새로운 B 객체를 생성해서 반환한다. 여기서 A 대신에 반환된 값인 B 가 스프링 컨테이너에 등록된다.
  • 실행 결과 참고)
    • 실행결과를 보면 beanName=beanA , bean=A 객체의 인스턴스가 빈 후처리기에 넘어온 것을 확인할 수 있다.
    • 그리고 최종적으로 "beanA" 라는 스프링 빈 이름에 A 객체 대신에 B 객체가 등록된 것을 확인할 수 있다. A 는 스프링 빈으로 등록조차 되지 않는다. (빈 이름은 beanA인데, 실제는 B객체가 등록됨.)

 

✔️ 정리

  • 빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이다.
  • 이것은 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다. 여기서 조작이라는 것은 해당 객체의 특정 메서드를 호출하는 것을 뜻한다.
  • 일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다.

✔️ 참고 - @PostConstruct의 비밀

  • @PostConstruct 는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화라는 것이 단순히 @PostConstruct 애노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다. 쉽게 이야기해서 생성된 빈을 한번 조작하는 것이다.
  • 따라서 빈을 조작하는 행위를 하는 적절한 빈 후처리기가 있으면 될 것 같다.
  • 스프링은 CommonAnnotationBeanPostProcessor 라는 빈 후처리기를 자동으로 등록하는데, 여기에서 @PostConstruct 애노테이션이 붙은 메서드를 호출한다. 따라서 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다.

4) 빈 후처리기 - 적용

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

  • 참고)

 

👉 코드로 적용해보자.

  • PackageLogTraceProxyPostProcessor 생성: src > main > java > hello > proxy > config > v4_postprocessor > postprocessor 패키지 내부에 PackageLogTraceProxyPostProcessor 클래스를 생성하자.
    • PackageLogTraceProxyPostProcessor 는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 이때 프록시 팩토리를 사용하는데, 프록시 팩토리는 advisor 가 필요하기 때문에 이 부분은 외부에서 주입 받도록 했다.
    • 모든 스프링 빈들에 프록시를 적용할 필요는 없다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. 여기서는 hello.proxy.app 과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.
    • 프록시 적용 대상의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환한다. 따라서 스프링 컨테이너에 원본 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 원본 객체는 스프링 빈으로 등록되지 않는다.
  • BeanPostProcessorConfig 생성: src > main > java > hello > proxy > config > v4_postprocessor 패키지 내부에 BeanPostProcessorConfig 클래스를 생성하자.
    • @Import({AppV1Config.class, AppV2Config.class}) : V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되지만, V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다. ProxyApplication 에서 등록해도 되지만 편의상 여기에 등록하자.
    • @Bean logTraceProxyPostProcessor() : 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다. 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다. 여기에 프록시를 적용할 패키지 정보( hello.proxy.app )와 어드바이저( getAdvisor(logTrace) )를 넘겨준다.
    • 이제 프록시를 생성하는 코드가 설정 파일에는 필요 없다. 순수한 빈 등록만 고민하면 된다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
  • ProxyApplication 수정: 다음과 같이 수정하자.
    • BeanPostProcessorConfig.class 를 등록하자.
  • 실행해보자.
    • 애플리케이션 로딩 로그 (중요 부분만 남기고 순서를 조정하고 축약했다.)
      • 여기서는 생략했지만, 실행해보면 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기를 통과하는 것을 확인할 수 있다. 여기에 모두 프록시를 적용하는 것은 올바르지 않다. 꼭 필요한 곳에만 프록시를 적용해야 한다. 여기서는 basePackage 를 사용해서 v1~v3 애플리케이션 관련 빈들만 프록시 적용 대상이 되도록 했다.
      • v1: 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
      • v2: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
      • v3: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다. (참고로 v3는 컴포넌트 스캔의 대상이 되지만, 빈 후처리기에는 다 들어온다.)
        • (참고) 컴포넌트 스캔에도 적용: 여기서 중요한 포인트는 v1, v2와 같이 수동으로 등록한 빈 뿐만 아니라 컴포넌트 스캔을 통해 등록한 v3 빈들도 프록시를 적용할 수 있다는 점이다. 이것은 모두 빈 후처리기 덕분이다.
    • v1 실행 ( http://localhost:8080/v1/request?itemId=hello )
    • v2 실행 ( http://localhost:8080/v2/request?itemId=hello )
    • v3 실행 ( http://localhost:8080/v3/request?itemId=hello )
      • 실행해보면 모두 동일한 결과가 나오는 것을 확인할 수 있다.

 

프록시 적용 대상 여부 체크

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

5) 빈 후처리기 - 정리

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

문제1 - 너무 많은 설정

  • 프록시를 직접 스프링 빈으로 등록하는 ProxyFactoryConfigV1 , ProxyFactoryConfigV2 와 같은 설정 파일은 프록시 관련 설정이 지나치게 많다는 문제가 있다.
  • 예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 프록시 설정 코드가 들어가야 한다. 무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다.
  • 스프링 빈을 편리하게 등록하려고 컴포넌트 스캔까지 사용하는데, 이렇게 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 했다.

문제2 - 컴포넌트 스캔

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

문제 해결

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

하지만 개발자의 욕심은 끝이 없다.

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

 

✔️ 중요

  • 프록시의 적용 대상 여부를 여기서는 간단히 패키지를 기준으로 설정했다. 그런데 잘 생각해보면 포인트컷을 사용하면 더 깔끔할 것 같다. 포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.

  • 참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다. 뒤에서 학습하겠지만 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.

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

    • 1) 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
    • 2) 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)
    • 1), 2) 상세 설명 예시
      • 1) 프록시는 객체 단위로 적용되기 때문에, 만약 메서드 10개중 하나만 어드바이스가 적용되어야 하는 경우라도 일단 프록시를 만들어야 한다.
      • 2) 위 1)에서 만들어진 프록시에서 내부 어떤 메서드에 어드바이스를 적용해야 하는지 판단한다.
      • 두개를 구분해서 이해하자. (=프록시를 만드는 단계에서 쓰는 포인트컷과 실제 실행 단계에서 쓰이는 포인트컷을 구분해서 이해하자.)
        • (참고) 다음 내용에서 더 자세히 설명

강의를 듣고 정리한 글입니다. 코드와 그림 등의 출처는 김영한 강사님께 있습니다.

profile
현실에서 한 발자국

0개의 댓글