본문 바로가기
Spring/spring AOP

스프링 코어 2 - 어드바이스 종류

by 킹차니 2022. 1. 14.

 

어드바이스 종류

- 어드바이스에는 앞서 배운 @Around외에도 여러가지 종류를 추가할 수 있다.

 

어드바이스 종류:

@Around : 메서드 호출 전후에 수행, 가장 강력한 어드바이스, 조인 포인트 실행 여부 선택, 반환 값 변환, 예외 변환 등이 가능

@Before : 조인 포인트 실행 이전에 실행

@AfterReturning : 조인 포인트가 정상 완료후 실행

@AfterThrowing : 메서드가 예외를 던지는 경우 실행

@After : 조인 포인트가 정상 또는 예외에 관계없이 실행(finally)

 (사실 @Before부터 아래 종류들은 @Around의 주변 기능들일 뿐이다.

 

위의 나머지 어드바이스들은 아래와 같은 타이밍에 적용된다.

@Slf4j
@Aspect
public class AspectV6Advice {
    @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());
        }
    }
}

 

@Before, @AfterReturning, @AfterThrowing, @After 들을 하나씩 알아보자.

 


 

 

@Before :

@Slf4j
@Aspect
public class AspectV6Advice {

    @Before("hello.aop.order.aop.Pointcuts.orderAndService()")//*Service에만 적용
    public void doBefore(JoinPoint joinpoint){
        log.info("[@before] {}", joinpoint.getSignature());
    }
}

@Around 와 다르게 작업 흐름을 변경할 수는 없다.
@Around 는 ProceedingJoinPoint.proceed() 를 호출해야 다음 대상이 호출된다. 만약 호출하지 않으면 다음 대상이 호출되지 않는다. 반면에 @Before 는 ProceedingJoinPoint.proceed() 자체를 사용하지 않는다. 메서드 종료시 자동으로 다음 타켓이 호출된다. 물론 예외가 발생하면 다음 코드가 호출되지는 않는다.

 

테스트를 진행해보면

@Import({AspectV6Advice.class})
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired OrderService orderService;
    @Autowired OrderRepository orderRepository;

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

실행결과:

[@before] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[orderRepository] 실행

분명 타겟의 로직을 호출한 적이 없는데도 실행된 것을 볼 수 있다. 나머지는 스프링이 알아서 호출해주고, @Before는 타겟 호출 이전까지만 동작하는 것이다.

 

 

 

 

@AfterReturning :

@Slf4j
@Aspect
public class AspectV6Advice {

    @AfterReturning(value = "hello.aop.order.aop.Pointcuts.orderAndService()", returning = "result")
    public void doReturn(JoinPoint joinpoint, Object result){
        log.info("[@Returning] {}, return = {}", joinpoint.getSignature(), result);
    }
}
/*
	@AfterReturning( ... returning = "result")의 result와 
	doReturn ( ... , Object result)의 두 result가 매칭된다.
*/

 

returning 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.

returning 절에 지정된 타입의 값을 반환하는 메서드만 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)

@Around 와 다르게 반환되는 객체를 변경할 수는 없다. 반환 객체를 변경하려면 @Around 를 사용해야 한다. 참고로 반환 객체를 조작할 수 는 있다.

 

테스트해보자.

@Import({AspectV6Advice.class})
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired OrderService orderService;
    @Autowired OrderRepository orderRepository;

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


실행결과:

[orderService] 실행
[orderRepository] 실행
[@Returning] void hello.aop.order.OrderService.orderItem(String), return = 회원정보

 

 

 

@AfterThrowing

package hello.aop.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 {

    @AfterThrowing(value = "hello.aop.order.aop.Pointcuts.orderAndService()", throwing = "ex")
    public void doReturn(JoinPoint joinpoint, Exception ex){
        log.info("[@AfterThrowing] {}, ex = {}", joinpoint.getSignature(), ex);
    }
}

throwing 속성에 사용된 이름은 어드바이스 메서드의 매개변수 이름과 일치해야 한다.

throwing 절에 지정된 타입과 맞은 예외를 대상으로 실행한다. (부모 타입을 지정하면 모든 자식 타입은 인정된다.)

 

테스트

package hello.aop;


@Import({AspectV6Advice.class})
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired OrderService orderService;
    @Autowired OrderRepository orderRepository;

    @Test
    void success(){
        orderService.orderItem("ex");//테스트를 위해 익셉션을 발생시키는 itemId
    }
}


실행결과:
[orderService] 실행
[@AfterThrowing] String hello.aop.order.OrderService.orderItem(String), ex message = {}
java.lang.IllegalStateException: 예외 발생!
...예외 메세지 생략

 

 

 

 

@After

@Slf4j
@Aspect
public class AspectV6Advice {
    @After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doReturn(JoinPoint joinpoint){
        log.info("[@After] {}", joinpoint.getSignature());
    }
}

 

메서드 실행이 종료되면 실행된다. (finally를 생각하면 된다.)

