스프링 AOP 구현하기 - 1 (@Aspect, @Around, @Pointcut)
스프링 AOP를 구현하는 기본 코드입니다. 총 2개 포스팅으로 작성하였습니다.
build.gradle
스프링 AOP 기능을 구현하기 위해서 아래 dependency 들로 구성하였습니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
// Spring aop 사용을 위한 dependency
implementation 'org.springframework.boot:spring-boot-starter-aop'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
//테스트에서 lombok 사용
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
예제 프로젝트 만들기
전체 package 구조는 아래와 같습니다.
OrderService와 OrderRepository 클래스
package hello.aop.order;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@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);
}
}
package hello.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와 OrderRepository 클래스의 동작 방식을 테스트하는 간단한 테스트 코드를 작성하였습니다.
@Slf4j
@SpringBootTest
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void success() {
orderService.orderItem("itemA");
}
@Test
void exception() {
assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
위와 같이 OrderService, OrderRepository 클래스를 구성 한 후에 스프링 AOP를 적용해 보겠습니다.
스프링 AOP 구현 - 1 (@Around)
스프링 AOP를 구현하는 일반적인 방법은 @Aspect를 사용하는 방법입니다. 먼저 가장 단순한 AOP를 구현해 보도록 하겠습니다.
package hello.aop.order.aop;
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 {
//hello.aop.order 패키지와 하위 패키지
@Around("execution(* hello.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
- @Around 애너테이션 값인 인 execution(* hello.aop.order..*(..)) 는 포인트컷이 됩니다. ( execution(* hello.aop.order..*(..)) 는 AspectJ 포인트컷 표현식)
- hello.aop.order 패키지 및 하위 패키지 내부의 빈의 모든 메서드가 AOP 적용의 대상이 됩니다.
- @Around 애노테이션의 메서드인 doLog 는 어드바이스(Advice)가 됩니다.
간단한 테스트 코드 작성으로 hello.aop.order 패키지 내부의 OrderService와 OrderRepository의 메서드들이 aop의 적용 대상인지 여부를 확인해 보겠습니다.
AopTest 코드
@Slf4j
@Import(AspectV1.class) // 빈 등록!!
@SpringBootTest
public class AopTest {
@Autowired
OrderService orderService;
@Autowired
OrderRepository orderRepository;
@Test
void aopInfo() {
// AopUtils 로 AopProxy 적용 여부 확인
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는 그저 Aspect 라는 표현을 나타내는 애너테이션으로 component 가 아닙니다. 그래서 componentScan의 대상이 되지 않습니다. 따라서, AspectV1을 AOP로 사용하려면 스프링 빈으로 등록해야 합니다.
스프링 빈으로 등록하는 방법은 크게 3가지가 있습니다.
- @Bean과 @Configuration 을 사용해서 직접 등록
- @Component과 @ComponentScan을 사용해서 자동 등록
- @Import : 주로 설정 파일(@Configuration 클래스)을 추가할 때 사용
@Import는 주로 설정 파일을 추가할 때 사용하지만,간단하게 bean을 생성하기 위해서 @Import를 사용하였습니다.
위 테스트 코드를 실행하면 아래와 같이 출력된 로그로 Aop 적용이 잘 되었음을 확인할 수 있습니다.
스프링 AOP 구현 - 2 (@Pointcut)
첫 번째 방법처럼 @Around에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있습니다.
package hello.aop.order.aop;
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 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
private void allOrder(){} //pointcut signature
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
- @Pointcut에 포인트컷 표현식을 사용합니다.
- @Pointcut이 적용된 메서드의 반환 타입은 void여야 하고, 메서드 내용은 비워둡니다.
- @Around 어드바이스에 포인트컷 시그니처를 사용하여 포인트컷을 지정합니다.
- private , public 같은 접근 제어자는 내부에서만 사용하면 private을 사용해도 되지만, 다른 Aspect에서 참고하려면 public을 사용해야 합니다.
결과적으로 AspectV1 과 같은 기능을 수행합니다. (AopTest 클래스 코드의 @Import 부분만 AspectV2로 변경한 후 수행해 보세요.)
이렇게 분리하면 하나의 포인트컷 표현식을 내부의 여러 어드바이스뿐만 아니라 외부 어드바이스에서도 함께 사용할 수 있습니다.
위와 같이 포인트 컷을 분리하여 조금 더 복잡한 AOP를 구현해 보겠습니다. 아래 코드에는 트랜잭션 동작을 로깅하는 기능을 추가하였습니다.
- allOrder() 포인트컷은 hello.aop.order 패키지와 하위 패키지를 대상으로 합니다. 따라서, OrderService와 OrderRepository 클래스 메서드에 모두 적용됩니다.
- allService() 포인트컷은 클래스 및 인터페이스의 이름 패턴이 Service로 끝나는 것을 대상으로 합니다.
package hello.aop.order.aop;
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 {
//hello.aop.order 패키지와 하위 패키지
@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()); //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());
}
}
}
위 코드에서 @Around("allOrder() && allService()") 와 같이 사용하고 있습니다. 포인트컷은 이렇게 조합도 가능합니다.
(&&(AND), ||(OR),!(NOT) 조합 가능)
결국, 이 조합은 hello.aop.order 패키지와 하위 패키지이면서, 클래스나 인터페이스 이름 패턴이 Service로 끝나는 것을 대상으로 하라라는 의미이기 때문에, OrderService 클래스에만 적용됩니다.
- orderService : doLog() , doTransaction() 어드바이스 적용
- orderRepository : doLog() 어드바이스 적용
AopTest 코드를 아래처럼 수정한 후 실행한 후 결과입니다.
@Import(AspectV3.class)
@SpringBootTest
public class AopTest {
}
다음 포스팅에는 AOP의 Advice가 적용되는 순서를 변경하고 싶을 때 어떻게 해야 하는지, 그리고 @Around 이외에 @Before, @After 등의 다양한 Advice의 종류에 대해서 작성해 보겠습니다~
전체 소스코드
https://github.com/eunhwa99/SpringAOP/tree/main/advanced/src/main/java/hello/aop
[참고자료]
김영한, "스프링 핵심 원리 - 고급편 ", 인프런