내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.
1. 빈 후처리기 - 소개
@Bean 이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다.
그리고 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.
▶ 빈 후처리기 - BeanPostProcessor
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.
1) 생성: 스프링 빈 대상이 되는 객체를 생성한다. (@Bean , 컴포넌트 스캔 모두 포함)
2) 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
3) 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
4) 등록: 빈 후처리기는 빈을 반환한다. 전달 된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.
만약 다른 빈으로 바꿔버린다면 다음과 같을 것 이다.
2. 빈 후처리기 - 예제 코드1
우선 일반적으로 빈을 등록하는 과정을 잠시 살펴보자.
public class BasicTest {
@Test
public void basicConfig() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);
A beanA = applicationContext.getBean("beanA", A.class);
beanA.helloA();
assertThatThrownBy(() -> applicationContext.getBean(B.class)).isInstanceOf(NoSuchBeanDefinitionException.class);
}
@Slf4j
@Configuration
static class BasicConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
}
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B {
public void helloB() {
log.info("hello B");
}
}
}
applicationContext 즉 Spring 컨테이너를 BasicConfig 기반으로 생성하게 되었다.
또한 "beanA"라는 이름으로 A 객체를 등록시켰다.
등록후, A객체를 다시 찾아와서 helloA()를 호출하게 된다.
또한 객체등록이 되어있지 않은 B객체 같은 경우 스프링 컨테이너에서 찾을수가 없다.
따라서 예외가 발생한다.
3. 빈 후처리기 - 예제 코드2
이번에는 빈 후처리기를 통해서 A 객체를 B 객체로 바꿔치기 해보자.
우선 BeanPostProcessor interface는 다음과 같다.
▶ BeanPostProcessor interface
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
- postProcessBeforeInitialization
객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.
- postProcessAfterInitialization :
객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.
우선 다음 테스트 코드를 살펴보자. 문제가 있는 코드다.
class BeanPostProcessorTest {
@Test
public void basicConfig() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
A beanA = applicationContext.getBean("beanA", A.class);
beanA.helloA();
assertThatThrownBy(() -> applicationContext.getBean(B.class)).isInstanceOf(NoSuchBeanDefinitionException.class);
}
@Slf4j
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AtoBPostProcessor helloPostProcessor() {
return new AtoBPostProcessor();
}
}
@Slf4j
static class A {
public void helloA() {
log.info("hello A");
}
}
@Slf4j
static class B {
public void helloB() {
log.info("hello B");
}
}
@Slf4j
static class AtoBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={} bean={}", beanName, bean);
if (bean instanceof A) {
return new B();
}
return bean;
}
}
}
인터페이스인 BeanPostProcessor 를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.
AtoBPostProcessor 를 등록했기 때문에 Spring Container에는 B 객체가 등록된다.
파라미터로 넘어오는 빈(bean)객체가 A의 인스턴스이면 새로운 B 객체를 생성해서 반환한다.
여기서 A 대신에 반환된 값인 B 가 스프링 컨테이너에 등록된다.
따라서 다음 코드 부분에서
A beanA = applicationContext.getBean("beanA", A.class);
다음과 같은 오류가 발생한다.
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'beanA' is expected to be of type 'hello.proxy.postprocessor.BeanPostProcessorTest$A' but was actually of type 'hello.proxy.postprocessor.BeanPostProcessorTest$B'
우리는 A.class를 원한다 명시하였지만, 실제로 등록된 type은 B.class라는 의이이다.
따라서 알맞게 수정해보자.
수정된 테스트 코드는 다음과 같다.
@Test
public void basicConfig() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
// beanA 이름으로 B 객체가 빈으로 등록된다.
B beanB = applicationContext.getBean("beanA", B.class);
beanB.helloB();
// A는 빈으로 등록되지 않는다.
assertThatThrownBy(() -> applicationContext.getBean(A.class)).isInstanceOf(NoSuchBeanDefinitionException.class);
}
실행 결과는 다음과 같다.
PostProcessor 메서드 내부로 들어와 우선 bean의 정보를 출력할때만 하더라도 beanA 에 대한 정보를 출력한다.
이후 B 객체가 대신 등록되기 때문에 helloB()를 출력시 "hello B"가 출력되게 된다.
"beanA"라는 스프링 빈 이름에 A 객체 대신에 B 객체가 등록된 것을 확인할 수 있다. A는 스프링 빈으로 등록조차 되지 않는다.
일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데, 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다.
이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다.
▶ 참고 - @PostConstruct 의 비밀
@PostConstruct는 에노테이션만 추가하면 빈이 생성된 이후 에노테이션이 있는 초기화 메서드를 간편하게 실행시켜준다.
그런데 생각해보면 빈의 초기화 라는 것이 단순히 @PostConstruct 애노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다.
4. 빈 후처리기 - 적용
이번 시간에는 빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보자.
이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다.
더 나아가서 설정 파일에 있는 수 많은 프록시 생성 코드도 한번에 제거할 수 있다.
우선 프록시를 생성할 후처리기를 만들어보자.
▶ PackageLogTraceProxyPostProcessor
@Slf4j
public class PackageLogTraceProxyPostProcessor implements BeanPostProcessor {
private final String basePackage;
private final Advisor advisor;
public PackageLogTraceProxyPostProcessor(String basePackage, Advisor advisor) {
this.basePackage = basePackage;
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("param beanName={} bean={}", beanName, bean.getClass());
// Proxy 적용 대상 여부 체크
String packageName = bean.getClass().getPackageName();
if(!packageName.startsWith(basePackage)) {
return bean;
}
// Proxy 대상이면 프록시를 만들어서 반환
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 가 필요하기 때문에 이 부분은 외부에서 주입 받는다.
또한 모든 Bean들을 Proxy로 만들 필요는 없다.
이를 위해 특정 패키지 이하의 빈들만 Proxy 적용이 되도록 만들었다.
// Proxy 적용 대상 여부 체크
String packageName = bean.getClass().getPackageName();
if(!packageName.startsWith(basePackage)) {
return bean;
}
여기서는 hello.proxy.app 과 관련된 부분에만 적용하면 된다. 다른 패키지의 객체들은 원본 객체를 그대로 반환한다.
Proxy 적용 대상 객체들은 프록시가 대신 등록되도록 후처리 된다.
다음으로는 config 파일을 만들어보자.
▶ BeanPostProcessorConfig
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {
@Bean
public PackageLogTraceProxyPostProcessor logTraceProxyPostProcessor(LogTrace logTrace) {
return new PackageLogTraceProxyPostProcessor("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);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
1) @Import({AppV1Config.class, AppV2Config.class})
V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되지만, V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다.
2) logTraceProxyPostProcessor()
특정 패키지를 기준으로 프록시를 생성하는 빈 후처리기를 스프링 빈으로 등록한다.
빈 후처리기는 스프링 빈으로만 등록하면 자동으로 동작한다.
여기에 프록시를 적용할 패키지 정보( hello.proxy.app )와 어드바이저( getAdvisor(logTrace) )를 넘겨준다.
프록시를 생성하는 반복되는 공통 코드가 설정 파일에서 모두 사라졌다. 순수한 빈 등록만 고민하면 된다.
프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
다음 과 같이 @Import 문을 설정해주면 성공적으로 실행이 된다.
@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();
}
}
실행시 출력되는 로그를 확인해보자.
#v1 애플리케이션 프록시 생성 - JDK 동적 프록시
create proxy: target=v1.OrderRepositoryV1Impl proxy=class com.sun.proxy. $Proxy50
create proxy: target=v1.OrderServiceV1Impl proxy=class com.sun.proxy.$Proxy51
create proxy: target=v1.OrderControllerV1Impl proxy=class com.sun.proxy. $Proxy52
#v2 애플리케이션 프록시 생성 - CGLIB
create proxy: target=v2.OrderRepositoryV2 proxy=v2.OrderRepositoryV2$ $EnhancerBySpringCGLIB$$x4
create proxy: target=v2.OrderServiceV2 proxy=v2.OrderServiceV2$ $EnhancerBySpringCGLIB$$x5
create proxy: target=v2.OrderControllerV2 proxy=v2.OrderControllerV2$ $EnhancerBySpringCGLIB$$x6
#v3 애플리케이션 프록시 생성 - CGLIB
create proxy: target=v3.OrderRepositoryV3 proxy=3.OrderRepositoryV3$ $EnhancerBySpringCGLIB$$x1
create proxy: target=v3.orderServiceV3 proxy=3.OrderServiceV3$ $EnhancerBySpringCGLIB$$x2
create proxy: target=v3.orderControllerV3 proxy=3.orderControllerV3$ $EnhancerBySpringCGLIB$$x3
여기서는 basePackage 를 사용해서 v1~v3 애플리케이션 관련 빈들만 프록시 적용 대상이 되도록 했다.
v1: 인터페이스가 있으므로 JDK 동적 프록시가 적용된다.
v2: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다.
v3: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다. 컴포넌트 스캔된 빈에도 후처리가 잘 적용되었다!
빈 후처리기 덕분에 프록시를 생성하는 부분을 하나로 집중할 수 있다.
그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다.
하지만 개발자의 욕심은 끝이 없다.
스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.
중요!
우리는 프록시의 적용 대상 여부를 간단히 패키지를 기준으로 설정했다.
하지만 잘 생각해보면 포인트컷을 사용하면 더 깔끔할 것 같다.
포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다.
참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다.
뒤에서 학습하겠지만 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다.
결과적으로 포인트컷은 다음 두 곳에 사용된다.
- 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
- 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)
'BackEnd > Spring' 카테고리의 다른 글
[Spring] @Aspect AOP (0) | 2022.08.16 |
---|---|
[Spring] 빈 후처리기 - 2 (0) | 2022.08.16 |
[Spring] 스프링이 지원하는 프록시 - 2 (0) | 2022.08.14 |
[Spring] 스프링이 지원하는 프록시 - 1 (0) | 2022.08.14 |
[Spring] 동적 프록시 기술 - JDK, CGLIB (0) | 2022.08.13 |
댓글