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 실행

 

이번 포스트에서 사용할 빈은 아래와 같다.

package com.doding.aoptest.advicetype;

import java.util.List;

import org.springframework.stereotype.Component;

import lombok.Getter;

@Component
@Getter
public class AdviceTypeBean {

  private String name;
  private List<String> skills;

  public void setData(String name, List<String> skills) {
    this.name = name;
    this.skills = skills;
  }

  public void divideBy(int divider) {
    int i = 100;
    System.out.printf("%d/%d=%d%n", i, divider, i / divider);
  }

  public int add(int a, int b) {
    return a + b;
  }

  public String toString() {
    return "name: " + name + ", skils: " + skills;
  }
}

 

@Before

 

동작과 annotation

@Before가 선언된 advice는 target 메서드가 실행되기 전에 호출되며 advice에서 예외가 발생하면 아예 target이 호출되지 못한다.

@Before는 이제까지 써온 녀석이라 익숙하다. 애너테이션의 파라미터로는 value 속성에 point cut을 적는다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Before {
  String value(); // point cut
}

 

advice 작성

advice 메서드에는 JoinPoint 타입의 파라미터를 선언할 수 있는데 이는 타겟 메서드의 signature, argument 등에 대한 다양한 정보를 주는 객체이다. 그 중 getArgs()는 전달되는 파라미터를 Object[] 형태로 확인할 수 있다. 이 배열에 담긴 객체의 내용은 변경할 수 있지만 완전히 새로운 객체를 할당할 수는 없다.

다음의 advice를 작성해보자.

@Before("execution(void *..AdviceTypeBean.setData(String, java.util.List))")
public void beforeAdvice(JoinPoint jp) {
  log.debug("@Before signature:{}", jp.getSignature());    // 전달되는 파라미터 확인
  Object[] args = jp.getArgs();
  args[0] = "new Value"; // 완전히 새로운 객체 할당
  List<String> skils = (List) args[1];
  skils.set(0, "spring"); // 객체의 내용 변경
}

advice 내에서는 전달된 파라미터를 수정하고 있는데 첫 번째는 아예 "new Value"라는 새로운 객체를 할당하고 두 번째는 0번째 요소를 spring으로 변경하고 있다. 이제 target이 호출될 때 메서드에 실제 전달된 값은 어떻게 될까?

 

단위 테스트

다음의 테스트로 결과를 확인해 보자.

@Autowired
AdviceTypeBean aBean;

@Test
@DisplayName("@Before 테스트: 첫 번째 인자는 EJB였지만 spring으로 변경됨")
public void beforeTest1() {
  // 처음 값은 "org name"과 ["EJB", "JSP"]을 넘겨준다.
  String name = "org name";
    aBean.setData(name, Arrays.asList("EJB", "JSP"));
    // 실제로 세팅된 값은?
    log.debug("aBean: {}", aBean.toString()); //aBean: name: org name, skils: [spring, JSP]
    assertEquals(aBean.getName(), name);
    assertEquals(aBean.getSkills().get(0), "spring");
  }

 

빈을 주입받고 setData를 호출하면서 String과 List<String>을 넘겨주었고 advice가 정상적으로 동작한다. 이때 넘겨주는 데이터는 'org name'과 ["EJB","JSP"]라는 값인데 실제 객체에 전달된 값은 name은 여전히 org name 이지만 skills의 경우 ["spring","JSP"]로 수정되었음을 알 수 있다.

 

@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 작성

다음의 advice를 작성해 보자.

// 반환된 리턴값이 String이며 name에 할당된다.
@AfterReturning(value = "execution(* *..AdviceTypeBean.getName())", returning = "name")
public void afterReturningName(JoinPoint jp, String name) {
  log.debug("@AfterReturning signature: {}, target 반환값: {}", jp.getSignature(), name);
  // 반환값을 바꿔보지만 반영되지는 않는다.
  name = "수정된 이름";
}

위 advice에서는 String name이라는 파라미터로 target 메서드의 리턴 값을 받아오고 있다. advice 내부에서는 반환된 name을 "수정된 이름"으로 변경해 보지만 값 자체를 변경하는 것이므로 반영되지는 않는다.

