참고자료
이번 포스팅부터 스프링 AOP를 직접 만들고 사용해보면서 AOP에 대해 자세히 익혀보자!
라이브러리 추가
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
이 라이브러리를 추가한 후에 다운된 외부 라이브러리 목록을 보면 다음 라이브러리가 있어야 한다.
- 이 라이브러리를 추가하면 스프링이 자동으로 무엇을 등록해준다고 했던가? 바로 빈 포스트 프로세서 중 AnnotationAwareAspectJAutoProxyCreator이 녀석을 등록해준다고 했다.
- 이 빈 포스트 프로세서는 빈으로 등록된 어드바이저, @Aspect 애노테이션이 붙은 빈(꼭 빈으로 등록해야 한다!)을 모두 찾아서 그 안에 포인트컷과 어드바이스를 통해 어드바이저로 만들어 둔 후, 모든 빈들에 대해 프록시가 적용될 수 있는지를 검토 후 적용해야 한다면 적용하여 프록시로 빈을 등록하거나 적용대상이 아니라면 빈을 그대로 빈으로 등록해주는 빈 포스트 프로세서다. 다시 복습 차원에서!
예제 프로젝트 만들기
AOP를 적용할 예제 프로젝트를 만들어보자. 지금까지 학습했던 내용과 비슷하다.
OrderRepository
package cwchoiit.springadvanced.aop.order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행");
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
return "ok";
}
}
OrderService
package cwchoiit.springadvanced.aop.order;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
public void orderItem(String itemId) {
log.info("[orderService] 실행");
orderRepository.save(itemId);
}
}
AopTest - 테스트 코드
package cwchoiit.springadvanced.aop;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.assertj.core.api.Assertions.*;
@Slf4j
@SpringBootTest
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
}
- AopUtils.isAopProxy(...)를 통해서 AOP 프록시가 적용되었는지 확인할 수 있다. 현재 AOP 관련 코드를 작성하지 않았으므로 프록시가 적용되지 않고, 결과도 false를 반환해야 정상이다.
- 여기서는 실제 결과를 검증하는 테스트가 아니라 학습 테스트를 진행한다. 앞으로 로그를 직접 보면서 AOP가 잘 작동하는지 확인해 볼 것이다. 테스트를 실행해서 잘 동작하면 다음으로 넘어가자.
스프링 AOP 구현1 - 시작
스프링 AOP를 구현하는 일반적인 방법은 앞서 학습한 @Aspect를 사용하는 방법이다. 이번 시간에는 @Aspect를 사용해서 가장 단순한 AOP를 구현해보자.
AspectV1
package cwchoiit.springadvanced.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* cwchoiit.springadvanced.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
- @Around 애노테이션의 값인 "execution(* cwchoiit.springadvanced.aop.order..*(..))"는 포인트컷이 된다.
- @Around 애노테이션의 메서드인 doLog는 어드바이스(Advice)가 된다.
- "execution(* cwchoiit.springadvanced.aop.order..*(..))"는 cwchoiit.springadvanced.aop.order 패키지와 그 하위 패키지(..)를 지정하는 AspectJ 포인트컷 표현식이다. 앞으로는 간단히 포인트컷 표현식이라고 하겠다.
- 이제 OrderService, OrderRepository의 모든 메서드는 AOP 적용 대상이 된다. 위 포인트컷 조건을 만족하니까.
- 이렇게만 만들었다고 해서 AOP가 바로 적용되는 것은 아니다. 이 @Aspect 애노테이션이 달린 클래스를 스프링 빈으로 등록해줘야 한다.
참고로, 스프링 AOP는 AspectJ의 문법을 차용하고, 프록시 방식의 AOP를 제공한다. AspectJ를 직접 사용하는 것이 아니다. 스프링 AOP를 사용할 때는 @Aspect 애노테이션을 주로 사용하는데, 이 애노테이션도 AspectJ가 제공하는 애노테이션이다.
또한, @Aspect를 포함한 `org.aspectj` 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능이다. 앞서, build.gradle에 spring-boot-starter-aop를 포함했는데 이렇게 하면 스프링의 AOP 관련 기능과 함께 aspectjweaver.jar도 함께 사용할 수 있게 의존 관계에 포함된다. 그런데 스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이고 실제 AspectJ가 제공하는 컴파일, 로드타임 위버 등을 사용하는 것은 아니다. 스프링은 지금까지 우리가 학습한 것처럼 프록시 방식의 AOP를 사용한다.
AopTest - 테스트 코드
package cwchoiit.springadvanced.aop;
import cwchoiit.springadvanced.aop.aspect.AspectV1;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.*;
@Slf4j
@SpringBootTest
@Import(AspectV1.class) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
}
- @Aspect는 애스팩트라는 표식이지, 컴포넌트 스캔이 되는 것은 아니다! 따라서 AspectV1을 AOP로 사용하려면 반드시 스프링 빈으로 등록을 해야 한다!
- 스프링 빈으로 등록하는 방법은 여러가지가 있다.
- @Bean을 사용해서 직접 등록
- @Component 컴포넌트 스캔을 사용해서 자동 등록
- @Import 주로 설정 파일을 추가할 때 사용하지만, 이 기능으로 스프링 빈도 등록할 수 있다.
실행 결과 - success()
[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행
- 어드바이스 기능이 적용된 모습을 확인할 수 있을 것이다.
스프링 AOP 구현2 - 포인트컷 분리
@Around에 포인트컷 표현식을 직접 넣을 수도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수도 있다.
AspectV2
package cwchoiit.springadvanced.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV2 {
/**
* 반환 타입은 'void' 여야 한다.
* 다른 곳에서 이 포인트컷을 사용하려면 public 이어야 하고 이 내부 안에서 사용하는 건 private 이어도 된다.
* */
@Pointcut("execution(* cwchoiit.springadvanced.aop.order..*(..))")
private void allOrder() {} // pointcut signature
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
- @Pointcut에 포인트컷 표현식을 사용한다.
- 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처라 한다.
- 메서드의 반환 타입은 void여야 한다.
- 블록 내부는 비워둔다.
- 포인트컷 시그니처는 allOrder()이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.
- @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다. 여기서는 @Around("allOrder()")를 사용한다.
- private, public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public을 사용해야 한다.
이렇게 포인트컷을 분리하여 얻는 이점은 다음과 같다.
- 포인트컷에 의미를 부여할 수 있다. (모든 주문에 대해: allOrder())
- 여러 어드바이스에서 해당 포인트컷을 가져다가 사용할 수 있다. (쉽게 말해 모듈화가 된다는 것)
이 애스팩트(AspectV2)를 임포트해서 테스트 코드를 돌려도 동일한 결과를 얻는다.
AopTest - 테스트 코드
package cwchoiit.springadvanced.aop;
import cwchoiit.springadvanced.aop.aspect.AspectV1;
import cwchoiit.springadvanced.aop.aspect.AspectV2;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.*;
@Slf4j
@SpringBootTest
@Import(AspectV2.class) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
}
실행 결과 - success()
[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행
스프링 AOP 구현3 - 어드바이스 추가
이번에는 어드바이스를 하나 더 추가해서 좀 더 복잡한 예제를 만들어보자.
앞서, 로그를 출력하는 기능에 추가로 트랜잭션을 적용하는 코드도 추가해보자. 여기서는 진짜 트랜잭션을 실행하는 것은 아니고 기능이 동작하는 것처럼 로그만 남겨보자.
AspectV3
package cwchoiit.springadvanced.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV3 {
/**
* 반환 타입은 'void' 여야 한다.
* 다른 곳에서 이 포인트컷을 사용하려면 public 이어야 하고 이 내부 안에서 사용하는 건 private 이어도 된다.
*/
@Pointcut("execution(* cwchoiit.springadvanced.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();
}
@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());
}
}
}
- allOrder() 포인트컷은 cwchoiit.springadvanced.aop.order 패키지와 그 하위 패키지를 대상으로 한다.
- allService() 포인트컷은 타입 이름 패턴이 *Service를 대상으로 하는데 쉽게 이야기해서, XxxService처럼 Service로 끝나는 것을 대상으로 한다.
- 여기서 타입 이름 패턴이라고 한 이유는 클래스, 인터페이스에 모두 적용되기 때문이다.
- @Around("allOrder() && allService()")
- 포인트컷은 이렇게 조합할 수도 있다. &&, ||, ! 3가지 조합이 가능하다.
- cwchoiit.springadvanced.aop.order 패키지와 그 하위 패키지이면서 타입 이름 패턴이 *Service인 것을 대상으로 한다.
- 결과적으로 doTransaction() 어드바이스는 OrderService에만 적용된다.
- doLog() 어드바이스는 OrderService, OrderRepository에 모두 적용된다.
AopTest - 테스트 코드
package cwchoiit.springadvanced.aop;
import cwchoiit.springadvanced.aop.aspect.AspectV1;
import cwchoiit.springadvanced.aop.aspect.AspectV2;
import cwchoiit.springadvanced.aop.aspect.AspectV3;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.*;
@Slf4j
@SpringBootTest
@Import(AspectV3.class) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
}
실행 결과 - success()
[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[트랜잭션 시작] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
전체 실행 순서를 분석해보자.
AOP 적용 전
클라이언트 → orderService.orderItem() → orderRepository.save()
AOP 적용 후
클라이언트 → [doLog() → doTransaction()] → orderService.orderItem() → [doLog()] → orderRepository.save()
orderService에는 doLog(), doTransaction() 두가지 어드바이스가 적용되어 있고, orderRepository에는 doLog() 하나의 어드바이스만 적용된 것을 확인할 수 있다. 그런데, 여기에서 로그를 남기는 순서가 [doLog() → doTransaction()] 순서로 작동한다. 만약, 어드바이스가 적용되는 순서를 변경하고 싶으면 어떻게 하면 될까? 예를 들어서 실행 시간을 측정해야 하는데 트랜잭션과 관련된 시간을 제외하고 측정하고 싶다면 [doTransaction() → doLog()] 이렇게 트랜잭션 이후에 로그를 남겨야 할 것이다. 그 방법을 알아보자!
스프링 AOP 구현4 - 포인트컷 참조
다음과 같이 포인트컷으르 공용으로 사용하기 위해 별도의 외부 클래스에 포인트컷들을 모아두어도 된다. 참고로 외부에서 호출할 때는 포인트컷의 접근 제어자를 public으로 열어두어야 한다.
Pointcuts
package cwchoiit.springadvanced.aop.aspect;
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
@Pointcut("execution(* cwchoiit.springadvanced.aop.order..*(..))")
public void allOrder() {
}
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {
}
@Pointcut("allOrder() && allService()")
public void allOrderAndService() {
}
}
- allOrderAndSerivce()는 allOrder() 포인트컷과 allService() 포인트컷을 조합해서 새로운 포인트컷을 만들 수도 있다는 것을 보여주기 위함이다.
AspectV4Pointcut
package cwchoiit.springadvanced.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Slf4j
@Aspect
public class AspectV4Pointcut {
@Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
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 애노테이션은 외부 포인트컷을 참조하면 된다. 살짝 불편한 부분은 패키지명까지 작성해줘야 한다는 것인데 이는 어쩔 수 없다. 문자로 입력해야 하기 때문에.
- 이러한 에스팩트를 가지고 위에서 사용한 테스트 코드를 수행해도 여전히 동일하게 동작한다.
스프링 AOP 구현5 - 어드바이스 순서
어드바이스는 기본적으로 순서를 보장하지 않는다. 순서를 지정하고 싶으면 @Aspect 적용 단위로 org.springframework.core.annotation.Order 애노테이션을 적용해야 한다. 문제는 이것을 어드바이스 단위가 아니라 @Aspect 적용 단위, 즉, 클래스 단위로 적용할 수 있다는 점이다. 그래서 지금처럼 하나의 애스팩트에 여러 어드바이스가 있으면 순서를 보장받을 수 없고 애스팩트를 별도의 클래스로 분리해야 한다.
AspectV5Order
package cwchoiit.springadvanced.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
@Slf4j
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect {
@Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
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());
}
}
}
}
- 하나의 애스팩트 안에 있던 어드바이스들을 LogAspect, TxAspect 애스팩트로 각각 분리했다. 이렇게 내부 클래스로 만드는 것도 하나의 방법이다.
- 그리고 원하는 순서대로 @Order(..) 애노테이션을 적용하면 된다.
AopTest - 테스트 코드
package cwchoiit.springadvanced.aop;
import cwchoiit.springadvanced.aop.aspect.*;
import cwchoiit.springadvanced.aop.order.OrderRepository;
import cwchoiit.springadvanced.aop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.*;
@Slf4j
@SpringBootTest
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class}) //@Import 만으로도 빈으로 등록하는것과 동일하다. 주로 @Configuration 에서 추가할 때 자주 사용됐지만, @Import 로도 그 안에 클래스들을 빈으로 등록한다.
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
log.info("isAopProxy, orderService = {}", AopUtils.isAopProxy(orderService));
log.info("isAopProxy, orderRepository = {}", AopUtils.isAopProxy(orderRepository));
}
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
}
- @Aspect를 반드시 빈으로 등록해야 한다고 했다. 그렇기 때문에 각각의 내부 클래스를 빈으로 등록해야 한다. 위 @Import처럼.
실행 결과 - success()
[트랜잭션 시작] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[log] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String cwchoiit.springadvanced.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void cwchoiit.springadvanced.aop.order.OrderService.orderItem(String)
- 결과를 보면 트랜잭션이 먼저 실행된 모습이다. 이렇게 어드바이스 적용 순서도 지정할 수 있다.
스프링 AOP 구현6 - 어드바이스 종류
어드바이스는 앞서 살펴본 @Around 외에도 여러가지 종류가 있다.
어드바이스 종류
- @Around: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환값 변환, 예외 변환 등이 가능
- @Before: 조인 포인트 실행 이전에 실행
- @AfterReturning: 조인 포인트가 정상 완료 후 실행
- @AfterThrowing: 메서드가 예외를 던지는 경우 실행
- @After: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)
코드로 보면 확실히 이해가 된다. 코드를 보자.
AspectV6Advice
package cwchoiit.springadvanced.aop.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
@Slf4j
@Aspect
public class AspectV6Advice {
@Before("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
log.info("[afterReturning] {}, return = {}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()", throwing = "ex")
public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[afterThrowing] {}, ex = ", joinPoint.getSignature(), ex);
}
@After("cwchoiit.springadvanced.aop.aspect.Pointcuts.allOrderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
- 첫번째, 우선 모든 어드바이스는 JoinPoint를 첫번째 파라미터로 받을 수 있다. 생략도 가능하다. 그러나, @Around는 반드시 ProceedingJoinPoint를 받아야 한다.
- 그 이유는 @Around 같은 경우 개발자가 직접 타겟을 호출하는 코드를 작성해야 한다. joinPoint.proceed() 이 코드. 그 외 나머지 어드바이스는 개발자가 직접 타겟을 호출하지 않는다. 그래서 @Around는 ProceedingJoinPoint를 첫번째 파라미터로 받아야 하고 그 외 나머지 어드바이스는 JoinPoint를 받거나 생략할 수 있다.
- 두번째, @Before는 실제 타겟을 호출하는 코드를 작성안하지만 @Before의 모든 코드가 다 수행되면 자동으로 호출한다. 물론, 예외가 발생할 경우엔 다음 코드가 호출되지는 않는다.
- 세번째, @AfterReturning, @AfterThrowing은 각각 실제 타겟 호출의 결과와 에러를 파라미터로 받고 그 파라미터의 이름은 애노테이션에서 작성한 이름과 동일해야 한다.
- 네번째, @AfterReturning, @AfterThrowing에서 파라미터로 받는 실제 타겟 호출 반환값과 에러의 타입은 해당 타입과 일치하거나 그 상위 타입이어야 한다.
- 다섯번째, @AfterReturning에서는 @Around와 다르게 실제 타겟 호출 반환값에 대한 변경이 불가능하다.
- 이는 단순하게 생각해보면 된다. @Around는 개발자가 직접 실제 타겟을 호출하여 돌려받는 결과를 리턴하는데 그렇기 때문에 리턴값에 변경이 가능한것이고 @AfterReturning은 그렇지 않기 때문에 불가능한 것. 다만, 이 반환값을 가지고 어떤 행동을 취할 순 있다. 그 반환값을 변경하지 못한다는 말이다.
- 여섯번째, @Around는 joinPoint.proceed()를 여러번 호출할 수도 있다.
- 일곱번째, @Around는 joinPoint.proceed()를 반드시 호출해야 한다. 그래야 다음 어드바이스 또는 실제 객체를 호출할 수 있다.
- 여덟번째, @After는 메서드 실행이 정상적이든 예외가 발생하든 상관없이 종료되면 실행된다.
참고로, ProceedingJoinPoint는 JoinPoint의 하위 타입이다.
JointPoint 인터페이스의 주요 기능
- getArgs() → 메서드 인수를 반환한다.
- getThis() → 프록시 객체를 반환한다.
- getTarget() → 대상 객체를 반환한다.
- getSignature() → 조인 포인트(스프링 AOP면 메서드에 한함)에 대한 여러 정보를 반환한다.
- toString() → 포인트컷에 대한 설명을 인쇄한다.
ProceedingJoinPoint 인터페이스의 주요 기능
- proceed() → 다음 어드바이스나 타겟을 호출한다.
이 여러 어드바이스의 호출 순서는 다음과 같다.
- 스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
- @Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing
- 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아두자. 위 그림을 보면 이해가 될 것이다.
- 물론, @Aspect 안에 동일한 종류의 어드바이스가 2개 이상이면 순서가 보장되지 않는다. 이 경우에 보장된 순서를 원한다면 @Aspect를 분리해서 @Order를 적용해서 순서를 적용해야 한다.
그럼 왜 @Around만 사용하더라도 모든게 가능한데 이렇게 부분적으로 나뉘어진 어드바이스가 있을까?
이 부분에 대한 답은 이런것들이다. 다음 코드엔 심각한 문제가 있다.
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(ProceedingJoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
- 어떤 문제가 있을까? 바로 @Around 어드바이스인데도 실제 객체를 호출하지 않는다. 이 코드를 작성한 개발자의 의도는 실제 객체를 호출하기 전에 무언가를 로그로 출력하고 싶었던 것 뿐인데 @Around이기 때문에 실제 객체를 반드시 호출해야 한다.
그럼 이 코드를 보자. 이 코드에는 문제가 전혀 없다.
@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
- @Before이기 때문에 실제 객체를 호출하는 고민을 전혀 할 필요가 없다.
이 말은, @Around는 가장 넓은 기능을 제공하나 실수할 가능성이 있다. 반면, @Before, @After 같은 어드바이스는 기능은 적더라도 실수할 가능성이 적으며 코드가 단순해진다. 그리고 가장 중요한 부분은 이 코드를 작성한 의도가 분명해진다는 것이다. @Before 애노테이션을 본 순간 "아, 이 코드는 실제 객체를 호출하기 전에 무언가를 하기 위해 만들어진 어드바이스구나." 라고 자연스레 생각할 수 있다.
즉, 좋은 설계는 제약이 있는 것이다. 제약은 실수의 가능성을 줄여준다. 애시당초 @Around가 아니라 @Before를 사용하면 실제 객체를 호출할 고민조차 할 필요가 없기 때문에 그 부분을 고려하지 않아도 되는것이다.
'Spring Advanced' 카테고리의 다른 글
Mockito를 사용한 스프링 프로젝트 단위 테스트 (4) | 2024.09.29 |
---|---|
스프링 AOP Part. 3 (포인트컷 지시자, 실전 예제) (0) | 2024.01.02 |
스프링 AOP Part. 1 (0) | 2023.12.29 |
AOP와 @Aspect (0) | 2023.12.29 |
빈 후처리기(BeanPostProcessor) (2) | 2023.12.27 |