
정리해서 주말중에 완성해 팀원들과 공유하기로!
머지 후에 내용에 대해 수정할 부분을 기록해두었다.



지난 목록 조회를 완료하고, 다시 API 명세서를 봤는데,StoreId로 필터링 된것 과 criteria를 통해 정렬이 필요해 추가 작업이 필요함을 알게 되었다.
즉, 여태까지 한 것으론 매니저 Role이 오더의 민족서비스의 전체 리뷰를 조회하는 시나리오 밖에 구현하지 못한 것이다.
(mimimya 개발자! 요구사항 정의 실패!)

금요일에 이어 주말에 개인적인 일을 완료하고 빠르게 개발을 이어나가기로 했다.
그치만 기절 이슈로 인해 일요일 늦은 오후부터 작업 시작ㅜㅜ
그리고 팀원들과 우당탕탕 컨플릭트 해결 허들이 있었다 ㅎㅎ (주말에도 열일하시는 우리 팀원분들😂)
뭐 어쩔 수 없다. 하는 데 까진 해봐야지!!!
오늘은 잠을 제대로 잤으니 말똥해진 정신으로 전화위복이 될 거라 생각한다.(?!)
지난 번 구현한 목록 조회에서는 이미
Pagable에서 제공되어 엔티티 필드명(프로퍼티)을 기준으로 정렬하는
sort를 쿼리 파라미터로 받아 정렬 기준을 정할 수 있었다.
GET /reviews?sort=user.nickname,asc&sort=createdAt,des
Pagable의 sort는 연관 관계의 필드까지 정렬 기준에 사용할 수 있긴하지만 쿼리 상 LIMIT절 이전에 join을 통한 정렬이 발생하기 때문에 성능상 좋지 않다.
게다가 OFFSET 페이징에서는 동점 데이터의 순서를 보장하기 위해 정렬 조건이 두 개 이상 필요하다.
게다가 클라이언트가 모든 정렬 조건을 알아야 하기 때문에 API요청의 복잡도가 커지고, 본래 API 의도에서 벗어나 각 클라이언트가 다른 정렬 기준을 사용할 위험도 있다.
Criteria = 기준, 조건, 척도 라는 뜻의 단어로
조회될 목록을 무엇을 기준으로 정렬할지 나타내기 위한 파라미터의 이름으로 사용한다.
criteria 방식은 정렬 규칙을 서버가 관리할 수 있다는 장점이 있다.
위의 Paging.sort 방법과 달리 서버가 알아서 내부적으로 tie-breaker 정렬을 추가할 수 있어 클라이언트는 하나의 정렬 기준만 전달하면 된다.
만약 두 가지 방식을 모두 사용해 criteria와 sort를 모두 허용하되, criteria가 존재하면 이를 우선 적용하고 없을 경우 Pageable.sort를 사용한다면,
이를 통해 클라이언트의 사용 복잡도를 낮추면서도 확장 가능한 정렬된 목록 API를 제공할 수 있다고 생각했다.
두 가지 방식으로 요청해도 받을 수 있도록
@RequestParam의 required 옵션을 false로 설정해서
criteria가 들어오면 criteria 기준으로, 파라미터가 없다면 Paging 요청의 정렬 기준으로 응답하도록 했다.
@GetMapping("/reviews")
public ResponseEntity<Page<ReviewResponse>> getReviews(
@RequestParam(required = false, defaultValue = "DEFAULT") ReviewCriteria criteria,
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<ReviewResponse> response = reviewService.getReviews(criteria, pageable);
return ResponseEntity.ok(response);
}
@QueryParam() 의 defaultValue 옵션의 타입은 내부적으로 String 타입을 사용한다.
서비스 단에서 쿼리 파라미터의 형식을 관리하기 편하게 하려면 이 값들을 Enum 타입으로 지정해 두는 것이 좋다고 생각했다.
리뷰의 정렬 기준은
- 평점 높은순
- 평점 낮은순
- 최신순
- 오래된 순
이 있을 것이라 생각하고 다음과 같은 Enum 파일을 만들어 두었다.
import org.springframework.data.domain.Sort; //JPA, Spring Data에서 정렬 조건을 만들 때 사용
@RequiredArgsConstructor
public enum ReviewCriteria {
RATING_HIGH("rating", "평점 높은순", Sort.Direction.DESC),
RATING_LOW("rating", "평점 낮은순", Sort.Direction.ASC),
RECENT("createdAt", "최신순", Sort.Direction.DESC),
OLDEST("createdAt", "오래된순", Sort.Direction.ASC),
DEFAULT("createdAt", "기본pageable에 따름", Sort.Direction.DESC);
private final String field;
private final String description;
private final Sort.Direction direction;
}
만약 컨트롤러 메서드 파라미터로 다음과 같이 받으면,
@RequestParam(required = false, defaultValue = "recent") ReviewCriteria criteria
Spring이 자동으로 쿼리나defaultValue의 String 값을 Enum 타입으로 바꿔준다.
Controller에서 받은 criteria를 토대로 Pageable 객체의 sort 값을 바꿔준다.
@Transactional(readOnly = true)
public Page<ReviewResponse> getReviews(ReviewCriteria criteria, Pageable pageable) {
if (!criteria.equals(ReviewCriteria.DEFAULT)) {
// Criteria에 따른 정렬기준 하나 생성
Sort.Order primaryOrder = switch (criteria) {
case RATING_HIGH -> Sort.Order.desc("rating");
case RATING_LOW -> Sort.Order.asc("rating");
default -> Sort.Order.desc("createdAt");
};
// Criteria를 우선으로 새로운 Pageable에 쓰일 Sort을 만든다.
// (pageable.getSort()의 기본값은 컨트롤러에서 @PageableDefault로 초기화되어있음)
Sort combinedSort = Sort.by(primaryOrder).and(pageable.getSort());
// 구현체 PageRequest로 combinedSort를 가진 새로운 Pagable생성
Pageable finalPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
combinedSort
);
Page<Review> reviewPage = reviewRepository.findAllByIsDeletedFalse(finalPageable);
return reviewPage.map(ReviewResponse::from);
}//if criteria != DEFAULT
else {
Page<Review> reviewPage = reviewRepository.findAllByIsDeletedFalse(pageable);
return reviewPage.map(ReviewResponse::from);
}
}
Pageable 객체는 다음과 같은 주요 정보를 가진다.
pageNumberpageSizeSortSort는 데이터 정렬 기준을 담는 객체이다.
여러개의 Sort.Order 객체들을 포함해 다중 정렬을 구성한다.
Sort
├─ Order
├─ Order
└─ Order
Sort.Order는 정렬 기준 '하나'를 나타내는 객체로, 정렬할 필드와 정렬 방향 정보를 가진다.
by(Sort.Order... orders)
주어진 Sort.Order 에 따라 새로운 Sort 객체를 생성한다.
and(Sort sort)
주어진 Sort객체를 Sort객체 자신에 포함시켜 새로운 Sort객체를 생성한다.
| 구현체 | 타입 | 주요 특징 | 사용 목적 |
|---|---|---|---|
PageRequest | 클래스 | Pageable의 대표적인 구현체, 페이지 번호/크기/정렬 정보를 포함 | 일반적인 페이징 처리 |
AbstractPageRequest | 추상 클래스 | PageRequest의 부모 클래스, 기본 페이징 로직 제공 | 직접 사용하지 않음 (상속용) |
QPageRequest | 클래스 | QueryDSL 정렬(OrderSpecifier)을 지원 | QueryDSL 기반 페이징 |
PageRequest는 Pageable 인터페이스의 대표적인 구현 클래스이다.
pageNumber : 조회할 페이지 번호 (0부터 시작)
pageSize : 페이지당 데이터 개수
sort : 정렬 기준
페이지 번호, 페이지 크기, 정렬 정보를 기반으로 Pageable 객체를 생성한다.



