AOP 어드바이스 종류

바그다드·2023년 10월 6일
0

지난 포스팅에서는 AOP의 구현방법에 대해서 알아보았다.
가장 기본적인 형태의 @Aspect부터, 포인트컷 적용 방법, 포인트컷을 별도로 관리하는 방법, 다중 포인트컷과 다중 어드바이스, 어드바이스 순서를 지정하는 방법 등에 대해 알아보았다.

이번 포스팅에서는 @Around 등 여러 어드바이스 종류에 대해 알아보려고 한다.

1. 어드바이스 종류

  1. @Around
    • 메서드 전후에 실행되며 가장 강력한 어드바이스이다.
    • 조인 포인트의 실행 여부 결정, 반환 값 변환, 예외 변환 등 여러 기능을 수행할 수 있다.
  2. @Before
    • 조인포인트 실행 이전 시점에서만 적용되는 어드바이스
  3. @After Returning
    • 조인포인트가 정상적으로 완료된 시점에 수행되는 어드바이스(try)
  4. @After Throwing
    • 조인포인트가 예외를 던지는 경우에만 실행되는 어드바이스(catch)
  5. @After
    • 조인포인트의 정상처리, 예외 발생에 관계없이 수행되는 어드바이스(finally)

@Around가 모든 시점에 적용이 가능하고,
나머지 어드바이스는 @Around의 기능을 작게 쪼개어 놓은거라고 생각하면 된다.

코드로 확인해보자.

