5.4.4 Advice 선언 by ks

Advice는 포인트컷 표현과 관련되어있고 전, 후 또는 포인트컷에 매치되는 실행 메소드 주변에서 동작한다. 포인트컷 표현은 명명된 포인트컷에 간단한 참조일 수도 있고 포인트컷 표현으로 선언된 포인트컷 표현일 수도 있다.

Advice 이전

@Before 어노테이션을 사용해서 advice를 전에 선언할 수 있다 :

Advice 안에 값은 포인트컷이고, 포인트컷은 조인포인트들의 집합이다. @Before는 조인포인트 전에 실행되는 것이므로 아래 예제의 경우에는 doAccessCheck() 메소드가 실행된 다음 dataAccessOperation()을 수행하는 거야

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

만약 그자리에 포인트컷 표현을 사용한다면, 아래 예제처럼 재작성할 수 있다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class BeforeExample {

    @Before("execution(* com.xyz.myapp.dao.*.*(..))")
    public void doAccessCheck() {
        // ...
    }

}

Advice 반환 후

일치하는 메소드실행이 정상적으로 반환될 때 Advice를 반환한 후 실행됩니다. @AfterReturning 어노테이션을 사용해서 선언할 수 있습니다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doAccessCheck() {
        // ...
    }

}

! 여러 advice 선언들 (그리고 다른 멤버들또한) 을 가질 수 있고, 같은 aspect안에 있을 수 있다. 오직 하나의 advice 선언만 이 예제에선 집중 조명하여 보여준다.

때때로 반환된 실제 값에 advice body안에 접근할 필요가 있다. 아래 예제 처럼 접근을 하기 위한 반환 값을 바인딩 한 @AfterReturning 형태를 사용할 수 있다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;

@Aspect
public class AfterReturningExample {

    @AfterReturning(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        returning="retVal")
    public void doAccessCheck(Object retVal) {
        // ...
    }

}

returning 속성에서 사용되는 이름은 advice 메소드의 파라미터의 이름과 대응되어야한다. 메소드 실행이 반환될 때 반환 값은 대응하는 인자 값으로서 advice 메소드로 통과된다. returning 구문은 또한 구체적인 타입의 값을 반환하는 메소드 실행에 유일하게 맞는것을 제한한다.(이 경우에, 달리 반환값과 같은 Object이다.)

afterReturning을 사용한 후에 전체적으로 다른 참조값을 반환할 수 없는 것을 기억하라.

Advice 를 던진 후에

advice를 던진 후에 예외가 던져진 것으로 인해 매치된 메소드 실행이 종료될 때 실행한다. @AfterThrowing 어노테이션을 사용해서 선언할 수 있다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doRecoveryActions() {
        // ...
    }

}

주어진 유형의 예외가 발생하는 경우에만 조언이 실행되기를 원할 때가 많으며, 조언 본문에서 던져진 예외에 대한 액세스가 필요할 수도 있다. throwing 속성을 사용하여 일치를 제한하고 (필요한 경우 예외 유형으로 Throwable을 사용) throw 된 예외를 advice 매개 변수에 바인딩 할 수 있다. 다음 예제에서는이를 수행하는 방법을 보여다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterThrowing;

@Aspect
public class AfterThrowingExample {

    @AfterThrowing(
        pointcut="com.xyz.myapp.SystemArchitecture.dataAccessOperation()",
        throwing="ex")
    public void doRecoveryActions(DataAccessException ex) {
        // ...
    }

}

throwing 속성에 사용된 이름은 advice 메소드에 파라미터의 이름과 대응되어야만 한다. 메소드 실행이 익셉션으로 인해 종료될 때, 예외는 대응하는 인자 값으로서 advice 메소드에 통과된다. throwing 구문은 또한 구체적인 타입( 이 경우엔 DataAccessException)의 예외를 던진 메소드 실행만 매칭을 제한한다.

ExceptionHandler와 같은 기능을 하는 것 같아서 찾아봤는데, 에러를 잡아내는 것은 AOP, 잡아내서 처리를 하는 것은 ExceptionHandler를 사용하라고 하네.

