@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect는 Aspect(Advice + Pointcut = Advisor)를 구성하기 위한 선언이다.
이 선언으로 하여금 스프링에 자동 등록된 프록시를 만들 빈 후처리기가 @Aspect를 스캔하여 등록된 hello.aop.order이하의 모든 범위에 있는 스프링 빈을 Aspect의 doLog가 적용된 형태의 프록시로 바꿔치기한다.
@Around를 통해 범위를 한정하고 적용될 타겟 객체(스프링 빈)을 조사하여 @Around에 주어진 범위의 타겟 객체에 해당한다면 기존 스프링빈 등록을 후처리기를 통해 프록시로 바꿔친다.
개발자가 원하는 시점에 타겟의 메서드를 호출할 경우 프록시의 doLog가 호출된다.
doLog()라는 메서드 이름은 어떻게 바꾸어도 상관없다.
doLog는 부가기능(log.info~)과 joinPoint.proceed()인 타겟객체의 메서드 실행을 수행한다.
doLog는 ProceedingJoinPoint를 파라미터로 받고 있는데 이 안에는 타겟 객체와 타겟 객체의 메서드 정보들이 담겨있다.
반복적으로 같은 포인트 컷 기준을 사용한다면 포인트 컷을 따로 보관해놓고 가져다 쓸 수 있다.
@Slf4j
@Aspect
public class AspectV2 {
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){} //pointcut signature
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
"execution(* hello.aop.order..*(..))"을 allOrder()로 축약해서 사용하는 것이다.
누군가는 그냥 static 스트링 변수로 보관해서 사용하면 안되냐고 물을 수 있을 것이다.
단순히 스트링으로 보관해서 사용할 경우 스트링 자체를 잘못작성했을 경우 이 에러에 대한 확인은 런타임 시점에 가능해지게 된다.
AOP와 통합적으로 사용하는 @Pointcut으로 하여금 메서드에 애노테이션으로 보관하는 것은 컴파일 시점에 오류를 잡아줄 수 있다.
또한 만약 allOrder()말고 우리가 allMember()와 같은 포인트컷을 만들었을 때 allOrder() && allMember()와 같이 확장성 있게 활용할 수 있다.
@Slf4j
@Aspect
public class AspectV3 {
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){} //pointcut signature
// 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//hello.aop.order 패키지와 하위 패키지이면서 동시에 클래스 이름 패턴이 *Service
@Around("allOrder() && allService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 종료] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
하나의 Aspect안에 두 가지 Advice를 가질 수 있다.
joinPoint.proceed()의 proceed=나아가다란 단어의 의미가 와닿는 단계이다.
proceed()를 만나는 시점에 다음 어드바이스가 없을 경우 타겟의 메서드를 호출하지만 다음 어드바이스가 있을 경우 다음 어드바이스를 호출한다.
이때 문제는 어드바이스 적용 순서의 결정기준이 없다는 것이다.
위 예시에서는 트랜잭션안에 로그를 띄우는 것이 자연스러울 것이므로 doTransaction의 proceed() 시점에서 다음 doLog가 수행되는 것이 순서상 자연스럽다.
이러한 순서를 @Order 로 제어할 수 있다.
다중 어드바이스일 때 @Order(int)로 하여금 순서를 지정해줄 수 있다.
문제는 @Order가 @Aspect가 붙는 클래스 레벨에서만 동작한다는 것이다.
그렇기에 다음과 같이 껍데기 클래스의 내부에 두 가지 애스팩트를 구성하여 사용할 수 있다.
@Slf4j
public class AspectV5OrderBy {
@Aspect
@Order(2)
public static class LogAspect {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
//hello.aop.order 패키지와 하위 패키지이면서 동시에 클래스 이름 패턴이 *Service
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
log.info("[트랜잭션 종료] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
}
@Around는 어드바이스들중에 가장 강력한 어드바이스이다.
다만 항상 .proceed()를 호출해야만 한다.
proceed()는 다음 과정으로 넘어가기 위한 필수 코드이다.
만약 개발자가 이를 잊는다면 프로그램의 로직이 다음으로 넘어가지 않는다.
어떻게보면 @Around와 함께 어드바이스를 자유롭게 작성할 수 있는 장점에 따르는 심각한 리스크이다.
@Before : 조인 포인트 실행 이전에 실행
@AfterReturning : 조인 포인트가 정상 완료후 실행
@AfterThrowing : 메서드가 예외를 던지는 경우 실행
@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
위의 네 가지 어드바이스는 @Around를 파편화 시킨 것이다.
또한 이중 하나라도 사용할 경우 joinPoint.proceed()는 자동 실행된다.
무슨 말인지 그림을 보며 다시 판단해보자.
위의 트랜잭션 예시를 가져와서 설명하면 바로 이해가 가능하다.

@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void deBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
// returning 설정으로 하여금 파라미터로 proceed() 결과를 받을 수 있음.
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", ex);
}
// 예외 객체를 파라미터로 받을 수 있음. "ex" -> 파라미터명 매핑
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
@Around는 자유도가 높지만 외 4가지 어드바이스로 어드바이스를 구성하면 더 직관적인 코드가 될 것이다.
예외시 작동할 코드, 예외가 발생하던 로직이 성공하던 마지막에 실행시킬 로직, proceed이전에 실행할 로직, proceed 성공 이후 실행할 로직으로 나뉘어 존재하기 때문에 시각적으로 역할이 명확해지는 효과가 발생한다.
또한 proceed처리가 순서가 결정되어 있어 자동적으로 호출된다. proceed 미호출로 발생할 리스크의 확률은 0%가 된다.
위와 같이 짧은 try-catch문이면 한 눈에 들어올수도 있지만 어드바이스 로직이 굉장히 복잡할 경우 파편화의 효용은 더욱 올라갈 것이다.
execution()을 거의 사용하기 때문에 다른 표현식 까지 완벽히 외우기보단 execution()의 문법만 확실히 잡고가는 것이 좋다고 생각한다.(보이는 것과 달리 그리 어렵지 않다.)
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
-> ? : 생략 가능
// 예시1
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String)) throws java.io.IOException");
-> public이며 String반환타입이면서 선언타입이 hello.aop.member.MemberServiceImpl이면서 메서드가 hello()이고 hello 메서드의 파라미터 타입이 String이면서 이 메서드가 IOException 이하의 예외를 던질 경우 포인트컷 대상이다.
// 예시2
pointcut.setExpression("execution(* *(..))");
-> 모든 반환타입이면서 모든 메서드이름이고 어떤 파라미터가 들어와도 상관없음 -> 모든 메서드에 AOP 적용한다는 뜻
.: 정확하게 해당 위치의 패키지
..: 해당 위치의 패키지와 그 하위 패키지도 포함
* : 아무 값이 들어와도 상관없다.
(String) : 정확하게 String 타입 파라미터
() : 파라미터가 없어야 한다.
(*) : 정확히 하나의 파라미터, 단 모든 타입을 허용한다.
(*, *) : 정확히 두 개의 파라미터, 단 모든 타입을 허용한다.
(..): 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 참고로 파라미터가 없어도 된다. 0..* 로 이해하면 된다.
(String, ..) : String 타입으로 시작해야 한다. 숫자와 무관하게 모든 파라미터, 모든 타입을 허용한다. 예) (String) , (String, Xxx) , (String, Xxx, Xxx) 허용
@Test
void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}
execution()에서 인터페이스를 포인트컷으로 지정할 경우 하위의 구현체까지 적용된다.
다만 인터페이스에 없는 메서드가 구현체에 존재할 경우 구현체를 직접 지정해줘야 해당 메서드에 AOP가 적용된다.