Spring에서 제공하는 AOP를 사용해보자.
plugins {
id 'org.springframework.boot' version '2.5.5'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
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'
}
test {
useJUnitPlatform()
}
@Slf4j
@Repository
public class OrderRepository {
public String save(String itemId) {
log.info("[orderRepository] 실행"); //저장 로직
if (itemId.equals("ex")) {
throw new IllegalStateException("예외 발생!");
}
return "ok";
}
}
@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
@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);}
}
Spring AOP가 적용되면 AopUtils.isAopProxy 의 결과가 true가 반환된다.
지금은 AOP를 적용하지 않았기 때문에 false가 반환된다.
[orderService] 실행
[orderRepository] 실행
아직 AOP가 적용되지 않아서 단순히 실행 로그만 찍힌다.
@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();
}
}
Pointcut : execution(* hello.aop.order..*(..))
-> hello.aop.order 패키지 하위에 있는 모든 메소드
Advice : @Around 어노테이션이 적용된 doLog 메소드가 Advice가 된다.
이제 hello.aop.order 패키지 하위에 있는
OrderRepository, OrderService의 모든 메소드들에 AOP가 적용된다.
@Slf4j
@Import(AspectV1.class) //추가
@SpringBootTest
public class AopTest {
... // 테스트 로직
}
@Aspect
는 단순히 자동 프록시 생성기가 프록시 생성 대상을 식별하기 위한 어노테이션이다.
Aspect 어노테이션은 ComponentScan을 포함하지 않으므로,
AspectV1
를 AOP로 사용하려면 스프링 빈으로 등록해야 한다.
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
@Around
어노테이션에 Pointcut을 포함시킬 수도 있지만,
별도로 분리하여 재활용도 가능하다.
@Slf4j
@Aspect
public class AspectV2 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))") //pointcut expression
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
을 사용해도 되지만, 다른 Aspect에서 참고하려면 public
을 사용해야 한다.@Slf4j
@Import(AspectV2.class) // V2로 변경
@SpringBootTest
public class AopTest {
... // 테스트 로직
}
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
실행 결과는 AspectV1과 동일하다.
@Slf4j
@Aspect
public class AspectV3 {
//hello.aop.order 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.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();
}
//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());
}
}
}
이전에는 Aspect 안에 로그를 찍는 doLog(...) Advice만 있었다.
이제는 트랜잭션 처리를 하는 doTransaction(...) 을 추가했다.
doTransaction은 allService() 포인트컷 시그니처에 의해 클래스 명이 Service
로 끝나야
Advice를 적용한다.
-> OrderRepository : doLog()
-> OrderService : doLog(), doTransaction()
Advice가 적용된다.
@Slf4j
@Import(AspectV3.class) // V3로 변경
@SpringBootTest
public class AopTest {
... // 테스트 로직
}
[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)
[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)
@Pointcut
을 별도로 분리하여 공통으로 재활용하여 사용할 수도 있다.
접근제한자만 public으로 지정하고,
Advice에서 포인트컷 시그니처에 패키지명을 함께 명시하면 된다.
public class Pointcuts {
//hello.springaop.app 패키지와 하위 패키지
@Pointcut("execution(* hello.aop.order..*(..))")
public void allOrder() {}
//타입 패턴이 *Service
@Pointcut("execution(* *..*Service.*(..))")
public void allService() {}
//allOrder && allService
@Pointcut("allOrder() && allService()")
public void orderAndService() {}
}
@Slf4j
@Aspect
public class AspectV4 {
@Around("hello.aop.order.aop.Pointcuts.allOrder()")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[log] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
@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());
}
}
}
@Slf4j
@Import(AspectV4.class) // V4로 변경
@SpringBootTest
public class AopTest {
... // 테스트 로직
}
[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)
기존 (V3)과 같다.
[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)
기존 (V3)과 같다.
Advice는 기본적으로 순서를 보장하지 않는다.
위 예시에서는 doLog가 먼저 실행되고, doTransaction이 그 다음에 실행됐음을 확인할 수 있다.
Advice가 적용되는 순서를 doTransaction -> doLog로 변경하려면 어떻게 해야할까?
Aspect 단위
로 @Order
어노테이션을 통해 Advice의 순서를 변경할 수 있다.
@Order
어노테이션은 Aspect 단위
로 적용되기 때문에, 클래스도 분리해야 한다.
@Slf4j
public class AspectV5 {
@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();
}
}
@Aspect
@Order(1)
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());
}
}
}
}
doTransaction과 doLog의 순서를 조정하기 위해 @Order
어노테이션을 적용했다.
그리고, @Order
어노테이션은 @Aspect
단위로 적용할 수 있기 때문에
doLog()
를 담당하는 LogAspect
와, doTransaction()
을 담당하는 TxAspect
로 클래스를 분리했다.
@Slf4j
@Import(AspectV5.class) // V5로 변경
@SpringBootTest
public class AopTest {
... // 테스트 로직
}
[트랜잭션 시작] 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)
트랜잭션이 먼저 시작된 것을 볼 수 있다.
[트랜잭션 시작] 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)
Advice는 앞에서 본 @Around
외에도 여러 종류가 존재한다.
@Around
: 메서드 호출 전후에 수행@Before
: 조인 포인트 실행 이전에 실행@AfterReturning
: 조인 포인트가 정상 완료후 실행@AfterThrowing
: 메서드가 예외를 던지는 경우 실행@After
: 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)실행 순서: @Around
> @Before
> @After
> @AfterReturning
> @AfterThrowing
* @Aspect
안에 동일한 Advice가 2개 이상 있으면 순서가 보장되지 않는다.
Aspect 안에 동일한 Advice의 순서를 보장 받고 싶다면, @Aspect
를 분리하여 @Order
를 적용하자.
@Around
하나로 나머지 Advice를 모두 커버할 수 있는데, 여러 Advice가 존재하는 이유?
@Around
는 책임이 많기 때문에, 실수할 가능성이 있다.
-> JoinPoint를 실수로 실행하지 않는다거나,
-> JoinPoint의 실행 결과를 제대로 return하지 못하거나...다른 Advice를 사용하면, 그 책임과 의도가 명확해진다.
@Slf4j
@Aspect
public class AspectV6 {
@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public Object doTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
try {
// @Before 시점
log.info("[around][트랜잭션 시작] {}", joinPoint.getSignature());
// Joinpoint 실행
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("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
@After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
public void doAfter(JoinPoint joinPoint) {
log.info("[after] {}", joinPoint.getSignature());
}
}
doTransaction Advice의 주석을 보면 @~~ 시점
을 볼 수 있다.
즉, @Around
를 사용하면, 다른 Advice들 (Before
, After
, AfterReturning
, AfterThrowing
)을 모두 커버할 수 있다.
Advice에서 참고 정보를 활용하기 위해서는 파라미터로 받은 JoinPoint를 사용하면 된다.
@Around
Advice에서는 ProceedingJoinPoint를 사용하며
그 외
Advice에서는 JoinPoint를 사용한다.
JoinPoint 는 다양한 정보를 제공하지만, 그 중에서 주로 사용되는 정보는 아래와 같다.
getArgs()
: 메서드의 인자를 반환한다.getThis()
: 프록시 객체를 반환한다.getTarget()
: 대상 객체를 반환한다.getSignature()
: 메소드의 시그니처를 반환한다.toString()
: 일반적으로 포인트컷 지시자를 반환한다.public String toString() {
return "execution(" + this.getSignature().toString() + ")";
}
JointpointImpl.classpublic final String toString() {
return this.toString(StringMaker.middleStringMaker);
}
String toString(StringMaker sm) {
StringBuffer buf = new StringBuffer();
buf.append(sm.makeKindName(this.getKind()));
buf.append("(");
buf.append(((SignatureImpl)this.getSignature()).toString(sm));
buf.append(")");
return buf.toString();
}
proceed()
: 다음 어드바이스나 타켓을 호출 @Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
log.info("[before] {}", joinPoint.getSignature());
}
@Around
와 다르게 작업 흐름을 변경할 수는 없다.
@Around
는 ProceedingJoinPoint.proceed()
를 호출해야 다음 대상이 호출된다.
만약 호출하지 않으면 다음대상이 호출되지 않는다.
반면에 @Before
는 ProceedingJoinPoint.proceed()
자체를 사용하지 않는다.
메서드 종료시 자동으로 다음 타켓이 호출된다.
물론 예외가 발생하면 다음 코드가 호출되지는 않는다.
@AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
public void doReturn(JoinPoint joinPoint, Object result) {
log.info("[return] {} return={}", joinPoint.getSignature(), result);
}
returning
속성에 사용된 이름은 Advice 메소드 매개변수 이름과 일치해야 한다.
result의 (자식을 포함한)타입을 반환하는 메소드에만 수행된다.
@Around
와 다르게 반환되는 객체를 변경할 수는 없다.
(반환되는 객체를 활용할 수는 있다.)
@AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
public void doThrowing(JoinPoint joinPoint, Exception ex) {
log.info("[ex] {} message={}", joinPoint.getSignature(), ex.getMessage());
}
throwing
속성에 사용된 이름은 Advice 메소드 매개변수 이름과 일치해야 한다.
ex의 (자식을 포함한)타입과 일치하는 Exception이 발생될 때 실행한다.
일반적으로 리소스를 해제하는 데 사용한다.
다른 Advice들을 모두 커버할 수 있다. (= 가장 강력하다.)
try ~ catch~ finally
모두 들어가는 구문 처리 가능@Around
Advice의 첫 번째 파라미터는 ProceedingJoinPoint
를 사용해야 한다.
proceed()
를 통해 대상을 실행한다.
proceed()
를 여러번 실행할 수도 있음(재시도)
[around][트랜잭션 시작] void hello.aop.order.OrderService.orderItem(String)
[before] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[orderRepository] 실행
[return] void hello.aop.order.OrderService.orderItem(String) return=null
[after] void hello.aop.order.OrderService.orderItem(String)
[around][트랜잭션 커밋] void hello.aop.order.OrderService.orderItem(String)
[around][리소스 릴리즈] void hello.aop.order.OrderService.orderItem(String)