[Spring] 빈 후처리기
AOP를 적용하기 위해 프록시 기술에 대해 알아봤는데...
프록시 기술을 적용해 기존 코드를 수정하지 않고 부가 기능을 적용할 수는 있었지만
추가로 수행해 줘야 하는 설정이 너무 많고 빈 등록도 @Configuration과 @Bean 애너테이션을 통해 수동으로 해 줘야 하는 문제점이 있었다.
요즘은 빈 등록을 ComponentScan 방식으로 수행하는데... ComponentScan을 사용하면 빈 등록 과정에 개발자가 개입할 공간이 없어 그대로 실제 클래스를 빈으로 등록할 수 밖에 없다.
이런 문제를 처리하기 위해 빈 후처리기가 도입됐다. (BeanPostProcessor)
빈 후처리기는 스프링이 빈으로 등록할 목적으로 생성한 객체를 등록하기 직전에 조작할 때 사용한다.
객체는 빈으로 등록되기 전 빈 후처리기에 전달되고, 빈 후처리기는 전달된 객체를 조작하거나 다른 객체로 바꿔칠 수 있다.
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
static class customProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ABean) {
return new Bbean();
}
return bean;
}
}
빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고 스프링 빈으로 등록해야 한다.
해당 인터페이스는 두 개의 메서드를 제공하는데, 각각 객체 생성 이후 @PostConstruct 같은 초기화가 발생하기 전, 후에 호출되는 작업을 정의해놓을 수 있다.
위의 예시에서는 beanName이 Abean 인 경우 실제 반환되는 객체는 Bbean으로 정의했다.
이렇듯 빈 후처리기는 빈을 조작하고 변경하는 후킹 포인트로, 스프링 컨테이너가 등록하는 빈을 조작해 빈 객체를 프록시로 교체할 때 사용할 수 있다.
@PostConstruct 애너테이션은 빈이 생성된 이후 초기화하는 역할을 수행한다.
스프링은 CommonAnnotationBeanPostProcessor 라는 빈 후처리기를 등록해서 사용하는데, 여기서 @PostConstruct 애너테이션이 붙은 메서드를 호출한다.
@PostConstruct가 제대로 작동하기 위해서 CommonAnnotationBeanPostProcessor 빈 후처리기가 필요하고, 스프링은 이미 빈 후처리기를 사용하고 있다.
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 {
String packageName = bean.getClass().getPackageName();
if (!packageName.startsWith(basePackage)) {
return bean;
}
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvisor(advisor);
Object proxy = proxyFactory.getProxy();
return proxy;
}
}
@Configuration
public class BeanPostProcessorConfig {
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
return new PackageLogTracePostProcessor("start.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);
}
}
빈 후처리기를 작성하고 빈으로 등록하는 예시이다.
빈 후처리기는 빈으로 등록되면 자동으로 동작해 빈이 등록될 때 마다 로직을 수행한다.
이제 @Configuration 클래스에는 프록시 관련 코드를 지울 수 있다.
여기서 프록시가 적용되는지를 필터링 하기 위해 packageName을 사용했는데, 이 부분도 포인트컷을 통해 쉽게 작성할 수 있다.
즉, 포인트컷은 빈 후처리기에서 프록시를 생성할 때와 프록시의 어떤 메서드에 어드바이스를 적용할지 모두 판단할 수 있다.
스프링 부트는 spring-boot-starter-aop 라이브러리를 통해 AOP 관련 클래스를 스프링 빈으로 등록한다.
스프링 빈으로 등록되는 클래스 중에는 AnnotationAwareAspectJAutoProxyCreator 클래스가 있는데, 스프링은 해당 클래스로 프록시를 등록한다.
이름에 Auto가 붙은 만큼 스프링 빈으로 등록된 어드바이저를 자동으로 찾고 프록시가 필요한 곳에 자동으로 프록시를 적용한다.
1. 스프링이 빈으로 등록할 객체를 생성한다.
2. 객체를 빈으로 등록하기 직전에 빈 후처리기에 빈을 전달한다.
3. AutoProxyCreator는 스프링 컨테이너에 있는 모든 어드바이저를 조회한다. (어드바이저는 빈으로 등록되어 있어야 한다)
4. 어드바이저 내부에 있는 포인트컷을 통해 전달받은 객체가 프록시를 적용할 대상인지 판단한다.
5. 프록시 적용 대상이라면 프록시를 생성하고 생성한 프록시를 스프링 빈으로 등록하고, 아니라면 원본 객체를 빈으로 등록한다.
프록시 내부에도 어드바이저가 있다.
프록시를 생성할 때 사용되는 포인트컷과 실제 실행 단계에서 사용되는 포인트컷을 구분해서 이해하자.
@Configuration
public class AutoProxyConfig {
@Bean
public Advisor advisor(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
public Advisor advisorAspectJ(LogTrace logTrace) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* start.proxy.app..*(..)) && !execution(* start.proxy.app..noLog(..))");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
이렇게 어드바이저를 빈으로 등록하면 위에서 설명한 일련의 과정을 거쳐 프록시 또는 객체 원본을 빈으로 등록한다.
AOP에 특화된 포인트컷 표현식인 AspectJ를 사용하면 좀 더 편하게 작성할 수 있다.
@Slf4j
@Aspect
public class LogTraceAspect {
private final LogTrace logTrace;
public LogTraceAspect(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Around("execution(* start.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;
}
}
}
@Aspect : 애너테이션 기반 프록시를 적용할 때 사용된다.
@Around : 포인트컷 표현식을 사용한다. 메서드 내용은 어드바이스가 된다.
@Aspect는 AutoProxyCreator가 찾아서 어드바이저로 만들어준다.
스프링 애플리케이션이 로딩될 때 AutoProxyCreator가 호출되고 @Aspect 애너테이션이 붙은 스프링 빈을 모두 조회한다.
이후 @Aspect 어드바이저 빌더를 통해 어드바이저를 생성하고 어드바이저 빌더 내부에 저장한다.
어드바이저 빌더는 어드바이저를 생성하고 관리하는 작업을 수행한다.
@Aspect 애너테이션이 붙은 메서드의 정보를 바탕으로 어드바이저를 만들고 빌더 내부 저장소에 캐시한다.
로그 남기기 같은 애플리케이션의 여러 기능들 사이에 걸쳐서 들어가는 관심사를 횡단 관심사라고 부른다.
이 횡단 관심사는 AOP를 통해 처리한다.
'Spring > Spring' 카테고리의 다른 글
[Spring] 포인트컷 지시자 (0) | 2023.07.28 |
---|---|
[Spring] AOP 적용 (0) | 2023.07.27 |
[Spring] 프록시 팩토리 (0) | 2023.07.24 |
[Spring] 동적 프록시 기술 (0) | 2023.07.23 |
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2023.07.22 |
댓글
이 글 공유하기
다른 글
-
[Spring] 포인트컷 지시자
[Spring] 포인트컷 지시자
2023.07.28 -
[Spring] AOP 적용
[Spring] AOP 적용
2023.07.27 -
[Spring] 프록시 팩토리
[Spring] 프록시 팩토리
2023.07.24 -
[Spring] 동적 프록시 기술
[Spring] 동적 프록시 기술
2023.07.23