@AfterReturning(value = "execution(* *..AdviceTypeBean.getSkills())", returning = "skills")
public void afterReturningNow(JoinPoint jp, List<String> skills) {
  log.debug("@AfterReturning signature: {}, target 반환값: {}", jp.getSignature(), skills);
  // 반환 객체의 내용 수정 - 반영된다.
  skills.set(1, "springboot");
}

첫 번째 advice와 유사하게 getSkills()에 연결되며 반환 값은 skills에 할당된다. 반환된 List의 1번째 요소를 "springboot"로 변경한다.

 

단위테스트

다음 테스트로 결과를 확인해 보자.

 @Test
 @DisplayName("리스트의 1번째 요소로 JSP를 담았고 리턴 되었지만 springboot로 변경됨")
 public void afterReturningTest() {
   String name = "org name";
   List<String> skills = Arrays.asList("EJB", "JSP");
   aBean.setData(name, skills);
   assertEquals(aBean.getName(), name);
   assertEquals(aBean.getSkills().get(1), "springboot");
 }

getName()의 결과는 원래의 org name이 반환되며 getList의 결과는 ["spring","JSP"]가 반환 되었지만 advice에 의해 ["spring","springboot"]로 변경된 것을 확인할 수 있다.

14:36:46 [DEBUG] c.d.aoptest.aspects.JoinPointAspect.afterReturningName.45 
 - @AfterReturning signature: String com.doding.aoptest.joinpoint.AdviceTypeBean.getName(), 
                   target 반환값: org name
14:36:46 [DEBUG] c.d.aoptest.aspects.JoinPointAspect.afterReturningNow.52 
 - @AfterReturning signature: List com.doding.aoptest.joinpoint.AdviceTypeBean.getSkills(), 
                   target 반환값: [spring, JSP]

 

@AfterThrowing

 

동작과 annotation

@AfterThrowing은 애너테이션 의미대로 타겟의 메서드가 예외를 던졌을 때에만 동작하며 정상적으로 값을 반환했을 때에는 동작하지 않는다. try~catch~finally에서 catch 정도에 해당한다고 볼 수 있다. @AfterThrowing advice에는 예외 객체가 throws로 전파되는 것이 아니라 파라미터로 전달되기 때문에 advide에서 예외 처리를 한다고 해서 예외가 없어지지 않는다.

@AfterThrowing은 속성으로 pointcut을 받는 value와 전달받은 예외 객체의 이름을 의미하는 throwing 속성을 갖는다. 이 애너테이션과 연결되는 메서드는 동일한 이름의 파라미터가 선언되어야 하며 파라미터의 타입은 타겟에서 전달되는 예외와 같거나 부모 타입으로 선언되어야 한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterThrowing {

  String value() default "";
  String throwing() default "";
}

 

advice 작성

@AfterThrowing을 이용하는 advice를 작성해 보자.

@AfterThrowing(value = "execution(* *..AdviceTypeBean.divideBy(int))", throwing = "ex")
public void afterThrowingAdvice(JoinPoint jp, RuntimeException ex) {
  log.debug("@AfterThrowing signature: {}, ex: {}", jp.getSignature(), ex.getClass().getName());
  log.debug("예외 내용: {}", ex.getMessage());
  log.debug("조치 내용: 담당자 이메일로 내용 전송");
}

타겟 메서드가 던진 예외는 ex 변수에 할당되며 예외의 종류는 RuntimeException의 하위 타입이어야 한다. 예외를 받으면 담당자에게 이메일을 보내는 등의 처리가 가능할 것이다.

 

단위테스트

단위테스트를 통해 @AfterThrowing의 동작을 살펴보자.  JoinPointTestBean의 divideBy는 전달된 파라미터로 나눗셈을 실행한다.

@Test
public void afterThrowingTest() {
  aBean.divideBy(10);
  assertThrows(RuntimeException.class, () -> aBean.divideBy(0));
}

 

타겟 메서드인 divideBy를 호출할 때 문제없는 10을 넘겨주는 경우는 advice가 동작하지 않는다. 하지만 0을 넘겨주면 예외가 발생하고 @AfterThrowing advide가 동작한다.

