관리 메뉴

나구리의 개발공부기록

스프링 AOP 구현, 프로젝트 생성 및 예제 프로젝트 만들기, 스프링 AOP 구현(시작/포인트컷 분리/어드바이스 추가/포인트컷 참조/어드바이스 순서/어드바이스 종류) 본문

인프런 - 스프링 완전정복 코스 로드맵/스프링 핵심원리 - 고급편

스프링 AOP 구현, 프로젝트 생성 및 예제 프로젝트 만들기, 스프링 AOP 구현(시작/포인트컷 분리/어드바이스 추가/포인트컷 참조/어드바이스 순서/어드바이스 종류)

소소한나구리 2024. 11. 13. 16:51

출처 : 인프런 - 스프링 핵심 원리 - 고급편 (유료) / 김영한님  
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용


1. 프로젝트 생성 및 예제 프로젝트 만들기

1) 프로젝트 생성

 

(1) Project

  • Gradle
  • Java 17
  • Spring Boot 3.3.5

(2) Metadata

  • Group: hello
  • Artifact: aop
  • Packaging: Jar

(3) Dependencies

  • Lombok
  • 스프링 부트로 프로젝트를 생성하면 스프링 프레임워크의 핵심 모듈들은 별도의 설정이 없어도 자동으로 추가됨

(4) 설정 추가

  • build.gradle에 아래의 코드를 추가하여 aop와 test에서 lombok을 사용할 수 있도록 설정
// aop 설정
implementation 'org.springframework.boot:spring-boot-starter-aop'

//테스트에서 lombok 사용
 testCompileOnly 'org.projectlombok:lombok'
 testAnnotationProcessor 'org.projectlombok:lombok'

 

(5) 동작 확인

  • AopApplication을 실행하여 스프링 부트 실행 로그가 나오면 성공(웹 프로젝트를 추가하지 않았기 때문에 서버가 실행되지 않음)

** 참고

  • @Aspect를 사용하려면 @EnableAspectJAutoProxy를 스프링 설정에 추가해야 하지만, 스프링 부트를 사용하면 자동으로 추가됨

2) 예제 프로젝트 만들기

  • 기존 예제와 구조와 기능이 거의 동일함

(1) OrderRepository

  • 파라미터의 itemId가 "ex"로 넘어오면 IllegalStateException 예외를 발생
  • 그 외에는 "ok"를 반환
package hello.aop.order;

@Slf4j
@Repository
public class OrderRepository {

    public String save(String itemId) {
        log.info("[orderRepository] 실행");
        if (itemId.equals("ex")) {
            throw new IllegalStateException("예외 발생");
        }
        return "ok";
    }
}

 

(2) OrderService

  • orderItem()메서드를 가지고 있는 Service 계층
  • orderRepository.save()를 동작함
package hello.aop.order;

@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);
    }
}

 

(3) AopTest

  • test하위에 작성
  • 각 계층의 프록시 동작 여부를 로그로 출력하는 테스트와 정상 동작 및 예외가 발생되는 상황을 테스트
  • 지금은 AOP 관련 코드를 작성하지 않았기 때문에 aopInfo()테스트의 로그는 false로 출력됨
  • itemId가 ex일 때는 예외가 발생하여 테스트가 정상 통과하는 모습을 확인할 수 있음
package hello.aop;

@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired
    OrderService orderService;
    @Autowired
    OrderRepository orderRepository;

    // 프록시로 동작하는지 확인하기 위한 로그
    @Test
    void aopInfo() {
        log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderService));
        log.info("isAopProxy, orderService={}", AopUtils.isAopProxy(orderRepository));
    }

    // 정상 동작 실행
    @Test
    void success() {
        orderService.orderItem("itemA");
    }
    
    // itemId가 ex면 예외 발생
    @Test
    void exception() {
        assertThatThrownBy(() -> orderService.orderItem("ex")).isInstanceOf(IllegalStateException.class);
    }
}

2. 스프링 AOP 구현

1) 시작 - @Aspect 사용

(1) AspectV1

  • order하위에 aop 패키지 생성후 작성
  • @Around 애노테이션의 값은 포인트컷이되고 메서드인 doLog는 어드바이스가 됨
  • execution(*hello.aop.order..*(..)): hello.aop.order 패키지와 그 하위 패키지를 지정하는 AspectJ 포인트컷 표현식으로 해당 문법은 중요하기때문에 따로 강의에서 자세히 설명함
  • OrderRepository, OrderService의 모든 메서드는 AOP 적용의 대상이됨
  • 스프링은 프록시 방식의 AOP를 사용하므로 프록시를 통하는 메서드만 적용 대상이 됨
