[WWCode Seoul 오리지널 콘텐츠 #7] 스프링 프레임워크(Spring Framework) 알아보기 : AOP[3]

Sabai
wwcodeseoul
Published in
35 min readNov 25, 2022

이 글은 간단한 Java 코드를 읽고 이해할 수 있고 AOP 개념이 생소한 스프링을 시작하는 분들을 대상으로 하고 있습니다.

출저 : https://spring.io

이전글 :

[WWCode Seoul 오리지널 콘텐츠 #4]스프링 프레임워크(Spring Framework) 알아보기 : AOP[1]

[WWCode Seoul 오리지널 콘텐츠 #6] 스프링 프레임워크(Spring Framework) 알아보기 : AOP[2]

1.Pointcut

JoinPoint의 관심을 정의하여 Advice가 실행될 때 이를 통해 부가 기능을 적용하기 위한 대상 시점을 선정하는 방법입니다. 이때 시점은 예외 발생, 필드(attribute) 수정, 객체가 생성(constructor), 특정 메소드 호출을 의미하며 Spring AOP에서 사용되는 JoinPoint는 항상 메소드 호출을 의미합니다. Pointcut은 이름과 파라미터를 포함하는 시그니처와 관심 있는 어떤 시점을 실행할지에 대한 표현식으로 구성되어 있습니다.
@AspectJ 어노테이션 스타일의 AOP의 경우, Pointcut 시그니처는 void 반환 유형을 가지는 메소드 선언에 의해 제공되고, Pointcut 표현식은 @Pointcut 어노테이션에 의해 나타나지며 AspectJ pointcut 언어를 사용합니다.
Spring AOP의 경우 스프링빈의 JoinPoint는 메소드의 실행만 지원함으로 스프링 빈의 메소드의 실행과 Pointcut이 일치한다고 생각할 수 있습니다.

1) Pointcut 표현식 문법

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)

execution(접근제어자패턴? 반환타입패턴 선언타입패턴?메서드이름패턴(매개변수패턴) 예외패턴?)

패턴 내 규칙
?는 생략 가능한 패턴을 의미합니다.
*는 아무 값이나 들어와도 됨을 의미합니다.
여려 표현 식을 함께 사용하기 위한 &&(and), ||(or), !(negation) 모두 지원합니다.

선언 타입 패턴
.는 정확히 해당 위치의 패키지를 ..는 해당 위치의 패키지와 그 하위 패키지 포함을 의미합니다.

매개변수 패턴
() 매개변수를 사용하지 않는 메서드와 일치하는 반면 (..) 매개변수의 개수(0개 이상)와 일치합니다.
(*) 패턴은 모든 유형의 하나의 매개변수를 취하는 메소드와 일치하며 (*,String)은 두 개의 매개변수를 사용하는 메서드와 일치하는데 첫 번째는 모든 유형이 될 수 있고 두 번째는 String 타입이어야 합니다.

2) Pointcut 지시자(Pointcut Designator, PCD)

execution

JoinPoint 실행 메소드와 일치시키는 것으로 가장 주된 PCD입니다.

package com.example.aop.application;

public interface DrinkService {
Latte make();
}

package com.example.aop.application;

@Service
public class LatteService implements DrinkService {
public Latte make() {
// ...
}

public void addPowder(Powder powder) {

}
}
public class ExecutionExpressionTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();

@Test
void checkParentTypeMethodMatchesParent() throws NoSuchMethodException {
//given
Method parentTypeMethod = LatteService.class.getMethod("make");

//when
pointcut.setExpression("execution(com.example.aop.domain.Latte com..DrinkService.*())");

//then
assertThat(pointcut.matches(parentTypeMethod, LatteService.class)).isTrue();
}

@Test
void checkChildTypeMethodDoesNotMatchesParent() throws NoSuchMethodException {
//given
Method childTypeMethod = LatteService.class.getMethod("addPowder", Powder.class);

//when
pointcut.setExpression("execution(* com.example.aop.application.DrinkService.*(*))");

//then
assertThat(pointcut.matches(childTypeMethod, LatteService.class)).isFalse();
}
}

