Spring을 공부하면서 AOP가 무엇인지 간단하게 알아봤습니다
구글에 AOP를 검색하면 이런 이미지를 볼 수 있습니다
이미지를 봐도 알 수 있듯이 클래스마다 비슷한 부분들이 있고 그 비슷한 부분들을 묶는 개념이라는 걸 알 수 있죠. 다수의 클래스에 같은 기능을 횡단으로 적용하는 것입니다.
무슨 말인지 잘 모르겠으니 바로 예제를 살펴보겠습니다
먼저, 간단한 Controller를 만들겠습니다
package com.zziri.test.controller;
// import ...
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/aop")
public String aop(@RequestParam String param) {
return param;
}
}
aop() 메서드를 호출하려면 이런 식으로 GET 요청을 하면 되겠죠
http://localhost:8080/api/aop?param=test
이제, AOP 기능을 구현할 클래스를 만들겠습니다
@Aspect
@Component
public class ParameterAop {
@Pointcut("execution(* com.zziri.test.controller..*.*(..))")
private void cut() {}
@Before("cut()")
public void before(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
System.out.println(String.format("method = %s", methodSignature.getMethod()));
Arrays.stream(joinPoint.getArgs()).forEach(arg -> {
System.out.println(String.format("arg = %s", arg.toString()));
});
}
@AfterReturning(value = "cut()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
System.out.println(String.format("returnObj = %s", returnObj));
}
}
특정 패키지의 모든 메서드에 횡단으로 적용되는 AOP 입니다. 메서드 호출 전 메소드 이름과 arguments를 출력하고 반환 후에 returnObj를 출력하는 간단한 로직입니다.
코드를 간단하게 살펴보겠습니다
@Component
-> 빈에 등록해서 AOP가 동작하도록 합니다
@Pointcut("execution(* com.zziri.test.controller..*.*(..))")
-> com.zziri.test.controller 패키지의 파라미터가 0개 이상인 모든 메서드를 대상으로 합니다
@Before("cut()")
-> cut() Pointcut 이전에 호출하는 메서드를 지정합니다
@AfterReturning(value = "cut()", returning = "returnObj")
-> cut() Pointcut이 적용된 메서드 호출 이후에 실행합니다
패키지를 기준으로 모든 메서드를 대상으로 AOP 적용을 했는데요, 어노테이션 기반으로 AOP를 적용하도록 클래스를 하나 더 만들겠습니다
그 전에 어노테이션을 먼저 만들어야겠습니다
package com.zziri.test.annotation;
// import ...
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}
@Aspect
@Component
public class TimerAop {
@Pointcut("@annotation(com.zziri.test.annotation.Timer)")
private void enableTimer() {
}
@Around("enableTimer()")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
proceedingJoinPoint.proceed();
stopWatch.stop();
System.out.println(String.format("total time : %s", stopWatch.getTotalTimeSeconds()));
}
}
@Timer annotation이 적용된 모든 메서드에 횡단으로 적용되는 AOP 입니다. 메소드 실행 시간을 측정해서 출력하는 기능입니다.
코드를 살펴보겠습니다
@Pointcut("@annotation(com.zziri.test.annotation.Timer)")
-> com.zziri.test.annotation.Timer 어노테이션이 적용된 메서드를 대상으로 합니다
@Around("enableTimer()")
-> enableTimer() Pointcut이 적용된 메서드를 @Around가 적용된 메서드 안에서 호출할 수 있도록 합니다. 주로 메서드 호출 전 후 동작을 하나의 메서드에 구현할 필요가 있을 때 사용합니다.
구현한 기능은 aop() 메서드 호출 전 어떤 메서드인지 출력하고 aop() 메서드의 arguments를 출력한 다음에 메서드 실행 시간을 출력하고 returnObj 출력 후에 끝날겁니다
이제 실행을 해봐야겠죠.... 그런데
method = public java.lang.String com.zziri.test.controller.ApiController.aop(java.lang.String)
arg = test
total time : 0.0203523
returnObj = null
returnObj가 null이 나오는건 왜일까요
aop() 메서드는 분명 Request parameter로 받은 param을 return 했을 겁니다. 그런데 @Around로 감싸져서 return value가 @AfterReturning이 적용된 메서드에 제대로 전달되지 않아서 생긴 문제죠.
그래서 코드를 수정했습니다
@Aspect
@Component
public class TimerAop {
@Pointcut("@annotation(com.zziri.test.annotation.Timer)")
private void enableTimer() {}
@Around("enableTimer()")
// Object return
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// Get return value
Object ret = proceedingJoinPoint.proceed();
stopWatch.stop();
System.out.println(String.format("total time : %s", stopWatch.getTotalTimeSeconds()));
// return
return ret;
}
}
method = public java.lang.String com.zziri.test.controller.ApiController.aop(java.lang.String)
arg = test
total time : 0.0171948
returnObj = test
제대로 출력되네요!
AOP를 알아보고 @Around 와 @AfterReturning을 함께 사용할 때 주의할 점도 알 수 있게 되었습니다
'소프트웨어' 카테고리의 다른 글
정적 팩터리 메서드는 생성자보다 좋을까? (0) | 2021.09.15 |
---|---|
Spring - 중복 타입으로 자동주입 (0) | 2021.09.04 |
406 Not Acceptable HttpMediaTypeNotAcceptableException (1) | 2021.08.21 |
Checked vs Unchecked Exception (0) | 2021.08.10 |
Task - MySQL SSL Error (com.mysql.cj.jdbc.exceptions.CommunicationsException) (0) | 2021.06.03 |