무엇을 언제 적용할껀데?


들어가기전

Spring에서 AOP를 구현해야 한다면, 여러 방법 중에 특히 @AspectJ 스타일을 활용하여 구현할 거라 생각합니다. 본 포스팅에선 @AspectJ에서 사용할 수 있는 여러 유형의 Advice를 소개해드리겠습니다.

학습목표

  1. Advcie에 대한 이해
  2. 다양한 Advice 사용법

1. 어드바이스란

어드바이스(Advice)란 AOP에서 사용되는 AOP 용어입니다.

어드바이스는 특정 조인 포인트에 적용할 횡단 관심사 코드입니다. 즉 비지니스 클래스에 적용할 부가적인 코드(횡단 관심사 코드)라 생각하시면 되겠습니다.

advice

2. 어드바이스의 동작에 대한 이해, 어드바이스 체인

Spring을 포함한 대부분의 AOP 프레임워크는 사용자의 요청을 인터셉터(Interceptors)하고, 지정된 어드바이스를 모델링합니다.

advice-chain

그다음 조인 포인트(JoinPoint) 주변에 지정된 어드바이스 코드가 결합된 상태로 유지합니다. 이 상태가 마치 체인처럼 형성하고 있어, 어드바이스 체인(Advice chain)이라 합니다.

마지막으로 실제 런타임 시 어드바이스 체인을 유지된 상태로 결합한 코드가 실행됩니다. 따라서 어드바이스를 구현할 때는 어디 조인 포인트와 결합할지 명시해줘야 합니다.

@Advice(조인 포인트 || 포인트  표현식)
public void method(){
  ... advice code
};

물론 @Advice라는 애노테이션은 없지만, 이해를 돕기위해 사용했습니다. 다음 코드처럼 @AspectJ 5 라이브러리에서 제공하는 어드바이스 애노테이션를 표기하고 포인트 컷이나 직접적으로 조인 포인트를 명시하여 구현해주셔야 합니다. 포인트 컷에 대한 부분은 이전에 작성한 “포인트 컷의 다양한 표현식” 포스팅을 참고해주시기 바랍니다.

3. @AspectJ 5 라이브러리의 @Advice

@AspectJ 5 라이브러리에서 제공하는 어드바이스 애노테이션은 다음과 같습니다.

  • @Before
  • @Aefore
  • @Around
  • @AfterReturning
  • @AfterThrowing

본격적으로 소개에 앞서 다음과 같은 타깃이 있다고 가정해봅시다.

@Service
public class UserService {
    public String findUserId(UserVO user) throws RuntimeException{
       	System.out.format("수행중... user.getId() >> %s \n", user.getId());
        ...
        return id;
    }
}

public class SimpleExecutionPCD {
    @Autowired private UserService service;

    @Test public void isAdviceTypeLearning() {
        UserVO user = new UserVO();
               user.setId(1);
        service.findUserId(user);
    }
}

3.1. @Before (이전)

우선 @Before 어드바이스는 조인 포인트가 실행되기 전에 실행되는 어드바이스입니다.

@Pointcut("execution(String findUserId(..)) "
	+ "&& args(user, ..)")
public void simplePointcut(UserVO user) {};

@Before("simplePointcut(user)")
public void beforeAdvice(JoinPoint jp, UserVO user) {
    System.out.format("@Before > %s \n" , jp);
}
@Before > execution(String com.moong.ahea.UserService.findUserId(UserVO))
수행중... user.getId() >> 1

다음 결과를 보면 findUserId 메소드(조인 포인트)가 실행되기 전 @Before 어드바이스 코드가 수행됨을 확인할 수 있고, JoinPoint 타입의 파라미터를 사용하여, 실제적인 메소드가 실행되기 전에 메소드의 호출 정보를 미리 확인할 수 있습니다.

또한, @Before 어드바이스에서 Exception을 강제로 발생하지 않는 한, 타깃의 메소드가 호출되는 것을 막을 수 없습니다. 따라서, 이 점을 이용하면 타깃의 메소드가 호출 되기 전에, @Before 어드바이스에서 파라미터의 특정 값을 판단하고 예외로 처리하는 로직을 구현할 수 있습니다.

3.2. @After (이후)

@After 어드바이스는 조인 포인트가 실행된 후 실행됩니다.

@After("simplePointcut(user)")
public void afterAdvice(JoinPoint jp, UserVO user) {
  System.out.format("@After > %s \n" , jp);
}
수행중... user.getId() >> 1
@After > execution(String com.moong.ahea.UserService.findUserId(UserVO))

다음 결과를 보면 비즈니스 코드가 수행되고 난 뒤 실행됨을 확인할 수 있었습니다.

@After는 흔히 사용하고 있는 try~catch~finally 블록 중에 finally 블록과 유사합니다. 즉 Exception이 발생 되도 어드바이스에 해당하는 코드를 수행합니다.

따라서 이 어드바이스는 사용자의 호출이 정상적으로 끝난다거나, 예외가 발생한다거나 상관없이, 끝나고 정리해야 할 외부 리소스의 접속 종료와 같은 코드, 즉 호출이 끝나는 시점에서 공통적인 다른 행위들을 할 수 있게 도와주는 어드바이스라 할 수 있습니다.

3.3. @Around (주변)

@Around 어드바이스는 @Before와 @After가 합쳐진 어드바이스입니다.