https://ecogeo.tistory.com/349

(Finally advice 후에)

(finally) advice 후에 매치된 메소드 실행이 종료될 때 동작한다. @After 어노테이션을 사용해서 선언된다. advice 후에 일반과 예외 반환 상태 둘다 다룰 준비가 되어있어야 한다. 전형적으로 리소스 릴리즈와 비슷한 목적으로 사용된다. 아래는 finally advice후에 어떻게 사용하는지 보여준다.

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.After;

@Aspect
public class AfterFinallyExample {

    @After("com.xyz.myapp.SystemArchitecture.dataAccessOperation()")
    public void doReleaseLock() {
        // ...
    }

}

@After, @AfterReturning의 차이

@After : finally구문과 같이 advice가 실행되고 나면 무조건 실행

@AfterReturning : Advice가 성공적으로 실행되면 실행

Advice 주변 (Around advice)

마지막 advice의 종류는 advice 주변이다. advice 주변은 매치된 메소드 실행 "주변에"서 동작한다. 메소드 실행의 전과 후 모두 동작할 기회를 가지고, 언제 어떻게, 메소드가 실제로 실행될 시기를 결정한다. advice 주변은 만약 thread-safe (예를 들어서 타이머가 멈추고 시작하는) 하게 메소드 실행 전과 후에 상태를 공유할 필요가 있다면 자주 사용된다.항상 요구 사항을 충족시키는 가장 강력한 형태의 advice를 사용하라(즉, 사전 조언이 있을 경우 around advice를 사용하지 말 것)

arount advice는 @Around 어노테이션을 사용해서 선언된다. advice 메소드의 첫 파라미터는 ProceedingJoinPoint 타입이 사용되어야만 한다. advice 바디에서 ProceedingJoinPoint에서 proceed()를 호출하면 기본 메소드가 실행된다. proceed 메소드는 또한 Object[]에서 패스할 수 있다. 배열의 값들은 실행될 때 메소드 실행에 인자로서 사용된다.

Object[] 와 불러질 때 proceed의 행동은 AspectJ 컴파일러로 컴파일된 around advice proceed 행위보단 살짝 다르다. 전통적인 AspectJ 언어를 사용하여 작성된 around advice에는, proceed 하기 위해 패스된 인자들이 around advice에 패스된 인자들과 매치되어야만 하고(조인 포인트 아래에서 취급되는 인자들이 아니라), 주어진 포지션에서 proceed하기 위해 패스된 값들은 값이 바운드된 (만약 바로 이해하지 못해도 걱정하지 마라) 엔티티를 위한 조인 포인트에 원래 값을 대신한다. 스프링이 취하는 목적은 더 간단하고 더 proxy 기반으로 더 잘 매치하는 것, 실행 전용 의미론이다. Spring 용으로 작성된 @AspectJ aspect를 컴파일하고 AspectJ 컴파일러와 위버로 인수를 사용하면이 차이점 만 알면된다. Spring AOP와 AspectJ에서 100 % 호환되는 aspect를 작성하는 방법이있다. 이것은 advice 파라미터에 대한 다음 절에서 논의된다. 아래는 어떻게 around advice를 사용하는지를 보여준다:

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;

@Aspect
public class AroundExample {

    @Around("com.xyz.myapp.SystemArchitecture.businessService()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {
        // start stopwatch
        Object retVal = pjp.proceed();
        // stop stopwatch
        return retVal;
    }

}

around advice에 의해 반환된 값은 메소드 호출자에 의해 보여지는 값을 반환한다. 예를 들어서, 간단하게 캐싱 aspect는 만약 하나를 가지고 있으면 캐시 값을 반환하고, 없으면 proceed()를 부른다. proceed는 around advice 바디 안에서 한번 또는 여러번 아니면 전혀 호출되지 않을 수 있다. 이 모든 것은 합법적이다.

Advice 파라미터

