Declaring Advice

Dev.Hammy·2024년 3월 6일
0

조언은 포인트컷 표현식과 연관되어 있으며 포인트컷과 일치하는 메소드 실행 전, 후 또는 주변에서 실행됩니다. 포인트컷 표현식은 인라인 포인트컷이거나 명명된 포인트컷에 대한 참조일 수 있습니다.

Before Advice

@Before 주석을 사용하여 관점에서 사전 조언을 선언할 수 있습니다. 다음 예에서는 인라인 포인트컷 표현식을 사용합니다.

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

@Aspect
public class BeforeExample {

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

명명된 포인트컷을 사용하면 이전 예제를 다음과 같이 다시 작성할 수 있습니다.

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

@Aspect
public class BeforeExample {

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

After Returning Advice

반환 후 일치하는 메서드 실행이 정상적으로 반환되면 조언이 실행됩니다. @AfterReturning 어노테이션을 사용하여 선언할 수 있습니다.

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

@Aspect
public class AfterReturningExample {

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

[Note]
동일한 관점 내에서 여러 개의 advice 선언(및 다른 멤버도 포함)을 가질 수 있습니다. 이 예제에서는 각 조언의 효과에 초점을 맞추기 위해 단 하나의 조언 선언만 표시합니다.

때로는 반환된 실제 값에 대한 조언 본문에 액세스해야 하는 경우가 있습니다. 다음 예제와 같이 반환 값을 바인딩하여 해당 액세스 권한을 얻는 @AfterReturning 형식을 사용할 수 있습니다.

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

@Aspect
public class AfterReturningExample {

	@AfterReturning(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		returning="retVal")
	public void doAccessCheck(Object retVal) {
		// ...
	}
}

returning 속성(attribute)에 사용된 이름은 조언 메소드의 매개변수 이름과 일치해야 합니다. 메서드 실행이 반환되면 반환 값은 해당 인수 값으로 조언 메서드에 전달됩니다. 또한 returning 절은 지정된 유형(이 경우 모든 반환 값과 일치하는 Object)의 값을 반환하는 메서드 실행으로만 일치를 제한합니다.

어드바이스 반환 후 사용시 완전히 다른 참조 반환은 불가능하므로 주의하시기 바랍니다.

After Throwing Advice

After throwing advice 는 예외를 던지면서 일치하는 메서드 실행이 종료될 때 실행됩니다. 다음 예제와 같이 @AfterThrowing 어노테이션을 사용하여 이를 선언할 수 있습니다.

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

@Aspect
public class AfterThrowingExample {

	@AfterThrowing("execution(* com.xyz.dao.*.*(..))")
	public void doRecoveryActions() {
		// ...
	}
}

종종 주어진 유형의 예외가 던져질 때만 advice가 실행되기를 원하며 advice 본문에서 던져진 예외에 액세스해야 하는 경우도 많습니다. throwing 속성(attribute)을 사용하여 일치(matching)를 제한하고(원하는 경우 Throwable을 예외 유형으로 사용) 던진 예외를 조언 매개변수에 바인딩할 수 있습니다. 다음 예에서는 그 방법을 보여줍니다.

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

@Aspect
public class AfterThrowingExample {

	@AfterThrowing(
		pointcut="execution(* com.xyz.dao.*.*(..))",
		throwing="ex")
	public void doRecoveryActions(DataAccessException ex) {
		// ...
	}
}

throwing 속성(attribute)에 사용된 이름은 advice 메소드의 매개변수 이름과 일치해야 합니다. 예외를 발생시켜 메소드 실행이 종료되면 해당 예외는 해당 인수 값으로 조언 메소드에 전달됩니다. throwing 절은 또한 지정된 유형의 예외(이 경우 DataAccessException)를 발생시키는 메소드 실행으로만 일치(matching)를 제한합니다.

[Note]
@AfterThrowing은 일반적인 예외 처리 콜백을 나타내지 않습니다. 특히, @AfterThrowing 어드바이스 메소드는 조인 포인트(사용자가 선언한 대상 메소드) 자체에서만 예외를 수신해야 하며 동반된 @After/@AfterReturning 메소드에서는 예외를 수신하지 않아야 합니다.

After (Finally) Advice

(최종적으로) 일치하는 메서드 실행이 종료되면 조언이 실행됩니다. @After 어노테이션을 사용하여 선언됩니다. 이후 advice은 정상 및 예외 반환 조건을 모두 처리하도록 준비되어야 합니다. 일반적으로 리소스 해제 및 유사한 목적에 사용됩니다. 다음 예에서는 after finally advice 사용하는 방법을 보여줍니다.

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

@Aspect
public class AfterFinallyExample {

