기록/Spring framework

한가지 Pointcut으로 여러 Aspect를 사용하기, 프록시 적용순서. 2021-06-09

최동훈1 2021. 6. 9. 16:20

우린 앞전에 @Pointcut 설정을 하면서, excution 명시자에 대해서 쓴 적이 있다. 간단히 설명하자면,

excution 명시자는 Adivce를 적용할 메서드를 지정할때 사용한다. 간단히 말해서 Pointcut은 "where" 이고, Advice는 "when"이다. 그런데 Advice를 Aspect의 실질적인 적용이라 해도 무방하다. 이유는 애초에 @Around 가 붙은 메서드에서 공통의 기능을 실행하고, 대상객체의 메서드도 호출하여서, 앞/뒤, 익셉션 유무등에 따라 코드를 짤수 있기 때문이다.

 

즉, Advice의 적용은 Aspect 의 적용이다. @Around 애너테이션에 공통의 기능과 "when" 그것을 실행할지 구현하기 때문이다.

그래서 Pointcut이 정하는 범위는 그냥 Advice의 적용범위가 아닌 "Aspect"(Advice 가 붙은 메서드에 구현하는 공통기능) 를 어디에 적용할지 메서드를 정한다 라고 생각하면 편하다.

 Pointcut은 Aspect를 적용시킬 '메서드'를 설정한다. 스프링은 메서드 호출에 따른 Aop의 적용만을 지원하기 때문이다.

즉, 이런 Pointcut의 값으로 execution 명시자를 통해 메서드를 지정할 수 있는데 간단한 규칙은 이렇다.

execution(수식어패턴 리턴타입패턴 클래스이름패턴 메서드이름패턴(파라미터 패턴))이다.

ex)excution(public void set*(..)) :리턴타입이 void 이고, 메서드이름이 set으로 시작하고, 파라미터가 0개 이상인 메서드에 Pointcut 적용.

 

그런데 우리는, 이전 포스트에서 구현한 팩토리얼 계산 기능에 또 다른 공통의 기능을 추가하고자 한다. 바로, 팩토리얼 값을 계산한 다음, 다른 메모리에 각 팩토리얼을 저장하고, 만약 저장된 값이 있으면, 굳이 팩토리얼을 계산하지 않고, 바로 저장된 값을 꺼내 쓰는 기능이다. 

코드를  보자.

@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;
	}

}

 

바로 이것이다. 그런데 @Pointcut의 범위를 보면, 이전포스트에서의 범위랑 동일하다. 즉, ExeTimeAspect랑 같이, Caclulator 를 대상객체로 설정한 것이다.

이를 빈 설정 객체에 등록시켜 보자.

@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();
	}

}

 

위 두 CacheAspect 프록시와 ExeTimeAspext 프록시 모두 calculator() 빈객체를 대상객체로 지정한다.

즉, 기존에는 함수시간을 쓰는 공통기능만 존재했는데, 새롭게 프록시를 만들고 그 Aspect의 범위인 Pointcut을 Calculator 클래스를 가리키도록  하니, 캐시에 저장하거나, 저장한 것을 쓰는 기능도 Aspect로 추가된 것이다.

이 프록시를 활용해 보자.

 

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();
	}

}

이렇게 main 함수에 실행을 하면 결과는 이렇게 나온다.

RecCalculator.factorial([7]) 실행 시간: 26775 ns//factorial(7)
CacheAspect: Cache에 추가[7]
CacheAspect: Cahche에서 구함[7]//factorial(7)

그런데 순서가 ExeTimeAspect 부터 실행한 뒤 CacheAspect를 실행한다. 잘 생가개보자. 우선, joinpoint에 7이라는 값이 처음으로 주어졌으므로, 바로 joinpoint.proceed를 통해 대상객체의 메서드를 실행한다. 그런데 이 대상객체인 Calculator()의 메서드가 실행되면, Calculator()를 대상객체로 삼는 ExeTimeAspect가 실행된다. 왜냐하면, 애초에 ExeTimeAspect도 Calculator의 factorial메서드가 호출됬을때, 공통기능을 수행하는 프록시이기 때문이다.

그럼 프록시 부터 실행된 뒤 맨 마지막으로 실질적인 factorial을 계산하는 RecCalculator 클래스가 실행되는것은 이햏ㅆ다. 의문이 들 수 있다.

1.CacheAspect->ExeTimeAspect->ReCalculator

2.ExeTimeAspect->CacheAspect->ReCalculator

이렇게 어느 프록시가 먼저 실행할지는 어떻게 정할까? 같은 Pointcut의 대상이되는 factorial 함수를 실행하면.

 답은 "어떤 Aspect가 먼저 적용될지는 스프링 프레임워크의 버전이나, 자바 버전에 따라 달라질 수 있다." 이다.

 

만약, 내가 CacheAspect를 먼저 호출하여 메모리에서 바로 꺼내쓸수 있다면, 굳이 시간을 구할필요 없게끔 프로그램을 작성한다면,(위와 같은 결과를 의도한다면) @Order 에너테이션을 프록시 클래스에 붙여준다. @Order의 값이 작을수록 먼저 실행되는 프록시이다.

 

*ProceedingJoinpoint 인터페이스에 대한 고찰

-과연 이 인터페이스는 무엇을 받는 것일까? 대상 객체일까? 대상객체의 메서드일까? 아니면 대상객체의 메서드의 파라미터일까?  

나는 이런 물음에 답을 하기 위해 책과 스프링 래퍼런스를 뒤지며, 꽤 많은 시간 고민을 했다.

그래서 내가 내린 결론은 : 인터페이스의 이름에 해답이 있었다. Proceeding Joinpoint: 직역하자면, 진행되고있는 joinpoint 이다. 바로, @Aspect프록시에 설정한 Pointcut의 범위에 해당하는 부분. 즉, 스프링에서는 "대상 객체의 매서드" 이다. 애초에 joinpoint는 Advice를 적용가능한 지점을 의미한다. 메서드 호출,필드값 변경 등.. 그런데 어짜피 스프링은 메서드 호출에 대한 Joinpoint를 지원한다.

 

정리하자면, 

ProceedingJoinpoint에 전달되는 값은, "대상객체의 메서드"이다.

사실,ProceedingJoinpoint 인터페이스에 정의된 메서드만 보더래도 쉽게 유추 가능하다.

getSignature(): 호출되는 메서드에 대한 정보를 구한다.

getTarget(): 대상객체를 구한다.

getArgs(): 파라미터 목록을 구한다.

 

마지막 밑줄친 ProceedingJoinpoint의 메서드에 주목하자. "무엇에 대한 파라미터 목록인가?". 파라미터라는 것은 매서드의 인자를 뜻하는 것인데, 그렇다면, ProceedingJoinpoint 가 매서드란 것인가? 맞다. 이런 결론을 도출할 수 있다.

 

page 177 공부완료.

 

공부시간:3시간

순공부시간 1시간.

 

열심히 할려고 하지도 말고, 그냥 하자. 마치 하루 일과처럼, 나의 생활 루틴처럼, 아침에 일어나면 밥먹는 것처럼. 당연한 일상의 과정으로 받아들이자.