이전 문제점
- 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용
- 두 기술을 함께 사용할 때 부가 기능을 제공하기 위해 JDK 동적 프록시가 제공하는
InvocationHandler
와 CGLIB가 제공하는MethodInterceptor
를 각각 중복으로 만들어서 관리- 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공
스프링은 유사한 구체적인 기술들이 있을 때, 그것들을 통합해서 일관성 있게 접근할 수 있고, 더욱 편리하게 사용할 수 있는 추상화된 기술을 제공한다.
스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(ProxyFactory
)라는 기능을 제공한다.
=> 이 프록시 팩토리 하나로 편리하게 동적 프록시를 생성할 수 있다.
두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler
와 CGLIB가 제공하는 MethodInterceptor
를 각각 중복으로 따로 만들 필요없이 Advice
만 만들면 된다. InvocationHandler
나 MethodInterceptor
는 Advice
를 호출하게 된다.
@Slf4j
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}ms", resultTime);
return result;
}
}
Object result = invocation.proceed();
invocation.proceed()
를 호출하면 target
클래스를 호출하고 그 결과를 받는다.target
클래스의 정보는 MethodInvocation invocation
안에 모두 포함되어 있다.@Slf4j
public class ProxyFactoryTest {
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.save();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
}
new ProxyFactory(target)
: 프록시 팩토리 생성할 때, 생성자에 프록시의 호출 대상을 함께 넘겨준다. 프록시 팩토리는 이 인스턴스의 정보를 기반으로 프록시를 만들어낸다. 인스턴스에 인터페이스가 있다면 JDK 동적 프록시를 기본으로 사용하고 인터페이스가 없고 구체 클래스만 있다면 CGLIB를 통해서 동적 프록시를 생성한다. proxyFactory.addAdvice(new TimeAdvice())
: 프록시 팩토리를 통해서 만든 프록시가 사용할 부가 기능 로직을 설정한다. InvocationHandler
와 MethodInterceptor
의 개념과 유사하다. 프록시가 제공하는 부가 기능 로직 Advice
실행 결과
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy13
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
Advice
하나로 편리하게 사용할 수 있다.포인트컷(
Pointcut
) : 어디에 부가 기능을 적용할지, 어디에 부가 기능을 적용하지 않을지 판단하는 필터링 로직
어드바이스(Advice
) : 프록시가 호출하는 부가 기능이다.
어드바이저(Advisor
) : 하나의 포인트컷과 하나의 어드바이스를 가지고 있는것 (포인트컷1 + 어드바이스1)
Advice
를 어디(Pointcut
)에 할 것인가?Advisor
를 어디(Pointcut
)에 조언(Advice
)에 해야할지 알고 있다.@Slf4j
public class AdvisorTest {
@Test
void advisorTest1() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(Pointcut.TRUE, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
}
new DefaultPointcutAdvisor
: Advisor
인터페이스의 가장 일반적인 구현체. 생성자를 통해 하나의 포인트컷과 하나의 어드바이스를 넣어준다.new TimeAdvice()
: 앞서 개발한 TimeAdvice
어드바이스 제공proxyFactory.addAdvisor(advisor)
: 프록시 팩토리에 적용할 어드바이저를 지정한다.실행 결과
#save() 호출
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0ms
#find() 호출
TimeAdvice - TimeProxy 실행
ServiceImpl - find 호출
TimeAdvice - TimeProxy 종료 resultTime=1ms
AOP 적용 수 만큼 프록시가 생성된다고 착각한다.
-> 스프링은 AOP를 적용할 때, 최적화를 진행해서 프록시는 하나만 만들고, 하나의 프록시에 여러 어드바이저를 적용한다.
하나의 target
에 여러 AOP가 동시에 적용되어도, 스프링의 AOP는 target
마다 하나의 프록시만 생성한다.
Bean
이나 컴포넌트 스캔으로 스프링 빈을 등록하면, 스프링은 대상 객체를 생성하고 스프링 컨테이너 내부의 빈 저장소에 등록한다. 이후에는 스프링 컨테이너를 통해 등록한 스프링 빈을 조회해서 사용하면 된다.
스프링이 빈 저장소에 등록할 목적으로 생성한 객체를 빈 저장소에 등록하기 직전에 조작하고 싶다면 빈 후처리기를 사용하면 된다.
객체를 조작할 수도 있고, 다른 객체로 바꿔치기 하는 것도 가능하다.
@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");
}
}
}
@Bean(name = "beanA")
public A a() {
return new A();
}
beanA
라는 이름으로 A
객체를 스프링 빈으로 등록했다.
A a = applicationContext.getBean("beanA", A.class)
beanA
라는 이름으로 A
타입의 스프링 빈을 찾을 수 있다.
applicationContext.getBean(B.class)
B
타입의 객체는 스프링 빈으로 등록한 적이 없기 때문에 스프링 컨테이너에서 찾을 수 없다.
public interface BeanPostProcessor {
Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException
Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException
}
BeanPostProcessor
인터페이스를 구현하고, 스프링 빈으로 등록하면 된다.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;
}
}
}
AtoBPostProcessor
BeanPostProcessor
를 구현하고, 스프링 빈으로 등록하면 스프링 컨테이너가 빈 후처리기로 인식하고 동작한다.bean
) 객체가 A
의 인스턴스이면 새로운 B
객체를 생성해서 반환한다. 여기서 A
대신에 반환된 값인 B
가 스프링 컨테이너에 등록된다. 다음 실행결과를 보면 beanName=beanA
, bean=A
객체의 인스턴스가 빈 후처리기에 넘어온 것을 확인..AToBPostProcessor - beanName=beanA
bean=hello.proxy.postprocessor...A@21362712
..B - hello B
B b = applicationContext.getBean("beanA", B.class)
"beanA"
라는 스프링 빈 이름에 A
객체 대신에 B
객체가 등록된 것을 확인할 수 있다. A
는 스프링 빈으로 등록조차 되지 않는다.빈 후처리기는 빈을 조작하고 변경할 수 있는 후킹 포인트
빈 객체를 프록시로 교체하는 것도 가능
build.gradle - 추가
implementation 'org.springframework.boot:spring-boot-starter-aop'
이 라이브러리를 추가하면
aspectweaver
라는aspectJ
관련 라이브러리를 등록하고, 스프링 부트가 AOP 관련 클래스를 자동으로 스프링 빈에 등록한다.
자동 프록시 생성기 - AutoProxyCreator
AnnotationAwareAspectAutoProxyCreator
라는 빈 후처리기가 스프링 빈에 자동으로 등록된다.Advisor
들을 자동으로 찾아서 프록시가 필요한 곳에 자동으로 프록시를 적용해준다.Advisor
안에는 Pointcut
과 Advice
가 이미 모두 포함되어 있다. 따라서 Advisor
만 알고 있으면 그 안에 있는 Pointcut
으로 어떤 스프링 빈에 프록시를 적용해야 할지 알 수 있다.@Bean
, 컴포넌트 스캔 모두 포함)Advisor
를 조회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);
}
}
Advisor1
이라는 어드바이저 하나만 등록@Import(AutoProxyConfig.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();
}
}
포인트컷은 2가지에 사용된다.
1. 프록시 적용 여부 판단 - 생성 단계
2. 어드바이스 적용 여부 판단 - 사용 단계
스프링은
@Aspect
애노테이션으로 매우 편리하게 포인트컷과 어드바이스로 구성되어 있는 어드바이저 생성 기능을 지원한다.
@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 {
TraceStatus status = null;
// log.info("target={}", joinPoint.getTarget()); //실제 호출 대상
// log.info("getArgs={}", joinPoint.getArgs()); //전달인자
// log.info("getSignature={}", joinPoint.getSignature()); //join point 시그니처
try {
String message = joinPoint.getSignature().toShortString();
status = logTrace.begin(message);
//로직 호출
Object result = joinPoint.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@Aspect
: 애노테이션 기반 프록시를 적용할 때 필요@Around("execution(* hello.proxy.app..*(..))")
@Around
의 값에 포인트컷 표현식을 넣는다.@Around
의 메서드는 어드바이스(Advice
)가 된다.ProceedingJoinPoint joinPoint
: 어드바이스에서 살펴본 MethodInvocation invocation
과 유사한 기능이다. 내부에 실제 호출 대상, 전달 인자, 그리고 어떤 객체와 어떤 메서드가 호출되었는지 정보가 포함되어 있다.joinPoint.proceed()
: 실제 호출 대상( target
)을 호출한다.@Configuration
@Import({AppV1Config.class, AppV2Config.class})
public class AopConfig {
@Bean
public LogTraceAspect logTraceAspect(LogTrace logTrace) {
return new LogTraceAspect(logTrace);
}
}
@Import({AppV1Config.class, AppV2Config.class})
: V1, V2 애플리케이션은 수동으로 스프링 빈으로 등록해야 동작한다.@Bean logTraceAspect()
: @Aspect
가 있어도 스프링 빈으로 등록을 해줘야 한다. 물론 LogTraceAspect
에 @Component
애노테이션을 붙여서 컴포넌트 스캔을 사용해서 스프링 빈으로 등록해도 된다.@Import(AopConfig.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();
}
}
자동 프록시 생성기 -
Advisor
를 자동으로 찾아와서 필요한 곳에 프록시를 생성하고 적용
@Aspect
를 찾아서Advisor
로 만들어준다.
지금까지 우리가 진행한 애플리케이션 전반에 로그를 남기는 기능은 특정 기능 하나에 관심이 있는 기능이 아니다. 애플리케이션의 여러 기능들 사이에 걸쳐서 들어가는 관심사이다.
이것을 바로 횡단 관심사(cross-cutting concerns)
라고 한다. 우리가 지금까지 진행한 방법이 이렇게 여러곳에 걸쳐 있는 횡단 관심사의 문제를 해결하는 방법이었다.
이제 횡단 관심사를 전문으로 해결하는 스프링 AOP에 대해 본격적으로 알아보자.