본문은 인프런의 [스프링 핵심 원리 - 고급편]를 수강하고 정리한 글입니다. 필요에 따라 생략/수정된 부분이 있을 수 있으며, 내용이 추후 변경될 수 있습니다.
스프링 핵심 원리 - 고급편
1. 쓰레드 로컬
2. 템플릿 메서드 패턴과 콜백 패턴
3. 프록시 패턴과 데코레이터 패턴
4. 동적 프록시 기술
5. 스프링이 지원하는 프록시
6. 빈 후처리기
7. @Aspect AOP
8. 스프링 AOP 개념
9. 스프링 AOP 구현
10. 포인트컷
11. 실무 주의사항
전 챕터를 통해 스프링이 제공하는 프록시 전반에 대해 살펴봤다. 프록시 팩토리를 사용함으로써 더욱 편리하게 생성할 수 있게 되었고, 어드바이저/어드바이스/포인트컷 개념 덕에 어떤 부가 기능 로직과 그 대상에 대해 명확하게 이해할 수 있었다. 그러나 늘 그렇듯 몇 가지 문제가 남아있다.
1) 너무 많은 설정
- 프록시를 생성하는 코드가 너무 많고 중복이 많다
- 프록시 적용 코드 + 빈을 수동 등록하는 코드를 작성하기 귀찮다
2) 컴포넌트 스캔 사용 불가
- 컴포넌트 스캔을 사용하면 실제 객체를 스프링 컨테이너에 빈으로 등록하기 때문에 위 방법으로 프록시 적용이 불가능하다
- 프록시를 적용하기 위해선 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다
빈 후처리기를 통해 이 두 가지 문제를 한 번에 해결할 수 있다.
1. 빈 후처리기
1) 빈 후처리기 소개
일반적으로 스프링 빈은 객체 생성 및 초기화 후에 곧바로 스프링 빈 저장소에 저장된다,
빈 후처리기(BeanPostProcessor)는 빈으로 등록하기 위해 생성한 객체를 빈 저장소에 등록하기 전에 조작하는 용도로 사용된다. 빈 후처리기를 통해 객체를 조작하거나 완전히 다른 객체로 바꿔치기 하는 것도 가능하다. 다시 말해, 빈 후처리기를 통해 실제 객체를 프록시로 교체할 수 있다.
참고로 빈 생성 이후에 초기화 역할을 수행하는 @PostConstruct가 붙은 메서드도 스프링 내부적으로 등록된 CommonAnnotationBeanPostProcessor에 의해 호출되는 방식으로 동작한다
2) 빈 후처리기 예제
BeanPostProcessor 인터페이스
public interface BeanPostProcessor {
// 객체 생성 이후 초기화가 발생하기 전에 호출되는 포스트 프로세서
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
// 객체 생성 이후 초기화가 발생한 후에 호출되는 포스트 프로세서
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
- 빈 후처리기를 사용하기 위해선 BeanPostProcessor 인터페이스를 구현한 후에 스프링 빈으로 등록하면 된다
- 호출시점에 따라 각각 다른 메서드를 구현하는데, 이때 메서드는 default로 선언되어 있어 기본적으로 bean을 리턴한다.
테스트 코드
public class BeanPostProcessorTest {
@Test
void basicConfig() {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
//B 타입인 beanA를 찾는다.
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
//A는 빈으로 등록되지 않는다.
Assertions.assertThrows(NoSuchBeanDefinitionException.class, () -> applicationContext.getBean(A.class));
}
@Configuration
static class BeanPostProcessorConfig {
//B객체는 빈으로 등록되지 않은 상태
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AToBPostProcessor helloPostProcessor() {
return new AToBPostProcessor();
}
}
// 직접 구현한 빈 후처리기
static class AToBPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
//beanA 이름으로 B 객체가 빈으로 등록된다.
if (bean instanceof A) {
return new B();
}
return bean;
}
}
}
- AToBPostProcessor
- BeanPostProcessor 인터페이스를 구현한 빈 후처리기
- A 객체를 B 객체로 바꿔치기 한다
3) 빈 후처리기 적용
빈 후처리기를 사용하면 실제 객체 대신 프록시를 간편하게 스프링 빈으로 등록할 수 있다. 여기에는 컴포넌트 스캔을 사용하는 빈도 포함된다. 또한, 설정 파일 내부의 프록시 생성 코드도 모두 제거할 수 있다.
PackageLogTracePostProcessor - BeanPostProcessor를 구현
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("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();
return proxy;
}
}
- 원본 객체를 프록시 객체로 변환하는 역할을 하고, 프록시 팩토리 생성을 위한 advisor를 주입받는다
- 특정 빈들에 한해 프록시 적용
- basePackage: 특정 패키지의 하위 컴포넌트만 스캔
- 스프링 컨테이너에는 원본 객체 대신에 프록시 객체가 스프링 빈으로 등록되므로 원본 객체는 스프링 빈으로 등록되지 않는다
BeanPostProcessorConfig - 설정 파일
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {
// 빈 후처리기를 빈으로 등록한다
// 빈 후처리기는 스프링 빈으로 등록되면 자동으로 동작한다
@Bean
public PackageLogTracePostProcessor logTracePostProcessor(LogTrace logTrace) {
return new PackageLogTracePostProcessor("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);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- 이제 프록시가 빈 후처리기를 통해서 생성되므로 설정 파일에는 프록시를 생성하는 코드가 필요없다
- 프록시를 빈으로 등록하는 것 또한 빈 후처리기가 처리해준다
4) 정리
Good
- 프록시를 직접 등록하는 코드가 사라졌다
- 컴포넌트 스캔을 통해 등록한 v3 빈들도 프록시를 적용할 수 있다
Bad
- 스프링 부트가 기본으로 등록하는 수많은 빈들이 프록시로 등록된다
2. 스프링이 제공하는 빈 후처리기
1) 스프링이 제공하는 빈 후처리기 소개
앞서 빈 후처리기를 직접 구현하는 예제를 살펴봤지만, 스프링은 기본적으로 빈 후처리기를 제공하고 있다. 이를 사용하기 위해선 build.gradle에 의존성을 추가해줘야 한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
- aspectjweaver라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다
- 원래는 @EnableAspectJAutoProxy를 직접 추가해야 하는데, 이를 스프링 부트가 자동으로 처리해준다.
- 이로써 스프링의 빈 후처리기를 사용할 준비가 되었다
AutoProxyCreator
- 스프링 부트의 자동 설정으로 AnnotationAwareAspectJAutoProxyCreateor라는 빈 후처리기가 스프링 빈에 자동으로 등록된다
- AnnotationAwareAspectJAutoProxyCreateor는@AspectJ와 관련된 AOP 기능도 자동으로 찾아서 처리해준다
- Advisor뿐만 아니라 @Aspect도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다
- 자동으로 프록시를 생성해주는 빈 후처리기 (스프링부트에 자동으로 등록)
- 스프링 빈으로 등록된 Advisor들을 자동으로 찾고, 필요한 곳에 적용해준다 (내부에 포인트컷이 있으므로)
자동 프록시 생성기의 작동 과정
1) 생성
- 빈 대상이 되는 객체를 생성한다
- 이때 대상은 @Bean과 @Component을 모두 포함한다
2) 전달
- 생성된 객체가 빈 저장소에 등록되기 직전에 빈 후처리기에 전달된다
3) 모든 Advisor 빈 조회
- 자동 프록시 생성기(= 스프링의 빈 후처리기)는 스프링 컨테이너에서 모든 Advisor를 조회한다
4) 프록시 적용 대상 체크
- 조회한 Advisor에 포함된 포인트컷을 통해 프록시 적용 대상 여부를 판단한다
- 메서드 하나라도 포인트컷 조건을 만족하면 프록시 적용 대상이 된다
5) 프록시 생성
- 프록시 적용 대상이면 프록시를 생성하고 반환한다
- 만약 적용 대상이 아니면 원본 객체를 반환한다
6) 빈 등록
- 반환된 객체(프록시 or 원본)를 스프링 빈으로 등록한다
2) 스프링이 제공하는 빈 후처리기 예제
포인트컷의 사용 지점엔 2가지가 있다. 포인트컷을 사용할 땐 항상 이를 유념해야 한다.
1. 프록시 적용 여부 판단 - 생성 단계
- 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용 (빈 후처리기 - 자동 프록시 생성)
2. 어드바이스 적용 여부 판단 - 사용 단계
- 프록시의 어떤 메서드에 어드바이스를 적용할 지 판단한다 (프록시 내부)
포인트컷을 적절히 사용하면 의도치 않게 빈이 프록시로 등록되는 문제를 해결할 수 있다. 또한, 프록시 내부의 특정 메서드 호출시에만 Advice가 실행되게끔 할 수도 있다. 아래 코드를 보자.
표현식을 통해 패키지명과 메서드명을 포인트컷에 매칭/제외한 것을 확인할 수 있다.
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor advisor3(LogTrace logTrace) {
//pointcut
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
// hello.proxy.app 패키지와 하위 패키지의 모든 메서드는 포인트컷에 매칭하되 noLog() 메서드는 제외하라
pointcut.setExpression("execution(* hello.proxy.app..*(..)) && !execution(* hello.proxy.app..noLog(..))");
//advice
LogTraceAdvice advice = new LogTraceAdvice(logTrace);
return new DefaultPointcutAdvisor(pointcut, advice);
}
}
- AspectJExpressionPointcut
- AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있는 포인트컷이다
- 구체적인 사용법은 뒤에서 살펴볼 예정이다
참고로 임의의 스프링 빈이 여러 Advisor의 포인트컷을 모두 만족한다 하더라도 프록시는 단 한 개만 생성된다. 프록시 팩토리가 생성하는 프록시 내부에 여러 Advisor를 포함할 수 있기 때문이다. 스프링 AOP도 같은 방식으로 동작하므로 꼭 기억하자.
3) 정리
- 자동 프록시 생성기(=스프링이 제공하는 빈 후처리기) 덕분에 프록시 적용은 매우 쉬워졌다
- Advisor만 스프링 빈으로 등록하면 된다
'Spring > 스프링 핵심 원리 - 고급편' 카테고리의 다른 글
[스프링 핵심 원리 - 고급편] 스프링 AOP 개념 (0) | 2023.03.08 |
---|---|
[스프링 핵심 원리 - 고급편] @Aspect AOP (0) | 2023.03.07 |
[스프링 핵심 원리 - 고급편] 스프링이 지원하는 프록시 (0) | 2023.03.06 |
[스프링 핵심 원리 - 고급편] 동적 프록시 기술 (0) | 2023.02.21 |
[스프링 핵심 원리 - 고급편] 프록시 패턴과 데코레이터 패턴 (0) | 2023.02.17 |