OrderService
의 핵심 기능은 주문 로직이다.여러 곳에서 공통으로 사용하는 부가 기능
부가 기능 적용 문제
try~catch~finally
같은 구조가 필요하다면 더욱 복잡해진다. (예: 실행 시간 측정)정리
@Aspect
바로 그것이다. 그리고 스프링이 제공하는 어드바이저도 어드바이스(부가 기능)과 포인트컷(적용 대상)을 가지고 있어서 개념상 하나의 애스펙트이다.AspectJ 프레임워크
기능
AOP를 사용할 때 부가 기능 로직은 어떤 방식으로 실제 로직에 추가될 수 있을까?
3가지 방법이 있다.
1. 컴파일 시점
.java
소스 코드를 컴파일러를 사용해서 .class
를 만드는 시점에 부가 기능 로직을 추가할 수 있다. 이때는 AspectJ가 제공하는 특별한 컴파일러를 사용해야 한다.단점
2. 클래스 로딩 시점
.class
파일을 JVM 내부의 클래스 로더에 보관한다. 이때 중간에서 .class
파일을 조작한 다음 JVM에 올릴 수 있다. 단점
java -javaagent
)을 통해 클래스 로더 조작기를 지정해야 하는데, 이 부분이 번거롭고 운영하기 어렵다.3. 런타임 시점
단점
AOP 적용 위치
중요
- AspectJ를 사용하려면 공부할 내용도 많고, 자바 관련 설정(특별한 컴파일러, AspectJ 전용 문법, 자바 실행 옵션)도 복잡하다.
- 반면에 스프링 AOP는 별도의 추가 자바 설정 없이 스프링만 있으면 편리하게 AOP를 사용할 수 있다.
- AOP 기능만 사용해도 대부문의 문제를 해결할 수 있다. 따라서 스프링 AOP가 제공하는 기능을 학습하는 것에 집중하자.
spring-aop라는 프로젝트를 새로 만들었습니다.
추가
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-aop'
간단한 예제를 추가해보겠습니다.
OrderRepository
package com.example.springaop.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 com.example.springaop.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);
}
}
AopTest
package com.example.springaop;
import com.example.springaop.order.OrderRepository;
import com.example.springaop.order.OrderService;
import lombok.extern.slf4j.Slf4j;
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.assertThatThrownBy;
@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 프록시가 적용 되었는지 확인 가능@Aspect
를 사용하는 방법
AspectV1
package com.example.springaop.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 {
@Around("execution(* com.example.springaop.order..*(..))")
public Object dolog(ProceedingJoinPoint joinPoint) throws Throwable{
log.info("[log] {}", joinPoint.getSignature()); // join 포인트 시그니처
return joinPoint.proceed();
}
}
@Around
애노테이션의 값인 execution(* hello.aop.order..*(..))
는 포인트컷이 된다.@Around
애노테이션의 메서드인 doLog
는 어드바이스(Advice
)가 된다.OrderService
, OrderRepository
의 모든 메서드는 AOP 적용의 대상이 된다.AopTest에 @Import(AspectV1.class)추가
@Aspect
는 애스펙트라는 표식이지 컴포넌트 스캔이 되는 것은 아니다. 따라서 AspectV1
를 AOP로 사용하려면 스프링 빈으로 등록해야 한다.
스프링 빈으로 등록하는 방법
@Bean
을 사용해서 직접 등록@Component
컴포넌트 스캔을 사용해서 자동 등록@Import
주로 설정 파일을 추가할 때 사용(@Configuration
)@Around
에 포인트컷 표현식을 직접 넣을 수 도 있지만, @Pointcut
애노테이션을 사용해서 별도로 분리할 수 도 있다.
AspectV2
package com.example.springaop.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 {
@Pointcut("execution(* com.example.springaop.order..*(..))") // 포인트컷 포현식
private void allOrder(){} // 포인트컷 시그니처
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Pointcut
allOrder()
이다. 이름 그대로 주문과 관련된 모든 기능을 대상으로 하는 포인트컷이다.AspectV3
package com.example.springaop.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 {
@Pointcut("execution(* com.example.springaop.order..*(..))")
public void allOrder(){}
//클래스 이름 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
private void allService(){}
@Around("allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
// com.example.springaop.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());
}
}
}
allService()
포인트컷은 타입 이름 패턴이 *Service
를 대상으로 하는데 쉽게 이야기해서 XxxService
처럼 Service
로 끝나는 것을 대상으로 한다. *Servi*
과 같은 패턴도 가능하다.@Around("allOrder() && allService()")
포인트컷은 이렇게 조합할 수 있다. &&(AND), ||(OR), !(NOT) 3가지 조합이 가능하다.doTransaction()
어드바이스는 OrderService
에만 적용된다.doLog()
어드바이스는 OrderService
, OrderRepository
에 모두 적용된다.success()
exception()
orderService
에는 doLog()
, doTransaction()
두가지 어드바이스가 적용되어 있고,
orderRepository
에는 doLog()
하나의 어드바이스만 적용된 것을 확인할 수 있다.
AOP 적용 후 실행순서
[ doLog() -> doTransaction() ] -> orderService.orderItem() -> [ doLog() ] -> orderRepository.save()
Pointcuts
package com.example.springaop.order.aop;
import org.aspectj.lang.annotation.Pointcut;
public class Pointcuts {
@Pointcut("execution(* com.example.springaop.order..*(..))")
public void allOrder(){}
//타입 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService(){}
//allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService(){}
}
AspectV4Pointcut
package com.example.springaop.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 AspectV4Pointcut {
@Around("com.example.springaop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@Around("com.example.springaop.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
애노테이션을 적용해야 한다.AspectV5Order
package com.example.springaop.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.springframework.core.annotation.Order;
@Slf4j
public class AspectV5Order {
@Aspect
@Order(2)
public static class LogAspect {
@Around("com.example.springaop.order.aop.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("com.example.springaop.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());
}
}
}
}
LogAspect
, TxAspect
애스펙트로 각각 분리했다. 그리고 각 애스펙트에 @Order
애노테이션을 통해 실행 순서를 적용했다. 숫자가 작을 수록 먼저 실행된다.AopTest 변경
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
실행 결과를 보면 트랜잭션 어드바이스가 먼저 실행되는 것을 확인할 수 있다.
어드바이스 종류
@Around
: 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능@Before
: 조인 포인트 실행 이전에 실행@AfterReturning
: 조인 포인트가 정상 완료후 실행@AfterThrowing
: 메서드가 예외를 던지는 경우 실행@After
: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)예제로 한번 확인해보자
AspectV6Advice
package com.example.springaop.order.aop;
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("com.example.springaop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable
{
try {
// @Before
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
Object result = joinPoint.proceed();
// @AfterReturning
log.info("[around][트랜잭션 커밋] {}", joinPoint.getSignature());
return result;
} catch (Exception e) {
// @AfterThrowing
log.info("[around][트랜잭션 롤백] {}", joinPoint.getSignature());
throw e;
} finally {
// @After
log.info("[around][리소스 릴리즈] {}", joinPoint.getSignature());
}
}
@Before("com.example.springaop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "com.example.springaop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "com.example.springaop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
@After(value = "com.example.springaop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
주석을 확인하면 조금 더 잘 이해 할 수 있다.
ProceedingJoinPoint 인터페이스의 주요 기능
proceed()
: 다음 어드바이스나 타켓을 호출한다.@Before
@Around
와 다르게 작업 흐름을 변경할 수는 없다.@Around
는 ProceedingJoinPoint.proceed()
를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음 대상이 호출되지 않는다.@Before
는 ProceedingJoinPoint.proceed()
자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출된다.@AfterReturning
returning
속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.returning
절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)@Around
와 다르게 반환되는 객체를 변경할 수는 없다. 반환 객체를 변경하려면 @Around
를 사용해야 한다.@AfterThrowing
throwing
속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.throwing
절에 지정된 타입과 맞는 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)@After
@Around
joinPoint.proceed()
호출 여부 선택joinPoint.proceed(args[])
try ~ catch~ finally
모두 들어가는 구문 처리 가능ProceedingJoinPoint
를 사용해야 한다.proceed()
를 통해 대상을 실행한다.proceed()
를 여러번 실행할 수도 있음(재시도)테스트 확인
순서
장점
@Around
외에 다른 어드바이스가 존재하는 이유 @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(ProceedingJoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@Around
는 항상 joinPoint.proceed()
를 호출해야 한다.@Before
는 joinPoint.proceed()
를 호출하는 고민을 하지 않아도 된다.@Around
가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다. 반면에 @Before
, @After
같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다.참고
김영한: 스프링 핵심 원리 - 고급편(인프런)
Github - https://github.com/b2b2004/Spring_ex