스프링 AOP 적용법
스프링 AOP를 사용하기 위해서는 build.gradle에 아래의 라이브러리를 의존성 추가해줘야합니다.
implementation 'org.springframework.boot:spring-boot-starter-aop' // aop 추가
스프링 AOP를 적용하기 위해서는 @Aspect 어노테이션을 사용합니다.
@Slf4j
@Aspect
@Component
public class AspectExample
@Around("execution(* hello.aop.test..*(..))") // AspectJ 표현식
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 실제 타깃 호출
}
}
@Aspect의 어노테이션이 붙은 클래스를 Advisor(어드바이저)라고 하고,
메서드는 Advice(어드바이스 : 부가 기능), @Around는 Pointcut으로 Advice를 적용할 대상을 지정합니다.
@Around 안에 옵션으로 작성한 "execution( hello.aop.test..*(..)) "는 hello.aop.test패키지와 그 하위 패키지( .. )를 지정하는 AspectJ 포인트컷 표현식입니다.
포인트컷 표현식 링크 ->>>
작성한 어드바이저인 AspectExample을 AOP로 사용하려면 위처럼 스프링 빈으로 등록해줘야 AOP가 적용됩니다.
이렇게 스프링 AOP를 적용하면 포인트 컷을 보고 적용 대상인 hello.aop.test패키지와 그 하위 패키지에 doLog 어드바이스가 적용되게 됩니다.
ProceedingJoinPoint 인터페이스
@Around 어드바이스를 사용할 경우 메서드의 파리미터로 "ProceedingJoinPoint"를 꼭 넣어줘야 합니다.
ProceedingJoinPoint의 proceed()는 다음 어드바이스나 타켓을 호출하는 것으로, 어드바이스를 사용하기 위해서는 꼭 proceed() 메서드를 호출해줘야 합니다.
이 외에도 호출되는 대상 객체에 대한 정보, 실행되는 메서드에 대한 정보 등이 필요할 때가 있는데 이 경우에는
ProceedingJoinPoint 인터페이스가 제공하는 아래의 메서드를 사용할 수 있습니다.
(이 외에도 더 있지만 일부만 설명)
메서드 | 설 명 |
Signature getSignature() | 호출되는 메서드에 대한 정보를 반환. |
Object getTarget() | 대상 객체를 반환 |
String getName | 메서드의 이름을 반환 |
String toLongString() | 메서드를 완전하게 표현한 문장을 반환 (메서드의 리턴 타입, 파라미터 타입 모두 표시) |
이렇게 ProceedingJoinPoint로 호출되는 객체에 대한 정보나, 실행되는 메서드의 정보를 알 수 있는 이유는
스프링 부트 자동 설정으로 "AnnotationAwareAspectJAutoProxyCreator" 이라는 자동 프록시 생성기가 빈 등록되어 있는데,
이 자동 프록시 생성기가 @Aspect가 붙은 클래스를 보고 Advisor(어드바이저)로 변환해 저장해줍니다.
그리고 이 Advisor(어드바이저)를 보고 포인트컷의 대상이 되는 것들을 "ProxyFactory"에 인자로 넘겨 자동으로 프록시를 생성하고 적용해줍니다.
여기서 생성된 프록시 객체가 메서드를 호출할 때, ProceedingJoinPoint 객체를 생성하고 이를 advice에 전달합니다.
즉, ProceedingJoinPoint는 프록시가 메서드를 호출하는 시점의 정보를 가져 어드바이스가 적용되는 대상을 이미 알고 있습니다.
@Pointcut
@Around 에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리해 재사용할 수 도 있습니다.
사용방법은 다음과 같습니다.
@Slf4j
@Aspect
@Component
public class AspectExample {
// hello.aop.test 패키지와 하위 패키지에 적용
@Pointcut("execution(* hello.aop.test..*(..))")
private void allTestLog() {} // 포인트컷 시그니쳐
@Around("allTestLog()") // 포인트컷을 메서드로 만들어 사용
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 실제 타깃 호출
}
}
- @Pointcut 에 포인트컷 표현식을 사용.
- 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라고 한다.
- 메서드의 반환 타입은 void 이어야 한다.
- 코드 내용은 비워둔다.
- @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처도 사용가능하다.
포인트컷을 하나의 클래스에 모아 관리하기
포인트컷을 하나의 클래스에 모아서 관리할 수 도 있습니다.
public class Pointcuts {
// hello.aop.test 패키지와 하위 패키지에 적용
@Pointcut("execution(* hello.aop.test..*(..))")
public void allTestLog() {} // 포인트컷 시그니쳐
}
}
지금은 하나의 포인트컷 시그니처만을 만들었지만 여러개의 포인트컷 시그니처를 이 하나의 클래스에 모아 관리하여 사용할 수 있습니다.
이 처럼 하나의 클래스에 포인트컷을 모아 놓고, 이 포인트 컷을 사용할 때
@Around에 "포인트컷의 경로, 클래스, 포인트컷 시그니처"를 적어줘야 합니다.
@Slf4j
@Aspect
public class AspectExample {
@Around("hello.aop.order.test.Pointcuts.allOrder()") // 포인트컷을 메서드로 만들어 사용
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 실제 타깃 호출
}
}
부가 기능 또 추가 하기
기존 부가 기능에서 또 부가 기능을 추가할 수도 있습니다.
앞서 로그를 출력하는 기능에 추가로 OrderService 클래스에 트랜잭션을 적용했다고 가정하고 포인트컷과 어드바이스를 추가해보겠습니다.
@Slf4j
@Aspect
@Component
public class AspectExample {
// hello.aop.order 패키지와 하위 패키지에 적용
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder() {} // 포인트컷 시그니쳐
// 클래스 이름 패턴이 *Service
@Pointcut("execute(* *..*Service.*(..))")
private void allService() {}
@Around("allOrder()") // 포인트컷을 메서드로 만들어 사용
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
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());
}
}
}
doTransaction 어드바이스는 @Around 옵션으로 패키지가 hello.aop.order 하위 이고 (&&) 클래스 이름 패턴이 *Service인 경우에 해당 어드바이스를 적용합니다.
그래서 OrderService에는 doLog 어드바이스와 doTransaction 어드바이스 총 2개의 부가기능이 추가됩니다.
어드바이스 순서 지정 ( @Order )
어드바이스는 기본적으로 순서를 보장해주지 않습니다.
위 코드의 로그를 찍어보면 (doLog 어드바이스 → doTransaction 어드바이스) 이 순서로 실행됩니다.
그런데 만약 (doTransaction 어드바이스 → doLog 어드바이스) 순서로 기능을 실행하고 싶다면 어떻게 해야할 까요?
바로 @Order 어노테이션으로 순서를 지정하는 방법이 있습니다
@Order
@Order 어노테이션은 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있습니다.
로그를 남기는 순서를 (doTransaction 어드바이스 → doLog 어드바이스) 순으로 변경해 보겠습니다.
(여기서는 코드를 보기 편하게 AspectExample 클래스 내부에서 static class로 분리하였습니다.)
@Slf4j
public class AspectExample {
// hello.aop.order 패키지와 하위 패키지에 적용
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {} // 포인트컷 시그니쳐
// 클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {}
@Aspect
@Order(2)
public static class LogAspect {
@Around("allOrder()") // 포인트컷을 메서드로 만들어 사용
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); // join point 시그니처
return joinPoint.proceed(); // 실제 타깃 호출
}
}
@Aspect
@Order(1)
public static class TransactionAspect {
// 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를 적용하고 Order로 순서를 지정하면 다음과 같이 순서가 변경된 것을 볼 수 있습니다.
어드바이스의 종류
- @Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능 (사실 이거 하나만 사용해도 무방)
- @Before : 조인 포인트 실행 이전에 실행
- @AfterReturning : 조인 포인트가 정상 완료후 실행
- @AfterThrowing : 메서드가 예외를 던지는 경우 실행
- @After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
@Around를 제외한 나머지 어드바이스들은 JoinPoint를 첫 번째 파라미터에 사용합니다. (생략도 가능)
( JoinPoint는 ProceedingJoinPoint의 부모 타입)
주로 사용하는 @Around 기능 살펴보기
메서드 호출 전후에 작업을 수행하는 가장 강력한 어드바이스로 아래와 같은 일이 가능합니다.
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
// @Before
log.info("[트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
// @AfterReturning
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
// @AfterThrowing
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
// @After
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
- 조인 포인트 실행 "joinPoint.proceed()" 여부 선택 : joinPoint.proceed()를 호출하지 않으면 다음 코드 실행 X
- 전달 값 변환 가능 : joinPoint.proceed(args[])
- 반환 값 변환 가능
- 예외 변환 가능
- 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능
- proceed() 를 여러번 실행할 수도 있다. (재시도)
@Around가 모든 기능을 다 할 수 있는데 왜 다른 어드바이스가 있을까?
@Around 어드바이스 만으로도 모든 기능을 수행 가능하지만, 위 처럼 다양한 어드바이스가 존재하는데,
그 이유는 @Around 는 항상 타겟 (즉, "joinPoint.proceed()")를 호출해야만 하지만 나머지 어드바이스들은 joinPoint.proceed() 를 호출하는 고민을 하지 않아도 됩니다.
@Before 어드바이스를 예를 들면,
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
위 코드는, joinPoint.proceed() 를 호출하는 고민을 하지 않고 단순히 타겟 실행 전에 한정해서 호출되는 메서드의 정보를 로그로 반환합니다.
즉, @Before , @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하며 이 코드를 작성한 의도가 명확하게 드러나게 됩니다.
스프링 AOP란 ?
포인트컷 표현식 알아보기
참고자료 : 김영한의 스프링 핵심 원리 - 고급편
'◼ Spring' 카테고리의 다른 글
[Spring] 특정상황에 스프링 AOP 적용하기 (0) | 2023.05.17 |
---|---|
[Spring] 스프링 AOP - Pointcut 표현식 (2) | 2023.05.16 |
[Spring] 스프링 AOP(Aspect Oriented Programming)란? - @Aspect (0) | 2023.05.12 |
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2023.05.09 |
[Spring] 동시성 문제와 해결 (쓰레드 로컬) (0) | 2023.05.01 |