24.09.10

윤지현·2024년 9월 10일

TIL

목록 보기
60/75

통합 테스트란 무엇일까?

  • 단위 테스트 VS 통합 테스트
    1. 단위 테스트 (Unit Test)
        - 하나의 모듈이나 클래스에 대해 세밀한 부분까지 테스트가 가능합니다.
        - 하지만 모듈 간에 상호 작용 검증은 할 수 없습니다.
        
    2. 통합 테스트 (Integration Test)
        - 두 개 이상의 모듈이 연결된 상태를 테스트할 수 있습니다.
        - 모듈 간의 연결에서 발생하는 에러 검증 가능합니다.
        
  • Spring Boot를 이용한 통합 테스트
    • 통합 테스트
      • 여러 단위 테스트를 하나의 통합된 테스트로 수행합니다.
      • 단위 테스트 시 Spring은 동작되지 않습니다.
    • "@SpringBootTest"
      • 스프링이 동작되도록 해주는 애너테이션입니다.
      • 테스트 수행 시 스프링이 동작합니다.
        • Spring IoC/DI 기능을 사용 가능합니다.
        • Repository를 사용해 DB CRUD가 가능합니다.

Spring AOP란 무엇일까?

  • Spring의 AOP 애너테이션
    1. @Aspect
      • Spring 빈(Bean) 클래스에만 적용 가능합니다.
    2. 어드바이스 종류
      • @Around: '핵심기능' 수행 전과 후 (@Before + @After)
      • @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행)
      • @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작)
      • @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능)
      • @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
    3. 포인트컷
      • 포인트컷 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개 (타입 상관없음)
        • @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() {
            		...
            	}	
            }
  • Spring AOP 적용
    • ProductController 에 추가했던 부가기능 제거 (Rollback)
      • [코드 스니펫] Rollback
        @PostMapping("/products")
        public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
            return productService.createProduct(requestDto, userDetails.getUser());
        }
    • AOP 사용해 FolderController, ProductController, NaverApiController 에 부가기능 추가
      • [코드 스니펫] aop > UseTimeAop
        package com.sparta.myselectshop.aop;
        
        import com.sparta.myselectshop.entity.ApiUseTime;
        import com.sparta.myselectshop.entity.User;
        import com.sparta.myselectshop.repository.ApiUseTimeRepository;
        import com.sparta.myselectshop.security.UserDetailsImpl;
        import lombok.extern.slf4j.Slf4j;
        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.security.core.Authentication;
        import org.springframework.security.core.context.SecurityContextHolder;
        import org.springframework.stereotype.Component;
        
        @Slf4j(topic = "UseTimeAop")
        @Aspect
        @Component
        public class UseTimeAop {
        
            private final ApiUseTimeRepository apiUseTimeRepository;
        
            public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
                this.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) 가 전달
        → createProduct(requestDto);
profile
첫 시작

0개의 댓글