execution에서는 다형성에서 부모 타입 = 자식 타입 이 할당 가능한 것과 같이 부모 타입을 선언해도 그 자식 타입은 매칭됩니다. 다만, 부모 타입을 표현식에 선언한 경우 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공합니다.

within

특정 타입 내의 JoinPoint를 일치시키는 것으로 제한하는 것으로 execution에서 선언 타입 패턴과 일치합니다.

within(com.example.aop.application.*)
within(com..*)

this & target

this의 경우 빈 참조(Spring AOP 프록시)가 CGLib 방식으로 생성되었고 주어진 타입의 인스턴스인 JoinPoint를 일치시키는 것으로 제한하고 target의 경우 JDK 동적 프록시로 프록시가 생성될 때 타깃 객체(스프링 AOP 프록시가 가르키는 실제 대상)가 주어진 타입의 인스턴스인 JoinPoint를 일치시키는 것으로 제한합니다.
둘 다 부모 타입을 허용하고 파라미터 바인딩에서 주로 사용됩니다.

@Slf4j
@Aspect
public class LoggingAspect {
@Before("this(com.example.aop.application.DrinkService)")
public void thisPCDParentLogging() {
log.info("1-thisPCDParentLogging");
}

@Before("target(com.example.aop.application.DrinkService)")
public void targetPCDParentLogging() {
log.info("2-targetPCDParentLogging");
}

@Before("this(com.example.aop.application.LatteService)")
public void thisPCDChildLogging() {
log.info("3-thisPCDChildLogging");
}

@Before("target(com.example.aop.application.LatteService)")
public void targetPCDChildLogging() {
log.info("4-targetPCDChildLogging");
}
}

JDK 동적 프록시로 테스트시

@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest(properties = "spring.aop.proxy-target-class=false")
public class ThisTargetExpressionTest {
@Autowired
DrinkService drinkService;

@Test
void executeMake() {
drinkService.make();
}
}

INFO 12348 --- [ Test worker] com.example.aop.aspect.LoggingAspect : 4-targetPCDChildLogging
INFO 12348 --- [ Test worker] com.example.aop.aspect.LoggingAspect : 2-targetPCDParentLogging
INFO 12348 --- [ Test worker] com.example.aop.aspect.LoggingAspect : 1-thisPCDParentLogging

JDK 동적 프록시의 경우 DrinkService 인터페이스를 기반으로 구현된 새로운 클래스로 LatteService를 알지 못함으로 AOP 가 적용되지 않습니다.

CGLIB 프록시의 경우

@SpringBootTest(properties = "spring.aop.proxy-target-class=true")
..
INFO 12503 --- [ Test worker] com.example.aop.aspect.LoggingAspect : 4-targetPCDChildLogging
INFO 12503 --- [ Test worker] com.example.aop.aspect.LoggingAspect : 2-targetPCDParentLogging
INFO 12503 --- [ Test worker] com.example.aop.aspect.LoggingAspect : 3-thisPCDChildLogging
INFO 12503 --- [ Test worker] com.example.aop.aspect.LoggingAspect : 1-thisPCDParentLogging

CGLIB 프록시의 경우 LatterService를 상속받아 만들었기 때문에 AOP가 적용되어 this(com.example.aop.application.LatteService) 의 경우 부모 타입 허용으로 AOP가 적용됩니다.

args

인자가 주어진 유형의 인스턴스인 JoinPoint를 일치시키는 것으로 제한합니다.
execution은 정적으로 클래스에 선언된 정보를 기반으로 판단하지만, args는 부모 타입을 허용하고 실제 넘어온 객체 인스턴스를 보고 판단하여 AOP를 적용합니다.

@Slf4j
@Aspect
public class LoggingAspect {
@Around("within(com.example.aop..*) && args(com.example.aop.domain.Powder)")
public Object argsPCDLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("==argsPCDLogging==");
return proceedingJoinPoint.proceed();
}
}


