스프링 AOP 구현
스프링 AOP 학습을 위해 아래와 같은 의존성들을 추가하고 프로젝트를 만들었다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-aop'//직접추가
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
다음으로 아래와 같이 아주 간단한 주문 서비스인 orderService, orderRepository를 만들었다.
@Slf4j
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void orderItem(String itemId) {
log.info("[orderService] 실행");
orderRepository.save(itemId);
}
}
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행"); //저장 로직
if (itemId.equals("ex")) throw new IllegalStateException("예외 발생!");
return "ok";
}
}
그리고 아래와 같이 간단한 테스트들을 해보았다.
@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));
}
// 실행결과
// isAopProxy, orderService=false
// isAopProxy, orderRepository=false
@Test
void success(){
orderService.orderItem("itemA");
}
// 실행결과
// [orderService] 실행
// [orderRepository] 실행
@Test
void exception(){
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
// --> 테스트 성공
}
aopInfo 테스트 : 당연히 현재 어플리케이션에 어떤 AOP도 적용하지 않았으므로 orderService와 orderRepository는 false이다.
AOP 적용
이제 Aspect를 만들어보자.
@Slf4j
@Aspect
public class AspectV1 {
@Around("execution(* hello.aop.order..*(..))")//Pointcut
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // joinPoint 시그니처 // Advice
return joinPoint.proceed();//타겟호출
}
}
위처럼 @Around()의 괄호안에는 aspectJ의 표현식을 사용한 포인트컷이 되고,
Object doLog(ProceedingJointPoint joinPoint)의 내부 로직은 어드바이스가 된다.
이와 같은 클래스에 @Aspect어노테이션을 붙이고 빈으로 등록하면 스프링이 이들을 조합하여 어드바이저를 만들어주고, 다른 빈들이 어드바이저의 조건에 만족한다면 해당 빈 객체는 프록시로 만드는 것이다.
이제 다시 위에서 했던 테스트를 해보자.
@Import(AspectV1.class) // AspectV1 빈 등록
@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));
}
// 실행결과
// isAopProxy, orderService=true
// isAopProxy, orderRepository=true
@Test
void success(){
orderService.orderItem("itemA");
}
// 실행결과:
//[log] void hello.aop.order.OrderService.orderItem(String)
//[orderService] 실행
//[log] String hello.aop.order.OrderRepository.save(String)
//[orderRepository] 실행
@Test
void exception(){
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
// --> 테스트 성공
}
AOP가 구현되고 난 뒤의 그림
참고
스프링 AOP는 AspectJ의 문법을 차용하고, 프록시 방식의 AOP를 제공한다. AspectJ를 직접 사용하는 것이 아니다.스프링 AOP를 사용할 때는 @Aspect 애노테이션을 주로 사용하는데, 이 애노테이션도 AspectJ가 제공하는 애노테이션이다.
참고
스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이고, 실제 AspectJ가 제공하는 컴파일, 로드타임 위버 등을 사용하는 것은 아니다. 스프링은 지금까지 우리가 학습한 것 처럼 프록시 방식의 AOP를 사용한다. @Aspect 를 포함한 org.aspectj 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능이다. 앞서 build.gradle 에 spring-boot-starter-aop 를 포함했는데, 이렇게 하면 스프링의 AOP 관련 기능과 함께 aspectjweaver.jar 도 함께 사용할 수 있게 의존 관계에 포함된다.
포인트컷 분리하기
@Around에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있다.
아래와 같다.
@Slf4j
@Aspect
public class AspectV2 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){} //pointcut signature
@Around("allOrder()")//Pointcut
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // joinPoint 시그니처 // Advice
return joinPoint.proceed();//타겟호출
}
}
@Pointcut
• @Pointcut 에 포인트컷 표현식을 사용한다.
• 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.
• 메서드의 반환 타입은 void 여야 한다.
• 코드 내용은 비워둔다.
• 포인트컷 시그니처는 allOrder() 이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.
• @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다. 여기서는 @Around("allOrder()") 를 사용한다.
• private , public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 애스팩트에서 참고하려면 public 을 사용해야 한다.
이렇게 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있다. 그리고 뒤에 설명하겠지만 다른 클래스에 있는 외부 어드바이스에서도 포인트컷을 함께 사용할 수 있다. (포인트컷들만 따로 모듈화가 가능)
어드바이스 추가
이번에는 어드바이스를 하나 더 추가하여 두 개의 어드바이스가 적용될 수 있도록 해보자. 트랜잭션을 적용하는 코드도 추가할 것이다. 진짜 트랜잭션을 실행하는 것은 아니고, 기능이 동작한 것처럼 로그만 남긴다.
트랜잭션 기능은 보통 다음과 같이 동작한다.
1. 핵심 로직 실행 직전에 트랜잭션을 시작
2. 핵심 로직 실행
3. 핵심 로직 실행에 문제가 없으면 커밋
4. 핵심 로직 실행에 예외가 발생하면 롤백
코드를 보자. (/*추가*/ 부분만 보면 된다.)
@Slf4j
@Aspect
public class AspectV3 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){} //pointcut signature
/* 추가 */
//클래스 이름 패턴이 *Service인 부분
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){} //pointcut signature
@Around("allOrder()")//Pointcut
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // joinPoint 시그니처 // Advice
return joinPoint.proceed();//타겟호출
}
/* 추가 */
//hello.aop.order 패키지와 하위 패키지이면서 클래스 이름 패턴이 *Service
@Around("allOrder() && allService()")//Pointcut
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
try{
log.info("[트랜잭션 시작] {}", joinPoint.getSignature()); // joinPoint 시그니처 // Advice
Object result = joinPoint.proceed();//타겟호출
log.info("[트랜잭션 커밋] {}", joinPoint.getSignature()); // joinPoint 시그니처 // Advice
return result;
}catch(Exception e){
log.info("[트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
}finally{
log.info("[리소스 릴리즈] {}", joinPoint.getSignature());
}
}
}
• allOrder() 포인트컷은 hello.aop.order 패키지와 하위 패키지를 대상으로 한다.
• allService() 포인트컷은 타입 이름 패턴이 *Service 를 대상으로 하는데 쉽게 이야기해서 XxxService 처럼 Service 로 끝나는 것을 대상으로 한다. *Servi* 과 같은 패턴도 가능하다.
• 여기서 타입 이름 패턴이라고 한 이유는 클래스, 인터페이스에 모두 적용되기 때문이다.
@Around("allOrder() && allService()") :
• 포인트컷은 이렇게 조합할 수 있다. && (AND), || (OR), ! (NOT) 3가지 조합이 가능하다.
• hello.aop.order 패키지와 하위 패키지 이면서 타입 이름 패턴이 *Service 인 것을 대상으로 한다. 결과적으로 doTransaction() 어드바이스는 OrderService 에만 적용된다.
• doLog() 어드바이스는 OrderService , OrderRepository 에 모두 적용된다.
이를 적용하여 위에서 했던 테스트를 진행하면 아래와 같다.
@Import(AspectV3.class) // AspectV3 빈 등록
@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));
}
// 실행결과
// isAopProxy, orderService=true
// isAopProxy, orderRepository=true
@Test
void success(){
orderService.orderItem("itemA");
}
// 실행결과
//[log] void hello.aop.order.OrderService.orderItem(String)
//[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
//[orderService] 실행
//[log] String hello.aop.order.OrderRepository.save(String)
//[orderRepository] 실행
//[트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
//[리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
@Test
void exception(){
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
}
// --> 테스트 성공
}
success 테스트를 보면 트랜잭션에 대한 로그가 추가된 것을 볼 수 있다.
포인트것이 적용된 AOP 결과는 다음과 같다.
orderService : doLog() , doTransaction() 어드바이스 적용
orderRepository : doLog() 어드바이스 적용
그림으로 보면 아래와 같다.
![](https://blog.kakaocdn.net/dn/uHhBn/btrqIthXPOg/MEiDQHyCm63cKnkdNpfYak/img.png)
AOP 적용 전 :
클라이언트 -> orderService.orderItem() -> orderRepository.save()
AOP 적용 후 :
클라이언트 -> [ doLog() doTransaction() ] -> orderService.orderItem() -> [ doLog() ] -> orderRepository.save()
orderService에는 doLog() doTransaction() 둘 다 적용되고, orderRepository에는 doLog()만 적용된 것을 볼 수 있다.
포인트컷 참조
- 포인트컷을 별도의 외부 클래스에 모아두고 사용하는 것을 보자.
아래와 같이 Pointcuts라는 클래스를 만든 뒤, Pointcut 메서드들을 Public으로 정의한다.
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder(){} //pointcut signature
//클래스 이름 패턴이 *Service인 부분
@Pointcut("execution(* *..*Service.*(..))")
public void allService(){} //pointcut signature
//allOrder && allService의 and 조합
@Pointcut("allOrder() && allService()")
public void orderAndService(){}
}
그리고 이를 사용하는 쪽에서는 패키지 경로를 포함하여 메서드 이름을 적어주면 된다.
@Slf4j
@Aspect
public class AspectV4Pointcut {
//외부 pointcut정의 사용
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
//외부 pointcut정의 사용
@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());
}
}
}
이렇게 한 뒤 테스트를 진행하면 이전과 똑같이 실행된다.
어드바이스 순서
어드바이스는 기본적으로는 순서를 보장하지 않는다. 순서를 지정하고 싶다면 '@Aspect' 적용 단위로org.springframework.core.annotation.@Order 어노테이션을 적용해야 한다. 문제는 이것을 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다는 점이다. 그래서 지금처럼 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다. 따라서 애스펙트를 별도의 클래스로 분리해야 한다.
즉 아래와 같이 메서드 단위로 넣는 것은 의미가 없다.
@Slf4j
@Aspect
public class AspectV4Pointcut {
@Order(1) // <---- 이거 안먹힘
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Order(2) // <---- 이거 안먹힘
@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());
}
}
}
아래와 같이 @Order 어노테이션을 클래스 단위에 붙여줘야 한다. 즉 "@Aspect"가 붙은 단위로 순서를 지정할 수 있다.
@Order(1) // 이렇게 클래스 단위에 넣야함
@Slf4j
@Aspect // 얘를 기준으로 순서를 지정 가능.
public class AspectV4Pointcut {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable{
// 생략..
}
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable{
// 생략..
}
}
하여 아래와 같이 클래스를 따로 분리해야 한다. (아래에서는 inner static 클래스로 하였는데, 물론 이들은 public 클래스로 떼어 내도 상관없다.)
@Slf4j
public class AspectV5Order {
@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();
}
}
@Order(1)
@Aspect
public static class TxAspect{
@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());
}
}
}
}
그리고 이제 이들을 임포트 할 때는 각 클래스 별로 따로 임포트 해줘야 한다. (물론 여기서는 @Import로 스프링 빈 등록을 하지만 @Component를 사용하여 컴포넌트 스캔 등록으로도 가능하다.)
//임포트를 따로 해줘야 한다.
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@Slf4j
@SpringBootTest
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void success(){
orderService.orderItem("itemA");
}
}
테스트 실행 결과:
[트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
[트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)
테스트를 실행 결과를 보면 트랜잭션을 먼저 시작하고 로그를 출력하는 것을 볼 수 있다.
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
'Spring > spring AOP' 카테고리의 다른 글
스프링 코어 2 - 포인트컷 (0) | 2022.01.14 |
---|---|
스프링 코어 2 - 어드바이스 종류 (0) | 2022.01.14 |
스프링 코어 2 - AOP 개념 (0) | 2022.01.12 |
스프링 코어 2 - @Aspect AOP (0) | 2022.01.12 |
스프링 코어 2 - 스프링이 제공하는 빈 후처리기 (0) | 2022.01.11 |