[스프링 핵심 원리 - 고급편] #8. 빈 후처리기 #733
Develop-KIM
started this conversation in
동환
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
빈 후처리기 - 소개
@Bean이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다.그리고 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.
빈 후처리기 - BeanPostProcessor
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.
빈 포스트 프로세서(
BeanPostProcessor)는 번역하면 빈 후처리기인데, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.빈 후처리기 기능
객체를 조작할 수도 있고, 완전히 다른 객체로 바꿔치기 하는 것도 가능하다.
빈 후처리기 과정

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

빈 후처리기 - 예제 코드1
BasicTest
new AnnotationConfigApplicationContext(BasicConfig.class)스프링 컨테이너를 생성하면서BasicConfig.class를 넘겨주었다.BasicConfig.class설정 파일은 스프링 빈으로 등록된다.등록
BasicConfig.classbeanA라는 이름으로A객체를 스프링 빈으로 등록했다.조회
A a = applicationContext.getBean("beanA", A.class)beanA라는 이름으로A타입의 스프링 빈을 찾을 수 있다.applicationContext.getBean(B.class)B타입의 객체는 스프링 빈으로 등록한 적이 없기 때문에 스프링 컨테이너에서 찾을 수 없다.빈 후처리기 - 예제 코드2
빈 후처리기 적용
BeanPostProcessor 인터페이스 - 스프링 제공
BeanPostProcessor인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.postProcessBeforeInitialization: 객체 생성 이후에@PostConstruct같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.postProcessAfterInitialization: 객체 생성 이후에@PostConstruct같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.BeanPostProcessorTest
AToBPostProcessor
BeanPostProcessor를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.bean) 객체가A의 인스턴스이면 새로운B객체를 생성해서 반환한다.여기서
A대신에 반환된 값인B가 스프링 컨테이너에 등록된다.다음 실행결과를 보면
beanName=beanA,bean=A객체의 인스턴스가 빈 후처리기에 넘어온 것을 확인할 수 있다.실행 결과
B b = applicationContext.getBean("beanA", B.class)실행 결과를 보면 최종적으로
"beanA"라는 스프링 빈 이름에A객체 대신에B객체가 등록된 것을 확인할 수 있다.A는 스프링 빈으로 등록조차 되지 않는다.정리
빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이다.
이것은 빈 객체를 조작하거나 심지어 다른 객체로 바꾸어 버릴 수 있을 정도로 막강하다.
여기서 조작이라는 것은 해당 객체의 특정 메서드를 호출하는 것을 뜻한다.
일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데
빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다.
빈 후처리기 - 적용
빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록
빈 후처리기 - 프록시 적용

PackageLogTraceProxyPostProcessor
PackageLogTraceProxyPostProcessor는 원본 객체를 프록시 객체로 변환하는 역할을 한다.이때 프록시 팩토리를 사용하는데, 프록시 팩토리는
advisor가 필요하기 때문에 이 부분은 외부에서 주입 받도록 했다.여기서는
hello.proxy.app과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.따라서 스프링 컨테이너에 원본 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 원본 객체는 스프링 빈으로 등록되지 않는다.
BeanPostProcessorConfig
@Import({AppV1Config.class, AppV2Config.class}): V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되지만,V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다.
ProxyApplication에서 등록해도 됨(편의상 등록)@Bean logTraceProxyPostProcessor(): 특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다.빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다.
여기에 프록시를 적용할 패키지 정보(
hello.proxy.app)와 어드바이저(getAdvisor(logTrace))를 넘겨준다.프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
ProxyApplication
애플리케이션 로딩 로그
여기에 모두 프록시를 적용하는 것은 올바르지 않다. 꼭 필요한 곳에만 프록시를 적용해야 한다.
여기서는
basePackage를 사용해서 v1~v3 애플리케이션 관련 빈들만 프록시 적용 대상이 되도록 했다.v1: 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
v2: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
v3: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
프록시 적용 대상 여부 체크
스프링 부트가 기본으로 등록하는 수 많은 빈들이 빈 후처리기에 넘어온다. 그래서 어떤 빈을 프록시로 만들 것인지 기준이 필요하다. 여기서는 간단히
basePackage를 사용해서 특정 패키지를 기준으로 해당 패키지와 그 하위 패키지의 빈들을 프록시로 만든다.빈 후처리기 - 정리
문제1 - 너무 많은 설정
프록시를 직접 스프링 빈으로 등록하는
ProxyFactoryConfigV1,ProxyFactoryConfigV2와 같은 설정 파일은 프록시 관련 설정이 지나치게 많다는 문제가 있다.예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 프록시 설정 코드가 들어가야 한다.
무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다. 스프링 빈을 편리하게 등록하려고 컴포넌트 스캔까지 사용하는데,
이렇게 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 했다.
문제2 - 컴포넌트 스캔**
애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능했다.
왜냐하면 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.
좀 더 풀어서 설명하자면, 지금까지 학습한 방식으로 프록시를 적용하려면, 원본 객체를 스프링 컨테이너에 빈으로 등록하는 것이 아니라
ProxyFactoryConfigV1에서 한 것 처럼, 프록시를 원본 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다.그런데 컴포넌트 스캔은 원본 객체를 스프링 빈으로 자동으로 등록하기 때문에 프록시 적용이 불가능하다.
문제 해결
빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다.
그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도
중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다.
덕분에 애플리케이션에 수 많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다.
그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용된다.
스프링이 제공하는 빈 후처리기1
build.gradle - 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'이 라이브러리를 추가하면
aspectjweaver라는aspectJ관련 라이브러리를 등록하고,스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.
스프링 부트가 없던 시절에는
@EnableAspectJAutoProxy를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다.aspectJ는 뒤에서 설명한다. 스프링 부트가 활성화하는 빈은AopAutoConfiguration를 참고하자.자동 프록시 생성기 - AutoProxyCreator
AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.Advisor안에는Pointcut과Advice가 이미 모두 포함되어 있다.따라서
Advisor만 알고 있으면 그 안에 있는Pointcut으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.그리고
Advice로 부가 기능을 적용하면 된다.자동 프록시 생성기의 작동 과정 - 그림

