관리자 페이지에서 사용자의 모든 기록을 볼 수 있는 탭을 하나 만들어두었다.
해당 탭에 들어가면 mounted가 되는 순간 서버로 모든 요청 기록을 조회하는 api를 호출하게 된다.
일단 50만 개의 더미 데이터를 넣어두고 전부 불러오도록 설정.
jpa의 findAll을 써서 단순 리스트로 반환했지만, 50만 개의 데이터다 보니 8초라는 시간이 걸렸다. 물론 동일한 데이터를 계속 불러오기에 나중에 캐싱을 이용하면 좋겠지만, 현재는 화면에 표시를 하는 게 목적이므로 패스.
일단 느린 시간만큼 조금 로딩화면을 보여줘야한다는 생각이 들었다.
isLoading: false,
vue data에 로딩상태를 두고
<div v-if="isLoading" class="overlay">요청 기록 불러오는중...</div>
이렇게 둬서 로딩이 되는 순간 화면 색을 바뀌게 만들었다.(어떻게 보면 사용자 경험?)
그래서 요청을 보내면 이렇게 화면이 뜨고, 응답이 오면 사라지게 되는 형태로 우선 만들어두었다.
이제 해야하는 건, 50만 개의 데이터를 리스트로 보내주는데, 이걸 페이징을 통해 데이터를 제공해야 한다.
가장 최근 데이터가 1번으로 오도록 50만 개의 데이터를 전송하고, 20개씩 데이터를 한 페이지로 구성해서 보여줄 예정.
QueryDSL을 이용하지 않고, 단순 JPA로만 해보려고 한다.
Pageable 인터페이스
우선 Spring에서는 Pageable 인터페이스를 제공한다.
- getPageNumber() : 현재 페이지의 번호를 리턴
- getPageSize() : 현재 페이지의 항목 수를 리턴(즉 한 페이지의 최대 항목 수)
- getOffset() : 현재 페이지의 시작 위치를 리턴
- getSort() : 정렬 정보를 리턴
- next() : 다음 페이지의 정보를 리턴
- previousOrFirst() : 이전 페이지가 있으면 이전 페이지 정보 리턴, 아니면 첫 페이지 정보 리턴
PageRequest 클래스
Pageable 구현체 중 하나이고, 페이지 정보를 생성하는 클래스이다.
AbstractPageRequest를 상속받고 있고
해당 추상클래스는 Pageable를 구현하고 있다.
PageRequest는 페이지 번호, 페이지당 항목 수, 정렬 정보를 이용하여 Pageable 인터페이스를 구현한다.
그래서 위와 같은 생성자가 만들어져 있다. 우리는 저기에 값을 넣어 페이지 정보를 생성하는 요청을 만들어야 한다.
일단 컨트롤러 파라미터로 현재 페이지의 번호(pageNumber), 한 페이지의 사이즈(pageSize), 정렬 기준(sort)을 파라미터로 보내줘야 한다.
내 경우에는 페이지의 사이즈가 20으로 고정, 정렬 기준도 최근 시간으로 고정이니 파라미터로 현재 pageNumber만 받아서 처리해주면 된다.
pageNumber, pageSize, sort 총 3개의 데이터를 통해 PageRequest를 생성하고, 얘네로 pageable를 만들어서 넘겨줘야 한다.
// API 요청 이력 조회
@GetMapping("admin/requests")
public ResponseEntity<ApiResponse<?>> getApiRequestHistory(@RequestParam(value = "page", defaultValue = "0") int pageNumber){
int pageSize = 10; // 한 페이지에 보여줄 요청 이력 수
Sort sort = Sort.by(Sort.Order.desc("requestDate")); // 요청 날짜 기준으로 내림차순 정렬
Pageable pageable = PageRequest.of(pageNumber, pageSize, sort);
List<RequestHistoryResponseDTO> apiRequestHistory = apiRequestService.getApiRequestHistory(pageable);
그래서 위와 같이 page의 번호만 파라미터로 받고, 나머지 2개는 고정으로 설정시켜 pageable를 생성시켜주었다.
public List<RequestHistoryResponseDTO> getApiRequestHistory(Pageable pageable) {
Page<ApiRequestHistory> all = apiRequestHistoryRepository.findAll(pageable);
서비스에서 pageable을 받아서 findAll에 데이터를 넣어주었다.
List<ApiRequestHistory> content = all.getContent();
List<RequestHistoryResponseDTO> responseDTOList = content.stream()
.map(apirequestHistory ->
RequestHistoryResponseDTO.builder()
.memberId(apirequestHistory.getMember().getId())
.requestDate(apirequestHistory.getRequestDate())
.methodType(apirequestHistory.getMethodType())
.requestContent(apirequestHistory.getRequestContent())
.responseContent(apirequestHistory.getResponseContent())
.build()
).collect(Collectors.toList());
Page 데이터에서 getContent()로 실제 데이터를 꺼낸 다음 stream을 이용해서 dto를 만들어주었다.
데이터는 잘 받아오는 걸 확인할 수 있다.
문제는 50만 개의 데이터를 조회하는 api를 날렸을 때 지금 시간이 말이 안 된다.
한 번의 요청을 보냈을 뿐인데 39초가 걸린다. 생각해 보면 50만 개의 데이터를 전부 찾아와서 그런 것 같은데??
결국 최적화를 진행해야 한다.
1. 정렬 기준에 인덱스 추가
현재 내 api는 시간을 기준으로 정렬을 진행한다. 그러니 시간에 인덱스를 걸어두면 조회측면에서 성능 향상이 있지 않을까? 싶었다.
CREATE INDEX idx_request_date ON api_request_history(request_date DESC);
해당 쿼리로 request_date 컬럼에 인덱스를 추가해 주었다.
정상적으로 인덱스 추가 완료. 다시 api를 돌려보자.
변화는 없었다.
2. N+1 문제 생각하기
현재 ApiRequestHistory는 Member와 ManyToOne 관계이다. LAZY로 설정했지만, member.getId()에서 추가 쿼리가 발생하기에 EntityGraph를 이용해 찾아오기로 결정.
@Override
@EntityGraph(attributePaths = {"member"})
List<ApiRequestHistory> findAll();
그래서 findAll을 오버라이드 해주었다.
변화는 없다.
그러나 50만 개의 데이터가 모두 memberId를 1로 넣어둬서 그렇지, 만약에 memberId가 제각각이면 유의미한 시간차가 있을 것이라 생각한다.
3. Slice로 리턴하기
현재는 findAll을 사용해서 Page타입으로 리턴을 진행하고 있다. 여기서 날아가는 쿼리를 보면
Hibernate:
select
count(arh1_0.id)
from
api_request_history arh1_0
위와 같은 쿼리가 날아가는데 count를 이용해서 모든 데이터의 개수를 찾는 것. 즉 50만 개의 데이터를 알아야 페이지 수를 계산할 수 있기에 필요한 쿼리지만, 내 경우 매 요청마다 count를 진행해서 오래 걸린다는 문제가 있었다.
그래서 Page대신 Slice를 사용하려고 한다.
Slice를 쓰면 조건에 맞는 데이터만 딱 잘라서 리턴하기에 시간이 훨씬 절약된다. 대신 전체 데이터의 개수를 알지 못하는 문제가 발생한다.
@EntityGraph(attributePaths = {"member"})
Slice<ApiRequestHistory> findSliceBy(Pageable pageable);
쿼리를 이렇게 수정하고
Slice<ApiRequestHistory> all = apiRequestHistoryRepository.findSliceBy(pageable);
List<ApiRequestHistory> content = all.getContent();
long startNo = (long) pageNumber * pageable.getPageSize() + 1;
AtomicLong index = new AtomicLong(startNo);
List<RequestHistoryResponseDTO> responseDTOList = content.stream()
.map(apirequestHistory ->
RequestHistoryResponseDTO.builder()
.no(index.getAndIncrement())
.memberId(apirequestHistory.getMember().getId())
.requestDate(apirequestHistory.getRequestDate())
.methodType(apirequestHistory.getMethodType())
.requestContent(apirequestHistory.getRequestContent())
.responseContent(apirequestHistory.getResponseContent())
.build()
).collect(Collectors.toList());
return responseDTOList;
}
Slice로 받은 다음에 List로 꺼내서 DTO로 변환시켰다. 데이터 번호를 위해서 index도 따로 설정해 주었다.
움짤처럼 데이터의 로딩은 확실히 개선됐다. 그런데 이제 문제가 있는데 전체 데이터의 개수를 파악하지 않으니 페이지를 표시할 수 없다는 문제가 발생...
결국 이걸 QueryDSL로 변경해야 한다. 어차피 나중에 조건에 따라 결과를 보여줘야 할 수도 있으니?? 아이고 내 두야
'프로젝트 > RESTAPI 추천 서비스' 카테고리의 다른 글
No validator could be found for constraint (Valid 에러 발생) (0) | 2024.04.08 |
---|---|
MySQL 버퍼 풀 사이즈 조정으로 count 쿼리 성능 개선(37초 -> 0.026초) (1) | 2024.04.08 |
jpa환경에서 repository 테스트코드 작성해보자 (0) | 2024.04.04 |
RESTFUL API 서비스에서 쿠폰을 발행하자(동시성 문제 해결) (0) | 2024.03.30 |