public CourseListResponseDto getCourseList() {
long start = System.currentTimeMillis();
try {
// 조회: 수업 엔티티 목록 조회
log.info("::: 수업 목록 조회 :::");
List<Course> foundCourseList = courseRepository.findAll(); // 조회 쿼리 1 발생
// DTO 변환: 수업 엔티티 -> CourseDto
log.info("::: DTO 변환 - Course -> CourseDto :::");
List<CourseDto> courseDtoList = foundCourseList.stream().map(course -> new CourseDto(
course.getId(),
course.getName(),
course.getMembers().size()
)).toList();
// 응답 반환
return new CourseListResponseDto(courseDtoList);
} finally {
//측정 완료
long end = System.currentTimeMillis();
long executionTime = end - start;
log.info("::: 측정 결과 ::: 서비스 목록 조회 완료, 측정시간: {} ms", executionTime);
}
}
이렇게 모든 메서드에다가 시간 측정을 등록해야할까?
- 추후 측정 단위가 변경되면?
- 추후 시간 측정 로직이 변경된다면?
핵심기능 시스템의 주요 목적
부가 기능 주요 목적이 아닌 기능
횡단 관심사 부가 기능이 여러곳에서 반복적용되는 상황
위 코드에서 횡단 관심사란? 시간 측정 로직 왜? 반복적용되니까
그래서 aop란? 핵심기능과 부가 기능(횡단 관심사)을 분리하는것
aop 용어 어드바이스 : 실제로 실행되는 횡단관심사 @Around, @Before, @After, @AfterThrowing 포인트컷 : 어드바이스를 적용할 구체적인 범위를 선택하는 구칙 @execution, @annotation, @within, @this, @target, @args 타겟 : 어드바이스가 적용될 객체 조인 포인트 : 어드바이스가 적용되는 실행지점 애스팩트 : 어드바이스와 포인트컷을 하나로 묶은 모듈
Given - when - then given : 주어진 전제 조건을 정의, 테스트 실행을 위한 준비. >> int a = 1; int b = 3; when : 테스트하려는 메서드나 기능을 실행하는 과정. >> int sum = a+b; then : 메서드나 기능이 실행된 후 예상되는 결과가 나오는지 확인. assertEquals(4, sum);
then에서 예상된 결과가 나오는지 테스트를 해본다.
Mock 가짜 객체를 만들어서 테스트를 도와준다
mockito프레임워크를 사용하여 구현한다.
BDD MOCKITO : 특정한 행동을 정의한다(레파지토리로직을 정의하겠다.)
Unit Test
단위테스트라고도 하며 단위는 프로젝트에서 가장 작은 테스트 가능 요소라고 할 수 있다. 실제 객체를 사용하지않고 MOCKING을 사용함으로써, 의존성이 적고 빠르게 테스트를 할 수 있다. 테스트는 개발간에 즉시 작성하는게 좋다.
클래스명 : 클래스 + Test 형태로 작성하는 평 DisplayName을 사용하고 함수명을 영문 테스트명을 작성하거나 DisplayName을 사용하지 않고 한글 테스트명을 작성하거나
컨트롤러는 어떻게 테스트 해야할까? controller를 테스트 하려면 @WebMvcTest어노테이션을 사용해야하며 security를 사용하고 있으니, 가짜 security와 필터를 만들어야하며 mockmvc를 사용하여 가짜 html메서드를 날릴 수 있고 가짜 유저를 만들어서 테스트 해야한다.
@Test
@DisplayName("신규 관심상품 등록")
void test3() throws Exception {
// given
this.mockUserSetup();
String title = "Apple 아이폰 14 프로 256GB [자급제]";
String imageUrl = "https://shopping-phinf.pstatic.net/main_3456175/34561756621.20220929142551.jpg";
String linkUrl = "https://search.shopping.naver.com/gate.nhn?id=34561756621";
int lPrice = 959000;
ProductRequestDto requestDto = new ProductRequestDto(
title,
imageUrl,
linkUrl,
lPrice
);
String postInfo = objectMapper.writeValueAsString(requestDto);
// when - then
mvc.perform(post("/api/products")
.content(postInfo)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.principal(mockPrincipal)
)
.andExpect(status().isOk())
.andDo(print());
}
신규 상품을 등록하기위해, user를 만들어야한다. 또한, 상품객체를 생성한다음
objectMapper를 사용하여, 객체를 json타입으로 변경한다.
perform을 통해 송신타입(?) 수신타입(?)을 지정하고 원하는 값을 설정한다.
요구사항 변경
유저가 얼마나 오래머무나를 측정할것이다. 페이지의 머문 시간? > 서버 사용시간으로 측정한다. 서버 사용시간? > controller에 요청이 들어온시간 ~ 응답이나간 시간 api 사용시간 = controller에 응답이 나간 시간 - controller에 요청이 들어온 시간
구현 사용자의 접속시간을 확인하는 테이블을 생성한다.
@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);
}
}
조회를 하고싶은 controller에 시간 측정을 구현한다. 접속시간 기록 로직을을 회원들이 알 필요가 있나?
x >> 상품 조회만 하면된다, 접속시간은 유저들이 몰라도되는부분 이처럼 api에서 사용해야할 비즈니스 로직을 핵심기능 이라고 하고 핵심을 보조하는 기능을 부가기능 이라고 한다
모든 핵심기능에 특정한 부가기능(api 사용시간 측정)을 넣어야한다. 부가기능 로직이 변경된다면 모든 핵심기능의 부가기능을 수정해야하나?
AOP를 사용해 부가기능을 모듈화를 한다 부가기능과 핵심기능은 관점(Aspect)가 다르다 따라서 핵심기능과 분리하여 부가기능 중심으로 설계해야한다.
스프링 AOP
출처 : 내일배움캠프 스프링 심화주차 8강
핵심기능을 모듈화하고, 부가기능을 모듈화 하여 합친다 advice : 부가기능을 핵심기능에 언제 수행할지를 정하는것이다 (전, 후, 전후 전부) point cut : 적용 위치 어디에서 수행할지
어드바이스 종류 @Around: '핵심기능' 수행 전과 후 (@Before + @After) @Before: '핵심기능' 호출 전 (ex. Client 의 입력값 Validation 수행) @After: '핵심기능' 수행 성공/실패 여부와 상관없이 언제나 동작 (try, catch 의 finally() 처럼 동작) @AfterReturning: '핵심기능' 호출 성공 시 (함수의 Return 값 사용 가능) @AfterThrowing: '핵심기능' 호출 실패 시. 즉, 예외 (Exception) 가 발생한 경우만 동작 (ex. 예외가 발생했을 때 개발자에게 email 이나 SMS 보냄)
modifiers-pattern : 접근제어자 지정 return-type-pattern : 반환타입 declearing-type-pattern : 클래스명 지정 (적용할 부분) method-name-pattern : 메서드 명을지정 (addFolders : addforders()메서드에만 적용 / add* : add로 시작하는 모든 메서드)
execution(public void com.example.MyClass.addFolders(..)) 접근제어자 : public 반환타입 : void 클래스명 : com > example > myclass 메서드명 : addFolders
execution(* com.example..*(..)) 접근제어자 : all 반환타입 : all 클래스명 com > example의 모든 하위 패키지의 클래스(..) 메서드명 모든 메서드
구현
@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);
}
}
위 코드를 수정하자.
그리고 aop > useTimeAop 만들자
@Aspect
@Component
public class UseTimeAop {
private final ApiUseTimeRepository apiUseTimeRepository;
public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
this.apiUseTimeRepository = apiUseTimeRepository;
}
@pointcut 어노테이션으로 aspect가 적용될수 있는 지점을 선언해준다. 각각 패키지의 모든 메서드들에 적용한다.
@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);
}
}
}
@around 어노테이션을 적용해 aspect가 실행될수 있는 시점을 적용해준다. around 는 메서드 실행 전 후 모든 시간을 측정한다
// 관심 상품 등록하기
@PostMapping("/products")
public ProductResponseDto createProduct(@RequestBody ProductRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return productService.createProduct(requestDto, userDetails.getUser());
}
이제 컨트롤러에는 핵심기능만 남아있고 부가기능은 따로 빠져있음을 확인 할 수 있다.
어떻게 작동하는걸까? 출처 : 내일배움캠프 스프링 심화주차 9강
AOP 적용 전에는 클라이언트 요청이 디스패처 서블릿을 통해 바로 컨트롤러에서 측정을해 결과값을 반환했다면
출처 : 내일배움캠프 스프링 심화주차 9강
AOP 적용 후에는 디스패처 서블릿과 컨트롤러사이에 AOP 프록시가 들어가게된다. 디스패처 서블릿이 보낸 메서드 호출을 가로채서 정의된 로직을 제어한다 포인트컷에 맞는지 확인한후, 어드바이스를 실행한다, 그후 CONTROLLER에 요청을 하고, AOP 프록시를 거쳐서 반환한다.