Spring 입문 5-4 (Spring AOP)

SJ.CHO·2024년 10월 29일

부가기능의 필요성 및 구현

  • 현재 기준코드에서 요구사항으로 서버 이용시간의 TOP5 회원을 선출해달라는 요구사항이 들어왔을때 어떻게 구현할 것인가?
  // 관심 상품 등록하기
  @PostMapping("/products")
  public ProductResponseDto createProduct(
      @RequestBody ProductRequestDto requestDto,
      @AuthenticationPrincipal UserDetailsImpl userDetails) {
    // 측정 시작 시간
    long startTime = System.currentTimeMillis();

    try {
      // 응답 보내기
      return productService.createProduct(requestDto, userDetails.getUser());
    } finally {
      // 측정 종료 시간
      long endTime = System.currentTimeMillis();
      // 수행시간 = 종료 시간 - 시작 시간
      long runTime = endTime - startTime;

      // 로그인 회원 정보
      User loginUser = userDetails.getUser();

      // API 사용시간 및 DB 에 기록
      ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser).orElse(null);
      if (apiUseTime == null) {
        // 로그인 회원의 기록이 없으면
        apiUseTime = new ApiUseTime(loginUser, runTime);
      } else {
        // 로그인 회원의 기록이 이미 있으면
        apiUseTime.addUseTime(runTime);
      }

      System.out.println(
          "[API Use Time] Username: "
              + loginUser.getUsername()
              + ", Total Time: "
              + apiUseTime.getTotalTime()
              + " ms");
      apiUseTimeRepository.save(apiUseTime);
    }
  }
  • 해당 API의 사용시간 (메소드 시작, 끝 시간)을 구하여서 DB상으로 저장하면 기능의 구현이 가능은하다.
    • 하지만 이 기능이 모든 핵심기능에 전부 필요하다면? 혹은 핵심기능이 지속적으로 추가가된다면? 아니면 코드를 추가하는 것을 까먹는다면?
      • 그러면 해당 데이터의 대한 신뢰성이 하락하는 결과가 초래됌.
    • 핵심기능이 변경사항이 부가기능의 영향을 미치거나 부가기능이 변경된다면?
      • 부가기능을 이해하거나 부가기능을 핵심기능의 수만큼 변경이 필요해짐.
  • 위 문제를 해결하기위해 Spring AOP를 활용할 수 있다.

Spring AOP

  • @Aspect

    • Spring 빈(Bean) 클래스에만 적용 가능. 실질적 AOP 클래스 지정
  • 어드바이스 : 부가기능이 핵심기능의 언제 수행할지 정함.

    • @Around: '핵심기능' 수행 전과 후 (@Before + @After)
    • @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
    • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
    • @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
    • @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
  • 포인트 컷 : 언제 뿐만 아니라 어디에 수행할지도 지정 지역적

  • 하지만 모든 부가기능에서 하나하나 포인트컷을 어드바이스에 적용하게된다면 생산성 면에서 너무 효율이 떨어진다.

  • 이를 대비하기 위하여 @Pointcut 을 제공한다.

@Component
@Aspect
public class Aspect {
	@Pointcut("execution(* com.sparta.myselectshop.controller.*.*(..))")
	private void forAllController() {}

	@Pointcut("execution(String com.sparta.myselectshop.controller.*.*())")
	private void forAllViewController() {}

	@Around("forAllContorller() && !forAllViewController()")
	public void saveRestApiLog() {
		...
	}

	@Around("forAllContorller()")
	public void saveAllApiLog() {
		...
	}	
}
  • @Pointcut("execution(* com.sparta.myselectshop.controller.*.*(..))") private void forAllController() {} 위 와같이 포인트컷의 지정을 메소드명으로 지정이 가능하다.

  • @Around("forAllContorller()") public void saveAllApiLog() { ... } 어드바이스 내부에선 메소드명으로 포인트컷 지정이 가능!

  • Spring 내부에서는 이런 방식으로 실행이 된다.

  • Spring 이 프록시 객체를 중간에 삽입. DispatcherServlet 과 ProductController 입장에서는 변화가 없다.
  • 호툴되는 함수의 input, output 이 완전히 동일.
  • "joinPoint.proceed()" 에 의해서 원래 호출하려는 함수, 인수가 전달된다.
@Slf4j(topic = "UseTimeAop")
@Aspect
@Component
@RequiredArgsConstructor
public class UseTimeAop {

  private final ApiUseTimeRepository apiUseTimeRepository;

  @Pointcut("execution(* com.sparta.myselectshop.controller.ProductController.*(..))")
  private void product() {}

  @Pointcut("execution(* com.sparta.myselectshop.controller.FolderController.*(..))")
  private void folder() {}

  @Pointcut("execution(* com.sparta.myselectshop.naver.controller.NaverApiController.*(..))")
  private void naver() {}

  @Around("product() || folder() || naver()")
  public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
    // 측정 시작 시간
    long startTime = System.currentTimeMillis();

    try {
      // 핵심기능 수행
      Object output = joinPoint.proceed();
      return output;
    } finally {
      // 측정 종료 시간
      long endTime = System.currentTimeMillis();
      // 수행시간 = 종료 시간 - 시작 시간
      long runTime = endTime - startTime;

      // 로그인 회원이 없는 경우, 수행시간 기록하지 않음
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      if (auth != null && auth.getPrincipal().getClass() == UserDetailsImpl.class) {
        // 로그인 회원 정보
        UserDetailsImpl userDetails = (UserDetailsImpl) auth.getPrincipal();
        User loginUser = userDetails.getUser();

        // API 사용시간 및 DB 에 기록
        ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser).orElse(null);
        if (apiUseTime == null) {
          // 로그인 회원의 기록이 없으면
          apiUseTime = new ApiUseTime(loginUser, runTime);
        } else {
          // 로그인 회원의 기록이 이미 있으면
          apiUseTime.addUseTime(runTime);
        }

        log.info(
            "[API Use Time] Username: "
                + loginUser.getUsername()
                + ", Total Time: "
                + apiUseTime.getTotalTime()
                + " ms");
        apiUseTimeRepository.save(apiUseTime);
      }
    }
  }
}
profile
70살까지 개발하고싶은 개발자

0개의 댓글