본문 바로가기

Web/spring

[Spring framework Core] 5. Aspect Oriented Programming with Spring (2)

728x90

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop-advice

 

Core Technologies

In the preceding scenario, using @Autowired works well and provides the desired modularity, but determining exactly where the autowired bean definitions are declared is still somewhat ambiguous. For example, as a developer looking at ServiceConfig, how do

docs.spring.io

 

 

틀린 해석이나 내용이 있다면 알려주세요 감사합니당 🌷

 

 


5.4.4. Declaring Advice ~ 5.4.7. An AOP Example

 

 

 

5.4.4. Declaring Advice

Advice 는 point cut 표현식과 연관하여 일치하는 method 의 실행 전, 후 또는 around 상태에서 실행된다

point cut expression 은 단순 참조이거나 표현식으로 표시할 수 있다

 

 

Before Advice

You can declare before advice in an aspect by using the @Before annotation:

@Aspect
public class BeforeExample {

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

in-place 에서 사용은 이렇게 할 수 있다

@Aspect
public class BeforeExample {

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

 

 

 

After Returning Advice

After returning advice runs when a matched method execution returns normally. You can declare it by using the @AfterReturning

 

@Aspect
public class AfterReturningExample {

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

참고로 동일한 aspect 내에서도 여러 advice 선언을 할 수 있다

 

 

Advice 에서 값 리턴 받기

@Aspect
public class AfterReturningExample {

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

 

returning 에 사용된 이름은 매개변수 이름과 일치해야 한다.

실행 후 return 되면 해당 값으로 전달받는다

 

Advice return 후엔 완전히 다른 참조로 반환할 수는 없다

 

 

After Throwing Advice

After throwing advice runs when a matched method execution exits by throwing an exception. You can declare it by using the @AfterThrowing annotation

 

Advice 가 메소드 실행에 예외처리 되면서 throw 될 때 실행

@Aspect
public class AfterThrowingExample {

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

 

 

예외의 유형에 따라 분리할 수도 있다 throwing 속성을 사용하여 일치를 제한시키고, 예외를 바인딩할 수 있다

@Aspect
public class AfterThrowingExample {

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

마찬가지로 throwing 에 사용된이름은 매개변수 이름과 같아야한다

A throwing clause also restricts matching to only those method executions that throw an exception of the specified type (DataAccessException, in this case).

 

 

@AtrerThrowing 은 일반 예외 처리 콜백이 나타나지 않는다

그러므로 @AfterThrowing 자체 join point 에서 예외를 수신해야하고,

@After/@AfterReturing 은 반대로 예외를 수신처리하지말아야한다

 

 

After (Finally) Advice

method 실행이 종료될 때 실행 된다

정상적인 반환 조건, 예외 반환 조건을 모두 처리 하려면 해당 advice 가 준비되어있어야하는데

리소스 나 similar purposes 를 해제할 때 사용한다

 

@Aspect
public class AfterFinallyExample {

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

 

@After 애노테이션은 try-catch 문의 finally block 과 비슷하게 "after finally advice"로 정의된다

@AfterReturining 은 성공 시에만 전상 반환되지만 @After 는 발생한 모든 결과, 반환 예외 모두 호출 된다

 

 

 

 

 

 

Around Advice

마지막 Advice는 Around Advice 이다

이는 Around 에서 실행되며, method 실행 전, 후, 수행 중, 어떤 방식으로 수행되는지 까지를 결정할 수 있다

타이머 시작 중지같은 thread 나 안전한 방식으로 실행 전후 상태를 공유해야하는 경우 이 Around Advice가 자주 사용 된다

 

그치만 이런 advice 말고 좀 더 좁은 의미의 advice 부터 사용할 것을 권고한다

before advice 로 충분하다면 Around advice를 사용하지 말 것

 

 

Around Advice는 @Around 를 사용한다

메소드는 return Object 를 선언해야하며 첫번째 매개 변수는 ProceedingJoinPoint 형식이여야한다

Advice method의 본문 내에서 기본 메소드를 실행하려면 ProceedingJoinPoint에서 progress()를 호출해야 한다다. 인수 없이 진행()을 호출시 기본 메서드에 호출자의 원래 인수가 제공된다.  인수 배열(Object[])을 허용하는 progress() 메서드의 오버로드된 변형을 사용할 수도 있다. 배열의 값은 호출될 때 기본 메서드에 대한 인수로 사용된다.

 

Object[] 로 호출 시에는 Aspect compiler 방식과 약간 다르다

기본 AspectJ Around Advice 의 경우 Around Advice에 전달 받는 인수와 수가 같아야하며(기본 joinpoint 인수 수 말고) 주어진 인수  위치는 값이 binding 된 엔티티 join point에서 원래 값을 대체한다

 

@AspectJ 관점을 컴파일하고 AspectJ 컴파일러 및 위버로 인수를 진행하는 경우에만 차이가 난다.

Spring AOP, AspectJ 모두 호환되는 작성법이 따로 있다

 

The value returned by the around advice is the return value seen by the caller of the method.

 

For example, a simple caching aspect could return a value from a cache if it has one or invoke proceed() (and return that value) if it does not.

Note that proceed may be invoked once, many times, or not at all within the body of the around advice. All of these are legal.

 

 

Around Advice 를 반환 할 유형을 void 로 선언하면 null 이 계속 반환되어 proceed() 결과를 무시당할 수 있다

그러므로 Object의 리턴 유형을 선언해주어야한다. void return 유형이 있더라도 proceed() 호출 안에서 리턴값을 리턴시켜야한다

예시에 따라 cache 값, wrapping 값 또는 다른 값을 선택적으로 반환할 수 있다

 

 

@Aspect
public class AroundExample {

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

 

 

Advice Parameters

모든 Advice method 는 첫번째 매개변수로 매개변수 type 을 선언할 수 있다

org.aspectj.lang.JoinPoint. ProceedingJoinPoint 의 하위클래스인 JoinPoint 첫번째 매개변수를 선언하려면 aroudn advice가 필요하다

 

interface JoinPoint

 

