참고자료
스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런
김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기
www.inflearn.com
@Aspect 애노테이션을 붙이고 그 안에 @Around, @AfterReturning, @Before, ... 등등의 애노테이션만 달아주면 어떻게 여기서 작성한 포인트컷에 해당하는 객체들이 프록시로 등록되고 그 안에 로직이 어드바이스가 될까? 즉, 결국 그 메서드가 어떻게 Advisor가 되어 프록시 팩토리에 의해 프록시를 만들까? 정답은 이 빈 후처리기다.
빈 후처리기
스프링에서 @Bean이나 컴포넌트 스캔으로 스프링 빈을 등록하면 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 그리고 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.
BeanPostProcessor는 번역하면 빈 후처리기로, 이름 그대로 빈을 생성한 후에 무언가를 처리하는 용도로 사용한다.
빈 후처리기는 강력하다. 객체를 조작하는게 가능하고 완전히 다른 객체로 바꿔치기 하는 것도 가능하다. 빈 후처리기 과정을 자세히 살펴보자.
빈 후처리기 과정
1. 생성: 스프링 빈 대상이 되는 객체를 생성한다 (@Bean, 컴포넌트 스캔 모두 포함)
2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 빈 후처리기에 전달한다.
3. 후 처리 작업: 빈 후처리기는 전달된 스프링 빈 객체를 조작하거나 다른 객체로 바꿔치기 할 수 있다.
4. 등록: 빈 후처리기는 빈을 반환한다. 전달된 빈을 그대로 반환하면 해당 빈이 등록되고, 바꿔치기 하면 다른 객체가 빈 저장소에 등록된다.
여기서 '3. 후 처리 작업'을 보면 스프링 빈 객체를 조작 또는 바꿔치기 한다고 되어 있는데 이 말은 무슨 말일까? 다음 그림을 보자.
빈 후처리기에서 객체 A를 객체 B로 바꿔버린 모습을 볼 수 있다. 그리고 그 바꾼 객체 B를 스프링 빈 저장소에 전달하면 최초 객체 A가 객체 B로 최종 등록된다. 이것을 객체를 조작 또는 바꿔치기한다 말한다.
빈 후처리기를 사용하려면 BeanPostProcessor 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
이 BeanPostProcessor 인터페이스는 두 개의 메서드를 제공한다.
- postProcessBeforeInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서
- postProcessAfterInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서
이 빈후처리기를 통해 특정 객체를 다른 객체로 변경해버리는 예시 코드를 작성해보자.
빈 후처리기 - 예제 코드1
BeanPostProcessorTest
package cwchoiit.springadvanced.proxy.postprocessor;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.*;
@Slf4j
public class BasicTest {
@Test
void basicConfig() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BasicConfig.class);
A a = applicationContext.getBean("beanA", A.class);
a.helloA();
assertThatThrownBy(() -> applicationContext.getBean(B.class))
.isInstanceOf(NoSuchBeanDefinitionException.class);
}
@Configuration
static class BasicConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
}
static class A {
public void helloA() {
log.info("helloA");
}
}
static class B {
public void helloB() {
log.info("helloB");
}
}
}
- new AnnotationConfigApplicationContext(BasicConfig.class) → 스프링 컨테이너를 생성하면서 BasicConfig.class를 넘겨주었다. BasicConfig.class 설정 파일은 스프링 빈으로 등록된다.
빈 등록
@Configuration
static class BasicConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
}
- beanA라는 이름으로 A 객체를 스프링 빈으로 등록했다.
빈 조회
A a = applicationContext.getBean("beanA", A.class);
- beanA라는 이름으로 A 타입의 스프링 빈을 찾는다. 당연히 잘 찾아질 것이다.
assertThatThrownBy(() -> applicationContext.getBean(B.class))
.isInstanceOf(NoSuchBeanDefinitionException.class);
- B 타입의 빈은 등록한 적이 없기 때문에 NoSuchBeanDefinitionException이 발생한다.
빈 후처리기 - 예제 코드2
이번에는 빈 후처리기를 통해서 A 객체를 B 객체로 바꿔치기 해보자.
BeanPostProcessor - 스프링 제공
package org.springframework.beans.factory.config;
import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;
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 인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.
- 참고로, 스프링 부트를 사용하면 이미 수많은 빈 후처리기가 자동 등록된 상태이다. 우리도 필요하면 추가적으로 빈 후처리기를 이렇게 등록할 수 있다.
- postProcessBeforeInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생하기 전에 호출되는 포스트 프로세서이다.
- postProcessAfterInitialization: 객체 생성 이후에 @PostConstruct 같은 초기화가 발생한 다음에 호출되는 포스트 프로세서이다.
BeanPostProcessorTest
package cwchoiit.springadvanced.proxy.postprocessor;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@Slf4j
public class BeanPostProcessorTest {
@Test
void basicConfig() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanPostProcessorConfig.class);
B b = applicationContext.getBean("beanA", B.class);
b.helloB();
assertThatThrownBy(() -> applicationContext.getBean(A.class))
.isInstanceOf(NoSuchBeanDefinitionException.class);
}
@Configuration
static class BeanPostProcessorConfig {
@Bean(name = "beanA")
public A a() {
return new A();
}
@Bean
public AToBPostProcessor toBPostProcessor() {
return new AToBPostProcessor();
}
}
static class A {
public void helloA() {
log.info("helloA");
}
}
static class B {
public void helloB() {
log.info("helloB");
}
}
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를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.
- 이 빈 후처리기는 A 객체를 새로운 B 객체로 바꿔치기한다. 파라미터로 넘어오는 빈 객체가 A의 인스턴스이면 새로운 B 객체를 생성해서 반환한다. 여기서 A 대신에 반환된 값인 B가 스프링 컨테이너에 등록된다. 다음 실행결과를 보면 beanName = beanA, bean = A 객체의 인스턴스가 빈 후처리기에 넘어온 것을 확인할 수 있다.
실행 결과
beanName = beanA, bean = cwchoiit.springadvanced.proxy.postprocessor.BeanPostProcessorTest$A@50916ff4
..B - hello B
- 실행 결과를 보면, 최종적으로 beanA 라는 스프링 빈 이름에 A 객체 대신에 B 객체가 등록된 것을 확인할 수 있다. A는 스프링 빈으로 등록조차 되지 않는다. 이게 빈 후처리기이다.
정리
이렇게 빈 후처리기를 통해 스프링이 빈 저장소(스프링 컨테이너)에 등록할 객체를 강력한 방식으로 조작하고 변경할 수 있다. 여기서 조작이란 메서드를 호출함을 의미한다. 일반적으로 스프링 컨테이너가 등록하는, 특히 컴포넌트 스캔의 대상이 되는 빈들은 중간에 조작할 방법이 없는데 빈 후처리기를 사용하면 개발자가 등록하는 모든 빈을 중간에 조작할 수 있게 된다. 이 말은 빈 객체를 프록시로 교체하는 것도 가능하다는 뜻이다. 약간 느낌이 애노테이션 프로세싱과 비슷하다. 중간에 후킹을 할 수 있는 어떤 포인트를 만들어주는 것이 유사하다.
실제로 스프링은 AOP를 구현할 때 빈 후처리기를 통해 컴포넌트 스캔으로 등록되는 빈 중 프록시로 만들어져야 하는 객체를 포인트컷을 통해 찾아 프록시로 변경하여 등록해준다.
참고로, @PostConstruct는 스프링 빈 생성 이후에 빈을 초기화 하는 역할을 한다. 그런데 생각해보면 빈의 초기화라는 것이 단순히 @PostConstruct 애노테이션이 붙은 초기화 메서드를 한번 호출만 하면 된다. 쉽게 이야기해서 생성된 빈을 한번 조작하는 것이다. 따라서 빈을 조작하는 행위를 하는 적절한 빈 후처리기가 있으면 될 것 같다. 스프링은 CommonAnnotationBeanPostProcessor라는 빈 후처리기를 자동으로 등록하는데, 여기에서 @PostConstruct 애노테이션이 붙은 메서드를 호출한다.
빈 후처리기 - 적용
빈 후처리기를 사용해서 실제 객체 대신 프록시를 스프링 빈으로 등록해보자. 이렇게 하면 수동으로 등록하는 빈은 물론이고, 컴포넌트 스캔을 사용하는 빈까지 모두 프록시를 적용할 수 있다. 더 나아가서 설정 파일에 있는 수많은 프록시 생성 코드도 한번에 제거할 수 있다.
일단 빈을 프록시로 바꿔치기하는 우리만의 빈 후처리기를 만들어보자.
PackageLogTracePostProcessor
package cwchoiit.springadvanced.proxy.config.v4_postprocessor.postprocessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
@Slf4j
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(this.basePackage)) {
return bean;
}
ProxyFactory proxyFactory = new ProxyFactory(bean);
proxyFactory.addAdvisor(this.advisor);
Object proxy = proxyFactory.getProxy();
log.info("target = {}, proxy = {}", bean.getClass(), proxy.getClass());
return proxy;
}
}
- 이 빈 후처리기는 원본 객체를 프록시 객체로 변환하는 역할을 한다. 이때 프록시 팩토리를 사용한다. 프록시 팩토리는 advisor가 필수이기 때문에 이 부분은 외부에서 주입받도록 했다.
- 모든 스프링 빈들에 프록시를 적용할 필요도 해서도 안된다. 여기서는 특정 패키지와 그 하위에 위치한 스프링 빈들만 프록시를 적용한다. 그러기 위해 외부에서 특정 패키지 경로를 주입받는다. 외부에서 프록시가 적용되길 원하는 특정 패키지 경로를 주입받고 postProcessAfterInitialization 메서드에서 이 패키지 또는 이 패키지 하위 경로인지 체크한다.
BeanPostProcessorConfig
package cwchoiit.springadvanced.proxy.config.v4_postprocessor;
import cwchoiit.springadvanced.proxy.config.AppV1Config;
import cwchoiit.springadvanced.proxy.config.AppV2Config;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.proxy.config.v4_postprocessor.postprocessor.PackageLogTracePostProcessor;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Slf4j
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class BeanPostProcessorConfig {
@Bean
public PackageLogTracePostProcessor packageLogTracePostProcessor(LogTrace trace) {
return new PackageLogTracePostProcessor("cwchoiit.springadvanced.proxy.app", getAdvisor(trace));
}
private Advisor getAdvisor(LogTrace trace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);
return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
}
}
- 빈 후처리기를 구현했으면 스프링 빈으로 등록해야 한다. 빈 후처리기는 스프링 빈으로 등록만 하면 자동으로 동작한다. 여기에 프록시를 적용할 패키지 정보와 어드바이저를 넘겨준다.
- 이제 프록시를 생성하는 코드가 설정 파일에는 필요가 없다. 순수한 빈 등록만 고민하면 된다. 프록시를 생성하고 프록시를 스프링 빈으로 등록하는 것은 빈 후처리기가 모두 처리해준다.
실행 결과
...
2025-01-09T16:03:55.688+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration, bean = class org.springframework.boot.autoconfigure.http.JacksonHttpMessageConvertersConfiguration
2025-01-09T16:03:55.688+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration, bean = class org.springframework.boot.autoconfigure.http.client.HttpClientAutoConfiguration
2025-01-09T16:03:55.689+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.http.client-org.springframework.boot.autoconfigure.http.client.HttpClientProperties, bean = class org.springframework.boot.autoconfigure.http.client.HttpClientProperties
2025-01-09T16:03:55.691+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = clientHttpRequestFactoryBuilder, bean = class org.springframework.boot.http.client.JdkClientHttpRequestFactoryBuilder
2025-01-09T16:03:55.692+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = clientHttpRequestFactorySettings, bean = class org.springframework.boot.http.client.ClientHttpRequestFactorySettings
2025-01-09T16:03:55.693+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties, bean = class org.springframework.boot.autoconfigure.info.ProjectInfoProperties
2025-01-09T16:03:55.693+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration, bean = class org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
2025-01-09T16:03:55.694+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration, bean = class org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration
2025-01-09T16:03:55.694+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties, bean = class org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties
2025-01-09T16:03:55.695+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$ThreadPoolTaskSchedulerBuilderConfiguration
2025-01-09T16:03:55.695+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
2025-01-09T16:03:55.696+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = threadPoolTaskSchedulerBuilder, bean = class org.springframework.boot.task.ThreadPoolTaskSchedulerBuilder
2025-01-09T16:03:55.697+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingConfigurations$SimpleAsyncTaskSchedulerBuilderConfiguration
2025-01-09T16:03:55.698+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = simpleAsyncTaskSchedulerBuilder, bean = class org.springframework.boot.task.SimpleAsyncTaskSchedulerBuilder
2025-01-09T16:03:55.698+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration, bean = class org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
2025-01-09T16:03:55.698+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration, bean = class org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration
2025-01-09T16:03:55.698+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = httpMessageConvertersRestClientCustomizer, bean = class org.springframework.boot.autoconfigure.web.client.HttpMessageConvertersRestClientCustomizer
2025-01-09T16:03:55.699+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = restClientSsl, bean = class org.springframework.boot.autoconfigure.web.client.AutoConfiguredRestClientSsl
2025-01-09T16:03:55.699+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = restClientBuilderConfigurer, bean = class org.springframework.boot.autoconfigure.web.client.RestClientBuilderConfigurer
2025-01-09T16:03:55.699+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration, bean = class org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
2025-01-09T16:03:55.700+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration, bean = class org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
2025-01-09T16:03:55.700+09:00 INFO 72763 --- [springadvanced] [ main] c.s.p.c.v.p.PackageLogTracePostProcessor : param beanName = multipartResolver, bean = class org.springframework.web.multipart.support.StandardServletMultipartResolver
...
- 무수히 많은 로그가 찍힌다. 왜일까? 자동으로 등록되는 스프링 빈이 이렇게 많다는 것이다. 그리고 빈 후처리기는 그 모든 빈들에 대해서 호출되기 때문에 당연히 이렇게 많은 로그가 찍힌다. 그래서 위에서 특정 패키지로 제한한 것이다.
- 우리의 프록시로 등록된 녀석들을 확인하기 위해 /v1/request, /v2/request, /v3/request 를 호출해보자. 아래와 같이 로그가 잘 찍히는 것을 확인할 수 있다.
2025-01-09T16:04:03.824+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace : [fee8454e] OrderControllerV1.request()
2025-01-09T16:04:03.824+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace : [fee8454e] |--->OrderServiceV1.orderItem()
2025-01-09T16:04:03.824+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace : [fee8454e] | |--->OrderRepositoryV1.save()
2025-01-09T16:04:04.825+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace : [fee8454e] | |<---OrderRepositoryV1.save() time=1001ms
2025-01-09T16:04:04.826+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace : [fee8454e] |<---OrderServiceV1.orderItem() time=1002ms
2025-01-09T16:04:04.826+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-1] c.s.trace.logtrace.ThreadLocalLogTrace : [fee8454e] OrderControllerV1.request() time=1002ms
2025-01-09T16:04:15.400+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace : [9f510d2e] OrderControllerV2.request()
2025-01-09T16:04:15.401+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace : [9f510d2e] |--->OrderServiceV2.orderItem()
2025-01-09T16:04:15.401+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace : [9f510d2e] | |--->OrderRepositoryV2.save()
2025-01-09T16:04:16.402+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace : [9f510d2e] | |<---OrderRepositoryV2.save() time=1001ms
2025-01-09T16:04:16.402+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace : [9f510d2e] |<---OrderServiceV2.orderItem() time=1001ms
2025-01-09T16:04:16.402+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-3] c.s.trace.logtrace.ThreadLocalLogTrace : [9f510d2e] OrderControllerV2.request() time=1002ms
2025-01-09T16:04:23.327+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace : [9e82fc0e] OrderControllerV3.request()
2025-01-09T16:04:23.327+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace : [9e82fc0e] |--->OrderServiceV3.orderItem()
2025-01-09T16:04:23.327+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace : [9e82fc0e] | |--->OrderRepositoryV3.save()
2025-01-09T16:04:24.328+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace : [9e82fc0e] | |<---OrderRepositoryV3.save() time=1001ms
2025-01-09T16:04:24.328+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace : [9e82fc0e] |<---OrderServiceV3.orderItem() time=1001ms
2025-01-09T16:04:24.328+09:00 INFO 72763 --- [springadvanced] [nio-8080-exec-4] c.s.trace.logtrace.ThreadLocalLogTrace : [9e82fc0e] OrderControllerV3.request() time=1001ms
빈 후처리기 - 정리
빈 후처리기는 스프링 빈을 생성한 후에 초기화 하기 직전 또는 직후 이 빈을 조작하거나 바꿔치기할 수 있는 기술이다. 그리고 이 빈 후처리기를 우리가 직접 만들어서 어떤 문제를 해결했는지 보자.
문제1 - 너무 많은 설정
프록시를 직접 스프링 빈으로 등록하는 ProxyFactoryConfigV1, ProxyFactoryConfigV2와 같은 설정 파일은 프록시 관련 설정이 지나치게 많다는 문제가 있다. 예를 들어, 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 프록시 설정 코드가 들어가야 한다.
문제2 - 컴포넌트 스캔
V3처럼 컴포넌트 스캔을 사용하는 경우, 프록시 설정 코드를 직접 작성하는 것으로 프록시를 등록하지도 못한다. 왜냐하면 컴포넌트 스캔으로 이미 스프링 컨테이너에 실제 객체를 스프링 빈으로 등록을 다 해버린 상태이기 때문이다.
문제 해결
빈 후처리기를 사용해서 프록시를 생성하는 부분을 하나로 집중할 수 있다. (바로 빈 후처리기를 작성하는 코드).
그리고 컴포넌트 스캔처럼 스프링이 직접 대상을 빈으로 등록하는 경우에도 중간에 빈 등록 과정을 가로채서 원본 대신에 프록시를 스프링 빈으로 등록할 수 있다. 덕분에 애플리케이션에 수많은 스프링 빈이 추가되어도 프록시와 관련된 코드는 전혀 변경하지 않아도 된다. 그리고 컴포넌트 스캔을 사용해도 프록시가 모두 적용된다.
하지만 개발자의 욕심은 끝이 없다.
스프링은 프록시를 생성하기 위한 빈 후처리기를 이미 만들어서 제공한다.
중요!
프록시의 적용 대상 여부를 여기서는 간단하게 패키지를 기준으로 설정했다. 그런데 잘 생각해보면 포인트컷을 사용하면 더 깔끔할 것 같다. 포인트컷은 이미 클래스, 메서드 단위의 필터 기능을 가지고 있기 때문에, 프록시 적용 대상 여부를 정밀하게 설정할 수 있다. 참고로 어드바이저는 포인트컷을 가지고 있다. 따라서 어드바이저를 통해 포인트컷을 확인할 수 있다. 스프링 AOP는 포인트컷을 사용해서 프록시 적용 대상 여부를 체크한다. 결과적으로 포인트컷은 다음 두 곳에서 사용된다.
- 프록시 적용 대상 여부를 체크해서 꼭 필요한 곳에만 프록시를 적용한다. (빈 후처리기 - 자동 프록시 생성)
- 프록시의 어떤 메서드가 호출 되었을 때 어드바이스를 적용할 지 판단한다. (프록시 내부)
스프링이 제공하는 빈 후처리기1
결론을 먼저 말하자면, 저렇게 직접 빈 후처리기를 만들 일은 없다. 적어도 프록시를 만들기 위해서라면 말이다.
왜냐? 스프링이 저 역할을 하는 빈 후처리기를 이미 다 만들어서 제공해주기 때문이다. 그것들을 사용하려면 다음 라이브러리를 추가해야한다.
implementation 'org.springframework.boot:spring-boot-starter-aop'
- 이 라이브러리를 추가하면 aspectjweaver 라는 aspectJ 관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다. 스프링 부트가 없던 시절에는 @EnableAspectJAutoProxy를 직접 사용해야 했는데, 이 부분을 스프링 부트가 자동으로 처리해준다. 스프링 부트가 활성화하는 빈은 AopAutoConfiguration인데 이 빈을 활성화하면 자동 프록시 생성기라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.
AnnotationAwareAspectJAutoProxyCreator
- 이 녀석이 스프링 부트가 자동으로 스프링 빈으로 등록해주는 빈 후처리기다.
- 이름 그대로 자동으로 프록시를 생성해주는 빈 후처리기. 이 빈 후처리기는 스프링 빈으로 등록된 Advisor들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.
- Advisor안에는 Pointcut과 Advice가 이미 모두 포함되어 있다. 따라서 Advisor만 알고 있으면 그 안에 있는 Pointcut으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다. 그리고 Advice로 부가 기능을 적용하면 된다.
- 그리고 @Aspect도 자동으로 인식해서 프록시를 만들고 AOP를 적용해준다.
자동 프록시 생성기의 작동 과정
1. 생성: 스프링이 스프링 빈 대상이 되는 객체를 생성한다. (@Bean, 컴포넌트 스캔 모두 포함)
2. 전달: 생성된 객체를 빈 저장소에 등록하기 직전에 저 AnnotationAwareAspectJAutoProxyCreator 빈 후처리기에 전달한다.
3. 모든 Advisor 빈 조회: AnnotationAwareAspectJAutoProxyCreator 빈 후처리기는 스프링 컨테이너에서 모든 Advisor를 조회한다.
4. 프록시 적용 대상 체크: 앞서 조회한 Advisor에 포함되어 있는 포인트컷을 사용해서 해당 객체가 프록시를 적용할 대상인지 아닌지 판단한다. 이 때 객체의 클래스 정보는 물론이고, 해당 객체의 모든 메서드를 포인트컷에 하나하나 모두 매칭해본다. 하나라도 조건이 만족하면 프록시 적용 대상이 된다. 예를 들어 10개의 메서드 중에 하나만 포인트컷 조건에 만족해도 프록시 적용 대상이 된다.
5. 프록시 생성: 프록시 적용 대상이면 프록시 팩토리를 사용해 프록시를 생성하고 반환해서 프록시를 스프링 빈으로 등록한다. 만약 프록시 적용 대상이 아니라면 원본 객체를 반환해서 원본 객체를 스프링 빈으로 등록한다.
6. 빈 등록: 반환된 객체는 스프링 빈으로 등록된다.
코드를 통해 바로 적용해보자.
AutoProxyConfig
package cwchoiit.springadvanced.proxy.config.v5_autoproxy;
import cwchoiit.springadvanced.proxy.config.AppV1Config;
import cwchoiit.springadvanced.proxy.config.AppV2Config;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor getAdvisor(LogTrace trace) {
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("request*", "order*", "save*");
LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);
return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
}
}
- 이전에 했던 방식을 잠깐 떠올려보자. 분명 내가 직접 만든 빈 후처리기를 빈으로 등록했다. 그런데 이제 그럴 필요가 없다. 왜냐? 스프링이 프록시를 만들어주는 빈 후처리기를 이미 자동으로 등록해주기 때문이다.
- 그래서 나는 Advisor만 빈으로 등록하면 된다. 그럼 이 안에 있는 포인트컷을 참고해서 프록시로 만들어야 할 녀석들이 누구인지 AnnotationAwareAspectJAutoProxyCreator는 알아서 판단 후 프록시로 만들어준다.
강조: 포인트컷은 2가지에 사용된다.
1. 프록시 적용 여부 판단 - 프록시를 생성하는 단계
- AnnotationAwareAspectJAutoProxyCreator는 등록된 모든 Advisor, @Aspect를 찾고 그 안에 포인트컷을 사용해서 해당 빈이 프록시를 생성할 필요가 있는지 없는지 체크한다.
- 클래스 + 메서드 조건을 모두 비교한다. 이 때 모든 메서드를 체크하는데, 포인트컷 조건에 하나하나 매칭해본다. 만약 조건에 맞는 것이 하나라도 있으면 프록시를 생성하고, 조건에 맞는 것이 하나도 없을 땐 프록시를 생성하지 않는다.
2. 어드바이스 적용 여부 판단 - 프록시를 사용하는 단계
- 프록시가 호출되었을 때 부가 기능인 어드바이스를 적용할지 말지 포인트컷을 보고 판단한다. 즉, 특정 메서드가 호출됐을 때 그 메서드가 포인트컷 조건에 만족하는 메서드인지 확인 한다는 뜻이다.
프록시를 모든 곳에 생성하는 것은 비용 낭비라고 했고 AnnotationAwareAspectJAutoProxyCreator는 포인트컷으로 한번 필터링해서 어드바이스가 사용될 가능성이 있는 곳에만 프록시를 생성한다고 했다. 그럼 만약 여러개의 포인트컷 조건을 만족한다고 하면 프록시는 여러개가 생길까? 아니다. 프록시는 딱 하나만 생기고 그 안에 여러 어드바이저가 등록되는 것이다.
AnnotationAwareAspectJAutoProxyCreator 상황별 정리
- advisor1의 포인트컷만 만족: 프록시 1개 생성, 프록시에 advisor1만 포함
- advisor1, advisor2의 포인트컷을 모두 만족: 프록시 1개 생성, 프록시에 advisor1, advisor2 모두 포함
- advisor1, advisor2의 포인트컷을 모두 만족하지 않음: 프록시가 생성되지 않음
스프링이 제공하는 빈 후처리기2
위 방식대로 애플리케이션을 실행하면 원하는 기능은 제대로 동작하는데 애플리케이션 로딩 로그를 자세히 보면 이상하다.
2025-01-09T18:17:51.287+09:00 INFO 79454 --- [springadvanced] [ main] c.s.trace.logtrace.ThreadLocalLogTrace : [af9b2e0c] WebMvcConfigurationSupport.requestMappingHandlerMapping()
2025-01-09T18:17:51.290+09:00 INFO 79454 --- [springadvanced] [ main] c.s.trace.logtrace.ThreadLocalLogTrace : [af9b2e0c] WebMvcConfigurationSupport.requestMappingHandlerMapping() time=3ms
2025-01-09T18:17:51.340+09:00 INFO 79454 --- [springadvanced] [ main] c.s.trace.logtrace.ThreadLocalLogTrace : [196ef571] WebMvcConfigurationSupport.requestMappingHandlerAdapter()
2025-01-09T18:17:51.346+09:00 INFO 79454 --- [springadvanced] [ main] c.s.trace.logtrace.ThreadLocalLogTrace : [196ef571] WebMvcConfigurationSupport.requestMappingHandlerAdapter() time=6ms
- 기대하지 않은 로그들이 막 올라온다. 그 이유는 지금 사용한 포인트컷이 단순히 메서드 이름에 "request*", "order*", "save*"만 포함되어 있으면 매칭 된다고 판단하기 때문이다.
- 결국, 스프링이 내부에서 사용하는 자동으로 등록되는 빈에도 메서드 이름에 저 중 하나가 들어가 있으면 프록시로 만들어지고 어드바이스도 적용되는 것이다.
- 결론적으로, 더 확실하고 매우 정밀한 포인트컷이 필요하다.
AspectJExpressionPointcut
AspectJ라는 AOP에 특화된 포인트컷 표현식을 적용할 수 있다. 이게 이제 실무에서 가장 많이 사용될 녀석이다.
AutoProxyConfig
package cwchoiit.springadvanced.proxy.config.v5_autoproxy;
import cwchoiit.springadvanced.proxy.config.AppV1Config;
import cwchoiit.springadvanced.proxy.config.AppV2Config;
import cwchoiit.springadvanced.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AutoProxyConfig {
@Bean
public Advisor advisor2(LogTrace trace) {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* cwchoiit.springadvanced.proxy.app..*(..)) &&" +
" !execution(* cwchoiit.springadvanced.proxy.app..noLog(..))");
LogTraceAdvice logTraceAdvice = new LogTraceAdvice(trace);
return new DefaultPointcutAdvisor(pointcut, logTraceAdvice);
}
}
- 위와 같이 작성하는 방법이 바로 AspectJExpressionPointcut이다. 매우 정밀하게 포인트컷을 작성할 수 있다.
- 이 문법에 대한 내용은 아예 별도의 포스팅을 만들 예정이다.
'Spring Advanced' 카테고리의 다른 글
스프링 AOP Part. 1 (0) | 2023.12.29 |
---|---|
AOP와 @Aspect (0) | 2023.12.29 |
Advisor, Advice, Pointcut (0) | 2023.12.15 |
CGLIB,스프링이 지원하는 ProxyFactory (0) | 2023.12.14 |
Proxy/Decorator Pattern 2 (JDK 동적 프록시) (0) | 2023.12.14 |