본문 바로가기

Back-End/Spring Boot

AOP

AOP(Aspect Oriented Programming): 관점지향 프로그래밍

 

여러가지 서비스들에 횡단 관심이 있을 때 사용이 가능한 프로그래밍 기법

주요 Annotation

Annotation 의미
@Aspect 자바에서 널리 사용하는 AOP 프레임워크에 포함되며, AOP를 정의하는 Class에 할당
@Pointcut 기능을 어디에 적용시킬지, 메소드? Annotation? 등 AOP를 적용 시킬 지점을 설정
@Before 메소드 실행하기 이전
@After 메소드가 성공적으로 실행 후, 예외가 발생 되더라도 실행
@AfterReturning 메소드 호출 성공 실행 시(Not Throws)
@AfterThrowing 메소드 호출 실패 예외 발생(Throws)
@Around @Befor/ @After 모두 제어

 


사용 예제

출력을 하는 부분이 공통적으로 들어갈 때,  AOP를 사용해서 일괄적으로 처리할 수 있다.

@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;
  }
}

위와 같은 컨트롤러가 있을 때 

실행 메서드, 입력 타입, 입력 값, 리턴 값에 대하여 출력하고 싶을 때

@Aspect
@Component
public class ParameterAop {

  // Pointcut 문법 알고갈것
  @Pointcut("execution(* com.example.springdiioc.controller..*.*(..))")
  private void cut() {

  }

  @Before("cut()")
  public void before(JoinPoint joinPoint){
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    Method method = methodSignature.getMethod();
    // 실행 메서드
    System.out.println("method.getName() = " + method.getName());
    Object[] args = joinPoint.getArgs();
    for(Object obj : args){
      // 입력 타입
      System.out.println("type : " + obj.getClass().getSimpleName());
      // 입력 값
      System.out.println("value : " + obj);
    }
  }

  @AfterReturning(value = "cut()", returning = "returnObj" /* 리턴 변수명과 일치*/) 
  public void afterReturn(JoinPoint joinPoint, Object returnObj) {
    System.out.println("return obj");
    // 리턴 값
    System.out.println("returnObj = " + returnObj);
  }

}

위와 같이 코드를 작성해서 컨트롤러에서 실행하는 모든 EndPoint에 대하여 AOP를 적용시킬 수 있다.

 

Custom Annotation 적용

이러한 방식 말고도 Custom Annotation을 제작해서 해당 Annotation이 붙은 곳에만 AOP를 적용시킬 수도 있다.

우선 Annotation을 만들고

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Timer {
}

 

이 Annotation에 대한 내용을 작성해준다.

@Aspect
@Component
public class TimerAop {
  @Pointcut("execution(* com.example.springdiioc.controller..*.*(..))")
  private void cut(){}
  // Timer Annotation에 대한 Pointcut
  @Pointcut("@annotation(com.example.springdiioc.controller.annotation.Timer)")
  private void enableTimer(){}

  @Around("cut() && enableTimer()")
  public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    StopWatch stopWatch = new StopWatch();
    // 시간 측정 시작
    stopWatch.start();
    
    // 실질적 메서드 실행
    Object reuslt = joinPoint.proceed();
    
    // 시간 측정 종료
    stopWatch.stop();
    
    // 시간 
    System.out.println("total time = " +  stopWatch.getTotalTimeSeconds());
  }
}

이렇게 하면 Controller 밑에 @Timer를 붙인 메서드에 대한 실행 시간을 구할 수 있다.

 

AOP로 값 변환

이러한 것들 말고도 들어온 값을 디코딩 하여 다른 로직으로 전달하고, 반환 할 때에는 다시 디코딩 하여 리턴을 해줄 수도 있다.

 

 우선 Decode Annotation을 만든다.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decode {
}

 

그리고 이를 사용하는 DecodeAop를 만든다.

@Aspect
@Component
public class DecodeAop {
  @Pointcut("execution(* com.example.springdiioc.controller..*.*(..))")
  private void cut(){}

  @Pointcut("@annotation(com.example.springdiioc.controller.annotation.Decode)")
  private void enableDecode(){}

  @Before("cut() && enableDecode()")
  public void before(JoinPoint joinPoint){
    Arrays.stream(joinPoint.getArgs()).forEach(arg -> {
      if(arg instanceof User user){ // 들어온 값이 User타입 일 때
        String base64Email = user.getEmail(); // 이메일 가져오고
        // 가져온 이메일을 base64로 디코딩
        String email = new String(Base64.getDecoder().decode(base64Email), StandardCharsets.UTF_8);
        // 디코딩 한 값으로 set
        user.setEmail(email);
      }
    });
  }

  @AfterReturning(value = "cut() && enableDecode()", returning = "returnObj")
  public void afterReturn(JoinPoint joinPoint, Object returnObj){
    if(returnObj instanceof User user){ // 리턴이 User타입이면
      String email = user.getEmail(); // 이메일 가져와서
      // base64로 다시 인코딩
      String base64Email = Base64.getEncoder().encodeToString(email.getBytes());
      // 인코딩 한 값으로 set
      user.setEmail(base64Email);
    }
  }
}

 

위와 같이 간단하게 반복되는 로직들를 AOP를 사용하여 분리해낼 수 있다.

'Back-End > Spring Boot' 카테고리의 다른 글

Spring Boot Filter란  (2) 2024.06.16
Spring Boot @RestControllerAdvice를 이용한 예외 처리 방법  (0) 2024.06.09
IoC와 DI  (0) 2024.06.08
Spring Boot Custom Validation만들기  (0) 2024.06.07
Spring Boot Validation 사용  (0) 2024.06.07