[TIL] 39. 스프링 AOP

김지수·2024년 6월 14일

TIL

목록 보기
39/53

AOP


AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 ` 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다. 

AOP를 활용하면 공통된 기능을 여러 곳에 중복해서 작성하지 않고도 쉽게 관리할 수 있어 애플리케이션의 유지보수성을 크게 향상시킬 수 있습니다.

AOP를 활용하면 공통된 기능을 여러 곳에 중복해서 작성하지 않고도 쉽게 관리할 수 있어 애플리케이션의 유지보수성을 크게 향상시킬 수 있습니다.

AOP의 주요


Aspect

횡단 관심사를 모듈화한 것. 주로 공통 기능을 정의합니다.

Join Point

어드바이스가 적용될 수 있는 지점, 예를 들어 메서드 호출이나 예외 발생 시점.

Advice

횡단 관심사 코드를 실제로 구현한 것. 언제(예: 메서드 호출 전후, 예외 발생 시 등) Aspect의 기능을 적용할지 정의합니다.

2. 어드바이스 종류

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

Pointcut

특정 Join Point를 선택하는 표현식. 어느 지점에 Advice를 적용할지를 정의합니다.

  • 포인트컷 Expression Language
    • 포인트컷 Expression 형태
      execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
      - ? 는 생략 가능
      - 포인트컷 Expression 예제
      @Around("execution(public com.sparta.myselectshop.controller..(..))")
      public Object execute(ProceedingJoinPoint joinPoint) throws Throwable { ... }
    • modifiers-pattern
      • public, private, *
    • return-type-pattern
      • void, String, List, *
    • declaring-type-pattern
      • 클래스명 (패키지명 필요)
      • com.sparta.myselectshop.controller.* - controller 패키지의 모든 클래스에 적용
      • com.sparta.myselectshop.controller.. - controller 패키지 및 하위 패키지의 모든 클래스에 적용
    • method-name-pattern(param-pattern)
      • 함수명
        • addFolders : addFolders() 함수에만 적용
        • add* : add 로 시작하는 모든 함수에 적용
      • 파라미터 패턴 (param-pattern)
        • (com.sparta.myselectshop.dto.FolderRequestDto) - FolderRequestDto 인수 (arguments) 만 적용
        • () - 인수 없음
        • (*) - 인수 1개 (타입 상관없음)
        • (..) - 인수 0~N개 (타입 상관없음)

Weaving

Aspect와 비즈니스 로직을 엮는 과정. 컴파일 시점, 클래스 로딩 시점, 런타임 시점에 이루어질 수 있습니다.

AOP 코드


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

AOP 적용 전

AOP 적용 후

  • Spring이 프록시(가짜 혹은 대리) 객체를 중간에 삽입해줍니다.

  • DispatcherServlet 과 ProductController 입장에서는 변화가 전혀 없습니다.

    • 호출되는 함수의 input, output 이 완전 동일합니다.
    • "joinPoint.proceed()" 에 의해서 원래 호출하려고 했던 함수, 인수(argument) 가 전달됩니다. → createProduct(requestDto);

    오늘의 회고


    스프링부트 테스트와 AOP에 대해 공부했다 개념과 사용법을 이해하고 예제를 통해 실습해봤다. 앞으로도 더많은 실습을 통해 내껄로 만들어야겠다.

profile
서툴고 부족한 점이 많지만, 배우고 발전하며 성장하기 위해 노력하겠습니다.

0개의 댓글