스프링은 매번 Object[] 배열들로 동작하는거 보다 오히려 advice 사인(우리가 일찍이 반환이나 throw한 예제를 봤듯이)에서 필요로 하는 파라미터를 정의하는 것을 의미하는 완전히 정형화된 advice를 제공한다. 우리는 어떻게 인자를 만들고, 이번세션에서 advice 본문 마지막에 사용가능한 다른 컨텍스트적인 값을 만드는지 본다. 먼저, 조언에서 현재 조언하고있는 방법을 알 수있는 일반적인 조언을 작성하는 방법을 살펴 본다.

현재 JoinPoint 에 접근

advice 메소드는 첫번째 파라미터로서 org.aspectj.lang.JoinPoint. 타입의 파라미터를 선언할 수 있다. JoinPoint의 서브 클래스인 ProceedingJoinPoint 타입의 첫번째 파라미터를 선언하기 위해서는 around advice가 필요하다. JoinPoint 인터페이스는 유용한 메소드를 제공한다.

  1. getArgs() : method 인자를 반환한다.

  2. getThis() : proxy 오브젝트를 반환한다.

  3. getTarget() : 타겟 오브젝트를 반환한다.

  4. getSigniture() : advice 된 메소드의 설명을 반환한다.

  5. toString() : advice된 메소드의 유용한 설명을 출력한다.

더 자세한 사항은 javadoc을 볼 것.

Advice하기위해 인자 패스

우리는 이미 어떻게 반환된 값 또는 예외 값(advice을 던진 후 그리고 반환한 후에 사용하는)을 반환하는지에 보았다. advice 본문에 사용 가능한 인자 값을 만들기 위해서는, args의 형식을 바인딩 하는 것을 사용할 수 있다. 만약 args 표현에서 타입 명의 위치에 파라미터 이름을 사용한다면, 대응하는 인자 값은 advice가 불러질 때 파라미터 값으로서 통과한다. 예제는 이를 더 확실하게 한다. 첫번째 파라미터로서 Account 오브젝트를 취하는 DAO 실행을 advice하기를 원하면, 그리고 advice 본문에 account에 접근할 필요가 있다면 제안한다. 아래와 같이 작성할 수 있다.