  • getArgs(): Returns the method arguments.
  • getThis(): Returns the proxy object.
  • getTarget(): Returns the target object.
  • getSignature(): Returns a description of the method that is being advised.
  • toString(): Prints a useful description of the method being advised.
 
 

Passing Parameters to Advice

advice 본문에서 인수 값을 사용하기 위해 arfs의 바인딩 형식을 사용할 수 있다

expression에서 유형 대신 매개변수 이름을 사용하면 해당 값으로 전달 받는다

DAO 작업 실행시 advice하여 Account advice로 액세스 하려면 이렇게 작성한다

 

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

 

 
args(acount, ..) 부분은 두가지 용도로 사용한다
1. method가 최소한 하나의 매개변수를 사용하고 매개변수에 전달 된 인수가 Account 인스턴스 메소드 실행으로만 일치할 수 있도록 제한한다
2. 실제 Account 개체를 매개변수로 받아 사용할 수 있도록 한다
 
이를 작성하는 다른 방법은
joinpoint 와 일치 시에 Account 개체 값을 provides 하는 point cut을 선언하여 참조하는 것이다
 
@Pointcut("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)")
private void accountDataAccessOperation(Account account) {}

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

 

The proxy object (this), target object (target), and annotations (@within, @target, @annotation, and @args) can all be bound in a similar fashion. The next two examples show how to match the execution of methods annotated with an @Auditable annotation and extract the audit code

 

다음 예제는 

@Auditable 애노테이션 정의이다

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

 

 

이 예제는 @Auditable 메소드 실행과 일치하는 어드바이스를 보여준다 

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

 

Advice Parameters and Generics

Spring AOP 는 클래식 선언 및 메소드 매개변수에 사용되는 Generic을 처리할 수 있다

 

예시를 보자면

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

 

 

 

메소드를 interceptor 하는 매개변수 유형에 advice 매개변수를 연결하여 제한할 수 있다

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

이 방식은 일반 컬렉션에서 적용되지 않기 대문에 pointcut을 정의할 수는 없다

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

일반적으로 값을 처리하려면 null값을 처리 하는 방법을 결정하기 어렵기 때문에 컬렉션의 모든 요소를 검사해야한다

이와 유사한 결과를 얻기 위해 Collection<?>에 매개변수를 입력하고 요수 유형을 수동으로 확인해야한다

 

 

Determining Argument Names

 

Advice 호출 시 매개변수 binding은 표현식에 사용되는 이름에의존한다

Java refelecton을 사용할 수 없으므로 매개변수 이름을 이렇게 결정하면 좋다

 

사용자가 명시적으로 지정한 경우 지정된 매개변수를 사용한다

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, or JoinPoint.StaticPart type 이면 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, or JoinPoint.StaticPart type 은 특수처리되어있어서 만약 해당 매개변수만 필요하다면 argNames를 사용하지않아도 된다

 

@Before("com.xyz.lib.Pointcuts.anyPublicMethod()")
public void audit(JoinPoint jp) {
    // ... use jp
}

Using the argNames attribute is a little clumsy, so if the argNames attribute has not been specified, Spring AOP looks at the debug information for the class and tries to determine the parameter names from the local variable table.

This information is present as long as the classes have been compiled with debug information (-g:vars at a minimum).

 

 

The consequences of compiling with this flag on are:

(1) your code is slightly easier to understand (reverse engineer),

(2) the class file sizes are very slightly bigger (typically inconsequential),

(3) the optimization to remove unused local variables is not applied by your compiler.

 

In other words, you should encounter no difficulties by building with this flag on.

 

If an @AspectJ aspect has been compiled by the AspectJ compiler (ajc) even without the debug information, you need not add the argNames attribute, as the compiler retain the needed information.

 

If the code has been compiled without the necessary debug information, Spring AOP tries to deduce the pairing of binding variables to parameters

(for example, if only one variable is bound in the pointcut expression, and the advice method takes only one parameter, the pairing is obvious).

 

If the binding of variables is ambiguous given the available information, an AmbiguousBindingException is thrown.

 

If all of the above strategies fail, an IllegalArgumentException is thrown.

 

 

 

Proceeding with Arguments

 

Spring AOP 및 AspectJ에서 일관되게 작동하는 인수를 사용하여 proceeding with Arguments를 사용할 수 있다.

Advice signature이 각 메소드 매개변수를 순서대로 바인딩 시키는 것이다

 

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

 

 

Advice Ordering

만약 여러 Advice 들을 하나의 join point에서 실행해야할 경우가 있다면, 우선 순위 규칙에 따라 실행 순서를 결정시킨다

The highest precedence advice runs first "on the way in" (so, given two pieces of before advice, the one with highest precedence runs first).

 

"On the way out" from a join point, the highest precedence advice runs last (so, given two pieces of after advice, the one with the highest precedence will run second).

When two pieces of advice defined in different aspects both need to run at the same join point, unless you specify

 

otherwise, the order of execution is undefined. You can control the order of execution by specifying precedence.

 

 

This is done in the normal Spring way by either implementing the org.springframework.core.Ordered interface in the aspect class or annotating it with the @Order annotation.

 

Given two aspects, the aspect returning the lower value from Ordered.getOrder() (or the annotation value) has the higher precedence.

 

유형에 따른 우선 순위

@Around, @Before, @After, @AfterReturning, @AfterThrowing. 

Note, however, that an @After advice method will effectively be invoked after any @AfterReturning or @AfterThrowing advice methods in the same aspect, following 

AspectJ’s "after finally advice" semantics for @After.

 

 

5.4.5. Introductions

Introductions 는 어드바이스 객체가 주어진 인터페이스를 구현한다고 선언하고 객체를 대신하여 구현엘 제공하는 aspect를 가능하게 한다

@DeclareParents 애노테이션을 사용하면 된다. 일치하는 유형에 parent가 있음을 선언한다

 

@Aspect
public class UsageTracking {

