일반적인 Controller 객체가 존재하고, 우리는 이를 프록시로 우회적으로 빈 등록을 하기 위해 Config 클래스를 만들어 수동등록을 해야했다.
심지어는 등록 코드 자체가 매우 복잡해지는 것을 볼 수 있었고 컴포넌트 스캔이라는 편리한 등록도구를 놓칠 위험에 있다.
빈 후처리기(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;
}
}
빈 후처리기 사용을 위해서는 위의 BeanPostProcessor 인터페이스를 구현해야한다.
두 개의 매서드(Before, After)는 Before의 경우 빈이 컨테이너에 주입된 이후 초기화 과정(ex.@PostConstruct)이전에 빈에 대한 후처리를 지원하고, After는 초기화 이후에 해당한다.
두 메서드는 모두 default 메서드이기 때문에 원하는 것만 구현할 수 있다.
메서드를 잘 살펴보면 초기화 이전, 초기화 이후의 빈에 대한 제어가 가능하다.
만약 구현하지 않았을 경우 빈은 메서드에 들어온 그대로 리턴으로 나가게 되어 아무 동작도 하지 않는다는 것을 알 수 있다.
우리가 잘 알고있는 빈 적재이후 초기화 기능인 @PostConstruct또한 사실은 위의 빈 후처리기를 활용한 애노테이션이다.
@Slf4j
@RequiredArgsConstructor
public class PackageLogTracePostProcessor implements BeanPostProcessor {
private final String basePackage;
private final Advisor advisor;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
log.info("param 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;
}
}
프록시 대상이 아닐 경우 들어온 빈을 그대로 return 시키고 프록시 대상일 경우 프록시 생성 로직으로 하여금 프록시를 리턴해준다.
그리고 이 빈 후처리기를 빈으로 등록하게 되면 수동등록 문제를 해결할 수 있다.
스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.(더욱이 추상화하여 간편한 활용이 가능하게끔 지원한다.)
우리는 위의 코드에서 basePackage에 직접 경로를 지정했지만 포인트컷을 이용한다면 이를 더욱 고도화할 수 있을 것이다. 이 또한 스프링에서 지원한다.
스프링부트는 자동 설정으로 AnnotationAwareAspectJAutoProxyCreator라는 빈 후처리기를 등록시킨다.
이 빈 후처리기는 스프링 빈으로 등록된 Advisor를 자동으로 찾아내어 프록시가 필요한 곳에 프록시를 적용해준다.
당연히 Advisor내에 포인트 컷을 기준으로 프록시 적용여부를 결정한다.
우리는 Advisor를 잘 만들어서 빈으로 등록해주면 된다는 것이다.
// Config
@Bean
public Advisor advisor1(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
스프링은 더 나아가서 Advisor에 대한 컴포넌트 스캔까지 지원한다.
@Slf4j
@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 {
공통로직
Object result = joinPoint.proceed();
공통로직
}
클래스레벨에 @Aspect를 선언해주고 공통로직에 해당하는 메서드(이름 상관없음 -> execute로 설정)에 ProceedingJoinPoint파라미터를 주입하도록 만들면 된다. 또한 포인트컷은 @Around로 처리한다.
@Around에 인자로 들어가는 문법은 AspectJ 표현식으로 포인트컷 기준이 되는 적용 범위를 설정한다. 이 문법에 관련해서는 추후 포스팅으로 자세히 남기도록 하겠다.
AspectJ 표현식으로 하여금 상세한 적용범위 설정이 가능해진다.
스프링부트가 자동적으로 등록해주는 빈 후처리기인 자동 프록시 생성기(AnnotationAwareAspectJAutoProxyCreator)는 Advisor를 찾아 프록시 생성여부를 결정하기도 하지만 @Aspect를 찾아 Advisor로 만들기도 한다. 즉 자동 프록시 생성기는 이미 등록되어있는 Advisor + @Aspect가 붙여진 클래스의 Advisor 생성 및
두 루트(이미 등록되어있는 Advisor + @Aspect가 붙여진 클래스)로 Advisor를 모아 프록시 적용 여부를 결정한다.
애플리케이션 로직은 크게 핵심 기능과 부가 기능으로 나뉜다.
예를 들면 입금 로직은 핵심 로직에 해당할 것이고 로그 추적 로직은 부가 기능에 해당한다. 부가 기능은 항상 핵심 기능과 함께 사용된다.
보통 이러한 부가기능은 여러 클래스에 걸쳐 함께 사용되며 이러한 부가 기능은 횡단 관심사(cross-cutting concerns)로 불리운다.

부가 기능 코드를 일일이 핵심 로직에 적용한다면 아주 많은 반복이 필요할 것이고, 부가기능의 수정이 일어난다면 아주 많은 곳의 수정이 필요할 것이다. 우리는 이 문제를 타파하기 위해 여러 디자인 패턴을 학습하면서 끝에는 기존 코드에 변경을 주지 않는 프록시 패턴을 학습했다.
AspectJ 프레임워크는 부가 기능과 부가 기능을 어디에 적용할지 선택하는 기능을 합해서 하나의 모듈로 만들었는데 이를 Aspect라고 부른다. 위에서 사용된 @Aspect의 개념과 동일하다.
@Aspect는 부가기능을 작성할 수 있었고 이를 어디에 적용할 지에 관련한 포인트컷또한 작성이 가능했다.
횡단관심사는 기존의 OOP(객체지향 프로그래밍)의 부족한 부분이었고 이에 AOP(Aspect-Oriented Programming)라는 OOP의 부족한 부분을 채워주는 새로운 개념을 만들어냈다. AOP의 대표적인 구현으로 AspectJ 프레임워크가 존재한다.
AspectJ 프레임워크는 3가지 적용방식이 존재하는데 두 가지 방식은 어디의 코드든 바꿔치기가 가능한 반면 마지막 방식인 프록시(런타임) 방식은 메서드 실행으로 제한된다.
하지만 스프링은 스프링 AOP로 AspectJ의 프록시 방식을 채택하여 제공한다. 앞 두방식의 설정 방법이 매우 복잡하며 타겟 코드에 추가적으로 코드가 붙는 단점이 더 큰 단점이라 파악했기 때문이다.
앞의 두 방식은 자바 언어를 넘어선다. 런타임 방식을 채택하기 위해서는 DI, 프록시 패턴, 빈 포스트 프로세서와 같은 개념들을 총 동원해야한다. 이렇게 하여금 스프링 빈에 부가기능을 적용할 수 있고 앞서 우리가 프록시 패턴부터 빈 후처리기 까지 모든 내용을 공부한 이유의 의의가 느껴진다.