package hello.aop.order.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());
        return joinPoint.proceed(); // 실제 로직 실행
    }
}

 

** 참고

  • 스프링 AOP는 AspectJ프레임워크를 직접 사용하는 것은 아니고 AspectJ의 문법을 차용하여 프록시 방식의 AOP를 제공함
  • 스프링 AOP를 사용할 때 자주 사용하는 @Aspect 애노테이션도 AspectJ가 제공하는 애노테이션임
  • @Aspect를 포함한 org.aspectj 패키지 관련 기능은 aspectjweaver.jar 라이브러리가 제공하는 기능임
  • build.gradle에 aop관련 라이브러리를 추가하면 스프링 AOP 관련 기능과 함께 aspectjweaver.jar도 함께 사용할 수 있게 의존관계에 포함됨
  • 스프링에서는 AspectJ가 제공하는 애노테이션이나 관련 인터페이스만 사용하는 것이고 실제 AspectJ가 제공하는 컴파일 로드타입 위버 등을 사용하는 것은 아니며 프록시 방식의 AOP를 사용함

(2) AopTest - 추가

  • @Import() 애노테이션을 사용하여 AspectV1을 스프링 빈으로 등록
  • @Aspect는 애스펙트라는 표식이지 컴포넌트 스캔이 되는것은 아니기에 AspectV1을 AOP로 사용하려면 스프링 빈으로 등록해야함

** 참고 - 스프링빈 등록

  • @Bean을 사용하여 수동등록하거나 @Component로 컴포넌트 스캔 적용 대상으로 등록하여 자동으로 등록할 수 있지만 @Import로도 스프링 빈을 등록할 수 있음
  • 보통 @Import는 설정 파일을 추가할 때 사용하는데 예제에서 버전을 올려가면서 변경하기 위해 간단하게 @Import 기능을 사용하였음
@Slf4j
@Import(AspectV1.class) // 스프링 빈으로 등록
@SpringBootTest
public class AopTest {
    // 테스트 코드 동일
}

 

(3) 테스트 실행 로그

  • 테스트 결과를 보면 isAopProxy의 로그 출력결과가 true로 출력되어 프록시가 적용되었음을 확인할 수 있음
  • success와 exception에서도 프록시가 적용되어 doLog()의 메서드가 실행된 것을 확인할 수 있음
// aopInfo()
isAopProxy, orderService=true
isAopProxy, orderService=true

// success
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행

// exception
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행

2) 포인트컷 분리

(1) AspectV2 생성

  • @Pointcut 애노테이션을 사용하여 포인트컷을 별도로 분리할 수 있음
  • 결과적으로 Aspectv1과 동일한 기능을 수행하지만 이렇게 분리하면 하나의 포인트컷 표현식을 여러 어드바이스에서 함께 사용할 수 있는 장점이 있으며, 다른 클래스에 있는 외부 어드바이스에서도 포인트컷을 함께 사용할 수 있음
package hello.aop.order.aop;

@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());
        return joinPoint.proceed(); // 실제 로직 실행
    }
}

 

(2) @Pointcut

  • @Pointcut에 포인트컷 표현식을 사용하며 메서드 이름과 파라미터를 합쳐서 포인트컷 시그니처(signature)라고함
  • 메서드의 반환타입은 void여야하고 코드 내용은 비워두어야 함
  • 예제서의 포인트컷은 allOrder()이며(파라미터가 없음), @Around("allOrder()")처럼 포인트컷 시그니처를 값으로 입력하면 됨
  • 내부에서만 사용하면 private을 사용하고 다른 애스팩트에서 참고하려면 public을 사용하면 됨

(3) AopTest - 수정

  • @Import(AspectV2.class)로 AspectV2를 스프링 빈으로 등록 후 테스트를 수행하면 기존과 동일하게 동작함

3) 어드바이스 추가

