![[Spring 핵심원리 - 고급] 스프링 AOP 구현](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWfdW5%2FbtsNt6kwUqa%2Fi369yZtNfMFWIG6BZmLLv0%2Fimg.png)
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.
리포지토리 & 서비스
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";
}
}
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);
}
}
기본 구현
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 {
//advanced.aop.order 패키지와 하위 패키지
@Around("execution(* advanced.aop.order..*(..))")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
- @Around 애노테이션의 값인 execution(* hello.aop.order..(..)) 는 포인트컷이 된다.
- @Around 애노테이션의 메서드인 doLog 는 어드바이스(Advice)가 된다.
- execution( advanced.aop.order..*(..)) 는 advanced.aop.order 패키지와 그 하위 패키지(..)를 지정하는 AspectJ 포인트컷 표현식이다.
- 이제 OrderService, OrderRepository 의 모든 메서드는 AOP 적용의 대상이 된다.
@Slf4j
@SpringBootTest
@Import(AspectV1.class)
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() {
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
@Aspect 는 애스펙트라는 표식이지 컴포넌트 스캔이 되는 것은 아니다. 따라서 AspectV1 를 AOP로 사용하려면 스프링 빈으로 등록해야 한다.
스프링 빈으로 등록하는 방법은 다음과 같다.
- @Bean 을 사용해서 직접 등록
- @Component 컴포넌트 스캔을 사용해서 자동 등록
- @Import 주로 설정 파일을 추가할 때 사용(@Configuration)
포인트컷 분리
@Around 에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut 애노테이션을 사용해서 별도로 분리할 수 도 있다.
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 {
//advanced.aop.order 패키지와 하위 패키지
@Pointcut("execution(* advanced.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 에 포인트컷 표현식을 사용한다.
- 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라 한다.
- 메서드의 반환 타입은 void 여야 한다.
- 코드 내용은 비워둔다.
- 포인트컷 시그니처는 allOrder() 이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.
- @Around 어드바이스에서는 포인트컷을 직접 지정해도 되지만, 포인트컷 시그니처를 사용해도 된다. 여기서는 @Around("allOrder()") 를 사용한다.
- private, public 같은 접근 제어자는 내부에서만 사용하면 private 을 사용해도 되지만, 다른 애스펙트에서 참고하려면 public 을 사용해야 한다.
@Slf4j
@SpringBootTest
@Import(AspectV2.class)
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() {
Assertions.assertThatThrownBy(() -> orderService.orderItem("ex"))
.isInstanceOf(IllegalStateException.class);
}
}
어드바이스 추가
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 {
//advanced.aop.order 패키지와 하위 패키지
@Pointcut("execution(* advanced.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());
}
}
}
- allOrder() 포인트컷은 advanced.aop.order 패키지와 하위 패키지를 대상으로 한다.
- allService() 포인트컷은 타입 이름 패턴이 *Service 를 대상으로 하는데, XxxService처럼 Service로 끝나는 것을 대상으로 한다. *Servi* 와 같은 패턴도 가능하다.
- 여기서 타입 이름 패턴이라고 한 이유는 클래스, 인터페이스에 모두 적용되기 때문이다.
@Around("allOrder() && allService()")
- 포인트컷은 이렇게 조합할 수 있다. && (AND), || (OR), ! (NOT) 3가지 조합이 가능하다.
- advanced.aop.order 패키지와 하위 패키지 이면서 타입 이름 패턴이 *Service 인 것을 대상으로 한다.
- 결과적으로 doTransaction() 어드바이스는 OrderService 에만 적용된다.
- doLog() 어드바이스는 OrderService, OrderRepository 에 모두 적용된다.
포인트컷이 적용된 AOP 결과는 다음과 같다.
- orderService : doLog(), doTransaction() 어드바이스 적용
- orderRepository : doLog() 어드바이스 적용
포인트컷 참조
포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 모아두어도 된다. 참고로 외부에서 호출할 때는 포인트컷의 접근 제어자를 public 으로 열어두어야 한다.
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* advanced.aop.order..*(..))")
public void allOrder(){} //pointcut signature
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService(){}
//allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
- orderAndService() : allOrder() 포인트컷와 allService() 포인트컷을 조합해서 새로운 포인트컷을 만들었다.
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 AspectV4Pointcut {
@Around("advanced.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
@Around("advanced.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 애노테이션을 적용해야 한다.
- 문제는 이것을 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있다는 점이다.
- 그래서 지금처럼 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장 받을 수 없다. 따라서 애스펙트를 별도의 클래스로 분리해야 한다.
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("advanced.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature()); //join point 시그니처
return joinPoint.proceed();
}
}
@Aspect
@Order(1)
public static class TxAspect {
@Around("advanced.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 : 메서드 호출 전후에 수행하며, 가장 강력한 어드바이스이다. 조인 포인트 실행 여부를 선택할 수 있고, 반환 값 변환이나 예외 변환도 가능하다.
- @Before : 조인 포인트 실행 이전에 실행된다.
- @AfterReturning : 조인 포인트가 정상적으로 완료된 후 실행된다.
- @AfterThrowing : 메서드가 예외를 던지는 경우 실행된다.
- @After : 조인 포인트가 정상 완료되든 예외가 발생하든 관계없이 실행되며, finally 블록처럼 동작한다.
@Around
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
@Slf4j
@Aspect
public class AspectV6Advice {
@Around("advanced.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());
}
}
}
- @Around 를 제외한 나머지 어드바이스들은 @Around 가 할 수 있는 일의 일부만 제공할 뿐이다.
- 따라서 @Around 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있다.
- proceed() 를 여러번 실행할 수도 있음(재시도)
참고 정보 획득
모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫 번째 파라미터에 사용할 수 있다. (생략해도 된다.)
단, @Around 는 ProceedingJoinPoint 를 사용해야 한다.
ProceedingJoinPoint 는 org.aspectj.lang.JoinPoint 의 하위 타입이다.
JoinPoint 인터페이스의 주요 기능
- getArgs() : 메서드 인수를 반환한다.
- getThis() : 프록시 객체를 반환한다.
- getTarget() : 대상 객체를 반환한다.
- getSignature() : 조언되는 메서드에 대한 설명을 반환한다.
- toString() : 조언되는 메서드에 대한 유용한 설명을 출력한다.
ProceedingJoinPoint 인터페이스의 주요 기능
- proceed() : 다음 어드바이스나 타겟을 호출한다.
추가로 proceed(Object[] args) 형태로 호출하면, 전달할 매개변수를 직접 지정할 수도 있다.
@Before
조인 포인트 실행 전
@Before("advanced.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
- @Around 와 다르게 작업 흐름을 변경할 수는 없다.
- @Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음 대상이 호출되지 않는다. 반면에 @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 즉, 메서드 종료시 자동으로 다음 타겟이 호출된다.
- 하지만 예외가 발생하면 다음 코드가 호출되지는 않는다.
@AfterReturning
메서드 실행이 정상적으로 반환될 때 실행
@AfterReturning(value = "advanced.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
- returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
- returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)
- @Around 와 다르게 반환되는 객체를 변경할 수는 없다. 반환 객체를 변경하려면 @Around 를 사용해야 한다.
- 참고로 반환 객체를 조작할 수는 있다.
@AfterThrowing
메서드 실행이 예외를 던져서 종료될 때 실행
@AfterThrowing(value = "advanced.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", ex);
}
- throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.
- throwing 절에 지정된 타입과 맞는 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)
@After
@After(value = "advanced.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
- 메서드 실행이 종료되면 실행된다. (finally를 생각하면 된다.)
- 정상 및 예외 반환 조건을 모두 처리한다.
- 일반적으로 리소스를 해제하는 데 사용한다.
순서
순서
스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
실행 순서
- @Around
- @Before
- @AfterThrowing
- @AfterReturning
- @After
어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대이다.
- 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서는 보장되지 않는다.
- 이 경우 @Aspect 를 분리하고 @Order 를 적용해야 한다.
'Back-End > Spring' 카테고리의 다른 글
[스프링 핵심원리 - 고급] 스프링 AOP 실무 주의사항 (0) | 2025.04.27 |
---|---|
[Spring 핵심원리 - 고급] 포인트컷 지시자(PCD) (0) | 2025.04.27 |
[Spring 핵심원리 - 고급] @Aspect AOP (0) | 2025.04.17 |
[Spring 핵심원리 - 고급] 빈 후처리기(BeanPostProcessor) (0) | 2025.04.16 |
[Spring 핵심원리 - 고급] 프록시 팩토리(Proxy Factory) (0) | 2025.04.15 |