@Before("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
public void validateAccount(Account account) {
    // ...
}

포인트컷 표현의 일부분인 args(account,...)는 두개의 목적을 제공한다. 첫째로 최소 하나이상의 파라미터를 받고 파라미터로 전달되는 아규먼트는 Account의 인스턴스인 메서드 실행만으로 매칭을 제한한다. 둘째로, account 파라미터를 통해 advice를 사용가능한 실제 Account 오브젝트를 만든다.

이를 작성하는 또다른 방법은 조인 포인트를 매치할 때 Account 오브젝트 값을 "제공하는" 포인트컷을 선언하는 것이고, advice로부터 명명화된 포인트컷을 언급한다. 아래에서 보자

@Pointcut("com.xyz.myapp.SystemArchitecture.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

@Before("accountDataAccessOperation(account)")
public void validateAccount(Account account) {
    // ...
}

더 자세한 사항은 AspectJ 프로그래밍 가이드를 보자.

프록시 오브젝트 (this), 타겟 오보젝트 (target), 어노테이션(@within, @target, @annotation, 그리고 @args)는 유사한 모양으로 바인딩될 수 있다. 다음 두 예제는 @Auditable 어노테이션이 붙은 메소드의 실행과 어떻게 매치되는지와 심사 코드를 확장하는 것을 보여준다:

두 예제 중 첫번 째는 @Auditable 어노테이션의 정의를 보여준다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Auditable {
    AuditCode value();
}

예제중 두번째는 @Auditable 메소드의 실행에 매치하는 advice를 보여준다.

@Before("com.xyz.lib.Pointcuts.anyPublicMethod() && @annotation(auditable)")
public void audit(Auditable auditable) {
    AuditCode code = auditable.value();
    // ...
}

advice 인자와 제네릭

스프링 AOP는 메소드 파라미터와 클래스 선언에 사용되는 제네릭들을 다룬다. 아래와같이 제네릭타입을 가지는 것을 제안한다.

public interface Sample<T> {
    void sampleGenericMethod(T param);
    void sampleGenericCollectionMethod(Collection<T> param);
}

메소드를 인터셉트하려는 매개 변수 유형에 advice 매개 변수를 입력하여 메소드 유형의 인터 셉션을 특정 매개 변수 유형으로 제한 할 수 있다.

@Before("execution(* ..Sample+.sampleGenericMethod(*)) && args(param)")
public void beforeSampleMethod(MyType param) {
    // Advice implementation
}

이 시도는 제네릭 컬렉션을 위해 동작하진 않는다. 그래서 아래와같이 포인트컷을 정의할 수 없다.

@Before("execution(* ..Sample+.sampleGenericCollectionMethod(*)) && args(param)")
public void beforeSampleMethod(Collection<MyType> param) {
    // Advice implementation
}

이걸 동작하게 만들기 위해,일반적으로 null 값을 어떻게 다루는지 결정할 수 없기 때문에 합당하게 컬렉션의 모든 요소를 검사해야만 한다. 이와 유사하게 하기 위해 Collection<?> 에 파라미터를 적어야하고 수동으로 요소의 타입을 체크해야한다.

인자 이름 결정

advice 호출에서 매개 변수 바인딩은 pointcut 표현식에서 사용 된 이름을 advice와 pointcut 메소드 서명에서 선언 된 매개 변수 이름과 일치시키는 것에 의존한다. 파라미터 이름은 자바 반영을 통해서 사용하지 않고 그래서 스프링 AOP는 파라미터 이름을 정하기 위해 아래와 같은 전략을 사용한다 :

  1. 만약 파라미터 이름이 사용자에 의해 엄격하게 구체화된다면, 구체화된 파라미터 이름이 사용된다. advice와 포인트컷 어노테이션 둘 다 부분적으로 어노테이트된 메소드의 인자명을 구체화하기 위해 사용할 수 있는 argNames attribute를 가진다.이 인자들은 런타임에 사용할 수 있다. 아래 예제는 argNames attribute를 어떻게 사용하는지를 보여준다.

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code and bean
}

만약 첫 파라미터 이름이 JoinPoint, ProceedingJoinPoint, JoinPoint.StaticPart 타입중 하나라면, argNames 어트리뷰트의 값으로부터 파라미터의 이름을 남길 수 있다. 예를 들어서 만약 조인 포인트 오브젝트를 받기 위해 advice를 처리한 것을 수정한다면, argNames 어트리뷰트는 포함할 필요가 없다.:

@Before(value="com.xyz.lib.Pointcuts.anyPublicMethod() && target(bean) && @annotation(auditable)",
        argNames="bean,auditable")
public void audit(JoinPoint jp, Object bean, Auditable auditable) {
    AuditCode code = auditable.value();
    // ... use code, bean, and jp
}

JoinPoint, ProceedingJoinPoint 및 JoinPoint.StaticPart 유형의 첫 번째 매개 변수에 대한 특수 처리는 다른 조인 포인트 컨텍스트를 수집하지 않는 advice 인스턴스에 특히 편리하다. 그런 상황에서, argNames 어트리뷰트를 생략해야한다. 예를 들어서 argNames 어트리뷰트를 선언할 필요가 없는 advice의 경우가 아래와 같다.:

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}
  1. 'argNames' 어트리뷰트를 사용하는 것은 조금 모양빠진다(ㅎㅎㅎㅎ). 그래서 만약 'argNames' 어트리뷰트는 구체화되지 않고 스프링 AOP는 클래스를 위한 디버그 정보에서 보고 로컬 변수 테이블로부터 파라미터 이름을 결정하려고 한다. 이 정보는 디버그 정보(최소한 '-g:vars')와 컴파일된 클래스만큼 길게 보여진다. 이 플래그와 컴파일한 결과는 : (1) 코드는 조금 이해하기에(리버스 엔지니어) 편하고, (2) 클래스 파일 사이즈들은 조금 많이 크고(전형적으로 대수롭지 않은), (3) 사용하지 않는 로컬 변수들을 제거하는것을 최소화하는 것이 컴파일러에 적용되지 않는다. 다른말로, 이 플래그를 키고 빌드해서 어려움이 없다.

