Spring Core/03. AOP

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을 받는 valuereturn 값을 받을 변수 명을 의미하는 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. 결과 조작 가능
}
  1. @Around advice 내에서 target 메서드를 호출하기 위해서 이제까지와 달리 JoinPoint를 상속받은 ProceedingJoinPoint를 파라미터로 갖는다. 
  2. advice 메서드의 리턴 타입은 타겟 메서드의 리턴 타입과 같아야 한다.
  3.  PorceedingJoinPoint는 JoinPoint를 상속받았기 때문에 getArgs()를 통해 타겟에 전달되는 파라미터들에 접근, 파라미터 조작이 가능하다.  즉 @Before에서는 파라미터 자체를 변경할 수는 없었지만 @Around에서는 문제 없다.
  4. target에서 전파한 예외는 proceed()가 받아서 throws 한다. 따라서 try~catch로 처리하거나 다시 throws 해줘야 하는데 호출자에게까지 예외를 전달해주기 위해서는 당연히 throws 해줘야 한다.
  5. 호출 결과를 내부적으로 리턴해주기 때문에 필요에 따라서는 결과의 조작도 가능하다.

 

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 호출 안함

 

https://inf.run/uTES7

 

참~쉬운 스프링 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
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.