팀 프로젝트를 하던 중 프론트 단에서 무한 스크롤을 적용한 조회 api를 구현해야 했다. 스프링 데이터 jpa의 Page 기능을 통해 구현할 수도 있지만, no offset 기능을 적용하기 위해 Querydsl을 사용하였다.
no offset을 적용하지 않을 경우, 데이터 값이 늘어나면 늘어날수록, 전체 데이터를 조회하는 과정에서 성능 저하가 늘어나기 때문이다.
추가적으로 연관된 데이터를 함께 조회하기 위해서, 성능을 고려해 패치 조인을 사용하려 했지만, OneToMany 형식의 연관관계를 가지고 있어 패치 조인이 불가능했다.(~ToMany의 경우, jpa에서 중복을 고려하여 limit, offset을 걸지 않고 일단 인메모리에 다 가져온 후, 어플리케이션 단에서 처리하므로 성능 저하는 그대로이다, ~ToOne은 중복이 발생하지 않으므로 페이징 처리를 해도 괜찮다.)
따라서 일단 페이징을 통해 데이터 리스트를 가져온 후, 연관관계를 가진 데이터에 대해 BatchSize 설정을 통해 n+1문제를 해결하기로 했다.
또한 조회한 데이터를 바로 Page 형식으로 반환하는게 아닌, 연관된 테이블의 데이터를 추가해서 반환해야 했기 때문에, 직접 생성한 DTO 형태로 변환하여 프론트단에 반환하였다.
AuctionController.java
// 지난 경매 작품 조회
@GetMapping("/auction/period-over/{auctionId}")
public ResponseEntity<ArtWorkTerminatedListResponseDto> getTerminatedAuctionArtWorkList(Pageable pageable,
@PathVariable("auctionId") Long auctionId,
@RequestParam(value = "artWorkId", required = false) Long artWorkId) {
ArtWorkTerminatedListResponseDto terminatedAuctionArtWorkList = artWorkService.getTerminatedAuctionArtWorkList(auctionId, artWorkId, pageable);
return ResponseEntity.status(HttpStatus.OK).body(terminatedAuctionArtWorkList);
}
ArtWorkTerminatedListResponseDto.java
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ArtWorkTerminatedListResponseDto {
private boolean nextPage;
private List<ArtWorkDto> artWorkList;
@Data
@Builder
public static class ArtWorkDto {
private Long id;
private String mainImage;
private String title;
private String material;
private Long topPrice;
private Integer biddingCount;
private String status;
public static ArtWorkDto from(ArtWork artWork, Long topPrice, Integer biddingCount) {
return ArtWorkDto.builder()
.id(artWork.getId())
.mainImage(artWork.getMainImage())
.title(artWork.getTitle())
.material(artWork.getMaterial())
.topPrice(topPrice)
.biddingCount(biddingCount)
.status(artWork.getSaleStatus())
.build();
}
}
}
ArtWorkCustomRepositoryImpl.java
@Override
public List<ArtWork> findTerminatedAuctionArtWorkList(Long auctionId, Long artWorkId, Pageable pageable) {
List<ArtWork> results = queryFactory
.select(artWork)
.from(artWork)
.where(
ltArtWorkId(artWorkId),
artWork.auction.id.eq(auctionId),
artWork.saleStatus.ne(ArtWorkStatus.REGISTERED.getType()),
artWork.saleStatus.ne(ArtWorkStatus.PROCESSING.getType())
)
.orderBy(artWork.id.desc())
.limit(pageable.getPageSize()+1)
.fetch();
return results;
}
private BooleanExpression ltArtWorkId(Long artWorkId) {
if (artWorkId == null) {
return null;
}
return artWork.id.lt(artWorkId);
}
ArtWorkService.java
public ArtWorkTerminatedListResponseDto getTerminatedAuctionArtWorkList(Long auctionId, Long artWorkId, Pageable pageable) {
Auction auction = auctionRepository.findById(auctionId)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_AUCTION_TURN));
if (!auction.getStatus().equals(AuctionStatus.TERMINATED.getType())) {
throw new CustomException(ErrorCode.IS_NOT_TERMINATED_AUCTION);
}
List<ArtWork> results = artWorkRepository.findTerminatedAuctionArtWorkList(auctionId, artWorkId, pageable);
List<ArtWorkTerminatedListResponseDto.ArtWorkDto> artWorkDtoList = transferToTerminatedArtWorkDto(results);
boolean hasNext = false;
// 조회한 결과 개수가 요청한 페이지 사이즈보다 클 경우, next = true
if (artWorkDtoList.size() > pageable.getPageSize()) {
hasNext = true;
artWorkDtoList.remove(pageable.getPageSize());
}
ArtWorkTerminatedListResponseDto artWorkTerminatedListResponseDto = ArtWorkTerminatedListResponseDto.builder()
.nextPage(hasNext)
.artWorkList(artWorkDtoList)
.build();
return artWorkTerminatedListResponseDto;
}
private List<ArtWorkTerminatedListResponseDto.ArtWorkDto> transferToTerminatedArtWorkDto(List<ArtWork> artWorkList) {
List<ArtWorkTerminatedListResponseDto.ArtWorkDto> artWorkDtoList = new ArrayList<>();
for (ArtWork artWork : artWorkList) {
List<Long> priceList = artWork.getBiddingList().stream().map(m -> m.getPrice()).collect(Collectors.toList());
Long topPrice = artWork.getPrice();
if (!priceList.isEmpty()) {
topPrice = Collections.max(priceList);
}
ArtWorkTerminatedListResponseDto.ArtWorkDto artWorkDto = ArtWorkTerminatedListResponseDto.ArtWorkDto.from(artWork, topPrice, artWork.getBiddingList().size());
artWorkDtoList.add(artWorkDto);
}
return artWorkDtoList;
}