package com.example.aop.controller;
import com.example.aop.annotation.Decode;
import com.example.aop.annotation.Timer;
import com.example.aop.dto.User;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api")
public class RestApiController {
@GetMapping("/get/{id}")
public String get(@PathVariable Long id, @RequestParam String name) {
return id + " " + name;
}
@PostMapping("/post")
public User post(@RequestBody User user) {
return user;
}
@Timer
@DeleteMapping("/delete")
public void delete() throws InterruptedException {
Thread.sleep(2 * 1000);
}
@Decode
@PutMapping("/put")
public User put(@RequestBody User user) {
return user;
}
}
간단한 REST API 컨트롤러 클래스이다.
package com.example.aop.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect // AOP에 쓰일 클래스 명명
@Component // Bean으로 등록
public class ParameterAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
// method 이름은 달라도 괜찮다. 중요한 건 어노테이션!
// 어떤 패키지에 어떤 파일에 어떤 메소드를 적용할 지
private void pointCut() {}
@Before("pointCut()")
public void before(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println(method.getName());
Object[] args = joinPoint.getArgs();
for(Object obj : args) {
System.out.println("type : " + obj.getClass().getSimpleName());
System.out.println("value : " + obj);
}
}
@AfterReturning(value = "pointCut()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
System.out.println("returnObj: " + returnObj);
}
}
@PointCut 어노테이션에 지정된 파일(위 RestApiController Class를 의미)의 모든 메서드의 실행 전, 후로 호출된 메서드의 정보와 인풋, 아웃풋 데이터를 로깅 하는 예제이다.
package com.example.aop.aop;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@Aspect
@Component
public class TimerAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
// method 이름은 달라도 괜찮다. 중요한 건 어노테이션!
// 어떤 패키지에 어떤 파일에 어떤 메소드를 적용할 지
private void pointCut() {}
@Pointcut("@annotation(com.example.aop.annotation.Timer)")
private void enableTimer() {}
@Around("pointCut() && enableTimer()")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
StopWatch stopWatch = new StopWatch();
// 메서드 실행 전
stopWatch.start();
Object result = proceedingJoinPoint.proceed(); // 메서드 실행 지점
// 메서드 실행 후
stopWatch.stop();
System.out.println("spent time: " + stopWatch.getTotalTimeSeconds());
}
}
AOP를 적용할 첫 번째 포인트 컷 조건 외에 추가적으로 @Timer 어노테이션이 적용된 메서드에만 AOP를 적용할 조건도 포인트 컷으로 추가했다. around 메서드는 메서드 실행 전, 후로 메서드 실행 시간을 측정한다.
package com.example.aop.aop;
import com.example.aop.dto.User;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
@Aspect
@Component
public class DecodeAop {
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
// method 이름은 달라도 괜찮다. 중요한 건 어노테이션!
// 어떤 패키지에 어떤 파일에 어떤 메소드를 적용할 지
private void pointCut() {}
@Pointcut("@annotation(com.example.aop.annotation.Decode)")
private void enableDecode() {}
@Before("pointCut() && enableDecode()")
public void before(JoinPoint joinPoint) throws UnsupportedEncodingException {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof User) {
User user = User.class.cast(arg);
String base64Email = user.getEmail();
String decodedEmail = new String(Base64.getDecoder().decode(base64Email), "UTF-8");
user.setEmail(decodedEmail);
}
}
}
@AfterReturning(value = "pointCut() && enableDecode()", returning = "returnObj")
public void afterReturn(JoinPoint joinPoint, Object returnObj) {
if (returnObj instanceof User) {
User user = User.class.cast(returnObj);
String email = user.getEmail();
String encodedEmail = Base64.getEncoder().encodeToString(email.getBytes());
user.setEmail(encodedEmail);
}
}
}
마찬가지로 @Decode 어노테이션이 지정된 메서드에 AOP를 적용하기 위해 포인트 컷을 추가했다. 해당 메서드의 실행 전의 인풋 데이터가 User의 인스턴스면 이메일 속성을 디코딩 된 결과로 저장하고 마찬가지로 실행 후의 리턴 값이 User의 인스턴스면 이메일 속성을 Base64로 인코딩된 결과를 저장하도록 하는 예제이다.
AOP 정리
1. 목적은 부가적인 기능을 비즈니스 로직에서 분리하는 것!
2. Spring AOP는 스프링 빈으로 등록된 객체만 사용할 수 있다.
3. @PointCut을 통해 AOP가 적용될 범위를 조정할 수 있다.