	@After("execution(* com.xyz.dao.*.*(..))")
	public void doReleaseLock() {
		// ...
	}
}

[Note]
AspectJ의 @After 어드바이스는 try-catch 문의 finally 블록과 유사한 "after finally 어드바이스"로 정의됩니다. 성공적인 일반 반환에만 적용되는 @AfterReturning과 달리 조인 포인트(사용자가 선언한 대상 메서드)에서 발생한 모든 결과, 일반 반환 또는 예외에 대해 호출됩니다.

Around Advice

마지막 종류의 advice은 around advice입니다. Around 어드바이스는 일치하는 메서드 실행 "주변"으로 실행합니다. 메서드 실행 전후에 작업을 수행하고 메서드가 실제로 실행되는 시기, 방법, 심지어 실행 여부를 결정할 수 있는 기회가 있습니다. 스레드로부터 안전한 방식으로 메서드 실행 전후에 상태를 공유해야 하는 경우(예: 타이머 시작 및 중지) Around 어드바이스가 자주 사용됩니다.

[Tip]
항상 귀하의 요구 사항을 충족하는 가장 강력한 형태의 advice을 사용하십시오.

예를 들어, before advice가 귀하의 요구 사항에 충분하다면 around advice을 사용하지 마십시오.

Around 어드바이스는 @Around annotation을 메소드에 추가하여 선언됩니다. 메서드는 Object를 반환 type으로 선언해야 하며 메서드의 첫 번째 매개 변수는 ProceedingJoinPoint type이어야 합니다. advice 메소드의 본문 내에서 기본 메소드를 실행하려면 ProceedingJoinPoint에서 proceed()를 호출해야 합니다. 인수 없이 proceed()를 호출하면 호출 시 호출자의 원래 인수가 기본 메서드에 제공됩니다. 고급 사용 사례의 경우 인수 배열(Object[])을 허용하는 proceed() 메서드의 오버로드된 변형이 있습니다. 배열의 값은 호출될 때 기본 메서드에 대한 인수로 사용됩니다.

[Note]
Object[]와 함께 호출될 때의 proceed 동작은 AspectJ 컴파일러에 의해 컴파일된 어라운드 어드바이스에 대한 proceed 동작과 약간 다릅니다. 전통적인 AspectJ 언어를 사용하여 작성된 around 어드바이스의 경우, proceed하기 위해 전달된 인수 수는 around 어드바이스에 전달된 인수 수(기본 조인 포인트에서 사용하는 인수 수가 아님)와 일치해야 하며, proceed하기 위해 주어진 인수 position에 전달된 값은 값이 바인딩된 엔터티에 대한 조인 포인트의 원래 값을 대체합니다(지금 당장 이해가 되지 않더라도 걱정하지 마세요).

Spring이 취한 접근 방식은 더 간단하고 프록시 기반의 실행 전용 의미 체계와 더 잘 일치합니다. Spring용으로 작성된 @AspectJ aspects을 컴파일하고 AspectJ 컴파일러 및 위버를 사용하여 인수를 proceed하는 경우에만 이 차이점을 인식하면 됩니다. Spring AOP와 AspectJ 모두에서 100% 호환되는 측면을 작성하는 방법이 있으며 이는 advice 매개변수에 대한 다음 섹션에서 논의됩니다.

around 어드바이스에 의해 반환된 값은 메서드 호출자가 본 반환 값입니다. 예를 들어, 간단한 캐싱 aspect은 캐시에 값이 있는 경우 캐시에서 값을 반환하거나, 그렇지 않은 경우 proceed()를 호출하고 해당 값을 반환할 수 있습니다. proceed 상황은 around 어드바이스 본문 내에서 한 번, 여러 번 호출되거나 전혀 호출되지 않을 수 있습니다. 이 모든 것은 합법적입니다.

[Warning]
around 어드바이스 메소드의 반환 유형을 void로 선언하면 항상 null이 호출자에게 반환되어 ㅔproceed() 호출 결과가 사실상 무시됩니다. 따라서 around 어드바이스 메소드는 반환 유형을 Object로 선언하는 것이 좋습니다. advice 메소드는 기본 메소드가 void 반환 유형을 가지고 있더라도 일반적으로 proceed() 호출에서 반환된 값을 반환해야 합니다. 그러나 advice은 선택적으로 캐시된 값, 래핑된 값 또는 사용 사례에 따라 다른 값을 반환할 수 있습니다.

다음 예에서는 around 어드바이스를 사용하는 방법을 보여줍니다.

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

@Aspect
public class AroundExample {

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

Advice Parameters

Spring은 완전히 type화된 advice을 제공합니다. 즉, 항상 Object[] 배열로 작업하는 대신 advice 시그니처에 필요한 매개변수를 선언한다는 의미입니다(앞서 returning 및 throwing 예제에서 본 것처럼). 이 섹션의 뒷부분에서 advice 본문에 인수 및 기타 상황별 값을 제공하는 방법을 살펴보겠습니다. 먼저, 현재 어드바이스가 조언하고 있는 메소드에 대해 알아볼 수 있는 제네릭 어드바이스를 작성하는 방법을 살펴본다.

Access to the Current JoinPoint

모든 어드바이스 메소드는 첫 번째 매개변수로 org.aspectj.lang.JoinPoint type의 매개변수를 선언할 수 있습니다. JoinPoint의 하위 클래스인 ProceedingJoinPoint 유형의 첫 번째 매개변수를 선언하려면 around 어드바이스가 필요합니다.

JoinPoint 인터페이스는 다음과 같은 유용한 메소드를 제공합니다.

