이번에는 Spring에서 Advice 적용 순서에 대해 알아보겠습니다.
한 Pointcut에 여러 Advice를 적용할 수도 있습니다.
CacheAspect
package aspect;
import java.util.HashMap;
import java.util.Map;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
* 간단하게 캐시 기능을 구현한 클래스
*/
@Aspect
public class CacheAspect {
private Map<Long, Object> cache = new HashMap<>();
@Pointcut("execution(public * chap07 ..*(long))") // 첫번째 인자를 Long 타입으로 구한다.
public void cacheTarget() {
}
@Around("cacheTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
Long num = (Long) joinPoint.getArgs()[0];
if (cache.containsKey(num)) {
System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
return cache.get(num);
} // Pointcut 지점에서 구한 키 값이 cache에 존재하면 키에 해당하는 값을 구해서 리턴
// Pointcut에서 구한 키값이 cache에 존재하지 않으면, 프록시 대상 객체를 실행.
Object result = joinPoint.proceed();
// 프록시 대상 객체를 실행한 결과를 cache에 추가
cache.put(num, result);
System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
// 프록시 대상 객체의 실행결과 리턴
return result;
}
}
@Around 값으로 cacheTarget() 메서드를 지정했습니다. @Pointcut 설정은 첫 번째 인자 long인 메서드를 대상으로 하여, execute() 메서드는 Calculator의 factorial(long) 메서드에 적용됩니다
public interface Calculator {
public long factorial(long num);
}
ExeTimeAspect
package aspect;
import java.util.Arrays;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class ExeTimeAspect {
@Pointcut("execution(public * chap07..*(long))")
private void publicTarget() {
}
@Around("publicTarget()")
public Object mesaure(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.nanoTime();
try {
Object result = joinPoint.proceed();
return result;
} finally {
long finish = System.nanoTime();
Signature sig = joinPoint.getSignature();
System.out.printf("%s.%s(%s) 실행시간 : %d ns\n"
, joinPoint.getTarget().getClass().getSimpleName()
, sig.getName()
, Arrays.toString(joinPoint.getArgs())
, finish - start);
}
}
}
새로운 Aspect를 위와 같이 구현했으니, 스프링 설정 클래스에 두 개의 Aspect를 추가할 수 있습니다.
package config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import aspect.CacheAspect;
import aspect.ExeTimeAspect;
import chap07.Calculator;
import chap07.RecCalculator;
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppCtxWithCache {
@Bean
public CacheAspect cacheAspect() {
return new CacheAspect();
}
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
public Calculator calculator() {
return new RecCalculator();
}
}
CacheAspect 와 ExeTimeAspect 이 두 Aspect에서 설정한 Pointcut은 모두 Calculator 타입의 factorial() 메서드에 적용됩니다.
해당 설정 코드를 이용하는 예제코드입니다.
package main;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import chap07.Calculator;
import config.AppCtxWithCache;
public class MainAspectWithCache {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(AppCtxWithCache.class);
Calculator cal = ctx.getBean("calculator", Calculator.class);
cal.factorial(7);
cal.factorial(7);
cal.factorial(5);
cal.factorial(5);
ctx.close();
}
}
RecCalculator.factorial([7]) 실행시간 : 6165250 ns
CacheAspect: Cache에 추가[7]
CacheAspect: Cache에서 구함[7]
RecCalculator.factorial([5]) 실행시간 : 2750 ns
CacheAspect: Cache에 추가[5]
CacheAspect: Cache에서 구함[5]
cal.factorial(7) 의 결과
RecCalculator.factorial([7]) 실행시간 : 6165250 ns
CacheAspect: Cache에 추가[7]
cal.factorial(7) 의 결과
CacheAspect: Cache에서 구함[7]
cal.factorial(5); 의 결과
RecCalculator.factorial([5]) 실행시간 : 2750 ns
CacheAspect: Cache에 추가[5]
cal.factorial(5); 의 결과
CacheAspect: Cache에서 구함[5]
RecCalculator.factorial(숫자) 실행시간 메시지는 ExeTimeAspect가 출력합니다.
CacheAspect: Cache에 추가와 CacheAspect: Cache에서 구함 메시지는 CacheAspect가 출력합니다.
cal.factorial(7)만 보면, 첫 번째 실행할 때와 두 번째 실행할 때 콘솔에 출력되는 내용이 다른 것을 볼 수 있습니다.
첫 번째 실행결과는 ExeTimeAspect와 CacheAspect가 모두 적용되었고, 두 번째 실행결과는 CacheAspect만 적용되었습니다.
이렇게 첫 번째와 두 번째 실행결과가 다른 이뉴는 Advice를 이와 같이 적용했기 때문입니다.
[Advice 적용 순서]
CacheAspect 프록시 -> ExeTimeAspect 프록시 -> 실제 대상 객체
여기서 구한 caculator 빈은 실제로는 CacheAspect 프록시 객체입니다. 그런데, CacheAspect 프록시 객체의 대상 객체는 ExeTimeAspect의 프록시 객체입니다. 그리고 ExeTimeAspect 프록시의 대상 객체가 실제 대상 객체입니다.
Calculator cal = ctx.getBean("calculator", Calculator.class);
cal.factorial(7); // CacheAspect 실행 -> ExeTimeAspect 실행 -> 대상객체실행
따라서 cal.factorial(7) 을 실행하면 CacheAspect의 코드가 먼저 실행됩니다.
실제 실행 순서는 다음과 같습니다.
CacheAspect
@Around("cacheTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
Long num = (Long) joinPoint.getArgs()[0];
if (cache.containsKey(num)) {
System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
return cache.get(num);
} // Pointcut 지점에서 구한 키 값이 cache에 존재하면 키에 해당하는 값을 구해서 리턴
// Pointcut에서 구한 키값이 cache에 존재하지 않으면, 프록시 대상 객체를 실행.
(1)Object result = joinPoint.proceed();
// 프록시 대상 객체를 실행한 결과를 cache에 추가
(4)
cache.put(num, result);
System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
// 프록시 대상 객체의 실행결과 리턴
return result;
}
ExeTimeAspect
@Around("publicTarget()")
public Object mesaure(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.nanoTime();
try {
(2) Object result = joinPoint.proceed();
return result;
} finally {
(3)
long finish = System.nanoTime();
Signature sig = joinPoint.getSignature();
System.out.printf("%s.%s(%s) 실행시간 : %d ns\n"
, joinPoint.getTarget().getClass().getSimpleName()
, sig.getName()
, Arrays.toString(joinPoint.getArgs())
, finish - start);
}
}
RecCalculator
package chap07;
public class RecCalculator implements Calculator {
@Override
(2) public long factorial(long num) {
try {
if (num == 0) {
return 1;
} else
return num * factorial(num - 1);
} finally {
}
}
}
CacheAspect는 cache 맵에 데이터가 존재하지 않으면 jointPoint.proceed()를 통해 대상을 실행합니다.
그 대상이 ExeTimeAspect이므로 ExeTimeAspect의 measure() 메서드가 실행됩니다(1)
ExeTimeAspect는 실제 대상 객체를 실행하고(2) 콘솔에 실행 시간을 출력합니다.(3)
ExeTimeAspect 실행이 끝나면 CacheAspect는 cache 맵에 데이터를 넣고 콘솔에 CacheAspect: Cache에 추가 메시지를 출력합니다.(4)
그래서 factorial(7)을 처음 실행 시에는 ExeTimeAspect가 출력하는 메시지가 먼저 출력되고 CacheAspect가 출력하는 메시지가 뒤에 출력되는 것입니다.
factorial(7)를 두 번째 실행할 때에는 실행흐름이 달라집니다.
@Around("cacheTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
Long num = (Long) joinPoint.getArgs()[0];
(1)if (cache.containsKey(num)) {
(2) System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
return cache.get(num);
} // Pointcut 지점에서 구한 키 값이 cache에 존재하면 키에 해당하는 값을 구해서 리턴
// Pointcut에서 구한 키값이 cache에 존재하지 않으면, 프록시 대상 객체를 실행.
Object result = joinPoint.proceed();
// 프록시 대상 객체를 실행한 결과를 cache에 추가
cache.put(num, result);
System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
// 프록시 대상 객체의 실행결과 리턴
return result;
}
처음 factorial(7)을 호출하면 CacheAspect의 cache 맵에 데이터를 추가합니다. 따라서 factorial(7)을 두 번째 호출하면
(1) if(cache.containsKey(num) 분기문에서 true 로, 콘솔에 CacheAspect: Cache에서 구함 메시지를 출력하고(2) cache 맵에 담긴 값을 리턴하고 끝이 납니다. 이러한 경우에는 joinPoint.proceed()를 실행하지 않으므로 ExeTimeAspect나 실제 객체가 실행되지 않습니다. 콘솔에도 CacheAspect가 생성한 메시지 CacheAspect: Cache에서 구함[7]만 출력됩니다.
@Order
어떤 Aspect가 먼저 적용될지는 스프렝 프레임워크나 자바 버전에 따라 달라질 수 있기 때문에 적용 순서가 중요하다면 직접 순서를 적용해야 합니다. 이럴 때 사용하는 것이 @Order입니다. @Aspect와 함께 @Order를 클래스에 붙이면 @Order에 지정한 값에 따라 적용순서를 결정합니다.
@Order에 값이 작으면 먼저 적용하고 크면 나중에 적용합니다.
예를 들어, 다음과 같이 두 Aspect 클래스에 @Order 를 적용했다고 가정해 봅시다.
@Aspect
@Order(1)
public class ExeTimeAspect {
...
}
@Aspect
@Order(2)
public class CacheAspect {
...
}
ExeTimeAspect에 적용한 @Order 가 1이고, CacheAspect에 적용한 @Order가 2이면 다음과 같이 ExeTimeAspect가 먼저 적용됩니다.
[Advice 적용 순서] @Order 사용
ExeTimeAspect 프록시 -> CacheAspect 프록시 -> 실제 대상 객체
CacheAspect: Cache에 추가[7]
RecCalculator.factorial([7]) 실행시간 : 7034250 ns
CacheAspect: Cache에서 구함[7]
RecCalculator.factorial([7]) 실행시간 : 98750 ns
CacheAspect: Cache에 추가[5]
RecCalculator.factorial([5]) 실행시간 : 83042 ns
CacheAspect: Cache에서 구함[5]
RecCalculator.factorial([5]) 실행시간 : 62500 ns
'Spring' 카테고리의 다른 글
[Spring] 스프링 DB 연동 (0) | 2024.02.16 |
---|---|
[Spring] Spring AOP - @Around의 Pointcut 설정과 @Pointcut 재사용 (0) | 2024.02.16 |
[Spring] AOP - execution 명시자 표현식 (0) | 2024.02.05 |
[Spring] AOP - 프록시 생성방식 (0) | 2024.02.05 |
[Spring] Spring AOP 구현해보기 (@Aspect, @Pointcut, @Around) (1) | 2024.02.02 |