빈 후처리기
남아있는 문제
- 이전 포스팅에서 프록시 팩토리를 사용해서 스프링이 지원하는 프록시를 만들고 어드바이저로 부가기능을 생성해보았다.
- 스프링 빈에서 프록시 객체로 등록하기 위해서 해당 스프링 빈 대신에 프록시 객체를 넣고 프록시 객체 내부에서 부가기능과 함께 실제 객체를 호출하는 식으로 작성을 했다.
- 이렇게 할 때 문제는 복잡한 빈 등록 설정을 해줘야 한다는 것인데 빈 등록을 할 때 프록시 객체를 넘겨서 설정을 해주는 과정이 번거롭다. 게다가 빈을 컴포넌트 스캔으로 자동 등록할 때에는 이런식으로 수동으로 등록하기가 어렵다.
- 위와 같은 문제를 해결하기 위해 빈 후처리기를 사용한다.
빈 후처리기 (BeanPostProcessor)
- 객체를 스프링 빈 저장소에 등록하기 전에 조작을 할 수 있게 해주는 인터페이스다.
- 객체를 조작하거나 아예 바꿔서 반환할 수 있다.
- 스프링 빈 등록과정
- 생성 : 빈 대상이 되는 객체를 생성한다.
- 전달 : 생성한 객체를 빈 후처리기에 전달한다.
- 후 처리 작업 : 빈 후처리기에서는 객체를 조작하거나 다른 객체로 변경한다.
- 등록 : 후 처리 작업이 끝난 객체를 빈 저장소에 등록한다.
- 위의 등록과정을 살펴보면 후 처리 작업에서 다른 객체로 변경을 하면 해당 객체가 빈 저장소에 등록됨을 확인할 수 있다.
- 이를 그림으로 살펴보면 다음과 같다.
다른 객체로 바꿔치는 빈 후처리기 예제
- 먼저 빈 후처리기를 만들어서 빈으로 등록해야 한다.
- 빈 후처리기를 만들기 위해서는 BeanPostProcessor 인터페이스를 구현해줘야 한다.
- BeanPostProcessor는 두 가지 디폴트 메서드가 존재한다.
- postProcessBeforeInitialization, postProcessAfterInitialization, 각각 Initialization(@PostConstruct) 전과 후에 적용이 된다고 보면 된다.
- 다음은 A객체를 B객체로 변경하는 후처리기이다.
static class A{
public void helloA(){
log.info("hello A");
}
}
static class B{
public void helloB(){
log.info("hello B");
}
}
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;
}
}
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a(){
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
- 위의 코드에서 Config 클래스를 살펴보면 beanA라는 이름으로 A객체를 생성하여 등록하고있다.
- 하지만 후처리기에서 A객체의 인스턴스이면 B 객체를 생성하여 객체를 변경하고있다.
- 아래의 테스트코드 결과에서도 볼 수 있듯이 실제로 beanA 로 등록된 객체는 B.class 타입이고 A.class 타입의 객체는 존재하지 않음을 확인할 수 있다.
@Test
void basicConfig(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//beanA 이름으로 B는 빈으로 등록된다.
B a = applicationContext.getBean("beanA", B.class);
a.helloB();
//A는 빈으로 등록되지 않는다.
assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
}
프록시와 빈 후처리기
- 다시 한 번 기존의 프록시의 구현 방식을 생각해보자.
- 빈을 등록할 때 직접 프록시 객체를 생성해서 등록해줬었는데 이를 빈 후처리기를 이용하면 훨씬 간결한 코드로 프록시를 만들 수 있다.
public class PackageLogTracePostProcessor implements BeanPostProcessor {
private final String basePackage;
private final Advisor advisor;
public PackageLogTracePostProcessor(String basePackage, Advisor advisor) {
this.basePackage = basePackage;
this.advisor = advisor;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("beanName={}, bean={}", beanName, bean.getClass());
//프록시 적용 대상여부 체크
//프록시 적용 대상이 아니면 원본을 그대로
String packageName = bean.getClass().getPackageName();
if (!packageName.startsWith(basePackage)) {
return bean;
}
//프록시 대상이면 프록시를 만들어서 반환
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
log.info("create proxy: target={}, proxy={}", bean.getClass(), proxy.getClass());
return proxy;
}
}
- 먼저 BeanPostProcessor 를 구현한 클래스를 생성하고 postProcessAfterInitialization 메서드를 재정의한다.
- 해당 메서드에서는 프록시를 생성해서 반환한다.
- 위의 코드에서는 패키지를 주입받아서 해당 패키지의 대상일 경우에만 프록시를 생성하여 반환한다.
- 어드바이저도 주입받아서 프록시를 생성할 때 추가해준다.
- 다음은 위의 후처리기를 빈으로 등록하는 과정이다.
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
return new PackageLogTracePostProcessor("hello.proxy.app", getAdvisor(logTrace));
}
private Advisor getAdvisor(LogTrace logTrace) {
NameMatchMethodPointcut pointCut = new NameMatchMethodPointcut();
pointCut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointCut, advice);
}
- 이렇게 빈 등록까지 마치면 정상적으로 빈 후처리기를 통해서 프록시 등록이 완성된다.
스프링이 제공하는 빈 후처리기
implementation 'org.springframework.boot:spring-boot-starter-aop'
- 위와 같이 spring-boot-starter-aop 의존성을 추가하면 AnnotationAwareAspectJAutoProxyCreator 라는 빈 후처리기를 자동으로 등록해준다.
- 이 빈 후처리기는 또 다시 빈으로 등록된 Advisor를 찾아서 자동으로 프록시를 생성해준다.
- Advisor는 Pointcut 과 Advice가 포함되어있기 때문에 Pointcut이 적용될 대상에 대해 프록시를 만들고 실제 프록시가 호출될 때 Pointcut으로 다시 한 번 대상 메서드임을 검증하고 Advice의 부가기능을 추가해준다.
- 생성 : 스프링 빈 대상이 되는 객체를 생성한다.
- 전달 : 빈 후처리기에 생성된 객체를 전달한다.
- 모든 Advisor 빈 조회 : 빈 후처리기는 빈으로 등록된 모든 Advisor를 조회한다.
- 프록시 적용 대상 체크 : 대상 객체가 Advisor의 포인트컷의 적용 대상인지 확인한다. 모든 어드바이저의 포인트컷을 확인해서 하나라도 대상이 되면 프록시를 생성한다.
- 프록시 생성 : 프록시 적용 대상이면 프록시를 생성하여 반환하므로 원래 객체 대신 프록시가 빈으로 등록된다. 대상이 아니면 원래 객체가 빈으로 등록된다.
- 빈 등록 : 반환된 객체를 빈으로 등록한다.
- 클라이언트가 타겟을 호출하면 타겟 대신 프록시를 호출한다. 프록시는 호출 요청이 포인트컷에 부합하는 메서드를 호출하는지 확인하고 대상이면 부가기능을 추가하고 아니면 부가기능 없이 타겟을 호출하게된다.
포인트컷의 두 가지 사용
- 프록시 적용 여부 판단
- 프록시를 생성할 때 생성할 필요가 있는지 없는지를 판단한다.
- 여러 어드바이저의 포인트컷 요건에 하나라도 맞으면 프록시를 생성한다.
- 어드바이스 적용 여부 판단
- 프록시가 호출되었을 때 부가기능을 적용할 지 말지를 판단한다.
어드바이저 적용
@Bean
public Advisor advisor3(LogTrace logTrace) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && " +
"!execution(* hello.proxy.app..noLog(..))");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
- 다음과 같이 어드바이저를 생성하여 빈으로 지정하면 자동 프록시 생성기가 advisor에 pointcut으로 빈으로 등록된 객체중에서 어드바이스 적용 대상인지 확인한다.
- 맞으면 프록시를 생성하고 아니면 본래 객체를 그대로 반환한다.
@Aspect
public class LogTraceAspect {
private final LogTrace logTrace;
public LogTraceAspect(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Around("execution(* hello.proxy.app..*(..))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
TraceStatus status = null;
try {
String message = joinPoint.getSignature().toShortString();
status = logTrace.begin(message);
Object result = joinPoint.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@Configuration
public class AopConfig {
@Bean
public LogTraceAspect logTraceAspect(LogTrace logTrace) {
return new LogTraceAspect(logTrace);
}
}
- 위와같이 Aspect 어노테이션을 사용해서 어드바이저를 만들 수 있다.
- @Around 어노테이션에서 들어가는 값이 pointcut이라고 볼 수 있고 execute 메서드 안의 내용은 Advice(부가기능) 이라고 생각할 수 잇다.
- 위의 객체를 빈으로 등록해줘도 자동 프록시 생성 빈 후처리기가 프록시를 생성하는 작업을 해준다.
AnnotationAwareAspectJAutoProxyCreator
- 어노테이션 기반의 자동 프록시 생성기인데 말 그대로 @Aspect 어노테이션이 붙어있는 스프링 빈을 찾아서 자동으로 프록시를 만들어 주는 역할을 한다.
- 이 자동 프록시 생성기는 두 가지 역할을 한다.
- @Aspect 어노테이션을 보고 어드바이저를 생성해준다.
- 이 때 BeanFactoryAspectJAdvisorsBuilder 클래스가 사용되는데, 포인트컷, 어드바이스, 어드바이저 등을 생성해준다.
- 어드바이저를 기반으로 프록시를 생성한다.
- @Aspect 어노테이션을 보고 어드바이저를 생성해준다.
Reference
- 스프링 핵심 원리 - 고급편, 김영한
'Spring' 카테고리의 다른 글
스프링 AOP 내부호출 문제와 한계 (0) | 2022.06.16 |
---|---|
스프링 AOP (0) | 2022.06.12 |
스프링이 제공하는 프록시, 프록시 팩토리 (0) | 2022.06.09 |
Servlet ? (0) | 2022.01.05 |
Fetch join, N+1 문제 (0) | 2021.11.19 |