Home Spring Framework - 스프링 AOP 구현
Post
Cancel

Spring Framework - 스프링 AOP 구현

스프링 AOP 구현

기본 구현

스프링 AOP를 구현하는 일반적인 방법은 @Aspect를 사용하는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@Aspect
public class AspectV1 {

  // hello.aop.order 패키지와 하위 패키지
  @Around("execution(* hello.aop.order..*(..))")
  public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
    // join point 시그니처
    log.info("[log] {}", joinPoint.getSignature());
    return joinPoint.proceed();
  }
}
  • @Around 어노테이션의 문자열이 포인트컷
    • 문자열의 형식은 AspectJ 포인트컷 표현식
  • @Around 어노테이션의 메소드인 doLog는 어드바이스
  • hello.aop.order 하위의 모든 패키지의 모든 메소드는 AOP 적용의 대상이 됨
    • 스프링 AOP는 프록시를 사용하므로 프록시를 통하는 메소드만 적용 대상이 됨
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
@Import(AspectV1.class)
@SpringBootTest
public class AopTest {

  @Autowired
  OrderService orderService;

  @Autowired
  OrderRepository orderRepository;

  @Test
  void success() {
    orderService.orderItem("itemA");
  }
}

@Aspect는 에스펙트라는 표식이지 컴포넌트 스캔의 대상이 되는건 아니다. 그러므로 위 예의 AspectV1을 AOP로 사용하기 위해선 스프링 빈으로 따로 등록해야 한다.

  • @Bean
    • 직접 등록
  • @Component를
    • 컴포넌트 스캔을 통해 자동 등록
  • @Import
    • 주로 설정 파일을 추가할 떄 사용(@Configurtion) 하지만 스프링 빈도 등록 가능
[log] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[log] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행

Alt text

포인트컷 분리

@Around에 포인트컷 표현식을 직접 넣을수도 있지만 @Pointcut 어노테이션을 통해 별도로 분리할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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 {
    // join point 시그니처
    log.info("[log] {}", joinPoint.getSignature());
    return joinPoint.proceed();
  }

}
  • @Pointcut
    • @Pointcut 어노테이션에 포인트컷 표현식을 사용
    • 메소드 이름과 파라미터를 합쳐서 포인트컷 시그니처(Pointcut Signature)라 함
    • 메소드 반환 타입은 void 이여야 함
    • 코드 내용은 비워야 함
    • @Arounc 어드바이스에는 포인트컷을 직접 지정해도 되지만 포인트컷 시그니처를 사용해도 됨
    • 내부에서만 사용하려면 private을 사용해도 되지만, 외부에서 사용할 떈 public 으로 선언
  • 하나의 포인트컷 표현식을 여러 어드바이스에서 함꼐 사용 가능
  • 다른 클래스에 있는 외부 어드바이스에서도 포인트컷을 함께 사용 가능

어드바이스 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Slf4j
@Aspect
public class AspectV3 {

  // hello.aop.order 패키지와 하위 패키지
  @Pointcut("execution(* hello.aop.order..*(..))")  // pointcut expression
  private void allOrder() {}                        // pointcut signature

  // 클래스 이름 패턴이 @Service
  @Pointcut("execution(* *..*Service.*(..))")
  private void allService() {}

  @Around("allOrder()")
  public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
    // join point 시그니처
    log.info("[log] {}", joinPoint.getSignature());
    return joinPoint.proceed();
  }

  //hello.aop.order 패키지와 하위 패키지 이면서 클래스 이름 패턴이 *Service
  @Around("allOrder() && allService()")
  public Object doTx(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)을 통해 조합이 가능
[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)

Alt text

포인트컷 참조

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Pointcuts {
  // hello.aop.order 패키지와 하위 패키지
  @Pointcut("execution(* hello.aop.order..*(..))")
  public void allOrder() {}

  // 클래스 이름 패턴이 @Service
  @Pointcut("execution(* *..*Service.*(..))")
  public void allService() {}

  // allOrder() && allService()
  @Pointcut("allOrder() && allService()")
  public void orderAndService() {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Slf4j
@Aspect
public class AspectV4Pointcut {
  // 포인트컷이 정의되어있는 클래스의 패키지명까지 기입
  @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 doTx(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 어노테이션을 사용해야 한다. 그러나 이는 어드바이스 단위가 아닌 클래스 단위로 적용할수 있는 문제가 있다. 그러므로 하나의 에스펙트에 여러 어드바이스가 있다면 순서를 보장받을 수 없고, 애스펙트를 별도의 클래스로 분리해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@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.orderAndService()")
    public Object doTx(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());
      }
    }
  }

}
  • TxAspect -> LogAspect 순으로 Aspect 적용
    • doTx() -> doLog() 순으로 어드바이스 수행