    @DeclareParents(value="com.xzy.myapp.service.*+", defaultImpl=DefaultUsageTracked.class)
    public static UsageTracked mixin;

    @Before("com.xyz.myapp.CommonPointcuts.businessService() && this(usageTracked)")
    public void recordUsage(UsageTracked usageTracked) {
        usageTracked.incrementUseCount();
    }

}

구현 할 인터페이스는 애노테이션 필드 유형에 따라 결정된다

@DeclareParents 애노테이션 attribute 는 AspectJ 유형 패턴으로 일치하는 유형의 빈은 인터페이스를 구현한다

 

예제는 이전 Advice에서 Service bean 구현으로 직접 사용 될 수 있다

UsageTracked usageTracked = (UsageTracked) context.getBean("myService");

 

 

5.4.6. Aspect Instantiation Models

 

기본적으로 Application Context 내에는 각 aspect 단일 인스턴스가 있다

AspectJ는 Singleton instatiation model이라고 하는데,

수명주기를 대체하여 Aspect를 정의할 수 있다

Spring은 AspectJ의 perthis 와 pertaget instatiation model 을 지원한다

percflow, percflowbelow, pertypewithin은 현재 지원되지 않는 상태이다

(2023년 2월 16일 기준임)

 

 

@Aspect 주석에 perthis를 지정하여 선언할 수 있다

@Aspect("perthis(com.xyz.myapp.CommonPointcuts.businessService())")
public class MyAspect {