1. @Around

    @Around("hello.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이외의 어드바이스가 적용되는 시점을 확인할 수 있다.

  • ProceedingJoinPoint
    JoinPoint의 하위타입으로 ProceedingJoinPoint을 이용해 타겟의 실행 여부를 제어할 수 있다.

    • proceed()로 다음 어드바이스나 타겟을 호출할 수 있다.
      호출하지 않을 경우 해당 어드바이스만 수행되고 리턴된다.
    • proceed()를 여러번 호출할 수 있다.

    @Around에서는 proceed()를 호출하지 않으면 다음 타겟의 로직을 수행하지 않으므로 주의하도록 하자.

2. @Before

    // 조인포인트 실행 전의 로직만 작성할 때
    // joinPoint.proceed 이전의 로직
    @Before("hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("[before] {}", joinPoint.getSignature());
    }
  • @Around와 달리 작업 흐름을 제어할 수 없다.
  • 조인포인트 실행 이전 시점에 수행할 로직을 작성하며, 다음 타겟이 자동으로 호출된다.

3. @AfterReturning

    // 메서드 실행이 정상적으로 반환될 때 실행
    // joinPoint.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 속성에 지정한 이름과 어드바이스 메서드의 파라미터 이름이 일치해야 한다.
  • returning 속성에 지정한 타입의 값을 반환하는 메서드만 타겟으로 수행된다.
    부모타입을 지정하면 자식타입도 인정이 된다.
    만약 타겟의 반환타입과 어드바이스의 파라미터 타입이 매칭되지 않을 경우 호출되지 않는다,
  • @Around와 달리 반환 객체를 변경할 수는 없으나, 조작은 가능하다.

4. @AfterThrowing

    // 메서드 실행이 예외를 반환할 때 실행
    // catch와 같은 역할
    @AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
    public void doThrowing(JoinPoint joinPoint, Exception ex) {
        log.info("[ex] {} message={}", ex);
    }
  • 메서드에서 예외를 던질 때 수행되는 어드바이스이다.
  • throwing 속성에 지정한 이름과 어드바이스 메서드의 파라미터 이름이 일치해야 한다.
    부모타입을 지정하면 자식타입도 인정이 된다.
    만약 타겟의 반환타입과 어드바이스의 파라미터 타입이 매칭되지 않을 경우 호출되지 않는다,

5. @After

    // finally 부분의 로직과 같은 역할을 함
    @After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doAfter(JoinPoint joinPoint) {
        log.info("[after] {}",joinPoint.getSignature());
    }
  • finally와 같은 역할을 하는 어드바이스이다.
  • 메서드가 정상 종료되든지, 예외가 발생하든지 수행되는 어드바이스
  • 일반적으로 리소스를 릴리즈할 때 사용한다.

JoinPoint

모든 어드바이스에서는 첫번째 파라미터로 JoinPoint를 사용할 수 있다.(생략 가능)
다만, @Around에서는 ProceedingJoinPoint를 사용해야 한다.

  • JoinPoint 인터페이스 주요 기능
  1. getArgs() : 메서드 인수를 반환
  2. getThis() : 프록시 객체를 반환
  3. getTarget() : 타겟(대상 객체)을 반환
  4. getSignature() : 조인되는 메서드에 대한 설명을 반환
  5. toString() : 조인되는 방법에 대한 설명을 반환
  • ProceedingJoinPoint(JoinPoint의 하위 타입)의 주요 기능
    proceed() : 다음 타겟이나 어드바이스를 호출

어드바이스 수행 순서

  • 동일한 @Aspect 안에서 수행될 때 어드바이스의 수행순서는 다음과 같다.

@Around외의 어드바이스가 등장한 이유?

앞서 어드바이스 중 @Around가 모든 기능을 수행할 수 있다는 것을 확인하였다.
그렇다면 @Around만 사용하면 되는게 아닌가? 하는 생각을 하게 된다. 일단 내가 그랬다.
그럼 @Around외의 어드바이스가 등장한 이유는 무엇일까?

  1. 다음 타겟이나 어드바이스를 직접 호출해야 한다는 것
    @Before나 @AfterReturning등의 어드바이스는 proceed를 호출하지 않아도 자동으로 다음 어드바이스나 타겟이 호출된다.
    반면에 @Around는 개발자가 직접 proceed()를 호출하지 않으면 타겟이 호출되지 않는 문제가 발생한다.

아래의 코드로 확인해보자.

@Around("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(ProceedingJoinPoint joinPoint) {
	log.info("[before] {}", joinPoint.getSignature());
}

@Around를 사용헀음에도 불구하고 proceed()를 호출하지 않아 타겟이 호출되지 않는다.

다음은 코드를 보자.

@Before("hello.aop.order.aop.Pointcuts.orderAndService()")
public void doBefore(JoinPoint joinPoint) {
	log.info("[before] {}", joinPoint.getSignature());
}

@Before를 사용하고 있기 때문에 타겟이 호출되지 않는 문제에 대해서 고민하지 않아도 된다.
덕분에 코드도 간결해지고 실수를 하게될 가능성도 낮아지며 그 영향도 줄어들게 된다.

  1. 코드의 의도가 명확하게 드러난다.
    @Around의 경우 타겟의 호출전후로 많은 기능을 수행할 수 있는만큼 다른 개발자가 봤을 때 그 코드의 의도를 파악하는데도 오랜 시간이 걸릴 수 있다.
    반면 @Before를 사용할 경우 타겟을 호출하기 전에 수행되는 코드라는 것을 쉽게 알 수 있다.

김영한님이 좋은 설계는 제약이 있는것이라는 말씀을 하셨는데, 바로 앞서 봤던 예제만 하더라도 @Around 대신에 @Before를 사용했다면 발생하지 않았을 문제였다.
예전에 학원 다닐 때 다른 팀에서 @ResponseBody로 http메세지를 보내려하는데 자꾸 페이지가 이동하는 문제가 발생했던 적이 있다. 알고보니 http메서드에 따라 url을 구분하지 않고 @RequestMapping만을 사용해 또 다른 메서드로 매칭이 되어 페이지 이동이 발생했던 것이다. 이 문제 또한 RequestMapping만 사용할게 아니라 http메서드에 따라 구분을 해줬다면 발생하지 않았을 간단한 일이었다.
이처럼 개발을 할 때 그떄 당시에는 조금 더 귀찮은 과정이 생기더라도 적절한 제약에 따라 프로그래밍을 하는게 결과적으로 더 나은 선택이 될 수 있다는 것을 느끼게 되는 시간이었다.

정리

이번 포스팅에서는 AOP어드바이스의 종류에 대해서 알아보았다.
어드바이스의 종류에는

  1. @Around - 메서드 호출 전후에 작용, 가장 강력하고, ProceedingJoinPoint를 사용, proceed()를 호출해야 함
  2. @Before - 타겟 호출 전에 작용, 타겟은 자동 호출
  3. @AfterReturning - 메서드가 정상 완료된 시점에 작용, 메서드 반환 타입과 returning타입이 일치해야 하고, returning과 파라미터 이름이 일치해야함, 부모 타입과 그 자식 타입까지 지원
  4. @AfterThrowing - 메서드에서 예외가 발생했을 때 작용, 메서드 반환 타입과 throwing타입이 일치해야 하고, throwing과 파라미터 이름이 일치해야함, 부모 타입과 그 자식 타입까지 지원
  5. @After - finally와 같은 역할로 메서드의 정상처리나 예외에 관계 없이 작용, 주로 리소스 릴리즈에 사용

등이 있었다.
그리고 @Around외의 어드바이스가 있는 이유에 대해서 알아보며, 제약 덕분에 의도가 명확해지고 이에 따라 고민해야할 범위가 줄어들고 의도를 파악하기도 수월해진다는 것을 알 수 있었다.

다음 포스팅에서는 포인트컷의 여러 지시자에 대해서 알아보도록 하자.

출처 : 김영한 - 스프링 핵심 원리 고급편

profile
꾸준히 하자!

0개의 댓글