[트랜잭션 시작] 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)

Alt text

어드바이스 종류

어드바이스의 종류는 @Around 외에도 여러 종류가 존재한다.

  • @Around
    • 메소드 호출 전후에 수행
    • 조인 포인트 실행 여부 선택, 반환값 변환, 예외 변환등 가장 강력한 어드바이스
  • @Before
    • 조인 포인트 실행 이전에 수행
  • @AfterReturning
    • 조인 포인트가 정상 완료후 수행
  • @AfterThrowing
    • 메소드가 예외를 던지는 경우 수행
  • @After
    • 조인 포인트가 정상 또는 예외에 관계없이 수행(finally)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Slf4j
@Aspect
public class AspectV6Advice {
  @Around("hello.aop.order.aop.Pointcuts.orderAndService()")
  public Object doTx(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());
    }
  }

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

모든 어드바이스는 org.aspectj.lang.JoinPoint 를 첫번째 파라미터에 사용할 수 있다.(생략 가능) 단, @Around는 ProceedingJoinPoint을 사용해야 한다. 참고로 ProceedingJoinPoint는 org.aspectj.lang.JoinPoint의 하위 타입이다

JoinPoint 인터페이스의 주요 기능

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

ProceedingJoinPoint 인터페이스의 주요 기능

  • proceed()
    • 다음 어드바이스나 타겟을 호출

어드바이스 종류

@Before

조인 포인트 실행전 수행

1
2
3
4
@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

메소드 실행이 정상적으로 반환되면 수행

1
2
3
4
@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 속성에 사용된 이름은 어드바이스 메소드의 매개변수 이름과 일치해야 함
  • returning 절에 지정된 타입의 값을 반환하는 베소드만 대상으로 실행됨
    • 부모 타입을 지정하면 모든 자식 타입은 인정됨
  • @Around와는 다르게 반환되는 객체를 변경할수는 없음
@AfterThrowing

메소드 실행이 예외를 던져서 종료될떄 수행

1
2
3
4
@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 속성에 사용된 이름은 어드바이스 메소드의 매개변수 이름과 일치해야 함
  • throwing 절에 지정된 타입의 값을 반환하는 베소드만 대상으로 실행됨
    • 부모 타입을 지정하면 모든 자식 타입은 인정됨
@After

메소드 실행이 종료되면 수행됨

  • 정상 및 예외 반환 조건을 모두 처리
  • 보통 리소스를 해제하는 데 사용
@Around

메소드 실행 전후에 작업을 수행

  • 가장 강력한 어드바이스
    • 조인 포인트 실행 여부 선택
    • 전달 값 변경
    • 반환 값 변환
    • 예외 변환
    • 트랜잭션 처럼 try ~ catch ~ finally 모두 들어가는 구문 처리 가능
  • 어드바이스의 첫번쨰 파라미터는 ProceedingJoinPoint를 사용해야 함
  • proceed()를 통해 다음 대상을 실행
  • proceed()를 여러번 실행할 수도 있음(재시도)

순서

스프링은 5.2.7 버전부터 동일한 @Aspect안에서 동일한 조인 포인트의 우선순위를 정했다.

  • 실행순서
    • @Around -> @Before -> @After -> @AfterReturing or @AfterThrowing
  • 호출순서와 리턴순서는 반대

@Aspect안에 동일한 종류의 어드바이스가 2개 있다면 순서가 보장되지 않는다. 이경우에는 @Aspect를 분리하고 @Order를 적용해야 한다.

Alt text

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

사실상 @Around 하나의 어드바이스만 있어도 모든처리가 가능하지만 여타 다른 어드바이스의 종류가 존재한다.

그 이유는 @Around는 가장 넓은 기능을 제공하긴 하지만 실수할 가능성이 있다. 예를 들어 @Around의 어드바이스 안에 proceed()를 호출하지 않는다면 다음 타겟을 실행되지 않지만, @Before, @After의 경우 proceed()를 호출여부를 상관하지않고 로직만 구현하면 된다.

제약을 통해 @Around 사용시의 실수를 미연에 방지하고자 좀더 좁은 범위의 기능을 가진 다른 어드바이스가 존재한다.


참고

  • 스프링 핵심 원리 - 고급편(김영한)
This post is licensed under CC BY 4.0 by the author.

Spring Framework - 스프링 AOP 개념

Spring Framework - 포인트컷