특히 AspectJ 5 라이브러리에 포함된 여러 어드바이스 중에서 유일하게 “메소드의 호출을 제어“할 수 있는 어드바이스입니다.

  • 메소드 실행 시점 제어
  • 요청된 파라미터 값 조작

대표적으로 @Around 어드바이스는 다음과 같은 상황을 수행하는 데 사용됩니다. 특히 호출을 제어하기 위해선 이전에 사용되었던 JoinPoint 타입의 파라미터가 아닌 ProceedingJoinPoint타입의 파라미터를 사용해야 합니다.

@Around("simplePointcut(user)")
public Object aroundAdvice(ProceedingJoinPoint jp, UserVO user){
    try {
        System.out.format("@Around STA > %s \n" , jp);

        // 실행 시점 제어와 요청된 파라미터 값 조작
        user.setId(100);
        Object result = jp.proceed(new Object[] {user});

        System.out.format("@Around END > %s \n" , jp);
        return result;
    }catch(Throwable e) {
        e.printStackTrace();
    }
    return jp;
}
@Around STA > execution(String com.moong.ahea.UserService.findUserId(UserVO))
수행중... user.getId() >> 100
@Around END > execution(String com.moong.ahea.UserService.findUserId(UserVO))

따라서 ProceedingJoinPoint 인터페이스에서 제공하는 proceed() 메소드를 활용하여, 사용자의 요청이 방영된 실제 메소드가 호출되기 전 또는 후에, 또 다른 부가적인 코드를 실행할 수 있고 자체 반환 값을 조작하거나 Exception를 강제로 발생하여 특정 메소드를 호출할 수 있습니다.

3.4. @AfterThrowing (예외)

만약 실제 사용자의 요청이 Exception을 발생하여 메소드가 종료된 경우에 특정 어드바이스를 부가하고 싶다면 @AfterThrowing 어드바이스를 사용하면 됩니다.

테스트를 하기 위해 findUserId 메소드에 강제로 예외를 발생했다고 가정해봅시다.

@Service
public class UserService{
    public String findUserId(UserVO user) throws RuntimeException{
        System.out.format("수행중... user.getId() >> %s \n", user.getId());
        throw new RuntimeException("예외 발생");
    }
}

이때 @AfterThrowing은 다음과 같이 코드를 작성할 수 있습니다.

@AfterThrowing(pointcut="simplePointcut(user)", throwing="ex")
public void throwAdvice(JoinPoint jp, UserVO user, Throwable ex) {
    System.out.format("@AfterThrowing > %s \n ex > %s \n" , jp, ex);
}
수행중... user.getId() >> 1
@AfterThrowing > execution(String com.moong.ahea.UserService.findUserId(UserVO))
 ex > java.lang.RuntimeException: 예외 발생
  • throwing

throwing 옵션을 활용하여 전달받을 파라미터 이름을 정하고, 파라미터에 Exception의 종류의 클래스를 지정해주면 됩니다.

@Around → @AfterThrowing

단 주의해야 될 상황이라면, @AfterThrowing 어드바이스가 호출되는 시점이 @Around 어드바이스가 호출된 이후 실행이 되기 때문에 @Around 어드바이스와 같이 사용된다면 호출이 되지 않습니다.

왜냐하면 @Around에서 메소드의 모든 시점을 제어하기 때문입니다. 즉 Exception이 발생해도 @Around에서 발생한 Exception을 인터셉터하여 수행하기 때문에 @AfterThrowing 어드바이스는 수행하지 않습니다.

3.5. @AfterReturning (완료)

마지막으로 @AfterReturning 어드바이스는 조인 포인트가 정상적으로 실행된 다음 실행되는 어드바이스입니다. 이 말인즉슨 사용자의 요청에 의해 호출되는 메소드가 Exception을 발생시키지 않고 리턴하는 경우를 의미합니다.

예를 들어 사용자가 findUserId 메소드를 호출한다고 가정해봅시다.

@Service
public class UserService{
    public String findUserId(UserVO user) throws RuntimeException{
        System.out.format("수행중... user.getId() >> %s \n", user.getId());
        // user.getName() -> 1;
        ...
        return "moong"; // 실제적인 반환될 사용자의 ID
    }
}

이때 id의 값은 1이라고 가정하고, 메소드는 내부 로직에 의해 id가 1에 해당되는 “moong”이라는 사용자의 아이디 값을 반환하고 있습니다.

@AfterReturning(pointcut="simplePointcut(user)", returning="retVal")
public void returnAdvice(JoinPoint jp, UserVO user, Object retVal) {
    System.out.format("@AfterReturning > %s \n retVal > %s \n" , jp, retVal);

}
수행중... user.getId() >> 1
@AfterReturning > execution(String com.moong.ahea.UserService.findUserId(UserVO))
retVal > "moong"
  • returning

이때 returning 옵션을 사용하여 반환 받고, 파라미터에 해당되는 타입으로 선정하여 반환된 값을 어드바이스 내부에서 활용할 수 있습니다. 즉 @AfterReturning 어드바이스는 호출된 메소드의 반환 값을 사용할 수 있으므로, 이를 활용하여 또 다른 행위를 할 수 있습니다.

마무리

다시 정리를 해보자면 어드바이스는 다음과 같은 라이프 사이클을 볼 수 있습니다.

advice-life-cycle

  1. @Around(주위)
  2. @Before(전에)
  3. 조인 포인트(실제 사용자에 의해 호출되는 메소드)
  4. @Around(주위)
  5. @After(이후)
  6. @AfterReturning(완료) 또는 @AfterThrowing(예외)

참고