(1) 트랜잭션 기능 추가

  • 로그를 출력하는 기능에 추가로 트랜잭션을 적용하는 코드를 추가
  • 실제로 트랜잭션을 실행하는것은 아니고 기능이 동작하는 것처럼 로그만 남기도록 예제를 작성
  • 보통 트랜잭션 기능은 아래처럼 동작함
  • 1. 핵심 로직 실행 직전에 트랜잭션을 시작
  • 2. 핵심 로직 실행
  • 3. 핵심 로직 실행에 문제가 없으면 커밋
  • 4. 핵심 로직 실행에 문제가 발생하면 롤백

(2) AspectV3

  • allOrder() 포인트컷은 hello.aop.order패키지와 하위 패키지를 대상으로 함
  • allService() 포인트컷은 타입이름 패턴이 *Service를 대상으로 함
  • 즉, XxxService처럼 Service로 끝나는 클래스나 인터페이스에 모두 적용(타입이름 패턴이라고 적은 이유)되며 *servi*과 같은 패턴으로도 포인트컷을 적용시킬 수 있음
  • 포인트 컷은 논리연산자 &&, ||, ! 3가지 조합이 가능함
  • doTransactional()메서드는 hello.app.order 패키지와 하위 패키지 이면서 타입 이름 패턴이 *Service인 것을 대상으로만 적용함
package hello.aop.order.aop;

@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()")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[log] {}", joinPoint.getSignature());
        return joinPoint.proceed(); // 실제 로직 실행
    }
    
    // hello.app.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
    @Around("allService() && allOrder()")   // && 조건으로 두개의 포인트컷 시그니처를 적용
    public Object doTransactional(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());
        }
    }
}

 

(3) 포인트컷이 적용된 AOP 결과

  • orderService: doLog(), doTransaction() 어드바이스 적용
  • orderRepository: doLog() 어드바이스 적용

(4) AopTest - 수정 및 실행 결과

  • @Import(AspectV3.class)로 변경 후 테스트 실행
  • success의 테스트 실행 로그를 보면 OrderService에서는 doLog()메서드의로그가 출력되고 doTransactional()메서드의 로그가 출력되고 OrderRepository에서는 doLog()메서드의 로그만 실행되며 정상적으로 포인트컷이 의도한대로 적용되어있는 모습을 확인할 수 있음
  • exception 테스트의 로그도 트랜잭션 success와 동일하게 동작하지만 catch문으로 이동하여 트랜잭션 롤백의 로그를 출력함
// success 실행 로그
[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)

4) 포인트컷 참조

(1) Pointcuts 생성

  • 포인트컷을 공용으로 사용하기 위해 별도의 외부 클래스에 포인트컷을 모아두어도 됨
  • 외부에서 호출해야하므로 접근제어자를 public으로 열어두어야 함
  • @Pointcut()으로 @Around에서 적용한 것 처럼 포인트컷 시그니처를 조합하여 값을 입력할 수 있음
package hello.aop.order.aop;

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
    @Pointcut("allOrder() && allService()")
    public void allOrderAndService() {}

}

 

(2) AspectV4Pointcut

  • AspectV4클래스에서는 포인트컷이 없고 어드바이스만있으며, @Around의 값으로 사용할 포인트컷의 패키지명과 클래스명 그리고 포인트컷시그니처명을 입력하여 적용하면 됨
  • doTransactional() 메서드에는 allOrder()와 allService()를 합쳐서 새로운 포인트컷 시그니처로 만든 allOrderAndService()를 적용
  • 포인트컷을 여러 어드바이스에서 함께 사용할 때 이 방법을 사용하면 효과적임
package hello.aop.order.aop;

@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.allOrderAndService()")
    public Object doTransactional(ProceedingJoinPoint joinPoint) throws Throwable {
        // 로직 동일
    }
}

 

(3) AopTest - 수정 및 실행 결과

  • @Import(AspectV4Pointcut.class)로 변경 후 테스트 실행하면 AspectV3와 동일하게 로드가 출력되어 정상적으로 분린된 포인트컷이 적용되는 것을 확인할 수 있음

5) 어드바이스 순서

(1) 어드바이스의 순서 지정

  • 어드바이스는 기본적으로 순서를 보장하지 않음
  • 순서를 지정하고 싶으면 @Aspect 적용 단위로 @Order(스프링 프레임워크) 애노테이션을 적용해야함
  • 문제는 어드바이스 단위가 아니라 클래스 단위로 적용할 수 있기 때문에 지금처럼 하나의 애스펙트에 여러 어드바이스가 있으면 순서를 보장받을 수 없어서 애스펙트를 별도의 클래스로 분리해야함
  • 로그 남기는 순서를 doTransaction() -> doLog() 으로 트랜잭션이 먼저 처리되고 이후에 로그가 남도록 변경

