04. advice의 타입
- -
advice는 실행되는 시점에 따라 여러가지 타입으로 작성할 수 있다. 이번 포스트에서는 다섯가지 advice의 타입에 대해 살펴보자.
advice 타입
advice가 동작하기 위해서는 어떤 시점에 advice를 실행할 것인가를 결정해야 한다. Spring에서는 다섯가지 annotation으로 이 시점을 설정할 수 있다.
애너테이션 | 설명 |
@Before | 타겟 메서드 호출 전 advice 실행 |
@AfterReturning | 타겟 메서드가 정상 종료(return) 후 advice 실행 |
@AfterThrowing | 타겟 메서드에서 예외가 던져졌을 때(throws XXException) advice 실행 |
@After | 타겟 메서드 성공 여부(return or throws XXException)와 무관하게 언제나 advice 실행 |
@Around | advice 내부에서 타겟 메서드 호출(타겟 메서드의 모든 것을 제어 가능) |
여기서는 테스트를 위해 두 개의 클래스를 만들어보자. 먼저 사용자 정보를 가지고 다닐 User라는 클래스이다. 이 클래스는 단순 DTO로 사용된다.
@Data
@AllArgsConstructor
public class User{
String id;
String pass;
}
다음은 핵심 비지니스 로직을 처리하기 위한 MyService이다. 이 클래스는 서비스의 용도로 사용되는 빈이기 때문에 @Service로 처리하자.
@Service
@Slf4j
public class MyService {
public User regist(User user){
if(user.getId().equals("hong")){
throw new RuntimeException("이미 존재함");
}
log.debug("저장완료: {}", user);
return user;
}
public long getFactorial(int n){
long result = 1;
for(int i=2; i<=n; i++){
result*=i;
}
return result;
}
}
MyService의 핵심 관심사 이외에 어떤 부가 관심사들이 필요할지 고민해보면서 Aspect를 작성해보자.
@Before
동작과 annotation
@Before가 선언된 advice는 target 메서드가 실행되기 전에 호출되며 advice에서 예외가 발생하면 아예 target이 호출되지 못한다.
@Before는 이제까지 써온 녀석이라 익숙하다. 애너테이션의 파라미터로는 value 속성에 point cut을 적는다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
String value(); // point cut
}
@Before에서는 타겟 호출 전에 전달되는 argument에 대한 조작이 가능하다. 단 완전히 대체할 수는 없으며 객체의 경우 그 속성의 변경은 가능하다.
기존 예제에서는 argument 정보를 얻기 위해서 JoinPoint의 getArgs를 사용했는데 단순 정보를 출력할때는 상관 없지만 내용을 사용할 때는 번거로움이 있었다. 이때 pointcut에 args(변수명)을 사용하면 바로 파라미터로 처리할 수 있다.
advice 작성 및 테스트
User 객체의 비밀번호가 저장될 때 암호화 해서 저장되는 상황을 @Before에서 처리하고 테스트해보자.
advice 작성
@Before("execution(com.doding.aop.dto.User com.doding..regist(com.doding.aop.dto.User)) && args(user)")
public void before(JoinPoint joinPoint, User user) {
log.debug("signature: {}, args: {}", joinPoint.getSignature(), user);
//user = new User("lim", "1234"); // 새로운 값으로 대체는 불가
user.setPass("암호화_" + user.getPass()); // 속성의 변경은 가능
}
advice 내에서는 전달된 아규먼트 user를 수정하고 있는데 주석된 내용처럼 새로운 user를 할당할 수는 없다. 다만 전달된 user의 비밀번호를 암호화 해서 저장하는 일은 가능하다.
단위 테스트
@Test
public void registTest(){
User user = new User("jang", "1234");
User returned = uservice.regist(user);
Assertions.assertEquals(user, returned);
}
로그를 확인하면 전달된 객체의 pass는 1234 지만 저장된 User의 pass는 암호화_1234임을 확인할 수 있다.
15:38:04 [DEBUG] c.d.a.aspect.UserServiceAspect.before.16 :
signature: User com.doding.aop.beans.UserService.regist(User),
args: User(id=jang, pass=1234)
15:38:04 [DEBUG] c.doding.aop.beans.UserService.regist.16 :
저장완료: User(id=jang, pass=암호화_1234)
@AfterReturning
동작과 annotation
@AfterReturning은 타겟 메서드가 정상적으로 종료(return)된 경우에 동작한다. 즉 target에서 예외가 발생하는 경우는 동작하지 않는다. 따라서 try~catch~finally에서 try 정도에 해당한다고 볼 수 있다.
@AfterReturning에서는 리턴 받은 값을 수정할 수 있는데 @Before에서의 파리미터와 마찬가지로 전달받은 객체의 내용을 수정할 수 있으나 완전히 새로운 값을 할당할 수는 없다.
@AfterReturning은 속성으로 pointcut을 받는 value와 return 값을 받을 변수 명을 의미하는 returning 속성을 갖는다. advice 작성 시는 returning을 메서드의 파라미터로 선언해주어야 한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterReturning {
String value() default ""; // pointcut
String returning() default ""; // target이 리턴하는 값을 할당받을 변수 명
}
advice 작성 및 테스트
User 객체가 반환될 때 User의 비밀번호가 마스킹되도록 @AfterReturning에서 처리해보자.
advice 작성
@AfterReturning(value = "userPointcut()", returning = "user")
public void maskingUserPassword(JoinPoint jp, User user){
log.debug("signature: {}, user: {}", jp.getSignature(), user);
user.setPass("*"); // return 값 조작!
}
단위테스트
@Test
public void BeforeTest(){
User user = new User("jang", "1234");
User returned = service.regist(user);
Assertions.assertEquals(user, returned);
Assertions.assertEquals("*", returned.getPass());
}
로그를 살펴보면 maskingUserPassword에 전달된 User 의 pass는 1234암호화됨이었지만 테스트에서는 *와 같음을 알 수 있다.
17:29:34 [DEBUG] c.d.aop.aspect.MyServiceAcpect.encryptUserPassword.18 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=jang, pass=1234)
17:29:34 [DEBUG] c.doding.aop.beans.MyService.regist.21 :
사용자 저장 성공: User(id=jang, pass=1234암호화됨)
17:29:34 [DEBUG] c.d.aop.aspect.MyServiceAcpect.maskingUserPassword.27 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=jang, pass=1234암호화됨)
@AfterThrowing
동작과 annotation
@AfterThrowing은 애너테이션 의미대로 타겟의 메서드가 예외를 던졌을 때에만 동작하며 정상적으로 값을 반환했을 때에는 동작하지 않는다. try~catch~finally에서 catch 정도에 해당한다고 볼 수 있다.
@AfterThrowing은 속성으로 pointcut을 받는 value와 전달받은 예외 객체의 이름을 의미하는 throwing 속성을 갖는다. 이 애너테이션과 연결되는 메서드는 동일한 이름의 파라미터가 선언되어야 하며 파라미터의 타입은 타겟에서 전달되는 예외와 같거나 부모 타입으로 선언되어야 한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterThrowing {
String value() default "";
String throwing() default "";
}
advice 작성 및 테스트
중복된 아이디의 User가 등록될 때 예외를 모니터링 하도록 @AfterThrowing을 이용하는 aspect를 작성해 보자.
advice 작성
@AfterThrowing(value = "userPointcut()", throwing = "e")
public void sendEmailWhenException(JoinPoint jp, RuntimeException e){
log.debug("signature: {}, exception: {}", jp.getSignature(), e);
log.debug("오류 발생 담당자에 이메일 통보!");
}
단위테스트
@Test
public void afterThrowingTest(){
User user = new User("jang", "1234");
service.regist(user);
User user2 = new User("hong", "1234");
Assertions.assertThrows(RuntimeException.class,() -> service.regist(user2));
}
로그를 살펴보면 첫번째 사용자인 jang을 등록할 때는 @AfterThrowing은 동작하지 않는다. 하지만 두번째 사용자인 hong을 등록할 때는 RuntimeException이 발생하고 중요한 것은 담당자에게 이메일로 통보가 되었다는 점이다.
18:01:50 [DEBUG] c.d.aop.aspect.MyServiceAcpect.encryptUserPassword.15 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=jang, pass=1234)
18:01:50 [DEBUG] c.doding.aop.beans.MyService.regist.23 :
사용자 저장 성공: User(id=jang, pass=1234암호화됨)
18:01:50 [DEBUG] c.d.aop.aspect.MyServiceAcpect.maskingUserPassword.24 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=jang, pass=1234암호화됨)
18:01:50 [DEBUG] c.d.aop.aspect.MyServiceAcpect.encryptUserPassword.15 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=hong, pass=1234)
18:01:50 [DEBUG] c.d.aop.aspect.MyServiceAcpect.notifyByEmailOnException.30 :
signature: User com.doding.aop.beans.MyService.regist(User),
exception: hong는 이미 존재합니다.
18:01:50 [DEBUG] c.d.aop.aspect.MyServiceAcpect.notifyByEmailOnException.31 :
이메일 발송: 당장 복귀하세요! hong는 이미 존재합니다.
@After
동작과 annotation
try~catch~finally의 finally와 유사한 개념으로 타겟의 예외 상황과 상관없이 언제나 실행된다.
@After는 @Before의 반대 상황으로 pointcut을 받는 value 속성을 갖는다.
@Target(ElementType.METHOD)
public @interface After {
String value();
}
advice 작성 및 테스트
메서드의 동작이 끝났음을 나타내는 로그를 출력하도록 @After를 이용한 aspect를 작성해보자.
advice 작성
@After("bean(myService)")
public void endlogging(JoinPoint jp){
log.debug("method over: {}", jp.getSignature());
}
단위테스트
@Test
public void afterThrowingTest(){
User user = new User("jang", "1234");
service.regist(user);
User user2 = new User("hong", "1234");
Assertions.assertThrows(RuntimeException.class,() -> service.regist(user2));
}
로그를 살펴보면 사용자 저장이 성공해서 정상적으로 return 했을 때는 물론 예외가 발생했을 때에도 로그가 잘 출력된 것을 확인할 수 있다.
18:12:10 [DEBUG] c.d.aop.aspect.MyServiceAcpect.encryptUserPassword.15 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=jang, pass=1234)
18:12:10 [DEBUG] c.doding.aop.beans.MyService.regist.24 :
사용자 저장 성공: User(id=jang, pass=1234암호화됨)
18:12:10 [DEBUG] c.d.aop.aspect.MyServiceAcpect.maskingUserPassword.24 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=jang, pass=1234암호화됨)
18:12:10 [DEBUG] c.d.aop.aspect.MyServiceAcpect.endlogging.36 :
method over: User com.doding.aop.beans.MyService.regist(User)
18:12:10 [DEBUG] c.d.aop.aspect.MyServiceAcpect.encryptUserPassword.15 :
signature: User com.doding.aop.beans.MyService.regist(User),
user: User(id=hong, pass=1234)
18:12:10 [DEBUG] c.d.aop.aspect.MyServiceAcpect.notifyByEmailOnException.30 :
signature: User com.doding.aop.beans.MyService.regist(User),
exception: hong는 이미 존재합니다.
18:12:10 [DEBUG] c.d.aop.aspect.MyServiceAcpect.notifyByEmailOnException.31 :
이메일 발송: 당장 복귀하세요! hong는 이미 존재합니다.
18:12:10 [DEBUG] c.d.aop.aspect.MyServiceAcpect.endlogging.36 :
method over: User com.doding.aop.beans.MyService.regist(User)
@Around
동작과 annotation
@Around는 다른 advice들과 달리 타겟 메서드의 호출을 advice 내부에서 직접 수행한다. 따라서 이전 advice들과 달리 파라미터, 리턴 값에 대한 완전한 대체 및 예외 처리가 가능하다.
@Around 역시 파라미터로 pointcut을 받는 value 속성만 사용하면 되는 간단한 애너테이션이다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {
String value();
}
다음은 @Around의 활용 예이다.
@Around("pointcut 작성")
public Integer modifyAdd(ProceedingJoinPoint pjp) throws Throwable { // 예외 전파
// target method 호출
Object[] args = pjp.getArgs();
args[0] = (Integer) args[0] * 10; // 1. 파라미터 완전 대체 가능
Integer result = (Integer) pjp.proceed(args); // 2. 타겟의 메서드 호출
return result % 2 == 0 ? result : result / 2; // 3. 결과 조작 가능
}
- @Around advice 내에서 target 메서드를 호출하기 위해서 이제까지와 달리 JoinPoint를 상속받은 ProceedingJoinPoint를 파라미터로 갖는다.
- advice 메서드의 리턴 타입은 타겟 메서드의 리턴 타입과 같아야 한다.
- PorceedingJoinPoint는 JoinPoint를 상속받았기 때문에 getArgs()를 통해 타겟에 전달되는 파라미터들에 접근, 파라미터 조작이 가능하다. 즉 @Before에서는 파라미터 자체를 변경할 수는 없었지만 @Around에서는 문제 없다.
- target에서 전파한 예외는 proceed()가 받아서 throws 한다. 따라서 try~catch로 처리하거나 다시 throws 해줘야 하는데 호출자에게까지 예외를 전달해주기 위해서는 당연히 throws 해줘야 한다.
- 호출 결과를 내부적으로 리턴해주기 때문에 필요에 따라서는 결과의 조작도 가능하다.
advice 작성 및 테스트
factorial 값을 캐싱해서 반환할 수 있도록 @Around를 활용해보자.
advice 작성
// factorial 값을 저장할 cache 생성
private final Map<Integer, Long> cache = new HashMap<>();
@Around("execution(long com.doding..MyService.getFactorial(int)) && args(n)")
public long getFactorialUseCache(ProceedingJoinPoint pjp, int n) throws Throwable{
// n에 대한 factorial 값이 구해졌나?
if(cache.containsKey(n)){
log.debug("cache 활용, target 호출 안함");
return cache.get(n);
}
// 없었다면 factorial 값 구하기 - target 메서드 호출
long result = (long)pjp.proceed(new Object[]{n});
// 결과값 caching
cache.put(n, result);
return result;
}
단위테스트
@Test
public void aroundTest(){
long result = service.getFactorial(10);
long result2 = service.getFactorial(10);
Assertions.assertEquals(result,result2);
}
테스트에서는 실제 타겟의 메서드를 2번 호출했지만 로그를 살펴보면 타겟은 1번만 호출되었다는 것을 확인할 수 있다.
18:50:55 [DEBUG] c.doding.aop.beans.MyService.getFactorial.35 :
factorial 요청: 10
18:50:55 [DEBUG] c.d.aop.aspect.MyServiceAcpect.endlogging.40 :
method over: long com.doding.aop.beans.MyService.getFactorial(int)
18:50:55 [DEBUG] c.d.aop.aspect.MyServiceAcpect.getFactorialUseCache.50 :
cache 활용, target 호출 안함
참~쉬운 스프링 Part 1. 스프링 프레임워크 이해 및 DI,AOP 강의 | 모두의 코딩:두딩 - 인프런
모두의 코딩:두딩 | 스프링 프레임워크의 핵심을 파고드는 강의로, DI와 AOP를 비롯한 스프링의 중심 개념을 마스터하게 됩니다. 환경 설정부터 단위테스트, 로깅, 메이븐 사용법까지, 스프링 프
www.inflearn.com
'Spring Core > 03. AOP' 카테고리의 다른 글
05. Spring 내부의 AOP들 (0) | 2021.10.29 |
---|---|
03. pointcut 작성 (0) | 2020.06.20 |
02. Aspect 작성과 동작 (0) | 2020.06.18 |
01. AOP 기본 컨셉 (0) | 2020.06.18 |
소중한 공감 감사합니다