정상 및 예외 반환 조건을 모두 처리한다.
일반적으로 리소스를 해제하는 데 사용한다.

 

테스트

package hello.aop;

@Import({AspectV6Advice.class})
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired OrderService orderService;
    @Autowired OrderRepository orderRepository;

    @Test
    void success(){
        orderService.orderItem("ex");//테스트를 위해 익셉션을 발생시키는 itemId
    }
}


실행결과:

[orderService] 실행
[@After] String hello.aop.order.OrderService.orderItem(String)

 

 

 

 

 

@Before, @AfterReturning, @AfterThrowing @After가 모두 존재할 때 테스트해보자.

package hello.aop.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 {

    @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("[@Returning] {}, return = {}", joinpoint.getSignature(), result);
    }

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

    @After(value = "hello.aop.order.aop.Pointcuts.orderAndService()")
    public void doReturn(JoinPoint joinpoint){
        log.info("[@After] {}", joinpoint.getSignature());
    }
}

 

테스트

package hello.aop;

@Import({AspectV6Advice.class})
@Slf4j
@SpringBootTest
public class AopTest {

    @Autowired OrderService orderService;
    @Autowired OrderRepository orderRepository;

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

실행결과:

[@before] String hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[@Returning] String hello.aop.order.OrderService.orderItem(String), return = 회원정보
[@After] String hello.aop.order.OrderService.orderItem(String)

 

모든 어드바이스는 'org.aspectj.lang.JoinPoint'를 첫번째 파라미터에 사용할 수 있다.(생략해도 된다.) 

다만 '@Around'는 ProceedingJoinPoint 를 사용해야 한다.

---> 왜냐하면 @Around는 반드시 본인이 target을 호출해야 한다. 그런데 target을 호출하기 위해서는 ProceedingJoint가 다음 target을 호출할 수 있는 proceed() 메서드를 제공하기 때문이다.

 

 

 

 

 

@Around

 

메서드의 실행의 주변에서 실행된다. 메서드 실행 전후에 작업을 수행한다

• 가장 강력한 어드바이스

  - 조인 포인트 실행 여부 선택 joinPoint.proceed() 호출 여부 선택

  - 전달 값 변환: joinPoint.proceed(args[])
  - 반환 값 변환
  - 예외 변환

  - 트랜잭션 처럼 try ~ catch~ finally 모두 들어가는 구문 처리 가능

• 어드바이스의 첫 번째 파라미터는 ProceedingJoinPoint 를 사용해야 한다.

• proceed() 를 통해 대상을 실행한다.

• proceed() 를 여러번 실행할 수도 있음(재시도)

 

 


 

 

어드바이스 실행 순서

• 스프링은 5.2.7 버전부터 동일한 @Aspect 안에서 동일한 조인포인트의 우선순위를 정했다.
• 실행 순서: @Around , @Before , @After , @AfterReturning , @AfterThrowing
• 어드바이스가 적용되는 순서는 이렇게 적용되지만, 호출 순서와 리턴 순서는 반대라는 점을 알아두자.
• 물론 @Aspect 안에 동일한 종류의 어드바이스가 2개 있으면 순서가 보장되지 않는다. 이 경우 앞서 배운 것 처럼 @Aspect 를 분리하고 @Order 를 적용하자.

 

 


 

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

 

@Around만 있어도 모든 모든 기능을 수행할 수 있는데, 굳이 다른 어드바이스들이 존재하는 이유가 있다.

이 코드는 타켓을 호출하지 않는 문제가 있다.
이 코드를 개발한 의도는 타켓 실행 전에 로그를 출력하는 것이다. 그런데 @Around 는 항상 joinPoint.proceed() 를 호출해야 한다. 만약 실수로 호출하지 않으면 타켓이 호출되지 않는 치명적인 버그가 발생한다.

 

 

다음 코드를 보자.

@Before는 joinPoint.proceed()를 호출하지 않아도 된다. 

 

@Around 가 가장 넓은 기능을 제공하는 것은 맞지만, 실수할 가능성이 있다. 반면에 @Before , @After 같은 어드바이스는 기능은 적지만 실수할 가능성이 낮고, 코드도 단순하다. 그리고 가장 중요한 점이 있는데, 바로 이 코드를 작성한 의도가 명확하게 들어난다는 점이다. @Before 라는 애노테이션을 보는 순간 이 코드는 타켓 실행 전에 한정해서 어떤 일을 하는 코드라는 것이 들어난다.

 

 

 

 

 

좋은 설계제약이 있는 것이다

 

좋은 설계는 제약이 있는 것이다. @Around는 어드바이스 종류 중에서 모든 것을 처리할 수 있는 만능이지만, 다른 어드바이스들이 존재함으로써 제약을 둔다. 제약을 둔다면 실수할 확률이 줄어든다. 또한 제약을 둠으로써 다른 개발자가 생각할 범위가 줄어든다.

 

 

 

 

 

 

 

 

 

김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.