** 참고

  • 현재는 대부분 doLog() -> doTransaction() 순서로 로그가 남겨질 텐데, JVM 실행 환경에 따라서 로그 출력 순서가 다를 수 있음

(2) AspectV5Order 생성

  • 하나의 애스펙트 안에 있던 어드바이스를 inner class를 사용하여 LogAspect, TxAspect로 분리한 후 각 애스펙트에 @Order 애노테이션을 통해 실행순서를 적용
  • 숫자가 적을수록 먼저 실행됨
  • 물론 내부클래스가 아닌 별도의 클래스를 각각 생성해도 상관없음
package hello.aop.order.aop;

@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(); // 실제 로직 실행
        }
    }
    
    @Aspect
    @Order(1)
    public static class TxAspect {
        @Around("hello.aop.order.aop.Pointcuts.allOrderAndService()")
        public Object doTransactional(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());
            }
        }
    }
}

 

(2) AopTest 수정 및 실행

  • 생성한 애스펙트를 @Import에 모두 등록
  • success와 exception의 실행 로그를 보면 TxAspect가 먼저 동작하여 doTransactional()의 로그가 먼저 실행되는 것을 확인할 수 있음
@Import({AspectV5Order.LogAspect.class, AspectV5Order.TxAspect.class})
@SpringBootTest
public class AopTest {
    // 테스트 로직 동일
}

/* exception 실행 로그
[트랜잭션 시작] 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)
*/

6-1) 어드바이스 종류

(1) AspectV6Advice 생성

  • 모든 어드바이스의 종류를 실행하기 위한 예제
package hello.aop.order.aop;

@Slf4j
@Aspect
public class AspectV6Advice {

    @Around("hello.aop.order.aop.Pointcuts.allOrderAndService()")
    public Object doTransactional(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("hello.aop.order.aop.Pointcuts.allOrderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
        // @Before는 실제 로직을 호출하는 코드를 작성할 필요없이 해당 메서드 종료시 자동으로 다음 target이 호출 됨
    }

    @AfterReturning(value = "hello.aop.order.aop.Pointcuts.allOrderAndService()", returning = "result")
    public void doReturn(JoinPoint joinPoint, Object result) {
        log.info("[return] {} return {}", joinPoint.getSignature(), result);
    }

    @AfterThrowing(value = "hello.aop.order.aop.Pointcuts.allOrderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[ex] {} message {}", ex, joinPoint.getSignature());
    }

    @After(value = "hello.aop.order.aop.Pointcuts.allOrderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}", joinPoint.getSignature());
    }
}

 

(2-1) 참고 정보 획득

  • 모든 어드바이스는 org.aspectj.lang.JoinPoint를 첫번째 파라미터에 사용할 수 있으며 생략도 가능함
  • 단, @Around만 ProceedingJoinPoint를 사용해야하는데 JoinPoint의 하위 타입임

(2-2) JoinPoint 인터페이스의 주요 기능

  • getArg() : 메서드 인수를 반환
  • getThis() : 프록시 객체를 반환
  • getTarget() : 대상 객체를 반환
  • getSignature() : 조언되는 메서드에 대한 설명을 반환
  • toString() : 조언되는 방법에 대한 유용한 설명을 인쇄

(2-3) ProceedingJoinPoint 인터페이스의 주요 기능

  • proceed() : 다음 어드바이스나 타겟을 호출
  • 추가로 호출시 전달한 매개변수를 파라미터를 통해서도 전달 받을 수도 있는데 이부분은 뒤에서 설명함

(3) @Around

  • 메서드의 실행 주변에서 실행되며 메서드 호출 전후에 작업을 수행함
  • 가장 강력한 어드바이스, @Around 어드바이스만 사용해도 필요한 기능을 모두 수행할 수 있기에 강력하다고 표현함
  • .proceed()로 조인 포인트 실행 여부를 선택할 수 있음
  • .proceed()를 호출해야 다음 대상이 호출되며 호출하지 않으면 다음 대상이 호출되지 않음
  • .proceed(args[])로 전달 값을 변환할 수 있음
  • 반환 값을 변환도 가능함
  • 예외변환도 가능함
  • 트랜잭션 처럼 try ~ catch ~ finally 모두 들어가는 구문 처리가 가능함
  • 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint를 사용해야함
  • .proceed()를 통해 대상을 실행하며 여러번 실행할 수도 있음(재시도)