@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest
public class argsAspectTest {
@Autowired
DrinkService drinkService;

@Test
void executeMake() {
drinkService.make();
}
}

INFO 32924 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ==argsPCDLogging==

@target & @within

@target의 경우 런타임에 타깃 클래스에 주어진 타입의 어노테이션이 있는 JoinPoint 일치시키는 것으로 제한 즉, 부모 클래스의 메서드까지 어드바이스를 적용하고 @within의 경우 정적으로 일치하는 주어진 어노테이션이 있는 타입 내 JoinPoint를 일치시키는 것으로 제한 즉, 자신 클래스에 정의된 메서드에만 어드바이스를 적용합니다.

@Slf4j
@Aspect
public class LoggingAspect {
@Around("within(com.example.aop..*) && @target(com.example.aop.annotation.ClassTestAop)")
public Object targetAnnotationPCDLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("==targetAnnotationPCDLogging=={}", proceedingJoinPoint.getSignature());
return proceedingJoinPoint.proceed();
}

@Around("@within(com.example.aop.annotation.ClassTestAop)")
public Object withinAnnotationPCDLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("==withinAnnotationPCDLogging=={}", proceedingJoinPoint.getSignature());
return proceedingJoinPoint.proceed();
}
}

@Service
public class LatteService implements DrinkService {
public void addFoam() {

}
}

@ClassTestAop
@Service
public class SeasonLatteService extends LatteService {
public SeasonLatteService(Shot shot, SteamMachine steamMachine) {
super(shot, steamMachine);
}

public void addFoam() {
//...
}
}

@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest
public class ThisTargetExpressionTest {

@Autowired
SeasonLatteService seasonLatteService;


@Test
void makeSeasonLatte() {
seasonLatteService.addPowder(new Powder());
seasonLatteService.addFoam();
}

INFO 41974 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ==targetAnnotationPCDLogging==void com.example.aop.application.LatteService.addPowder(Powder)
INFO 41974 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ==targetAnnotationPCDLogging==void com.example.aop.application.SeasonLatteService.addFoam()
INFO 41974 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ==withinAnnotationPCDLogging==void com.example.aop.application.SeasonLatteService.addFoam()

@args

전달된 인수의 런타임에 지정된 유형의 어노테이션이 있는 JoinPoint를 일치시키는 것으로 제한합니다.

@Slf4j
@Aspect
public class LoggingAspect {
@Around("within(com.example.aop..*) && @args(org.springframework.stereotype.Component)")
public Object argsAnotationPCDLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("==argsAnotationPCDLogging==");
return proceedingJoinPoint.proceed();
}
}


@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest
public class argsAnnotationAspectTest {
@Autowired
LatteService latteService;

@Test
void excuteAddPowder() {
latteService.addPowder(new Powder());
}
}

INFO 32924 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ==argsAnotationPCDLogging==

@annotation

메소드가 주어진 어노테이션을 가지고 있는 JoinPoint를 일치시키는 것으로 제한합니다.

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

@Service
public class LatteService implements DrinkService {
//...
@MethodTestAop("test value")
public void addPowder(Powder powder) {
//...
}
}

@Slf4j
@Aspect
public class LoggingAspect {
@Around("@annotation(com.example.aop.annotation.MethodTestAop)")
public Object anotationPCDLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("==anotationPCDLogging==");
return proceedingJoinPoint.proceed();
}
}

@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest
public class annotationAspectTest {

@Autowired
LatteService latteService;

@Test
void excuteAddPowder() {
latteService.addPowder(new Powder());
}
}

INFO 35855 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ==anotationPCDLogging==

bean

빈의 이름 혹은 이름 목록과 일치하는 JoinPoint로 제한하는 PCD로 스프링 AOP에서만 사용되며 타입 레벨이 아닌 인스턴스 레벨에서 작동합니다.

@Aspect
public class LoggingAspect {
@Around("bean(*Service)")
public Object beanPCDLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("==beanPCDLogging==");
return proceedingJoinPoint.proceed();
}
}

