[Spring] AOP

thingzoo·2023년 8월 15일
0

Spring

목록 보기
54/54
post-thumbnail
post-custom-banner

부가기능 모듈화의 필요성

  • '핵심기능': 각 API 별 수행해야 할 비즈니스 로직

    • ex) 상품 키워드 검색, 관심상품 등록, 회원 가입, 관심상품에 폴더 추가, ...
  • '부가기능': 핵심기능을 보조하는 기능

    • ex) 회원 패턴 분석을 위한 로그 기록, API 수행시간 저장
  • 문제점

    • 모든 '핵심기능'의 Controller 에 '부가기능' 코드를 추가했을 때..
      • '핵심기능' 이 100개라면??
        • 100개의 '핵심기능' 모두에 동일한 내용의 코드 추가 필요합니다.
      • '핵심기능' 이 나중에 추가된다면?
        • 항상 '부가기능' 추가를 신경써야 합니다.
        • '부가기능' 추가를 깜박한다면?
          • 일부 API 수행시간이 추가되지 않음 → Top5 회원의 신뢰성 이슈가 발생합니다.
    • '핵심기능' 수정 시
      • 같은 함수 내에 '핵심기능'과 '부가기능'이 섞여 있습니다.
      • '핵심기능' 이해를 위해 '부가기능'까지 이해 필요합니다.
    • '부가기능'의 변경이 필요하다면??
      • '핵심기능'의 개수만큼 '부가기능'도 수정해 줘야 합니다.

AOP(Aspect Oriented Programming)

AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.

@Aspect

AOP 설정, Spring 빈(Bean) 클래스에만 적용 가능

어드바이스 종류

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

포인트컷

적용 위치 지정

포인트컷 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<String>, *****

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개 (타입 상관없음)

@Pointcut

  • 포인트컷 재사용 가능
  • 포인트컷 결합 (combine) 가능
@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() {
		...
	}	
}

AOP 예시

@Slf4j(topic = "UseTimeAop")
@Aspect // aop 설정(빈에만 적용 가능)
@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);
            }
        }
    }
}

Spring AOP 동작 이해

  • 개념적 이해

  • 스프링 실제 동작

  • 시퀀스 다이어그램 (Sequence Diagram)

    • AOP 적용 전
    • AOP 적용 후
  • Spring이 프록시(가짜 혹은 대리) 객체를 중간에 삽입해준다.

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

    • 호출되는 함수의 input, output 이 완전 동일하다.
    • joinPoint.proceed() 에 의해서 원래 호출하려고 했던 함수, 인수(argument) 가 전달된다.
profile
공부한 내용은 바로바로 기록하자!
post-custom-banner

0개의 댓글