100/10=10
16:16:14 [DEBUG] c.d.aoptest.aspects.JoinPointAspect.afterThrowingAdvice.75 
- @AfterThrowing signature: void com.doding.aoptest.joinpoint.AdviceTypeBean.divideBy(int), 
    ex: java.lang.ArithmeticException
16:16:14 [DEBUG] c.d.aoptest.aspects.JoinPointAspect.afterThrowingAdvice.76 
- 예외 내용: / by zero
16:16:14 [DEBUG] c.d.aoptest.aspects.JoinPointAspect.afterThrowingAdvice.77 
- 조치 내용: 담당자 이메일로 내용 전송

 

@After

 

동작과 annotation

try~catch~finally의 finally와 유사한 개념으로 타겟의 예외 상황과 상관없이 언제나 실행된다.

 

@After는 @Before의 반대 상황으로 pointcut을 받는 value 속성을 갖는다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface After {
  String value();
}

 

동작 시점을 제외하고는 추가로 살펴볼 내용은 없으므로 advice나 단위테스트는 생략한다.

 

@Around

 

동작과 annotation

@Around는 다른 advice들과 달리 타겟 메서드의 호출을 advice 내부에서 직접 수행한다. 따라서 이전 advice들과 달리 파라미터, 리턴 값에 대한 완전한 대체 및 예외 처리가 가능하다.

 

@Around 역시 파라미터로 pointcut을 받는 value 속성만 사용하면 되는 간단한 애너테이션이다. 

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Around {
  String value();
}

 

advice 작성

@Around advice 내에서 다른 메서드를 호출하기 위해서 이제까지와 달리 JoinPoint를 상속받은 ProceedingJoinPoint를 파라미터로 갖는다. 

@Around("execution(int *..AdviceTypeBean.add(int, int))")
public Integer modifyAdd(ProceedingJoinPoint pjp) throws Throwable {
  long start = System.nanoTime();
  Object[] args = pjp.getArgs();
  args[0] = (Integer) args[0] * 10;             // 1. 파라미터 완전 대체 가능
  Integer result = (Integer) pjp.proceed(args); // 2. 타겟의 메서드 호출
  log.debug("소요 시간: {}, 결과: {}", System.nanoTime() - start, result);
  return result % 2 == 0 ? result : result / 2; // 3. 결과 조작 가능
}

advice 메서드의 리턴 타입은 타겟 메서드의 리턴 타입과 같아야 한다. 타겟의 메서드를 호출하는 proceed()는 Throwable을 던질 수 있다. 따라서 try~catch로 처리하거나 throws 해줘야 하는데 호출자에게까지 예외를 전달해주기 위해서는 당연히 throws 해줘야 한다.

  PorceedingJoinPoint는 JoinPoint를 상속받았기 때문에 getArgs()를 통해 타겟에 전달되는 파라미터들에 접근, 파라미터 조작이 가능하다.  즉 @Before에서는 파라미터 자체를 변경할 수는 없었지만 @Around에서는 문제 없다.

proceed 메서드를 통해 실제 타겟 메서드를 호출하고 결과를 직접 받아서 활용이 가능하다. 전달받은 결과를 다시 호출자에게 반환하므로 필요에 따라 값을 변경할 수도 있다.

 

단위테스트

단위 테스트로 위 동작을 테스트해 보자.

@Test
@DisplayName("전달한 값의 파라미터 및 결과 자체의 조작 가능")
public void aroundAddTest() {
  int a = 10;
  int b = 3;
  int temp = a * 10 + b;
  int result = aBean.add(a, b);

  assertEquals(result, temp % 2 == 0 ? temp : temp / 2);
}

target의 메서드는 단순히 전달받은 두 개의 숫자를 더해서 결과를 반환하는데 테스트 결과를 살펴보면 10과 3을 전달했을 때 13이 아니라 51임을 알 수 있다.

14:44:36 [DEBUG] c.d.aoptest.aspects.JoinPointAspect.modifyAdd.91 
              - 소요 시간: 72300, 타겟 리턴: 103

 

반응형

'Spring Core > 03. AOP' 카테고리의 다른 글

06. AOP 작성 주의 사항  (0) 2024.02.16
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

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

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