만약 @AspectJ 느 디버그 정보 없이 AspectJ compiler(ajc) 에 의해 컴파일 되면, 컴파일러가 필요한 정보를 얻기 때문에 argNames 어트리뷰트를 더할 필요가없다.

  1. 만약 코드가 필요한 디버그 정보 없이 컴파일 된다면, 스프링 AOP는 파라미터에 변수를 바인딩하는 쌍을 추론하려고 시도한다.(예를 들어서 만약 오진 하나의 변수가 포인트컷 표현에 바인드되면, 그리고 advice 메소드가 오직 하나의 파라미터를 취하면, 쌍은 명백하다.). 사용 가능한 정보에 따라 변수의 바인딩이 모호하면 AmbiguousBindingException이 발생한다.

  2. 모든 전략이 실패하면 IllegalArgumentException 이 던져진다.

인자와 함께 발생

우리는 이전에 Spring AOP와 AspectJ에서 일관되게 작동하는 인수를 사용하여 진행 호출을 작성하는 방법을 설명 할 것이라고 언급했다. 해결책은 조언 기호가 각 메소드 매개 변수를 순서대로 바인드하는지 확인하는 것이다. 다음 예제에서는이를 수행하는 방법을 보여다.

@Around("execution(List<Account> find*(..)) && " +
        "com.xyz.myapp.SystemArchitecture.inDataAccessLayer() && " +
        "args(accountHolderNamePattern)")
public Object preProcessQueryPattern(ProceedingJoinPoint pjp,
        String accountHolderNamePattern) throws Throwable {
    String newPattern = preProcess(accountHolderNamePattern);
    return pjp.proceed(new Object[] {newPattern});
}

대부분의 경우이 바인딩을 앞의 예에서와 같이 수행한다.

Advice 순서

같은 조인포인트에 여러 어드바이스가 동작하기를 원할 때 무슨 일이 일어날까? 스프링 AOP는 advice 실행 순서를 결정하기 위한 AspectJ로서 상위 규칙을 따른다. 우선 순위가 가장 높은 조언이 먼저 실행된다. (그래서, 주어진 before advice 두개 중 가장 우선순위가 높은 것이 먼저 실행된다). 조인 포인트에서 "나가는 중", 우선 순위가 가장 높은 조언이 마지막으로 실행된다.(그래서, after advice 중 가장 우선순위가 높은 것이 두번째로 실행된다.)

우선순위가 높은 advice가 조인포인트 실행 가장 직전에 실행

서로 다른 측면에서 정의 된 두 가지 조언이 둘 다 동일한 조인 포인트에서 실행되어야하는 경우, 달리 지정하지 않으면 실행 순서가 정의되지 않는다. 구체화한 상위 실행 순서를 제어할 수 있다. 이는 Order 어노테이션으로 어노테이트하거나 aspect 클래스에서 org.springframework.core.Ordered 인터페이스를 확장하거나 해서 평범한 스프링 방법으로 된다. 주어진 두 aspects에서 Ordered.getValue() ((또는 어노테이션 값))로부터 더 낮은 값을 반환하는 aspect가 더 높은 우선순위를 가진다.

동일한 aspect에서 정의 된 두 개의 advice가 같은 join point에서 실행되어야 할 때, 순서는 정의되지 않는다. (javac 컴파일 된 클래스에 대한 reflection을 통해 선언 순서를 검색 할 방법이 없기 때문에). 이러한 aspect 메소드를 각 aspect 클래스의 조인 포인트마다 하나의 advice 메소드로 축소 시키거나 aspect 레벨에서 주문할 수있는 별도의 aspect 클래스로 advice 조각을 리팩터링하는 것을 고려해라.

Last updated