프록시는 기존 코드를 수정하지 않고 코드 중복을 피할 수 있는 방법으로써 다음과 같은 특징을 지님
프록시(proxy)
대상 객체
공통으로 적용할 수 있는 기능
을 구현AOP는 Aspect Oriented Programming의 약자로,
여러 객체에 공통으로 적용할 수 있는 기능을 분리해 재사용성
을 높여주는 기법으로써 다음과 같은 특징을 지님
분리
공통 기능을 삽입
하며 다음과 같은 방법이 존재
이 중, 스프링이 제공하는 AOP 방식은
프록시
를 이용한세 번째 방식
자동으로
생성해줌Aspect
라 칭하며, 아래와 같은 주요 용어가 존재
Advice
: 언제 공통 관심 기능을 핵심 로직에 적용할 지를 정의Joinpoint
: Advice를 적용 가능한 지점을 의미매서드 호출
에 대한 Joinpoint만 지원)Pointcut
: Joinpoint의 부분 집합, 실제 Advice가 적용되는 Joinpoint를 나타냄Weaving
: Advice를 핵심 로직 코드에 적용하는 것Aspect
: 여러 객체에 공통으로 적용되는 기능Before Advice
: 대상 객체의 매서드 호출 전에 공통 기능을 실행After Returning Advice
: 대상 객체의 매서드가 익셉션 없이 실행된 이후에 공통 기능을 실행After Throwing Advice
: 대상 객체의 매서드를 실행하는 도중 익셉션이 발생한 경우에 공통 기능을 실행After Advice
: 익셉션 발생 여부에 상관없이 대상 객체의 매서드 실핼 후 공통 기능을 실행Around Advice
: 대상 객체의 매서드 실행 전, 후 또는 익셉션 발생 시점에 공통 기능을 실행
이 중 널리 사용되는 것은
Around Advice
이며,
그 이유는 대상 객체의 매서드를 실행하기 전/후, 익셉션 발생등다양한 시점
에원하는 기능
을 삽입할 수 있기 때문
스프링 AOP를 통해 공통 기능을 구현 및 적용하는 순서
@Aspect
어노테이션을 붙임@Pointcut
어노테이션으로 공통 기능을 적용할 Pointcut을 정의@Around
어노테이션을 적용
후술할 예제는 크게
핵심기능
: Calculator(인터페이스) <- RecCalculator(상속)공통기능
: ExeTimeAspect(시간 측정) / CachceAspect(캐싱 기능)로 구현// Caculator.java public interface Calculator { public long factorial(long num); }
// RecCalculator.java public class RecCalculator implements Calculator { @Override public long factorial(long num) { if (num == 0) return 1; else return num * factorial(num - 1); } }
//ExeTimeAspect
// 공통 기능을 제공하는 클래스 설정
@Aspect
public class ExeTimeAspect {
// 공통 기능을 적용할 Pointcut 설정
@Pointcut("execution(public * chap07..*(..))")
private void publicTarget() {
}
// 공통 기능을 구현할 매서드 설정
@Around("publicTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
try {
// proceed() 매서드를 사용해서 실제 대상 객체의 매서드를 호출
Object result = joinPoint.proceed();
return result;
} finally {
System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n",
...
}
}
}
@Aspect
어노테이션을 적용한 클래스는 Advice와 Pointcut을 함께
제공@Pointcut
어노테이션은 cahp07 패키지나 그 하위 패키지에 속한 Bean 객체의 public 매서드를 설정
@Pointcut이 붙은 매서드는 다음의 조건을
일반적
(반드시?)으로 만족해야 함
void
리턴 타입비어있는
매서드- 어떤 throws 문도
포함하지 않음
@Around
어노테이션은 publicTarget() 매서드에 정의한 Pointuct에 공통 기능을 적용joinPoint.proceed()
매서드를 실행시, 대상 객체의 매서드가 실행되므로 이 코드 전후로 공통 기능을 위한 코드를 위치시킴
결과적으로 위 코드를 실행하게 되면,
Calculator 타입이 spring 패키지에 속하므로,
calculator Bean에 ExeTimeAspect 클래스에 정의한공통 기능
measure()를 적용
// AppCtx.java
@Configuration
@EnableAspectJAutoProxy
public class AppCtx {
// exeTimeAspect 매서드가 공통 기능으로 사용될 프록시를 위한 Bean 객체를 생성하는 매서드
// 앞선 코드 ExeTimeAspect 클래스의 @Pointcut과 @Around 설정을 사용
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
public Calculator calculator() {
return new RecCalculator();
}
}
@Aspect
어노테이션을 붙인 클래스를 공통 기능으로 적용하려면,@EnableAspectJAutoProxy
어노테이션을 설정 클래스에 붙여야 함@Aspect
어노테이션이 붙은 Bean 객체를 찾아 Bean 객체의 @Pointcut
설정과 @Around
설정을 사용
위의 코드를 수행하면 결과적으로 아래와 같은 흐름으로 진행
- Main -> Proxy -> ExeTimeAspect(
공통기능
) -> ProceedingJoinPoint -> RecCalculator(핵심기능
)
@Before
어노테이션의 사용 예는 다음과 같다.@Aspect public class ExeTimeAspect { @Pointcut("execution(public * chap07..*(..))") private void publicTarget() { } @Before("publicTarget()") public void beforeMethod() { System.out.println("실행전 시작!"); } @Around("publicTarget()") public Object measure(ProceedingJoinPoint joinPoint) throws Throwable { ...
실행결과
는 다음과 같다.실행전 시작!
———————————————————————// 여기까지 매서드 실행전 공통기능 수행 (@Before
)
RecCalculator.factorial([7]) 실행 시간 : 126851 ns
CacheAspect: Cache에 추가[7]
CacheAspect: Cache에서 구함[7]
———————————————————————// 여기까지 원하는 위치에서 공통기능 수행 (@Around
)
Around Advice에서 사용할 공통 기능 매서드는 대부분
파라미터로 전달받은 ProceedingJoinPoint의 proceed()
매서드만 호출하면 됨
(앞선 코드 ExeTimeAspect.java의 joinPoint.proceed() 부분)
이때 세부적인 정보가 필요하다면 아래와 같은 매서드를 통해 정보를 획득 가능
getSignature()
: 호출되는 매서드의 대한 정보를 구함getTaget()
: 대상 객체를 구함getArgs()
: 파라미터 목록을 구함
// 수정 전 MainAspect.java
Calculator cal = ctx.getBean("calculator", Calculator.class);
// 수정 후 MainAspect.java
RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);
RecCalculator
객체를 반환하므로 문제가 없어 보임
// AppCtx.java
@Bean
public Calculator calculator() {
return new RecCalculator();
}
하지만 getBean() 매서드에 사용한 타입이
RecCalculator
인데 반해 실제 타입은$Proxy17
이라는 메세지와 함께, 다음과 같은 이유로에러
발생
Calculator(인터페이스)
를 RecCalculator
와 $Proxy17
가 상속받는 관계인터페이스를 이용해
프록시를 구현따라서 아래 코드처럼 Bean의 실제 타입이 RecCalculator 라고 해도,
“calculator”에 해당하는 Bean 객체 타입은 Caculator 를 상속받은 프록시 타입
이됨
// AppCtx.java
// AOP 적용시 RecCalculator가 상속받은 Calculator 인터페이스를 이용해 프록시 생성
@Bean
public Calculator calculator(){
return new RecCalculator();
}
// MainAspect.java
// calculator Bean의 실제 타입은 Calculator를 상속한 프록시 타입이므로
// RecCalculator로 타입변환을 할 수 없기 때문에 익셉션 발생
RecCalculator cal = ctx.getBean("calculator", RecCalculator.class);
execution 명시자는 Advice를 적용할 매서드를 지정할 때 아래와 같이 사용
@Pointcut("execution(public * cahp07..*(..))")
private void public Target(){
}
Pointcut
설정스프링 AOP는 public 매서드에만 적용할 수 있기 때문에, 사실상 public
만 가능
execution(수식어패턴? 리턴타입패턴 클래스이름패턴?매서드이름패턴(파라미터패턴)) 방식으로 사용
수식어패턴
은 생략 가능하며 public, protected등이 올 수 있음리턴타입패턴
은 리턴 타입을 명시클래스이름패턴
,매서드이름패턴
은 클래스 이름 및 매서드 이름을 패턴으로 명시파라미터패턴
은 매칭될 파라미터에서 대해 명시- 각 패턴에서
'..'
은 0개 이상이라는 의미
기존의 코드에는 ExeTimeAspect
클래스만 공통기능으로 구현했지만,
이에 추가로 CacheAspect
클래스를 공통기능으로 구현해, 한 Pointcut에 여러 Advice
를 적용
//CacheAspect.java
@Aspect
public class CacheAspect {
private Map<Long, Object> cache = new HashMap<>();
@Pointcut("execution(public * chap07..*(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);
}
Object result = joinPoint.proceed();
cache.put(num, result);
System.out.printf("CacheAspect: Cache에 추가[%d]\n", num);
return result;
}
}
위 클래스는 캐싱의 기능을 제공하며, 키 값이 hashmap에
존재하면
그 값을 리턴하고,
없다면
프록시 대상 객체를 실행하여 그 결과를 캐시에추가
해 준 후, 그 값을 리턴
ExeTimeAspect 클래스와 동일하게,
공통기능으로사용할 클래스
에 @Aspect
공통기능을적용할 부분
에 @Pointcut
공통기능을구현한 매서드
에 @Around 어노테이션을 차례로 적용하여 구현
@Around
값으로 cacheTarget() 매서드를 지정@Pointcut
설정은 첫 번째 인자가 long인 매서드를 대상으로 함execute()
매서드는 앞서 작성한 Calculator의 fatoring(long) 매서드에 적용새로운 Aspect를 구현했으므로 아래와 같이 두개의 Aspect를 추가하는 작업이 필요
// AppCtxWithCahce.java
@Configuration
@EnableAspectJAutoProxy
public class AppCtxWithCache {
@Bean
public CacheAspect cacheAspect() {
return new CacheAspect();
}
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
public Calculator calculator() {
return new RecCalculator();
}
}
위의 설정 클래스를 적용해 실행하는 코드는 아래와 같다.
// MainAspectWithCache.java
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]) 실행 시간 : 16584 ns
CacheAspect: Cache에 추가[7]
—————————————————- // 여기까지 처음 cal.factorial(7) 결과
CacheAspect: Cache에서 구함[7] // 두 번째 cal.factorial(7) 결과
…
모두
적용되었고만
적용됨
이렇게 실행결과가 다른 이유는 Advice를 다음 순서로 적용했기 때문
CacheAspect
프록시 ->ExeTimeAspect
프록시 -> 실제대상 객체
Bean 객체
는 실제로 CacheAspect 프록시 객체
대상 객체
는 ExeTimeAspect의 프록시 객체
대상 객체
는 실제 대상 객체
실제 코드상의 흐름은 다음과 같다.
// CacheAsepct.java
...
if (cache.containsKey(num)) {
System.out.printf("CacheAspect: Cache에서 구함[%d]\n", num);
return cache.get(num);
}
/* 이 부분에서 해당 객체의 대상 객체를 호출
* 현재 CacheAspect -> ExeTimeAspect -> RecCalculator의 순서이므로
* jointPoint.proceed()를 호출하게 되면
* 대상 객체인 ExeTimeAspect 프록시를 호출
*/
Object result = joinPoint.proceed(); // 실행순서 1)
cache.put(num, result);
System.out.printf("CacheAspect: Cache에 추가[%d]\n", num); // 실행순서 5)
return result;
// ExeTimeAspect.java
...
try {
// 위와 마찬가지로 joinPoint.proceed()에서 해당 객체의 대상 객체를 호출
// 여기서는 실제 대상 객체 (RecCalculator)를 호출
Object result = joinPoint.proceed(); // 실행순서 2)
return result;
} finally {
...
System.out.printf("%s.%s(%s) 실행 시간 : %d ns\n", // 실행순서 4)
...
}
...
// RecCalculator.java
...
@Override
public long factorial(long num){ // 실행순서 3)
...
앞선 예제 코드의 흐름은 아래와 같다.
하지만 우리는
순서
를 지정해준 적이 없는데, 어떤 정책에 의해 위와 같은 순서로 진행 되는가?
그 이유는 설정 파일에 아래와 같은 순서
로 작성했기 때문
// AppCtxWithCahce.java
@Configuration
@EnableAspectJAutoProxy
public class AppCtxWithCache {
// 1번째로 적용될 Aspect
@Bean
public CacheAspect cacheAspect() {
return new CacheAspect();
}
// 2번째로 적용될 Aspect
@Bean
public ExeTimeAspect exeTimeAspect() {
return new ExeTimeAspect();
}
@Bean
public Calculator calculator() {
return new RecCalculator();
}
}
실제 두 Apsect의 위치를 맞바꾸면, 실행결과가
달라짐
직접
순서를 지정해야 함@Order
어노테이션을 해당 클래스에 적용함으로써 그 순서를 아래와 같이 지정 가능
import org.springframework.core.annotation.Order;
// ExeTimeAspect.java
@Aspect
@Order(1)
public class ExeTimeAspect{
...
}
// CacheAspect.java
@Aspect
@Order(2)
public class CacheAspect{
...
}
위의 설정대로 수행 시, 아래와 같은 순서로 실행
ExeTimeAspect
프록시 ->CacheAspect
프록시 ->RecCalculator
실제 대상 객체
@Pointcut 어노테이션이 아닌 @Around
어노테이션에 execution
명시자를 지접 아래와 같이 지정 가능
// CacheAspect.java
@Aspect
public class CacheAspect {
...
@Around("execution(public * chap07..*(long))")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
}
만약 같은 Pointcut을 여러 Advice가 함께 사용한다면, 아래와 같이 공통 Pointcut
을 재사용 가능
// ExeTimeAspect.java
@Aspect
public class ExeTimeAspect {
// 다른 Advice가 참조하기 위해 prviate -> public으로 변경
@Pointcut("execution(public * chap07..*(..))")
public void publicTarget() {
}
...
}
@Aspect
public class CacheAspect {
...
// 앞선 코드의 Pointcut 재사용
@Around("aspect.ExeTimeAspect.publicTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
}
만약 여러 Aspect에서 공통으로 사용하는 Pointcut이 있다면,
별도 클래스
에 Pointcut을 정의하고,
각 Aspect 클래스에서 해당 Pointcut을 사용하도록 구성이렇게 구성된 별도의 클래스는 따로 Bean에 등록할 필요 없고,
@Around
어노테이션에서 해당 클래스에 접근 가능하면 사용 가능