(4) @Before

  • 조인 포인트 실행 이전에 실행
  • ProceedingJoinPoint.proceed() 자체를 사용하지 않으며 메서드 종료시 자동으로 다음 타겟이 호출됨
  • 예외가 발생하면 다음 코드가 호출되지 않음

(5) @AfterReturning

  • 조인 포인트가 정상 완료 후 실행
  • returning속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야함
  • returning 절에 지정된 메서드의 타입을 기준으로 반환 타입이 맞는 메서드를 대상으로 실행하며, 부모 타입을 지정하면 모든 자식타입은 지정됨
  • 만약 타입이 맞지 않으면 어드바이스 자체가 동작하지 않음
  • @Around와 다르게 반환되는 객체를 변경할 수는 없지만 반환 객체를 조작할 수는 있음

(6) @AfterThrowing

  • 메서드가 예외를 던지는 경우 실행
  • 예외가 터졌을때 동작한다는 것을 제외하면 @AfterReturning과 똑같은 로직으로 실행됨
  • throwing 속성의 이름은 어드바이스 메서드의 매개변수 이름과 일치해야하고 지정된 타입과 맞은 예외를 대상으로 실행해야하며 부모타입을 지정하면 자식 타입은 모두 인정됨

(7) @After

  • 조인 포인트가 정상 또는 예외에 관계없이 실행(finally로직을 생각하면 됨)
  • 정상 및 예외 반환 조건을 모두 처리하며 일반적으로 리소스를 해제하는 데 사용함

(8) AopTest - 변경 후 실행 및 어드바이스 순서

  • @Import(aspectV6Advice.class)로 변경 후 실행해보면 각 어드바이스들이 실행되는 로그들을 확인할 수 있음
  • 스프링은 5.2.7버전부터 동일한 @Aspect안에서 동일한 조인포인트의 우선순위를 정했음
  • 실행순서: @Around -> @Before -> @After -> @AfterReturning -> @AfterThrowing
  • 어드바이스가 적용되는 순서는 이렇게 적용되며 호출 순서와 리턴 순서는 반대로 적용되며 결과적으로 @Around가 마지막에 한번더 호출되긴하지만 그것을 제외하면 @After가 제일 마지막에 적용됨(finally를 생각해보면 이해가 쉬움)
  • 물론 @Aspect안에 동일한 종류의 어드바이스에 2개 있으면 순서가 보장되지 않으므로 이런 경우는 앞에서 배운것 처럼 @Aspect를 분리하고 @Order를 적용해야함
success 실행 로그
[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)

(9) @Around외에 다른 어드바이스가 존재하는 이유

  • @Around는 항상 proceed()를 호출을 해주어야 다음 타겟이나 어드바이스가 실행되며 실수로 호출하지 않으면 타겟이 호출되지 않는 치명적인 버그가 발생함
  • 그러나 @before는 proceed()를 호출하는 고민을 하지 않아도 알아서 자동으로 호출함
  • 즉, @Around가 가장 넓은 기능을 제공하는 것은 맞지만 실수할 가능성이 있으며 반면에 @Before, @After같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고 코드도 단순하며 이코드를 작성한 의도가 명확하게 드러남
  • @Before라는 애노테이션을 보는 순간 개발자는 이코드가 타겟 실행 전에 한정해서 어떤 동작을 하는 코드라고 쉽게 이해할 수 있음

(10) 좋은 설계는 제약이 있는 것

  • @Around만 있으면 모든 기능을 수행할 수 있는데 이렇게 제약을 두는 이유는 실수를 미연에 방지하여 일종의 가이드 역할을 하기 때문임
  • 만약 @Around를 사용했는데 중간에 다른 개발자가 해당 코드를 수정해서 호출하지 않았다면 큰 장애가 발생했을 것인데, @Before를 사용했더라면 이런 문제가 발생하지 않음
  • 즉, 제약 덕분에 역할이 명확해지며 다른 개발자들도 이 코드를 보고 고민해야 하는 범위가 줄어들고 코드의 의도가 파악하기 쉬워짐