@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest
public class beanAspectTest {

@Autowired
LatteService latteService;

@Test
void excuteAddPowder() {
latteService.addPowder(new Powder());
}
}

INFO 36260 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ==beanPCDLogging==

3) 주의할 점

컴파일하는 동안 AspectJ는 코드를 검사하고 각 JoinPoint가 주어진 Pointcut과 정적 / 동적으로 일치하는지 확인하는 것은 비용이 많이 들기 때문에 매칭 성능을 최적화하기 위해 DNF(Disjunctive Normal Form)으로 재작성되며 평가됩니다. 이 덕분에 더 저렴한 구성요소가 먼저 확인되도록 정렬하여 다양한 PCD의 성능을 이해하는 것에 대해 걱정할 필요가 없고 Pointcut 선언에서 임의의 순서로 제공 할 수 있습니다. 하지만, 최적의 일치 성능을 위해서는, 가능한 한 검색 공간을 좁혀야 합니다.

종류 지정자(kinded) : 특정 종류의 JoinPoint를 선택합니다. (예 : execution)
범위 지정자(scoping) : 관심 JoinPoint 그룹을 선택합니다. (예 : within, withincode)
컨텍스트 지정자(contextual) : 컨텍스트 기반으로 선택합니다. (예 : this, target, @annotation)

잘 작성된 Pointcut이라면 적어도 종류 지정자와 범위 지정자를 포함해야 합니다.
종류 지정자 / 컨텍스트 지정자만 사용 시에는 추가 처리 및 분석으로 인해 시간, 메모리 사용 등 위빙 성능에 영향을 줄 수 있지만 범위 지정자의 경우 처리할 필요 없는 JoinPoint 그룹을 무시할 수 있고 빠르게 동작함으로 가능한 하나를 포함하면 좋습니다.

위에 설명에서 알 수 있듯 args , @args , @target은 런타임에서 즉, 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있습니다. 이 PCD는 모든 스프링 빈에 AOP를 적용을 시도하게 되므로 의도한 대로 PCD를 사용하기 위해서는 범위 지정자를 동반하여 작성해야 합니다.

2. Advice

Advice는 Pointcut 표현 식과 연관되어 이와 일치하는 메서드 실행 시 실행되는 부가기능입니다.
이때, Pointcut은 앞의 예제와 같이 선언된 Pointcut 실행식으로 작성할 수 있지만 이름이 있는 Pointcut으로도 작성 후 참조도 가능합니다.

1) 종류

Before

일치하는 메서드 실행 전 실행됩니다.

선언된 Pointcut

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before() {
String value();
...
}
@Slf4j
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.aop.application.*Service.*(..))")
public void serviceLoggingPCD() {
log.info("===service===");
}
}

이름이 있는 Pointcut

@Slf4j
@Aspect
public class CommonPointcut {
@Pointcut("execution(* com.example.aop.application.*Service.*(..))")
public void serviceLoggingPCD() {
}
}

@Slf4j
@Aspect
public class LoggingAspect {
@Before("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()")
public void serviceLogging() {
log.info("===service===");
}
}

After Returning

일치하는 메서드 실행이 정상적으로 반환될 때 실행됩니다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterReturning() {
String value() default "";

String pointcut() default "";

String returning() default "";
}
@Slf4j
@Aspect
public class LoggingAspect {
@AfterRetuning("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()")
public void serviceSuccessLogging() {
log.info("===service success===");
}
}

이때, 실제로 반환되는 값을 Advice body 내에서 접근해야 한다면 returning을 사용하여 반환 값을 바인드 할 수 있습니다.
returning 속성에서 사용되는 이름은 Advice 시그니처의 파라미터 명과 동일해야 하고 파라미터의 타입과 같은 유형을 반환하는 메서드의 실행으로 JoinPoint 일치를 제한합니다.

@Slf4j
@Aspect
public class LoggingAspect {
@AfterReturning(
pointcut = "com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()",
returning = "result")
public void serviceSuccessLogging(Object result) {
log.info("===service success==={}", result);
}
}

After Throwing

exception을 던진 후 종료하는 메서드 실행과 일치할 때 실행됩니다.

