PageDto를 제대로 구현하기 위해 많은 고민을 하였다. PageDto를 구현하면서 스스로 건 제약 조건은
- 모든 곳에서 사용 가능할 것
- 동적으로 구현이 가능할 것(ex - 정렬, 키워드 검색 등)
PageDto 초기 Entity
첫번째 조건인 모든 곳에서 사용가능하게 하기 위해, PageDto에는 최소한의 데이터만 가지고 있도록 하였다. 데이터가 더 필요한 API에서는 PageDto를 상속받아 이용했다.
@Getter
public class PageDto {
@Schema(description = "페이지 아이템 개수", example = "30", defaultValue = "10")
private int pageSize = 10;
@Schema(description = "현재 페이지 번호", example = "0", defaultValue = "0")
private int page = 0;
@Schema(description = "정렬 타입")
private SortType sortType;
@Schema(description = "오름차순 여부", example = "true", defaultValue = "true")
@JsonProperty("isAscending")
private boolean isAscending = true;
}
초기 SortType Enum
@Getter
@RequiredArgsConstructor
public enum SortType {
CREATED_DATE("생성일 순", goods.createdDate),
MODIFIED_DATE("수정일 순", goods.modifiedDate);
private final String description;
private final Expression<? extends Comparable<?>> expression;
}
초기에 SortType을 생성하였을 때에는 Expresson 필드를 생성하여, Querydsl에 바로 사용 할수 있도록 구현하였다.
하지만 이 코드의 문제점은
- 상품(goods) 에만 종속적이다.
- 그렇다면 SortType에 user.createdDate, user.modifiedDate....추가해 준다면?
나중에 특정 API에서만 사용해야 하는 정렬이 있다면 valid검증이 까다로워진다. - 그렇다고 사용하는 API마다 SortType을 생성하여 제네릭타입으로 PageDto를 처리하기에는, 중복 코드도 늘어나고 추후 유지보수에서 굉장히 피곤해 질것 같았다.
- 그리고 enum에 Q파일 Importer가 생기는데 이게 RestAPI 적으로 옳은 일일까..
그렇다고 리포지토리에서 처리해주기에는 PageDto가 값을 다 가지고 있으니 왠지 PageDto에서 처리하고 싶은 고집이 있었다. 일관성도 있고 말이다.
PathBuilder 기능 구현
StackOverFlow를 찾아보다 키워드 하나가 눈에 들어왔다. "PathBuilder" 감사합니다.

개선 SortType
@Getter
@RequiredArgsConstructor
public enum SortType {
CREATED_DATE("생성일 순", "createdDate"),
MODIFIED_DATE("수정일 순", "modifiedDate"),
private final String description;
private final String fieldName;
}
먼저 SortType Enum을 수정했다. Q파일 을 제외하고 createdDate, modifiedDate만 남겨두어 동적으로 경로를 생성할 것이다.
PathBuilder 동적 경로 생성
PathBuilder는 여러 생성자가 있다. 이번 프로젝트에서 활용한 생성자는 Class타입과, String타입 두개의 인자를 받는 생성자를 활용했다.
PathBuilder<Object> pathBuilder = new PathBuilder<>(Goods.class, "goods");
- PathBuilder를 생성할 때 상위 생성자를 호출하여 QueryDSL이 자동으로 pathMetadataFactory.forVariable("goods") 를 사용해서 pathMetadata를 생성한다.
- 따라서 필드에 "goods" 가 존재하지 않는다면 오류를 발생 시킨다.
PathMetadata pathMetadata =
PathMetadataFactory.forProperty(new PathBuilder<>(Sale.class, "sale"), "quantity");
PathBuilder<Object> customPathBuilder =
new PathBuilder<>(Sale.class, pathMetadata, PathBuilderValidator.DEFAULT);
PathMetadataFactory를 통해 더 복잡한 메타데이터를 생성하고 전달 할 수 있다. 예를 들어 상품의 판매수를 구하고 싶다면 새로운 "quantity" 이름을 가진 PathMetadata를 생성해 주면된다. 이 방식을 활용하여 추후 상품 판매량순에 대한 정렬을 구현할 수 있을 것 같다.
PathBuilder 의 getComparable()
QueryDSL의 PathBuilder 는 동적으로 Comparable 타입의 경로를 생성하는 역할을 한다.
파라미터
- String property : 동적으로 접근하려는 필드의 이름이다. ex) - createdDate
- Class<A> type : 해당 필드의 타입이다. 이 타입은 Comparable 인터페이스를 구현하는 타입만 들어올 수 있다.
리턴값
- ComparablePath<A> QueryDSL에서 사용되는 Comparable 타입의 경로 객체(QueryDSL 의 표현식)를 반환한다.
내부 동작
- validate : property 이름과 type이 올바른지 검증한다. 만약 유효하지 않다면 예외를 던진다.
ComparableExpressionBase<?> sortExpression =
pathBuilder.getComparable(sortType.getFieldName(), Comparable.class);
개선된 PageDto
@Getter
public class PageDto {
@Schema(description = "페이지 아이템 개수", example = "30", defaultValue = "10")
private int pageSize = 10;
@Schema(description = "현재 페이지 번호", example = "0", defaultValue = "0")
private int page = 0;
@Schema(description = "정렬 타입")
private SortType sortType;
@Schema(description = "오름차순 여부", example = "true", defaultValue = "true")
@JsonProperty("isAscending")
private boolean isAscending = true;
public PageDto(int pageSize, int page, SortType sortType, boolean isAscending) {
this.pageSize = pageSize;
this.page = page;
this.sortType = sortType;
this.isAscending = isAscending;
}
public OrderSpecifier<?> getOrderSpecifier(Class<?> entityClass, String alias) {
if (sortType == null) {
throw new IllegalArgumentException("SortType cannot be null");
}
PathBuilder<Object> pathBuilder = new PathBuilder<>(entityClass, alias);
ComparableExpressionBase<?> sortExpression = pathBuilder.getComparable(sortType.getFieldName(), Comparable.class);
return new OrderSpecifier<>(
isAscending ? Order.ASC : Order.DESC,
sortExpression
);
}
public Pageable toPageable() {
return PageRequest.of(page, pageSize);
}
}
사용방법
- pathMetadata 를 생성할 alias이름 지정
- Enum의 필드 이름도 경로를 잘 찾아 갈 수 있게 필드를 하나 생성해 준다.
ComparableExpressionBase<?> sortExpression =
pathBuilder.getComparable(sortType.getFieldName(), Comparable.class);
- 생성된 goods의 pathBuilder에 QueryDSL에서 사용되는 .Comparable 클래스가 반환된다.
return new OrderSpecifier<>(
isAscending ? Order.ASC : Order.DESC,
sortExpression
);
- 그후 isAscending을 검사하여 최종적으로 PageDto에서 OrderSpecifier을 반환한다.
이제 어느 곳에서든 PageDto를 활용하여
OrderSpecifier<?> orderSpecifier = pageDto.getOrderSpecifier(Goods.class, "goods");
OrderSpecifier<?> orderSpecifier = pageDto.getOrderSpecifier(User.class, "user");
OrderSpecifier<?> orderSpecifier = pageDto.getOrderSpecifier(Market,class, "market");
정렬로직을 생성할 수 있다.
회고
아직까지 완벽하게 마음에 들지는 않는다. PathBuilder에 대한 정보가 많이 없어 계속 실험해 보고 있는 중이다. 아직 개발 초기 단계라 특이사항은 없지만, 나중에 문제가 될 일은 없는지, 더 좋은 방법은 없는지 계속 공부해 봐야겠다.