- 스프링에서 프록시 패턴을 어떻게 적용시키는지 실제로 스프링을 통해 적용해본다.
proxy 패턴
은decorator 객체
로도 활용할 수 있다. 기능 추가와 확장에 초점이 맞춰져있다.
짤막한 프록시의 핵심
- 프록시의 특징은 핵심 기능은 구현하지 않는다는 점이다.
- 프록시는 핵심 기능을 구현하지 않는 대신 여러 객체에 공통으로 적용할 수 있는 기능을 구현한다.
- AOP의 기본 핵심은 공통 기능을 삽입하는 것 이다.
AOP의 주요 용어
1. Advice
- 공통 관심 기능을 핵심 로직에 적용하는 시점을 정의한다.
- ex) 메소드를 호출하기 전에 트랜잭션을 시작하는 기능을 적용한다.
2. Joinpoint
- Advice를 적용하는 지점을 의미한다.
- 메소드 호출, 필드 값 변경 등을 포함한다.
- 스프링은 메소드 호출에 대한 Joinpoint만 지원.
3. Pointcut
- Advice가 실제 적용되는 Joinpoint를 의미한다.
- 스프링에서는 정규표현식이나 AspectJ의 문법을 이용해 Poincut을 정의한다.
4. Weaving
- Advice를 핵심 로직 코드에 적용하는 행위.
5. Aspect
- 여러 객체에 공통으로 적용되는 기능을 Aspect라고 한다.
Aspect의 종류
- 스프링에서 구현 가능한 Aspect의 종류
- Before Advice : 메소드 호출 전 Aspect를 실행한다.
- After Returning Advice : 메소드가 Exception 없이 실행 된 이후 Aspect를 실행한다.
- After Throwing Advice : 메소드가 Exception 이 발생한 경우 Aspect를 실행한다.
- After Advice : Exception 상관 없이 메소드 실행 후 Aspect를 실행한다.
- ? Around Advice : 메소드 실행 전, 후, Exception 발생 시점에 Aspect를 실행한다.
이 중에서 가장 많이 사용되는 것은 Around Advice 방식인데, 시점을 다양하게 사용할 수 있기 때문이다.
스프링의 AOP구현
- 프록시를 적용할 클래스에 @Aspect 어노테이션을 붙인다.
- @Pointcut 어노테이션으로 Joinpoint 를 지정한다.
- 공통 기능을 구현한 메소드에 @Around 어노테이션을 지정한다.
1. @Aspect, @Pointcut, @Around를 이용한 AOP 구현
- 스프링AOP 적용 하기 이전 프록시 패턴 코드
- 코드의 요점은
ExecutiveTimeCacluator
클래스의 생성자에서 구현체를 위임(delegate) 받아 프록시를 구현한다는 점이다.
- 코드의 요점은
- 다음은 기존 코드에 어노테이션을 적용했다
@Aspect
public class ExecutiveTimeAspect {
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long finish = System.nanoTime();
Signature signature = joinPoint.getSignature();
System.out.printf("%s.%s(%s) runtime : %s ns\n",
joinPoint.getTarget().getClass().getSimpleName(),
signature.getName(), Arrays.toString(joinPoint.getArgs()),
(finish - start));
}
}
- @Aspect 어노테이션을 적용한 클래스는 Advice(@Around)와 Pointcut을 제공한다.
- @Pointcut 어노테이션은 공통 기능을 적용할 대상을 지정한다.
- @Around 어노테이션은
publicTarget()
의 pointcut에 공통 기능을 적용한다는 의미다.- measure()의
ProceedingJoinPoint
타입 파마리터는 프록시 대상 객체의 메소드를 호출할 때 사용한다. joinPoint.proceed()
메소드를 호출하면 대상 객체의 메소드가 실행되므로 이 코드 이전과 이후에 공통 기능을 위한 코드를 위치시키면 된다. (기존 코드의 delegate 위치와 같다)
- measure()의
Bean설정
@Configuration
@EnableAspectJAutoProxy
public class AppContext {
@Bean
public ExecutiveTimeAspect executiveTimeAspect() {
return new ExecutiveTimeAspect();
}
@Bean
public Calculator calculator() {
return new ReCalculator();
}
}
- @EnableAspectJAutoProxy 어노테이션을 bean 설정에 붙이면, @Aspect 클래스를 공통 기능으로 사용할 수 있다.
- 스프링은 @Aspect 어노테이션이 붙은 bean 객체를 찾아 @Pointcut 설정과 @Around 설정을 사용한다.
스프링 컨테이너
public class MainAspect {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
AppContext.class);
Calculator calculator = context.getBean("calculator", Calculator.class);
long fiveFactorial = calculator.factorial(5);
System.out.println("calculator.factorial(5) = " + fiveFactorial);
System.out.println(calculator.getClass().getName());
context.close();
}
- 아래는 실행 결과다
ReCalculator.factorial([5]) runtime : 22574 ns
calculator.factorial(5) = 120
com.sun.proxy.$Proxy17
- 첫번째 줄은
ExecutiveCalculatorAspect
클래스의measure()
가 출력한 것이다. - 세번째 줄은
MainAspect
의 main에서getName()
을 출력한 것이다. - 아래와 같은 실행 구조를 가진다.
- 여기서 AOP를 적용하지 않으면
$Proxy17
대신ReCalculator
를 반환한다. (ExecutiveTimeAspect bean설정 주석처리)
calculator.factorial(5) = 120
chap07.ReCalculator
ProceedingJoinPoint의 메소드
- Around Advice 방식에서 사용할 공통 기능 메소드는 대부분 파라미터로 전달받은
ProceedingJoinPoint
의proceed()
메소드만 호출하면 된다.
ProceedingJoinPoint
인터페이스에서 제공하는 메소드
Signature getSignature()
: 호출 되는 메소드에 대한 정보를 구한다.Object getTarget()
: 대상 객체를 구한다.Object[] getArgs()
: 파라미터 목록을 구한다.
org.aspectj.lang.Signature
인터페이스에서 제공하는 메소드
String getName()
: 호출되는 메소드의 이름을 구한다.String toLongString()
: 호출되는 메소드의 모든 정보를 구한다.String toShortString()
: 호출되는 메소드를 축약한다 (이름만)
스프링에서 프록시를 생성할 때
- 스프링에서 에서 다음과 같이 ReCalculator를 사용한다.
// 스프링 컨테이너
Calculator calculator = context.getBean("calculator", Calculator.class);
// bean 설정
@Bean
public Calculator calculator() {
return new ReCalculator();
}
ReCalculator
를 사용하는데Calculator
타입을 사용한다. 이것을 ReCalculator를 사용하도록 바꾸면 Expection이 발생한다.- 이유는 런타임에 생성한 프록시 객체인
$Proxy17
도ReCalculator
구현체가 상속받는Calculator
인터페이스를 상속받기 때문이다.
- bean 객체가 인터페이스를 상속할 때 인터페이스가 아닌 구현체를 이용해서 프록시를 생성하고 싶다면 다음과 같이 하면된다.
// 스프링 컨테이너
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppContext.class);
ReCalculator calculator = context.getBean("calculator", ReCalculator.class);
// bean 설정
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppContext {
//...
- 아래는 실행 결과다
ReCalculator.factorial([5]) runtime : 15285343 ns
calculator.factorial(5) = 120
chap07.ReCalculator$$EnhancerBySpringCGLIB$$7bd3a4d7
ReCalculator를 받는다.
- 다음엔 excution 필터와 Advice 적용 순서에 대해서 마저 정리하겠다.