    private int someState;

    @Before("com.xyz.myapp.CommonPointcuts.businessService()")
    public void recordServiceUsage() {
        // ...
    }
}

이는 각 고유 서비스 객체(pointcut expression과 일치하는 joinpoint에 바인딩 된 각각의 고유 객체)에 대해 aspect instance가 생성된다. aspect instance는 service 객체의 method가 처음 호출 될때 생성된다

해당 서비스 개체가 범위를 벗어나면 aspect도 벗어나게 된다. aspect instance가 생성되기 전에는 어떤 advice도 싱행되지 않는 상태이고, 이 생성이 일어나면 advice와 일치하는 joinpoint 에서 실행된다. (sevice 객체가 aspect와 관련이 있을 때만)

 

The pertarget instantiation model 은 perthis와 같은 방식으로 작동하겠지만, 일치하는 joinpoint에서 각각의 고유한 대상 객체에 대해 하나의 aspect를 실행한다

 

 

 

5.4.7. An AOP Example

The execution of business services can sometimes fail due to concurrency issues (for example, a deadlock loser).

동시성 문제에 대한 비지니스 서비스 실행 오류 시 다시 재시도할 수 있도록 하는 AOP예제이다

 

 

If the operation is retried, it is likely to succeed on the next try. For business services where it is appropriate to retry in such conditions (idempotent operations that do not need to go back to the user for conflict resolution), we want to transparently retry the operation to avoid the client seeing a PessimisticLockingFailureException. This is a requirement that clearly cuts across multiple services in the service layer and, hence, is ideal for implementing through an aspect.

Because we want to retry the operation, we need to use around advice so that we can call proceed multiple times.

 

@Aspect
public class ConcurrentOperationExecutor implements Ordered {

    private static final int DEFAULT_MAX_RETRIES = 2;

    private int maxRetries = DEFAULT_MAX_RETRIES;
    private int order = 1;

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    @Around("com.xyz.myapp.CommonPointcuts.businessService()")
    public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
        int numAttempts = 0;
        PessimisticLockingFailureException lockFailureException;
        do {
            numAttempts++;
            try {
                return pjp.proceed();
            }
            catch(PessimisticLockingFailureException ex) {
                lockFailureException = ex;
            }
        } while(numAttempts <= this.maxRetries);
        throw lockFailureException;
    }
}

 

Aspect가 Ordered interface를 구현하여 transaction advice보다 높은 aspect 우선 순위를 설정할 수 있다. (재시도 마다 새 transaction 으로 시도) 

maxRetries 와 order properties는 모두 Spring으로 구성된다

 

advice around에서 doConcurrentOperation 이 발생할 경우

각 businessService() 에 재시도를 적용하고

진행을 시도 후에도 PessimisticLockingFailureException 실패 시 다시 시도한다

 

<aop:aspectj-autoproxy/>

<bean id="concurrentOperationExecutor" class="com.xyz.myapp.service.impl.ConcurrentOperationExecutor">
    <property name="maxRetries" value="3"/>
    <property name="order" value="100"/>
</bean>

 

 

 

idempotent라는 작업만 재시도하도록 구체화 하는 예시이다

@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
    // marker annotation
}

이를 서비스 작업 구현에 추가한다

이는 pointcut expression에서 idempotent 작업만 재시도 하도록 표시한 것이다

@Around("com.xyz.myapp.CommonPointcuts.businessService() && " +
        "@annotation(com.xyz.myapp.service.Idempotent)")
public Object doConcurrentOperation(ProceedingJoinPoint pjp) throws Throwable {
    // ...
}

 

 

 

728x90