반응형
본 내용은 인프런의 김영한 님의 강의 "스프링 핵심 원리 - 고급편" 내용을 바탕으로 정리한 내용입니다.
프록시 팩토리란?
- 스프링 프레임워크에서 지원하는 기능으로, 동적 프록시를 편리하게 생성해주는 역할을 한다.
- 프록시 객체를 생성하고 필요한 설정을 처리하여 반환한다.
- 프록시 팩토리는 주로 인터페이스의 유무에 따라 JDK 동적 프록시 또는 CGLIB를 사용하여 프록시를 생성한다.
프록시 팩토리의 Advice 만들기
- Advice 는 프록시에 적용하는 부가 기능 로직이다.
- Advice만 작성하면 프록시 팩토리가 내부적으로 해당 프록시를 생성하고 관리한다.
Spring에서 프록시의 실행 로직을 위해 제공하는 인터셉터
package org.aopalliance.intercept;
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
프록시 팩토리 활용 예시
ConcreteService.class
- call() 에서 로그를 출력한다.
package hello.proxy.common.service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}
ServiceInterface.interface
package hello.proxy.common.service;
public interface ServiceInterface {
void save();
void find();
}
ServiceImpl.class
- save() 와 find() 메서드에서 로그를 출력한다.
package hello.proxy.common.service;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ServiceImpl implements ServiceInterface {
@Override
public void save() {
log.info("save 호출");
}
@Override
public void find() {
log.info("find 호출");
}
}
TimeAdvice.class
MethodInterceptor
를 구현하여 메서드의 실행 시간을 로그로 남기는 어드바이스를 제공한다.
package hello.proxy.common.advice;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@Slf4j
public class TimeAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// 알아서 target 클래스를 호출하고 그 결과를 받는다.
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
ProxyFactoryTest.class
- 테스트 실행 코드
- AopUtils 클래스에서 프록시 팩토리 사용시 사용가능한 메소드를 제공한다.
- AopUtils.isAopProxy : 프록시가 생성했는지 판단
- AopUtils.isJdkDynamicProxy : JDK 동적 프록시로 생성했는지 판단
- AopUtils.isCglibProxy : CGLIB 동적 프록시를 생성했는지 판단
package hello.proxy.proxyfactory;
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ConcreteService;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.AopUtils;
import static org.assertj.core.api.Assertions.*;
@Slf4j
public class ProxyFactoryTest {
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
ServiceInterface target = new ServiceImpl();
// target을 등록한다.
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();
}
@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
void concreteProxy() {
ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
}
테스트 결과
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy9
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0
targetClass=class hello.proxy.common.service.ConcreteService
ProxyFactoryTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerBySpringCGLIB$$f8064e9a
TimeAdvice - TimeProxy 실행
TimeAdvice - TimeProxy 종료 resultTime=9
- interfaceProxy() : 인터페이스를 구현한 클래스에 대해 JDK 동적 프록시를 생성하는 것을 테스트한다.
- concreteProxy() : 메서드에서 구체 클래스에 대해 CGLIB 프록시를 생성하는 것을 테스트한다.
- 프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용하고 클래스 기반의 프록시를 생성한다.
- 스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true로 설정해서 사용한다.
포인트컷(Pointcut)
- AOP(Aspect-Oriented Programming)에서 부가 기능을 적용할 조인트 포인트(메서드 실행 지점 등)를 선별하는 필터링 규칙이다.
- 일반적으로 클래스와 메서드 이름으로 필터링한다.
- 대상 여부를 확인하는 필터 역할을 담당한다.
어드바이스(Advice)
- 포인트컷에 의해 선택된 조인트 포인트에 적용되는 부가 기능으로 메서드 호출 전후에 로깅이나 트랜잭션 관리 등의 기능을 수행할 수 있다.
- 부가 기능 로직을 담당한다.
어드바이저(Advisor)
- 하나의 포인트컷과 하나의 어드바이스를 결합한 것으로, 부가 기능을 어디에(포인트컷) 적용할지와 어떤 기능(어드바이스)을 적용할지를 결정한다.
- 포인트 컷에 필터링 되는 프록시들만 어드바이스를 적용한다.
외우기 좋은 Tip
- 조언자( Advisor )는 어디( Pointcut )에 조언( Advice )을 해야할지 알고 있다.
스프링에서 제공해주는 포인트 컷 활용
AdvisorTest.class
package hello.proxy.advisor;
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import java.awt.*;
import java.lang.reflect.Method;
@Slf4j
public class AdvisorTest {
@Test
void advisorTest() {
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();
}
@Test
void saveAdvisorTest() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// 포인트 컷 : 메서드 이름이 save 인 경우만 어드바이스를 적용한다.
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
pointcut.setMappedNames("save");
// 포인트컷과 어드바이저를 변수로 넘겨 어드바이저를 만든다.
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(pointcut, new TimeAdvice());
proxyFactory.addAdvisor(advisor);
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
proxy.save();
proxy.find();
}
}
테스트 결과
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0
TimeAdvice - TimeProxy 실행
ServiceImpl - find 호출
TimeAdvice - TimeProxy 종료 resultTime=0
ProxyFactoryTest - targetClass=class hello.proxy.common.service.ServiceImpl
ProxyFactoryTest - proxyClass=class com.sun.proxy.$Proxy9
TimeAdvice - TimeProxy 실행
ServiceImpl - save 호출
TimeAdvice - TimeProxy 종료 resultTime=0
advisorTest()
메서드 실행 시 Spring AOP를 사용하여ServiceImpl
클래스의 모든 메서드(save
와find
)에 대해TimeAdvice
를 적용시킨다.saveAdvisorTest()
메서드 실행 시 Spring AOP를 사용하여ServiceImpl
의save
메서드에만TimeAdvice
를 적용하는 방법을 보여준다.
스프링에서 제공하는 포인트 컷
1. NameMatchMethodPointcut
- 메서드 이름을 기반으로 매칭한다.
2. JdkRegexpMethodPointcut
- JDK 정규 표현식을 기반으로 포인트컷을 매칭한다.
3. TruePointcut
- 항상 참을 반환한다.
4. AnnotationMatchingPointcut
- 애노테이션으로 매칭한다.
5. AspectJExpressionPointcut
- aspectJ 표현식으로 매칭한다.
- 실무에서 가장 사용하기 좋은 포인트 컷이다.
하나의 프록시에 여러개의 어드바이저 적용하기
- 스프링의 AOP는 적용 수 만큼 프록시가 생성되는 것이 아니라 하나의 프록시에 여러 어드바이저를 적용시킬 수 있다.
MultiAdvisorTest.class
package hello.proxy.advisor;
import hello.proxy.common.advice.TimeAdvice;
import hello.proxy.common.service.ServiceImpl;
import hello.proxy.common.service.ServiceInterface;
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
public class MultiAdvisorTest {
@Test
@DisplayName("하나의 프록시, 여러 어드바이저")
void multiAdvisorTest() {
//client -> proxy -> advisor2 -> advisor1 -> target
DefaultPointcutAdvisor advisor1 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice1());
DefaultPointcutAdvisor advisor2 = new DefaultPointcutAdvisor(Pointcut.TRUE, new Advice2());
//프록시1 생성
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory1 = new ProxyFactory(target);
// 어드바이저를 2개 등록한다.
proxyFactory1.addAdvisor(advisor2);
proxyFactory1.addAdvisor(advisor1);
ServiceInterface proxy = (ServiceInterface) proxyFactory1.getProxy();
//실행
proxy.save();
}
@Slf4j
static class Advice1 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice1 호출");
return invocation.proceed();
}
}
@Slf4j
static class Advice2 implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("advice2 호출");
return invocation.proceed();
}
}
}
테스트 결과
MultiAdvisorTest$Advice2 - advice2 호출
MultiAdvisorTest$Advice1 - advice1 호출
ServiceImpl - save 호출
- 하나의 프록시에 여러 어드바이저가 등록되어 실행된다.
스프링 AOP 어드바이스 적용 예시
LogTraceAdvice.class
- 메소드 호출 전후에 로깅을 수행하는 로깅 어드바이스를 구현한다.
package hello.proxy.config.v3_proxyfactory.advice;
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method;
public class LogTraceAdvice implements MethodInterceptor {
private final LogTrace logTrace;
public LogTraceAdvice(LogTrace logTrace) {
this.logTrace = logTrace;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
TraceStatus status = null;
try {
Method method = invocation.getMethod();
String message = method.getDeclaringClass().getSimpleName() + "." +
method.getName() + "()";
status = logTrace.begin(message);
//로직 호출
Object result = invocation.proceed();
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
ProxyFactoryConfigV1.class
- 인터페이스가 존재하는 애플리케이션의 프록시를 설정하는 구성 클래스
package hello.proxy.config.v3_proxyfactory;
import hello.proxy.app.v1.*;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
/**
* 포인트 컷 사용하여 메소드 이름으로 필터링
* @param logTrace
* @return
*/
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);
}
}
ProxyFactoryConfigV2.class
- 구현 클래스만 존재하는 애플리케이션의 프록시를 설정하는 구성 클래스
package hello.proxy.config.v3_proxyfactory;
import hello.proxy.app.v2.OrderControllerV2;
import hello.proxy.app.v2.OrderRepositoryV2;
import hello.proxy.app.v2.OrderServiceV2;
import hello.proxy.config.v3_proxyfactory.advice.LogTraceAdvice;
import hello.proxy.trace.logtrace.LogTrace;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
@Bean
public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderController);
factory.addAdvisor(getAdvisor(logTrace));
OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderController.getClass());
return proxy;
}
@Bean
public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));
ProxyFactory factory = new ProxyFactory(orderService);
factory.addAdvisor(getAdvisor(logTrace));
OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderService.getClass());
return proxy;
}
@Bean
public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
OrderRepositoryV2 orderRepository = new OrderRepositoryV2();
ProxyFactory factory = new ProxyFactory(orderRepository);
factory.addAdvisor(getAdvisor(logTrace));
OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
log.info("ProxyFactory proxy={}, target={}", proxy.getClass(), orderRepository.getClass());
return proxy;
}
/**
* 포인트 컷 사용하여 메소드 이름으로 필터링
* @param logTrace
* @return
*/
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);
}
}
ProxyApplication.class
ProxyFactoryConfigV1
과ProxyFactoryConfigV2
를 임포트하여 설정하고,LogTrace
빈을 등록하여 서버 기동 시 애플리케이션에서 적용할 수 있다.
package hello.proxy;
import hello.proxy.config.v3_proxyfactory.ProxyFactoryConfigV1;
import hello.proxy.config.v3_proxyfactory.ProxyFactoryConfigV2;
import hello.proxy.trace.logtrace.LogTrace;
import hello.proxy.trace.logtrace.ThreadLocalLogTrace;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@Import(ProxyFactoryConfigV1.class, ProxyFactoryConfigV2.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
/**
* 동시성 문제 해결
* @return
*/
@Bean
public LogTrace logTrace() {
return new ThreadLocalLogTrace();
}
}
반응형
'Spring' 카테고리의 다른 글
[스프링] @Aspect (0) | 2024.05.18 |
---|---|
[스프링] 빈 후처리기란? (0) | 2024.05.11 |
[스프링] 동적 프록시 (0) | 2024.05.11 |
[스프링] 프록시란? (0) | 2024.05.11 |
[스프링] 템플릿 메서드 패턴 (0) | 2024.05.04 |