  • getArgs(): 메서드 인수를 반환합니다.

  • getThis(): 프록시 객체를 반환합니다.

  • getTarget(): 대상 객체를 반환합니다.

  • getSignature(): advice되는 메소드에 대한 설명을 반환합니다.

  • toString(): advice되는 메소드에 대한 유용한 설명을 인쇄합니다.

자세한 내용은 javadoc을 참조하세요.

Passing Parameters to Advice

우리는 반환된 값이나 예외 값을 바인딩하는 방법을 이미 살펴보았습니다(어드바이스를 반환한 후 및 던진 후 사용). 어드바이스 본문에서 인수 값을 사용할 수 있도록 하려면 args의 바인딩 형식을 사용할 수 있습니다. args 표현식에서 type 이름 대신 매개변수 이름을 사용하는 경우 해당 인수의 값은 advice가 호출될 때 매개변수 값으로 전달됩니다. 예를 들어 이를 더 명확하게 해야 합니다. Account 객체를 첫 번째 매개변수로 사용하는 DAO 작업 실행을 advice하고 싶고 advice 본문의 계정에 액세스해야 한다고 가정해 보겠습니다. 다음과 같이 작성할 수 있습니다.

@Before("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
public void validateAccount(Account account) {
	// ...
}

포인트컷 표현식의 args(account,..) 부분은 두 가지 용도로 사용됩니다. 첫째, 메서드가 하나 이상의 매개 변수를 사용하고 해당 매개 변수에 전달된 인수가 Account 인스턴스인 메서드 실행으로만 일치를 제한합니다. 둘째, account 매개변수를 통해 조언에 실제 Account 개체를 사용할 수 있게 만듭니다.

이를 작성하는 또 다른 방법은 조인 포인트와 일치할 때 Account 개체 값을 "제공"하는 포인트컷을 선언한 다음 어드바이스에서 명명된 포인트컷을 참조하는 것입니다. 이는 다음과 같이 보일 것입니다:

@Pointcut("execution(* com.xyz.dao.*.*(..)) && args(account,..)")
private void accountDataAccessOperation(Account account) {}

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

자세한 내용은 AspectJ 프로그래밍 가이드를 참조하세요.

프록시 객체(this), 대상 객체(target) 및 annotation(@within, @target, @annotation@args)은 모두 비슷한 방식으로 바인딩될 수 있습니다. 다음 예제 세트에서는 @Auditable annotation이 달린 메서드 실행을 일치시키고 감사 코드를 추출하는 방법을 보여줍니다.

다음은 @Auditable annotation의 정의를 보여줍니다.

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

다음은 @Auditable 메서드 실행과 일치하는 advice을 보여줍니다.

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

포인트컷 표현식 결합에 정의된 pointcut이라는 publicMethod를 참조합니다.

Advice Parameters and Generics

Spring AOP는 클래스 선언과 메소드 매개변수에 사용되는 제네릭을 처리할 수 있습니다. 다음과 같은 일반 유형이 있다고 가정해 보겠습니다.

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

메소드를 가로채려는 매개변수 type에 advice 매개변수를 연결하여 메소드 type의 가로채기를 특정 매개변수 type으로 제한할 수 있습니다.

@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<?>에 매개변수를 입력하고 요소 유형을 수동으로 확인해야 합니다.

Determining Argument Names

advice 호출의 매개변수 바인딩은 포인트컷 표현식에 사용된 이름을 조언 및 포인트컷 메소드 시그니처에 선언된 매개변수 이름과 일치시키는 데 의존합니다.

[Note]
AspectJ API는 매개변수 이름을 인수 이름으로 참조하므로 이 섹션에서는 인수와 매개변수라는 용어를 같은 의미로 사용합니다.

Spring AOP는 매개변수 이름을 결정하기 위해 다음 ParameterNameDiscoverer 구현을 사용합니다. 각 발견자에게는 매개변수 이름을 발견할 수 있는 기회가 주어지며, 첫 번째로 성공한 발견자가 승리합니다. 등록된 발견자 중 어느 것도 매개변수 이름을 결정할 수 없으면 예외가 발생합니다.

AspectJAnnotationParameterNameDiscoverer

해당 조언이나 포인트컷 annotation의 argNames 속성(attribute)을 통해 사용자가 명시적으로 지정한 매개변수 이름을 사용합니다. 자세한 내용은 명시적 인수 이름을 참조하세요.

KotlinReflectionParameterNameDiscoverer
Kotlin 리플렉션 API를 사용하여 매개변수 이름을 결정합니다. 이 Discoverer는 해당 API가 classpath에 있는 경우에만 사용됩니다.

StandardReflectionParameterNameDiscoverer
표준 java.lang.reflect.Parameter API를 사용하여 매개변수 이름을 결정합니다. javac에 대해 -parameters 플래그를 사용하여 코드를 컴파일해야 합니다. Java 8 이상에서 권장되는 접근 방식입니다.

AspectJAdviceParameterNameDiscoverer
포인트컷 표현식, returningthrowing 절에서 매개변수 이름을 추론합니다. 사용된 알고리즘에 대한 자세한 내용은 javadoc를 참조하세요.

Explicit Argument Names

@AspectJ 어드바이스와 포인트컷 annotation에는 annotation이 달린 메서드의 인수 이름을 지정하는 데 사용할 수 있는 선택적 argNames 속성이 있습니다.

[Tip]
@AspectJ aspect가 디버그 정보 없이도 AspectJ 컴파일러(ajc)에 의해 컴파일된 경우 컴파일러가 필요한 정보를 유지하므로 argNames 속성을 추가할 필요가 없습니다.

마찬가지로 @AspectJ aspect가 -parameters 플래그를 사용하여 javac로 컴파일된 경우 컴파일러가 필요한 정보를 유지하므로 argNames 속성(attribute)을 추가할 필요가 없습니다.

다음 예에서는 argNames 특성을 사용하는 방법을 보여줍니다.

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

(1) 포인트컷 표현식 결합에 정의된 pointcut이라는 publicMethod를 참조합니다.
(2) 인수 이름으로 Bean과 auditable을 선언합니다.

첫 번째 매개 변수가 JoinPoint, ProceedingJoinPoint 또는 JoinPoint.StaticPart type인 경우 argNames 특성(attribute) 값에서 매개 변수 이름을 생략할 수 있습니다. 예를 들어, 조인 포인트 객체를 수신하기 위해 이전 advice을 수정하는 경우 argNames 속성(attribute)은 이를 포함할 필요가 없습니다.

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

(1) 포인트컷 표현식 결합에 정의된 pointcut이라는 publicMethod를 참조합니다.
(2) 인수 이름으로 Bean과 auditable을 선언합니다.

JoinPoint, ProceedingJoinPoint 또는 JoinPoint.StaticPart 유형의 첫 번째 매개변수에 제공되는 특별 처리는 다른 조인 포인트 컨텍스트를 수집하지 않는 어드바이스 메소드에 특히 편리합니다. 이러한 상황에서는 argNames 속성을 생략할 수 있습니다. 예를 들어 다음 조언에서는 argNames 속성(attribute)을 선언할 필요가 없습니다.

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

(1) 포인트컷 표현식 결합에 정의된 pointcut이라는 publicMethod를 참조합니다.

Proceeding with Arguments

우리는 Spring AOP와 AspectJ에서 일관되게 작동하는 인수를 사용하여 proceed 호출을 작성하는 방법을 설명할 것이라고 앞서 언급했습니다. 해결책은 advice signature이 각 메소드 매개변수를 순서대로 바인딩하는지 확인하는 것입니다. 다음 예에서는 그 방법을 보여줍니다.

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

(1) 명명된 Pointcut 정의 공유에 정의된 inDataAccessLayer 명명된 pointcut을 참조합니다.

대부분의 경우 앞의 예와 같이 이 바인딩을 수행합니다.

Advice Ordering

여러 개의 어드바이스가 모두 동일한 조인 포인트에서 실행되길 원하면 어떻게 될까요? Spring AOP는 advice 실행 순서를 결정하기 위해 AspectJ와 동일한 우선순위 규칙을 따릅니다. 우선순위가 가장 높은 advice가 "들어가는 중에" 먼저 실행됩니다(따라서 before advice 두 개가 주어지면 우선순위가 가장 높은 advice이 먼저 실행됩니다). 조인 포인트에서 "나가는 동안" 가장 높은 우선순위의 advice이 마지막에 실행됩니다(따라서 두 개의 after advice이 주어지면 가장 높은 우선순위를 가진 advice이 두 번째로 실행됩니다).

서로 다른 aspect에 정의된 두 가지 advice이 모두 동일한 조인 포인트에서 실행되어야 하는 경우 달리 지정하지 않는 한 실행 순서는 정의되지 않습니다. 우선순위를 지정하여 실행 순서를 제어할 수 있습니다. 이는 aspect 클래스에서 org.springframework.core.Ordered 인터페이스를 구현하거나 @Order annotation으로 주석을 달아 일반적인 Spring 방식으로 수행됩니다. 두 가지 aspect이 주어지면 Ordered.getOrder()(또는 annotation 값)에서 더 낮은 값을 반환하는 aspect이 더 높은 우선 순위를 갖습니다.

[Note]
특정 aspect의 각각의 고유한 advice type은 개념적으로 조인 포인트에 직접 적용되도록 의도됩니다. 결과적으로 @AfterThrowing 어드바이스 메소드는 동반된 @After/@AfterReturning 메소드로부터 예외를 수신해서는 안 됩니다.

Spring Framework 5.2.7부터 동일한 조인 포인트에서 실행되어야 하는 동일한 @Aspect 클래스에 정의된 어드바이스 메서드는 어드바이스 type에 따라 다음 순서로 가장 높은 우선 순위에서 가장 낮은 우선 순위로 우선 순위가 할당됩니다. @Around, @Before , @After, @AfterReturning, @AfterThrowing. 그러나 @After 어드바이스 메서드는 @After에 대한 AspectJ의 "after finally 어드바이스" 의미 체계에 따라 동일한 aspect에서 @AfterReturning 또는 @AfterThrowing 어드바이스 메서드 이후에 효과적으로 호출된다는 점에 유의하세요.

동일한 @Aspect 클래스에 정의된 두 개의 동일한 유형의 advice(예: 두 개의 @After advice 메소드)이 모두 동일한 조인 포인트에서 실행되어야 하는 경우 순서는 정의되지 않습니다(소스를 검색할 방법이 없기 때문에). javac 컴파일된 클래스에 대한 리플렉션을 통한 코드 선언 순서). 이러한 조언 메서드를 각 @Aspect 클래스의 조인 포인트당 하나의 조언 메서드로 축소하거나 Ordered 또는 @Order를 통해 측면 수준에서 주문할 수 있는 별도의 @Aspect 클래스로 advice 조각을 리팩터링하는 것을 고려하세요.

0개의 댓글