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을 받는 value와 return 값을 받을 변수 명을 의미하는 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 |
소중한 공감 감사합니다