일반적인 빈 등록은 아래와 같이 진행된다. 빈 후처리기를 사용하면, 아래와 같이 진행된다.
이처럼 빈 후처리기는
@Bean
+ @Configuration
@ComponentScan
과 같이 컨테이너에 저장되는 모든 빈에 대해, 컨테이너에 저장되기 이전에 무언가 작업을 할 수 있다.
객체의 동작중 일부를 조작하거나, 완전히 다른 객체로 바꾸는것까지 가능하다.
빈 등록 과정을 빈 후처리기와 함께 살펴보자
@Bean
, 컴포넌트 스캔 등)빈 후처리기를 등록하기 이전에, 일반적인 빈 등록 절차에 대해 복습해보자.
public class BasicTest {
@Test
void basicConfig() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);
//A는 빈으로 등록된다.
A a = applicationContext.getBean("beanA", A.class);
a.helloA();
//B는 빈으로 등록되지 않는다.
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(B.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");}
}
}
빈 후처리기를 사용하기 위해서는, BeanPostProcessor
인터페이스를 구현해서 빈으로 등록해야한다.
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
메소드 명에서도 알 수 있지만,
postProcessBeforeInitialization : 객체 생성 이후 @PostConstruct
초기화가 발생하기 이전
실행
postProcessAfterInitialization : 객체 생성 이후 @PostConstruct
초기화가 발생하기 이후
실행
public class BeanPostProcessorTest {
@Test
void postProcessor() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//beanA 이름으로 B 객체가 빈으로 등록된다.
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
//A는 빈으로 등록되지 않는다.
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.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;
}
}
}
BeanPostProcessorConfig 에서
BeanPostProcess 인터페이스를 구현한 AToBPostProcessor 를 빈으로 등록했다.
-> 빈 후처리기를 등록했다.
AToBPostProcessor 로직을 보면, bean이 A객체이면 B로 바꿔치기하고있다.
..AToBPostProcessor - beanName=beanA bean=hello.proxy.postprocessor...A@21362712
..B - hello B
@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());
//프록시 적용 대상 여부 체크
//프록시 적용 대상이 아니면 원본을 그대로 반환
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;
}
}
basePackage 와 Advisor를 받아서
특정 패키지에 속한 클래스(프록시 대상)들에는 프록시를 적용한다.
@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);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
@Import({AppV1Config.class, AppV2Config.class})
V3는 컴포넌트 스캔으로 자동으로 스프링 빈으로 등록되지만, V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다.
@Bean logTraceProxyPostProcessor()
빈 후처리기를 스프링 빈으로 등록한다.
프록시를 적용할 패키지 정보(hello.proxy.app
)와 어드바이저(getAdvisor(logTrace)
)를 넘겨준다.
@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
를 사용하여 필요한 패키지에만 적용하는 것이 중요하다.
또한, 스프링이 사용하는 빈 중에는, 프록시로 만들지 못하는 객체도 있다. (final class)
여기에 빈을 생성하려고 하면 오류가 발생한다.
이전 포스팅의 마지막에 언급한 문제점이 해결되었는지 확인해보자.
-> 이젠 빈 후처리기 설정 하나만 등록하면 된다.
-> 컴포넌트 스캔을 사용하는 V3 까지 프록시가 적용된 것을 확인할 수 있었다.
스프링이 제공하는 빈 후처리기를 사용하기 위해서는
반드시 의존성을 추가
해야 한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
위 의존성을 추가하면, aspectjweaver
라는 aspectJ 관련 라이브러리가 함께 추가되며,
스프링 부트가 AnnotationAwareAspectJAutoProxyCreator
라는 자동 프록시 생성기를 포함한 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.
스프링 부트가 등록하는 빈은 AopAutoConfiguration
를 참고하자.
위에서 언급한 대로, 의존성을 추가하면 AnnotationAwareAspectJAutoProxyCreator
라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
이 빈 후처리기는 스프링 빈으로 등록된 Advisor
들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
자동 프록시 생성기의 동작 과정은 다음과 같다.
@Bean
, 컴포넌트 스캔 등)Advisor
를 조회한다.@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor advisor1(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
빈 후처리기를 등록할 필요 없이, 위에 의존성을 추가하면 AnnotationAwareAspectJAutoProxyCreator
자동 프록시 생성기가 Advisor를 알아서 처리해준다.
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor advisor1(LogTrace logTrace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
EnableWebMvcConfiguration.requestMappingHandlerAdapter()
EnableWebMvcConfiguration.requestMappingHandlerAdapter() time=63ms
애플리케이션을 실행해보면, 위와 같이 스프링에서 사용하는 빈이 로그로 출력되는 것을 확인할 수 있다.
이는, 우리가 사용한 포인트컷이 단순히 메서드 이름에 "request*", "order*", "save*"
만 포함되어 있으면 매칭 된다고 판단하기 때문이다.
결국 스프링이 내부에서 사용하는 빈에도 메서드 이름에 request
라는 단어만 들어가 있으면 프록시가 만들어지고, 어드바이스도 적용되는 것이다.
결론적으로 패키지에 메서드 이름까지 함께 지정할 수 있는 매우 정밀한 포인트컷이 필요하다.
AspectJExpressionPointcut
은 이름 그대로, AspectJ
표현식을 사용한 포인트컷이다.
AspectJ 표현식은 이전 포스팅에도 언급했지만, 조건을 매우 정밀하게 설정할 수 있다.
관련해서는 뒤에서 다시 자세하게 설명한다.
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor advisor2(LogTrace logTrace) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* hello.proxy.app..*(..))");
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
execution(* hello.proxy.app..*(..))
: AspectJ가 제공하는 포인트컷 표현식
-> *
: 모든 반환 타입
-> hello.proxy.app..
: 해당 패키지와 그 하위 패키지
-> *(..)
: *
모든 메서드 이름, (..)
파라미터는 상관 없음
위에 표현식을 보면, *(..)
에 의해 모든 메서드가 프록시 적용 대상이 된다.
그래서 http://localhost:8080/v1/no-log
를 호출하면 로그가 찍히면 안되지만,
로그가 찍히는 문제가 생긴다.
위에서 다룬 AspectJExpressionPointCut에서는 package를 기준으로 표현식을 사용했기 때문에 문제가 발생했다. 이제 package + 메소드명
을 통해 정밀한 표현식을 만들어보자.
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@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);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))
-> &&
: 두 조건을 모두 만족해야 함
-> !
: 반대
-> hello.proxy.app
패키지와 하위 패키지의 모든 메서드는 포인트컷에 매칭하되, noLog()
메서드는 제외