빈 후처리기 - 소개
일반적인 스프링 빈으로 등록
- @Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면,
스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. - 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.
빈 후처리기 (Bean PostProcessor)
- 빈 후처리기는 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.
- 스프링이 빈 저장소에 객체를 전달 및 등록하는 과정 중간에 빈 후처리기를 거쳐서 추가 처리를 진행하고, 그 다음에 빈 저장소에 전달된다.
- 빈 후처리기는 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
- postProcessBeforeInitialization
- 객체 생성 이후에 @PostConstruct같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
- postProcessAfterInitialization
- 객체 생성 이후에 @PostConstruct같은 초기화가 발생한 다음에 호출되는 포스트 프로세서
빈 후처리기 기능
- 객체를 조작할 수도 있다.
- 단순한 조작이 아닌 아예 다른 객체로 바꿔치기할 수도 있다.
빈 후처리기 동작 과정
- 생성
- 스프링 빈 대상이 되는 객체를 생성한다.
- @Bean 애노테이션이나 컴포넌트 스캔 모두 포함한다.
- 전달
- 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
- 후 처리 작업
- 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
- 등록
- 빈 후처리기는 빈을 반환한다.
- 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고,
바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.
빈 후처리기 - 예제 코드1
- 확실한 이해를 위해 우선 일반적인 스프링 빈 등록 과정을 확인해보자.
테스트 생성 및 실행
- 간단한 스프링 빈 등록 과정이다.
new AnnotationConfigApplicationContext(BasicConfig.class)
- 스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주었다.
- BasicConfig.class 설정 파일은 스프링 빈으로 등록된다.
빈 후처리기 - 예제 코드2
테스트 생성 및 실행
- 첫번째 예제처럼 내부 클래스 A와 B가 있다.
- 첫번째 예제와의 차이점은 이번에는 만약 빈으로 저장하려는 객체가 A라면 그걸 B로 치환시키는 빈 후처리기를 등록했다.
- 인스턴스가 B가 되는 것이지 빈 이름은 처음에 설정한 빈 이름 그대로 저장된다.
빈 후처리기 정리
- 빈 후처리기는 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다.
- 여기서 조작은 해당 객체의 특정 메서드를 호출하는 것을 의미한다.
- 일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없다.
- 하지만 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다.
- 모든 빈을 중간에 조작할 수 있다는 것은 무려 빈 객체를 프록시로 교체하는 것도 가능하다는 것을 의미한다.
@PostConstruct
- @PostConstruct는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다.
- 그저 @PostConstruct 애노테이션이 붙은 어노테이션을 호출하는 것만으로도 쉽게 생성된 빈을 한 번 조작할 수 있다.
- 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록한다.
- 여기에서 @PostConstruct 애노테이션이 붙은 메서드를 호출한다.
- 이것은 스프링 스스로도 스프링 내부의 기능을 확장하기 위해 빈 후처리기를 사용한다는 것을 의미한다.
빈 후처리기 - 적용
- 이번에는 실제로 애플리케이션에 적용해보자.
- 드디어 우리는 빈 후처리기를 통해 프록시를 생성하는 코드를 설정 파일에 집어넣을 필요가 없어졌다.
- 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
빈 후처리기
- 원본 객체를 프록시 객체로 변환하는 역할의 빈 후처리기다.
- postProcessAfterInitialization의 반환 값을 보면 원본 객체 대신에 프록시 객체를 반환한다.
- 원본 객체는 스프링 빈으로 등록되지 않고, 프록시 객체가 스프링 빈으로 등록된다.
환경설정
@Import({AppV1Config.class, AppV2Config.class})
- v3는 컴포넌트 스캔을 통해서 자동으로 스프링 빈으로 등록된다.
- 하지만 v1, v2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다.
- Chapter2Application에서 등록해도 되지만 편의상 여기에 등록하자.
@Bean logTraceProxyPostProcessor()
- 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다.
- 빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다.
테스트
- 드디어 떄가 왔다. 차례대로 v1, v2, v3를 실행해보자.
- v1은 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
- v2와 v3는 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
v1 (인터페이스 O)
- localhost:8082/v1/request?itemId=test로 접속해보자.
[ad3dd7b4] OrderControllerV1.request()
[ad3dd7b4] |–>OrderServiceV1.orderItem()
[ad3dd7b4] | |–>OrderRepositoryV1.save()
[ad3dd7b4] | |<–OrderRepositoryV1.save() time=1014ms
[ad3dd7b4] |<–OrderServiceV1.orderItem() time=1014ms
[ad3dd7b4] OrderControllerV1.request() time=1014ms
v2 (인터페이스 X)
- localhost:8082/v2/request?itemId=test로 접속해보자.
[88baf58f] OrderControllerV2.request()
[88baf58f] |–>OrderServiceV2.orderItem()
[88baf58f] | |–>OrderRepositoryV2.save()
[88baf58f] | |<–OrderRepositoryV2.save() time=1013ms
[88baf58f] |<–OrderServiceV2.orderItem() time=1013ms
[88baf58f] OrderControllerV2.request() time=1013ms
v3 (컴포넌트 스캔)
- localhost:8082/v3/request?itemId=test로 접속해보자.
[81355978] OrderControllerV3.request()
[81355978] |–>OrderServiceV3.orderItem()
[81355978] | |–>OrderRepositoryV3.save()
[81355978] | |<–OrderRepositoryV3.save() time=1014ms
[81355978] |<–OrderServiceV3.orderItem() time=1014ms
[81355978] OrderControllerV3.request() time=1014ms
프록시 적용 대상 여부 체크
애플리케이션을 실행해서 로그를 확인해보면 알겠지만, 우리가
- 직접 등록한 스프링 빈들 뿐만 아니라 스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다.
- 애플리케이션을 실행해서 로그를 확인해보면 알 수 있다.
- 이 점때문에 어떤 빈을 프록시로 만들 것인지 기준이 필요하다.
- 스프링 부트가 기본으로 제공하는 빈 중에는 프록시 객체를 만들 수 없는 빈들도 있다.
- 그래서 모든 객체를 프록시로 만들 경우 오류가 발생한다.
- 물론 스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공하고 있다.
스프링이 제공하는 빈 후처리기1
build.gradle
- 이 라이브러리를 추가하면 aspectjweaver라는 aspectJ 관련 라이브러리를 등록하고,
스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. - 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy 를 직접 사용해야 했는데,
이 부분을 스프링 부트가 자동으로 처리해준다.
자동 프록시 생성기
- 스프링 부트 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
- 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기이다.
- 이 빈 후처리기는 스프링 빈으로 등록된 어드바이저들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
- 어드바이저 안에는 이미 포인트컷과 어드바이스가 포함되어 있다.
- 어드바이저만 알고 있으면 그 안에 있는 포인트컷으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.
- 어드바이스로 부가 기능을 적용하기만 하면 된다.
- AnnotationAwareAspectJAutoProxyCreator는 @AspectJ와 관련된 AOP 기능도 자동으로 찾아서 처리해준다.
- 어드바이저는 물론이고 @Aspect 애노테이션도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.
자동 프록시 생성기의 작동 과정
- 자동 프록시 생성기를 통해 생성된 프록시는 내부에 어드바이저와 실제 호출해야할 대상 객체(target)을 알고 있다.
- 생성
- 스프링이 스프링 빈 대상이 되는 객체를 생성한다.
- @Bean과 컴포넌트 스캔 모두 포함
- 전달
- 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
- 모든 Advisor 빈 조회
- 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다.
- 프록시 적용 대상 체크
- 앞서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다.
- 이 때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다.
- 조건이 하나라도 만족하면 프록시 적용 대상이 된다.
- 즉, 조건이 10개가 있든지 100개가 있던지 그 중 하나만 만족해도 프록시 적용 대상이 된다.
- 프록시 생성
- 프록시 적용 대상이면 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다.
- 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
- 빈 등록
환경설정
- 실제로 적용해보자.
- AutoProxyConfig 코드를 보면 advisor1이라는 어드바이저 하나만 등록했다.
- 빈 후처리기는 이제 등록하지 않아도 된다.
- 스프링은 자동 프록시 생성기라는 빈 후처리기를 자동으로 등록해준다.
테스트
- 아래 주소들에 접속해보면 실제로 잘 동작하는 것을 알 수 있다.
- localhost:8082/v1/request?itemId=test
- localhost:8082/v2/request?itemId=test
- localhost:8082/v3/request?itemId=test
포인트컷의 사용 용도
- 프록시 적용 여부 판단
- 자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
- 클래스 + 메서드 조건을 모두 비교한다.
- 이 때 ,모든 메서드를 체크하는데 포인트컷 조건에 하나하나 매칭해본다.
- 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.
- 어드바이스 적용 여부 판단
- 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다.
프록시도 결국 자원이다.
- 프록시를 모든 곳에 생성하는 것은 비용 낭비이다.
- 꼭 필요한 곳에 최소한의 프록시를 적용해야 한다.
- 그래서 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라,
포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.
스프링이 제공하는 빈 후처리기2
- 직전에 만든 advisor1은 이름만 비교하기 때문에 원치 않은 로그가 올라올 때가 있다.
- 스프링 내부에서 사용하는 빈에도 메소드명에 request라는 단어만 들어가있으면 프록시가 만들어지고 어드바이스도 적용된다.
- 즉, 패키지에 메소드명까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.
AspectJExpressionPointcut
- AspectJ라는 AOP에 특화된 포인트컷 표현식을 통해 이를 해결할 수 있다.
- 아까 만든 AutoProxyConfig에 이번에는 패키지까지 필터링할 수 있는 어드바이저를 만들어보자.
- 어드바이저가 중복되니 advisor1의 @Bean은 주석 처리하자.
어드바이저 개선 1
- http://localhost:8082/v1/request?itemId=test로 접속해보면 패키지 필터링이 되는 것을 확인할 수 있다.
- 문제는 http://localhost:8082/v1/no-log에서는 여전히 로그가 출력된다.
- 단순히 package 를 기준으로 포인트컷 매칭을 했기 때문이다.
어드바이저 개선 2
- 이번에는 패키지에 메소드명까지 필터링할 수 있는 어드바이저를 만들어보자.
- 어드바이저가 중복되니 advisor2의 @Bean은 주석 처리하자.
하나의 프록시, 여러 Advisor 적용
- 만약 어드바이저가 10개가 있는데 어떤 스프링 빈이 모든 어드바이저의 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 몇 개 생성할까?
- 정답은 1개다.
- 프록시 자동 생성기는 프록시를 하나만 생성한다.
- 왜냐하면 프록시 팩토리가 생성하는 프록시는 내부에 여러 어드바이저들을 포함할 수 있기 때문이다.
- 따라서 프록시를 여러 개 생성해서 비용을 낭비할 이유가 없다.
- 상황별 정리
- 어드바이저1의 포인트컷만 만족
- 프록시1개 생성, 프록시에 어드바이저1만 포함
- 어드바이저1과 어드바이저2의 포인트컷을 모두 만족
- 프록시1개 생성, 프록시에 어드바이저1과 어드바이저2 모두 포함
- 어드바이저1과 어드바이저2의 포인트컷을 모두 만족하지 않음
출처