자동 프록시 생성기의 작동 과정을 알아보자
@Bean, 컴포넌트 스캔 모두 포함)Advisor를 조회한다.Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다.이때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다.
그래서 조건이 하나라도 만족하면 프록시 적용 대상이 된다. 예를 들어서 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
생성된 프록시

프록시는 내부에 어드바이저와 실제 호출해야할 대상 객체(
target)을 알고 있다.AutoProxyConfig
AutoProxyConfig코드를 보면advisor1이라는 어드바이저 하나만 등록했다.스프링은 자동 프록시 생성기라는(
AnnotationAwareAspectJAutoProxyCreator) 빈 후처리기를 자동으로 등록해준다.중요: 포인트컷은 2가지에 사용된다.
만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성한다.
orderControllerV1은request(),noLog()가 있다. 여기에서request()가 조건에 만족하므로 프록시를 생성한다.orderControllerV1은 이미 프록시가 걸려있다.orderControllerV1의request()는 현재 포인트컷 조건에 만족하므로 프록시는 어드바이스를 먼저 호출하고,target을 호출한다.orderControllerV1의noLog()는 현재 포인트컷 조건에 만족하지 않으므로 어드바이스를 호출하지 않고 바로target만 호출한다.참고: 프록시를 모든 곳에 생성하는 것은 비용 낭비이다. 꼭 필요한 곳에 최소한의 프록시를 적용해야 한다.
그래서 자동 프록시 생성기는 모든 스프링 빈에 프록시를 적용하는 것이 아니라
포인트컷으로 한 번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다.
스프링이 제공하는 빈 후처리기2
애플리케이션 로딩 로그
애플리케이션 서버를 실행해보면, 스프링이 초기화 되면서 기대하지 않은 이러한 로그들이 올라온다. 그 이유는 지금 사
용한 포인트컷이 단순히 메서드 이름에
"request*", "order*", "save*"만 포함되어 있으면 매칭 된다고 판단하기 때문이다.결국 스프링이 내부에서 사용하는 빈에도 메서드 이름에
request라는 단어만 들어가 있으면 프록시가 만들어지고 되고, 어드바이스도 적용되는 것이다.결론적으로 패키지에 메서드 이름까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.
AspectJExpressionPointcut
AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다.
AutoProxyConfig - advisor2 추가
주의
advisor1에 있는@Bean은 꼭 주석처리해주어야 한다. 그렇지 않으면 어드바이저가 중복 등록된다.AspectJExpressionPointcut: AspectJ 포인트컷 표현식을 적용할 수 있다.execution(* hello.proxy.app..*(..)): AspectJ가 제공하는 포인트컷 표현식이다.*: 모든 반환 타입hello.proxy.app..: 해당 패키지와 그 하위 패키지*(..):*모든 메서드 이름,(..)파라미터는 상관 없음hello.proxy.app패키지와 그 하위 패키지의 모든 메서드는 포인트컷의 매칭 대상이 된다.실행하면 로그가 나오면 안됨 API
advisor2에서는 단순히package를 기준으로 포인트컷 매칭을 했기 때문이다.AutoProxyConfig advisor3 추가
주의
advisor1,advisor2에 있는@Bean은 꼭 주석처리해주어야 한다. 그렇지 않으면 어드바이저가 중복 등록된다.표현식을 다음과 같이 수정했다.
&&: 두 조건을 모두 만족해야 함!: 반대hello.proxy.app패키지와 하위 패키지의 모든 메서드는 포인트컷에 매칭하되,noLog()메서드는 제외하라는 뜻이다.하나의 프록시, 여러 Advisor 적용
예를 들어 어떤 스프링 빈이
advisor1,advisor2가 제공하는 포인트컷의 조건을 모두 만족하면 프록시 자동 생성기는 프록시를 몇 개 생성할까?프록시 자동 생성기는 프록시를 하나만 생성한다. 왜냐하면 프록시 팩토리가 생성하는 프록시는 내부에 여러
advisor들을 포함할 수 있기 때문이다.따라서 프록시를 여러 개 생성해서 비용을 낭비할 이유가 없다.
프록시 자동 생성기 상황별 정리
advisor1의 포인트컷만 만족 프록시1개 생성, 프록시에advisor1만 포함advisor1,advisor2의 포인트컷을 모두 만족 프록시1개 생성, 프록시에advisor1,advisor2모두 포함advisor1,advisor2의 포인트컷을 모두 만족하지 않음 프록시가 생성되지 않음이후에 설명할 스프링 AOP도 동일한 방식으로 동작한다.

정리
자동 프록시 생성기인
AnnotationAwareAspectJAutoProxyCreator덕분에 개발자는 매우 편리하게 프록시를 적용할 수 있다.이제
Advisor만 스프링 빈으로 등록하면 된다.Advisor=Pointcut+AdviceBeta Was this translation helpful? Give feedback.
All reactions