빈 후처리기 - 소개
- @Bean 이나 컴포넌트 스캔으로 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다.
- 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.
BeanPostProcessor : 생성한 객체를 빈 저장소에 등록하기 직전에 조작
- 객체를 조작할 수 있다.
- 완전히 다른 객체로 바꾸는 것도 가능하다.
- 생성: 스프링 빈 대상이 되는 객체를 생성한다. ( @Bean , 컴포넌트 스캔 모두 포함)
- 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
- 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바뀌치기 할 수 있다.
- 등록: 빈 후처리기가 빈을 반환한다. 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.
빈 후처리기 - 예제
BeanPostProcessor 인터페이스 - 스프링 제공
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
- postProcessBeforeInitialization : 객체 생성 이후 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
- postProcessAfterInitialization : 객체 생성 이후 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서
BeanPostProcessorTest
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));
}
// BeanPostProcessorConfig
@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;
}
}
}
AToBPostProcessor - 빈 후처리기
→ BeanPostProcessor 를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.
★ 정리
- 빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트이다.
- 개발자가 등록하는 모든 빈을 중간에 조작할 수 있다. 즉, 빈 객체를 프록시로 교체하는 것도 가능하다.
빈 후처리기 - 적용
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());
//프록시 적용 대상 여부 체크
//프록시 적용 대상이 아니면 원본을 그대로 반환
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;
}
}
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);
//advisor = pointcut + advice
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
→ 프록시를 적용할 패키지 정보(basePackage : hello.proxy.app)와 어드바이저( getAdvisor(logTrace) )를 넘겨준다.
- 프록시를 생성하는 코드가 설정 파일에는 필요 없다. 순수한 빈 등록만 고민하면 된다.
- 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
v1: 인터페이스가 있으므로 JDK 동적 프록시가 적용된다. (인터페이스)
v2: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다. (구체 클래스)
v3: 구체 클래스만 있으므로 CGLIB 프록시가 적용된다. (컴포넌트 스캔)
프록시 적용 대상 여부 체크 : basePackage 를 사용해서 특정 패키지를 기준으로 해당 패키지와그 하위 패키지의 빈들을 프록시 만듬.
빈 후처리기 - 정리
문제점
- 너무 많은 설정
→ 프록시를 직접 스프링 빈으로 등록하는 프록시 관련 설정(@Configuration)이 지나치게 많다는 문제가 있다. - 컴포넌트 스캔
→ 컴포넌트 스캔을 쓸 경우 프록시 적용 X.
문제 해결
- 프록시를 생성하는 부분을 하나로 집중
- 컴포넌트 스캔을 쓰더라도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다.
★ 중요 - 포인트컷 사용
- 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
- 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)
스프링이 제공하는 빈 후처리기1
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
자동 프록시 생성기 - AutoProxyCreator
→ 자동으로 프록시를 생성해주는 빈 후처리기 (AnnotationAwareAspectJAutoProxyCreator)
→ Advisor 와 @Aspect 을 자동으로 인식해서 프록시를 만들고 AOP를 적용
★ 중요: 포인트컷 사용처
- 프록시 적용 여부 판단 - 생성 단계
자동 프록시 생성기는 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크 - 어드바이스 적용 여부 판단 - 사용 단계
프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단
스프링이 제공하는 빈 후처리기2
AspectJExpressionPointcut
→ 특별한 표현식으로 복잡한 포인트컷을 만들 수 있다.
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..*(..)) : AspectJ가 제공하는 포인트컷 표현식이다.
- * : 모든 반환 타입
- hello.proxy.app.. : 해당 패키지와 그 하위 패키지
- *(..) : * 모든 메서드 이름, (..) 파라미터는 상관 없음
하나의 프록시, 여러 Advisor 적용
※ 프록시 자동 생성기 상황별 정리
- advisor1 포인트컷만 만족 ▶ 프록시1개 생성, 프록시에 advisor1 만 포함
- advisor1 , advisor2 포인트컷을 모두 만족 ▶ 프록시1개 생성, 프록시에 advisor1 , advisor2 모두 포함
- advisor1 , advisor2 포인트컷을 모두 만족하지 않음 ▶ 프록시가 생성되지 않음
1. Advisor만 스프링 빈 등록하면 된다.
2. Advisor = Pointcut + Advice
'Spring > Advanced' 카테고리의 다른 글
스프링 AOP 개념 (0) | 2023.08.21 |
---|---|
@Aspect AOP (0) | 2023.08.21 |
스프링이 지원하는 프록시 (0) | 2023.08.17 |
동적 프록시 기술 (0) | 2023.08.16 |
프록시 패턴과 데코레이터 패턴 (0) | 2023.08.14 |