목차

스프링5; AOP의 실제 #2

🗓️

execution 패턴 표현식

  • execution(public void set*(..)) : 반환형이 void이고, 메소드 이름의 set으로 시작하고 인자가 0개 이상인 메소드를 호출한다.
  • executation(* dto.*.*()) : dto 패키지 탕비에 속한 인자가 없는 모든 메소드를 호출한다.
  • executation(* dto..*.*(..)) : dto 패키지 및 하위 패키지에 있고 인자가 0개 이상인 메소드를 호출한다.
  • execution(Long dto.StudentRepository.getAge(..)) : 반환형이 Long인 StudentRepository 타입의 getAge() 메소드를 호출한다.
  • execution(* get*(*)) : 이름이 get으로 시작하고 인자가 1개인 메소드를 호출한다.
  • execution(* get*(*, *)) : 이름이 get으로 시작하고 인자가 2개인 메소드를 호출한다.

Advice 적용 순서

  • 하나의 pointcut에 여러 Advice를 적용할 수 있다.
// Aspect 지정 객체
@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 number = (Long) joinPoint.getArgs()[0];
        if (cache.containsKey(number)) {
            System.out.printf("CacheAspect: Get the item from Cache [%d]\n", number);
            return cache.get(number);
        }

        Object result = joinPoint.proceed();
        cache.put(number, result);
        System.out.printf("CacheAspect: Put the item in Cache [%d]\n", number);
        return result;
    }

}
  • 새로운 Aspect를 구현했다.
  • bean 설정에서 두개의 Aspect를 아래와 같이 추가할 수 있다.
@Configuration
@EnableAspectJAutoProxy
public class AppContextWithCache {

    @Bean
    public CacheAspect cacheAspect() {
        return new CacheAspect();
    }

    @Bean

    public ExecutiveTimeAspect executiveTimeAspect() {
        return new ExecutiveTimeAspect();
    }

    @Bean
    public Calculator calculator() {
        return new RecurrenceCalculator();
    }
}
  • 스프링컨테이너는 아래와 같이 생성한다
public class MainAspectWithCache {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(AppContextWithCache.class);

        Calculator calculator = context.getBean("calculator", Calculator.class);
        calculator.factorial(7);
        calculator.factorial(7);
        calculator.factorial(3);
        calculator.factorial(7);
        calculator.factorial(7);
        calculator.factorial(3);
        calculator.factorial(5);
        calculator.factorial(5);
        calculator.factorial(5);
        calculator.factorial(5);
        context.close();
    }

}
  • 실행 결과는 아래와 같다
RecurrenceCalculator.factorial([7]) runtime : 16534 ns
CacheAspect: Put the item in Cache [7]
CacheAspect: Get the item from Cache [7]
RecurrenceCalculator.factorial([3]) runtime : 6481 ns
CacheAspect: Put the item in Cache [3]
CacheAspect: Get the item from Cache [7]
CacheAspect: Get the item from Cache [7]
CacheAspect: Get the item from Cache [3]
RecurrenceCalculator.factorial([5]) runtime : 8021 ns
CacheAspect: Put the item in Cache [5]
CacheAspect: Get the item from Cache [5]
CacheAspect: Get the item from Cache [5]
CacheAspect: Get the item from Cache [5]

캐시에 객체가 존재하면 계산을 하지 않고 캐시를 가지고 온다.

  • 이렇게 Aspect의 실행순서를 알 수 있다
  • [CacheAspect] Proxy → [ExecutiveTimeAspect] Proxy → [RecurrenceCacluator] Instance

@Order

  • 여러개의 Aspect를 지정한다고 항상 의도된대로 실행되는 것은 아니다.
  • 순서 지정이 필요한 경우 Aspect가 지정된 클래스에 @Order 어노테이션을 지정하면된다.
@Aspect
@Order(2)
public class CacheAspect {
//..

@Aspect
@Order(1)
public class ExecutiveTimeAspect {
//..
  • 실행 결과는 Cache를 뒤에 타도록 달라진다
CacheAspect: Put the item in Cache [7]
RecurrenceCalculator.factorial([7]) runtime : 201529 ns
CacheAspect: Get the item from Cache [7]
RecurrenceCalculator.factorial([7]) runtime : 276267 ns
CacheAspect: Put the item in Cache [3]
//... 중략
CacheAspect: Get the item from Cache [5]
RecurrenceCalculator.factorial([5]) runtime : 99159 ns
CacheAspect: Get the item from Cache [5]
RecurrenceCalculator.factorial([5]) runtime : 85201 ns
CacheAspect: Get the item from Cache [5]
RecurrenceCalculator.factorial([5]) runtime : 88659 ns

@Pointcut의 재사용

  • CacheAspect와 ExecutiveTimeAspect를 구성하면서 @Pointcut 어노테이션의 필터를 사용해 대상을 지정하고있다.
  • 둘다 같은 Joinpoint를 바라보고 있고 이것을 한쪽으로 합칠 수 있다.
  • 아래에서는 CacheAspect에서 ExevuciteTimeAspect쪽의 Joinpoint를 바라보도록 만들어보겠다
// CacheAspect

//    @Pointcut("execution(public * chap07..*(long))")
//    public void cacheTarget() {
//    }
@Around("ExecutiveTimeAspect.publicTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
//..

// ExecutiveTimeAspect
@Pointcut(value = "execution(public * chap07..*(..))")
public void publicTarget() {
}
//..

@Pointcut 어노테이션을 받는 메소드를 public으로 변경하면 다른 Aspect에서 Joinpoint를 바라볼 수 있다.

  • @Pointcut을 외부로 빼내어 관리가 가능하다.
  • 우선 joinpoint만을 위한 클래스를 만들어 @Pointcut 어노테이션과 필터를 적용한다.
// CommonPointcut
public class CommonPointcut {
    @Pointcut(value = "execution(public * chap07..*(..))")
    public void commonTarget() {
    }
}
  • 그다음 각 Aspect의 @Around 필드를 수정한다.
// CacheAspect
@Around("CommonPointcut.commonTarget()")
public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    //..

    // ExecutiveTimeAspect
@Around("CommonPointcut.commonTarget()")
public Object measure(ProceedingJoinPoint joinPoint) throws Throwable {
    //....
  • 이렇게 하면 joinpoint를 효율적으로 관리할 수 있다.

여기까지 스프링의 핵심 기능인 스프링컨테이너, Bean객체와 설정, DI, 컴포넌트, AOP까지 알아봤다. 이후는 스프링 부트를 통한 실제 웹서비스를 만들기 위한 과정으로 넘어가겠다.