@Slf4j
@Aspect
public class LoggingAspect {
@AfterThrowing("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()")
public void serviceFailLogging() {
log.info("===service fail===");
}
}

이때, throwing을 사용하여 throw 된 예외를 advice 파라미터에 바인드 할 수 있으며 returning 속성에서 사용되는 이름은 Advice 시그니처의 파라미터 명과 동일해야 하고 파라미터의 타입과 같은 유형의 Exception을 throw 하는 메서드의 실행으로 JoinPoint 일치를 제한합니다.

@Slf4j
@Aspect
public class LoggingAspect {
@AfterThrowing(
pointcut = "com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()"
,throwing = "ex")
public void serviceFailLogging(IllegalAccessException ex) {
log.info("===service fail==={}", ex);
}
}

After

일치하는 메소드의 실행 종료 시 정상과 Exception 반환 조건을 다룰 수 있도록 준비되어 있어야 하며 일반적으로 리소스 등을 해제하는 데 사용됩니다.

@Slf4j
@Aspect
public class LoggingAspect {
@After("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()")
public void serviceFinishLogging() {
log.info("===service finish===");
}
}

Around

메서드 실행 전후 실행 시 사용됩니다.
메서드의 반환 형식은 Object로 메서드의 호출자가 바라보는 반환 값이며, 첫 번째 매개변수는 ProceedingJoinPoint를 사용하고 메서드 본문에서 proceed()를 호출하여 타깃 메소드를 실행합니다. (이는 전혀 사용되지 않거나 여러 번 사용이 가능합니다.)

@Slf4j
@Aspect
public class LoggingAspect {
@Around("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()")
public Object serviceExecuteLogging(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("===service start==={}", proceedingJoinPoint.getSignature());
Object result = proceedingJoinPoint.proceed();
log.info("===service finish===");
return result;
}
}

2) 파라미터 바인딩

args를 표현식 내 형식이 아닌 파라미터 명을 쓰면 아래와 같이 Advice 본문에서 사용할 수 있고 파라미터가 지정된 타입인 메소드로 제한할 수 있습니다.

@Service
public class LatteService implements DrinkService {
public void addLabel(String name) {

}
}

@Slf4j
@Aspect
public class LoggingAspect {
@Around("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD() && args(str)")
public Object serviceExecuteLogging(ProceedingJoinPoint proceedingJoinPoint, String str) throws Throwable {
log.info("===service start==={}", str);
Object result = proceedingJoinPoint.proceed();
log.info("===service finish===");
return result;
}
}

@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest
public class ArgsParameterTest {
@Test
void executeLable() {
latteService.addLabel("label name");
}
}

INFO 83711 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ===service start===label name
INFO 83711 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ===service finish===

위의 예제는 아래와 같이 작성 시 동일하게 작동합니다.

@Slf4j
@Aspect
public class CommonPointcut {
@Pointcut("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD() && args(str)")
public void serviceExecuteLoggingPCD(String str) {

}
}

@Slf4j
@Aspect
public class LoggingAspect {
@Around("com.example.aop.aspect.CommonPointcut.serviceExecuteLoggingPCD(str)")
public Object serviceExecuteLogging(ProceedingJoinPoint proceedingJoinPoint, String str) throws Throwable {
log.info("===service start==={}", str);
Object result = proceedingJoinPoint.proceed();
log.info("===service finish==={}");
return result;
}
}

프록시 오브젝트(this), 타깃 오브젝트(target), 어노테이션(@within, @target, @annotation, @args)은 모두 유사한 방식으로 바인딩 되며 첫 파라미터가 JoinPoint, ProceedingJoinPoint, JoinPoint.StaticPart 타입이면 생략할 수 있습니다.

@Service
public class LatteService<T> implements DrinkService {
public void addLabel(T name) {

}

public void addLabels(Collection<T> name) {

}
}