sort와 criteria 기준이 두 가지 있어도 criteria의 기준이 우선됨을 알 수 있다.
이제 가게별 리뷰 목록을 조회하기 위해 QueryParameter에 StoreId를 추가한다.
@GetMapping("/reviews")
public ResponseEntity<Page<ReviewResponse>> getReviews(
@RequestParam(required = false) UUID storeId,
@RequestParam(required = false, defaultValue = "DEFAULT") ReviewCriteria criteria,
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable) {
Page<ReviewResponse> response = reviewService.getReviews(criteria, pageable, storeId);
return ResponseEntity.ok(response);
}
storeId 값이 있는지 없는지에 따라 repository호출을 분기 해준다.
@Transactional(readOnly = true)
public Page<ReviewResponse> getReviews(ReviewCriteria criteria, Pageable pageable, UUID storeId) {
Page<Review> reviewPage = (storeId != null)
? reviewRepository.findAllByStoreIdAndIsDeletedFalse(storeId, pageable):
reviewRepository.findAllByIsDeletedFalse(pageable);
return reviewPage.map(ReviewResponse::from);
}
findAllByStoreId를 추가해줬다.
public interface ReviewRepository extends JpaRepository<Review, UUID> {
@EntityGraph(attributePaths = {"user", "order", "store"})
Page<Review> findAllByStoreIdAndIsDeletedFalse(UUID storeId, Pageable pageable);
}

DefaultHandlerExceptionResolver.java
에 적힌 Well-known Exception 을 처리하는 공통 Exception Handler 만들어서
처리 할 수 있다.
@ 애너테이션을 사용하면,
Exception들을 하나의 배열로 만들어서 묶어서 해당 배열마다 응답 요청을 발생하는 예외를 정해줄 수 있다.
예외 처리는 두 가지로 나누는 것이 관리 하기 좋다.


생성자 오버로딩이 많아져서
분업이 이루어질 때
어떤 경우에 어떤 생성자를 사용해야 하는지 알 수 있도록
순서에 상관없이 타입이 같아도 각 필드가 자리자리에 넣게 된다
나중에