무엇을 언제 적용할껀데?
들어가기전
Spring에서 AOP를 구현해야 한다면, 여러 방법 중에 특히 @AspectJ 스타일을 활용하여 구현할 거라 생각합니다. 본 포스팅에선 @AspectJ에서 사용할 수 있는 여러 유형의 Advice를 소개해드리겠습니다.
학습목표
- Advcie에 대한 이해
- 다양한 Advice 사용법
1. 어드바이스란
어드바이스(Advice)란 AOP에서 사용되는 AOP 용어입니다.
어드바이스는 특정 조인 포인트에 적용할 횡단 관심사 코드입니다. 즉 비지니스 클래스에 적용할 부가적인 코드(횡단 관심사 코드)라 생각하시면 되겠습니다.
2. 어드바이스의 동작에 대한 이해, 어드바이스 체인
Spring을 포함한 대부분의 AOP 프레임워크는 사용자의 요청을 인터셉터(Interceptors)하고, 지정된 어드바이스를 모델링합니다.
그다음 조인 포인트(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 어드바이스는 호출된 메소드의 반환 값을 사용할 수 있으므로, 이를 활용하여 또 다른 행위를 할 수 있습니다.
마무리
다시 정리를 해보자면 어드바이스는 다음과 같은 라이프 사이클을 볼 수 있습니다.
- @Around(주위)
- @Before(전에)
- 조인 포인트(실제 사용자에 의해 호출되는 메소드)
- @Around(주위)
- @After(이후)
- @AfterReturning(완료) 또는 @AfterThrowing(예외)