@Slf4j
@Aspect
public class LoggingAspect {
@Around("com.example.aop.aspect.CommonPointcut.serviceExecuteLoggingPCD(str)")
public Object serviceExecuteLogging(ProceedingJoinPoint proceedingJoinPoint, String str) throws Throwable {
log.info("===service start==={}", str);
Object result = proceedingJoinPoint.proceed();
log.info("===service finish==={}");
return result;
}
}

@Slf4j
@Import(LoggingAspect.class)
@SpringBootTest
public class GenericAspectTest {
...
@Test
void executeLabel() {
latteService.addLabel("label name");
latteService.addLabels(new ArrayList<String>());
}
}

제네릭 사용 시 메소드의 파라미터 타입에 Advice 파라미터를 바인딩하여 특정 파라미터 타입으로 제한이 가능합니다.

@Slf4j
@Aspect
public class LoggingAspect {
@Around("com.example.aop.aspect.CommonPointcut.serviceExecuteLoggingPCD(str)")
public Object serviceExecuteLogging(ProceedingJoinPoint proceedingJoinPoint, Collection<String> str) throws Throwable {
...
}
}

nested exception is java.lang.IllegalArgumentException: error at ::0
incompatible type, expected java.lang.String found BindingTypePattern(java.util.Collection, 0).
Check the type specified in your pointcut

하지만 제네릭 컬렉션의 경우 모든 요소를 탐색해야 하고 null 값 처리를 결정할 수 없으므로 위와 같은 에러가 발생합니다.

3) 순서

여러 개의 Advice 들을 동일한 JoinPoint 에서 실행 시 타깃 메소드 실행 전은 우선순위에 따라, 실행 후는 우선순위에 반대로 실행됩니다.

우선순위 : @Around >@Before > @After > @AfterReturning > @AfterThrowing

@Slf4j
@Aspect
public class FirstLoggingAspect {
@Before("com.example.aop.aspect.CommonPointcut.serviceLoggingPCD()")
public void firstServiceLogging() {
log.info("===service first logging===");
}
}

@Slf4j
@Aspect
public class LoggingAspect {
@Around("com.example.aop.aspect.CommonPointcut.serviceExecuteLoggingPCD(str)")
public Object serviceExecuteLogging(ProceedingJoinPoint proceedingJoinPoint, String str) throws Throwable {
log.info("===service start==={}", str);
Object result = proceedingJoinPoint.proceed();
log.info("===service finish===");
return result;
}
}

@Slf4j
@Import({LoggingAspect.class, FirstLoggingAspect.class})
@SpringBootTest
public class ThisTargetExpressionTest {
@Test
void executeLable() {
latteService.addLabel("label name");
}
}

INFO 88500 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ===service start===label name
INFO 88500 --- [ Test worker] c.example.aop.aspect.FirstLoggingAspect : ===service first logging===
INFO 88500 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ===service finish==={}

동일한 타입의 N개의 Advice는 순서는 정의되지 않음으로 이를 순서화를 원하거나 우선순위에 따른 실행을 변경하고 싶다면 org.springframework.core.Ordered 인터페이스를 구현하거나, @Order 어노테이션을 붙이면 됩니다. 이때, 낮은 숫자가 우선순위를 가지게 됩니다.

@Slf4j
@Aspect
@Order(1)
public class FirstLoggingAspect {
...
}

@Slf4j
@Aspect
@Order(2)
public class LoggingAspect {
...
}


INFO 88500 --- [ Test worker] c.example.aop.aspect.FirstLoggingAspect : ===service first logging===
INFO 88500 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ===service start===label name
INFO 88500 --- [ Test worker] com.example.aop.aspect.LoggingAspect : ===service finish==={}

글쓴이 이슬기

해당 글은 WWCode Seoul의 입장을 대변하고 있지 않습니다.

출저 : 이일민 저, 토비의 스프링 3.1 Vol. 1 스프링의 이해와 원리』, 에이콘 출판사

https://github.com/spring-projects/spring-framework
https://gmoon92.github.io/spring/aop/2019/05/06/pointcut.html
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html
https://stackoverflow.com/questions/51124771/difference-between-target-and-within-spring-aop
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8

--

--