2025. 3. 14. 21:35ㆍDevelop/Spring
저번 글에 이어서 QueryDSL을 이용해서 이제 더 복잡한 쿼리들을 작성할 수 있게 되었다!
이런 상황을 가정해 보자.
일정을 불러올때 검색 조건이
1. 제목을 이용한 검색
2. 생성일을 기준으로 구간 검색
3. 일정의 매니저 이름으로 검색 (일정은 생성 시에 작성한 유저로 자동등록된다 but 일정은 여러개의 유저를 가질 수 있다)
와 같을 때
컨트롤러를 이렇게 작성할 수 있다.
@GetMapping("/todos/querydsl")
public ResponseEntity<Page<ProjectionTodoResponse>> getTodosByQueryDSL(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String title,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate,
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate,
@RequestParam(required = false) String managerNickname)
required = false 조건을 이용해 여러가지 다양한 검색조건의 조합이 될 수 있도록 한다.
그럼 이렇게 검색조건의 시나리오가 다양한데 (3개의 조건인데 3x2x1로 벌써 6개의 시나리오다) 그럼 시나리오별로 쿼리를 작성해서 메서드를 만들어야하나??
// // 제목 검색
// Page<ProjectionTodoResponse> findByTitle(String title, Pageable pageable); // 메서드 이름 나중에 바꿔야 할듯
//
// // 생성일 기간 검색
// Page<ProjectionTodoResponse> findByCreatedAtBetween(LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
//
// // 매니저 이름 검색
// Page<ProjectionTodoResponse> findByManagerNickname(String managerNickname, Pageable pageable);
//
// // 제목 검색 & 생성일 기간 검색
// Page<ProjectionTodoResponse> findByTitleAndCreatedAtBetween(String title, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
//
// // 제목 검색 & 매니저 이름 검색
// Page<ProjectionTodoResponse> findByTitleAndManagerNickname(String title, String managerNickname, Pageable pageable);
//
// // 제목 검색 & 생성일 기간 검색 & 매니저 이름 검색
// Page<ProjectionTodoResponse> findByTitleAndManagerNicknameAndCreatedAtBetween(String title, String managerNickname, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
//
// // 아무 조건도 걸지 않은 경우
// Page<ProjectionTodoResponse> findAllProjection(Pageable pageable);
BooleanExpression을 알지못하는 예전의 내가 이렇게 시나리오별로 쿼리메서드를 다 만들었었다....
이렇게 하다보니 어떠한 쿼리메서드를 적용할 지 서비스에서 조건문을 만들어야 했고
if ((startDate == null && endDate == null) &&title != null && managerNickname == null) {
todos = todoRepository.findByTitle(title, pageable);
} else if ((startDate != null && endDate != null) &&title == null && managerNickname == null) {
todos = todoRepository.findByCreatedAtBetween(startDate, endDate, pageable);
} else if (managerNickname != null && (startDate == null && endDate == null) &&title == null ) {
todos = todoRepository.findByManagerNickname(managerNickname, pageable);
} else if (startDate != null && endDate != null && managerNickname == null) {
todos = todoRepository.findByTitleAndCreatedAtBetween(title, startDate, endDate, pageable);
} else if (startDate == null && endDate == null && title != null && managerNickname != null) {
todos = todoRepository.findByTitleAndManagerNickname(title, managerNickname, pageable);
} else if(title != null && startDate != null && endDate != null && managerNickname != null) {
todos = todoRepository.findByTitleAndManagerNicknameAndCreatedAtBetween(title, managerNickname, startDate, endDate, pageable);
} else {
todos = todoRepository.findAllProjection(pageable);
}
이런 끔찍한 else if 문이 만들어졌다..
코드를 직접 작성한 내가 봐도 어느 조건문에서 무슨 조건을 적용하고 있는지 한참을 봐야한다.
이건 아닌 것 같아 싶어서 코드를 어떻게 리팩토링하면 좋을 지, 아니면 내가 지금 잘못하고 있는 건지 튜터님께 도움을 청했다.
그 후 BooleanExpression에 대한 존재를 알게되었다!
BooleanExpression이란 WHERE 절의 조건을 동적으로 만들고, 여러 개의 조건을 조합할 때 유용하게 사용되는 클래스이다. BooleanExpression은 null 반환 시 자동으로 조건절에서 제거 된다.
직접 코드를 보는 것이 더 쉽게 이해가 된다.
private BooleanExpression titleCheck(String title) {
return title != null ? todo.title.containsIgnoreCase(title) : null;
}
private BooleanExpression DateCheck(LocalDateTime startDate, LocalDateTime endDate) {
return startDate != null & endDate!= null ? todo.createdAt.between(startDate, endDate) : null;
}
private BooleanExpression managerNicknameCheck(String managerNickname) {
return managerNickname != null ? user.nickname.containsIgnoreCase(managerNickname) : null;
}
앞서 보았던 3가지의 조건을 이렇게 BooleanExpression을 이용해서 3개의 함수로 만들 수 있다.
만약 사용자가 어느 한 조건을 적용하지 않았다면, 파라미터가 null로 들어오게 되고 삼항연산자에서 null 값에 해당되어 where조건 절에서 제거 된다.
null값이 아니라면 where 절에 적어 줄 조건들을 반환하게 된다.
@Override
public Page<ProjectionTodoResponse> findByTitleOrCreatedAtOrManagerNickname(String title, LocalDateTime startDate, LocalDateTime endDate, String managerNickname, Pageable pageable) {
List<ProjectionTodoResponse> results = jpaQueryFactory.select(
Projections.constructor(ProjectionTodoResponse.class,
todo.title,
manager.countDistinct(),
comment.countDistinct())
).from(todo)
.leftJoin(todo.managers, manager)
.leftJoin(todo.comments, comment)
.leftJoin(manager.user, user)
.where(titleCheck(title),
DateCheck(startDate, endDate),
managerNicknameCheck(managerNickname))
.groupBy(todo.id)
.offset(pageable.getOffset()) // 페이지 시작 위치
.limit(pageable.getPageSize()) // 페이지 크기
.fetch();// 전체 개수와 함께 가져오기
return new PageImpl<>(results, pageable, results.size());
}
QueryDSL의 where에서 BooleanExpression함수들을 사용해주면 이제 하나의 쿼리메서드로 처리할 수 있다!
그런데 코드를 보면 저번에는 보지 못한 Projections라는 것이 보인다.
Projection이란?
Projections는 QueryDSL에서 특정 필드만 선택적으로 조회할 때 사용하는 기능이다.
즉, 엔티티 전체가 아니라 필요한 데이터만 DTO로 매핑해서 반환할 때 사용한다.
SQL에서 SELECT * FROM todo 대신 SELECT title, comment_count FROM todo처럼 필요한 컬럼만 조회하는 것과 비슷한 개념이다.
사용법은 Projections를 사용할 DTO를 일단 만들어준다.
@Getter
@AllArgsConstructor
public class ProjectionTodoResponse {
private String title;
private Long managerCount;
private Long commentCount;
}
그리고 쿼리를 작성할 때 select에서 Projections.constructor를 이용해서 작성했던 Projection DTO를 명시해준다.
이는 생성자를 이용하는 방식인데, DTO의 생성자 매개변수 순서와 타입이 정확히 일치해야 한다.
생성자 방식 이외에도 필드기반, 세터기반 방식이 있는데 필드기반은 별칭을 사용해야하며 세터는 DTO에 세터를 설정한다는 것이 단점이다. 생성자 기반 방식이 제일 편리하고 위험성이 작은 것 같아서 채택했다.
jpaQueryFactory.select(
Projections.constructor(ProjectionTodoResponse.class,
todo.title,
manager.countDistinct(),
comment.countDistinct())
).from(todo)
이렇게 하면 todo를 조회해올 때 내가 Projection DTO에 명시한 값들만 가져올 수 있다!
+ QueryDSL에서 페이지네이션 하는 법
QueryDSL 파라미터로 pageable을 넣는다. 페이지 정보를 넘겨주는 것과 같다.
- offset(pageable.getOffset()) → 어디서부터 가져올지 설정 (몇 번째 데이터부터 가져올지)
- limit(pageable.getPageSize()) → 한 페이지에 몇 개의 데이터를 가져올지 설정
- fetch() → 데이터를 리스트로 가져오기
- fetchCount() 또는 fetch().size() → 전체 개수를 가져오기
- PageImpl<>(데이터 리스트, 페이지 정보, 전체 개수) → Spring Data의 Page 객체로 변환
List<ProjectionTodoResponse> results = jpaQueryFactory.select(
Projections.constructor(ProjectionTodoResponse.class,
todo.title,
manager.countDistinct(),
comment.countDistinct())
).from(todo)
.leftJoin(todo.managers, manager)
.leftJoin(todo.comments, comment)
.leftJoin(manager.user, user)
.where(titleCheck(title),
DateCheck(startDate, endDate),
managerNicknameCheck(managerNickname))
.groupBy(todo.id)
.offset(pageable.getOffset()) // 페이지 시작 위치
.limit(pageable.getPageSize()) // 페이지 크기
.fetch();// 전체 개수와 함께 가져오기
return new PageImpl<>(results, pageable, results.size());
쿼리 반환 값으로는 List가 넘어오기 때문에 return 할 때 PageImpl<>()을 통해서 페이지 자료구조로 넘겨준다.
'Develop > Spring' 카테고리의 다른 글
컴파일? 빌드? Gradle? gradle wrapper? gradlew? (0) | 2025.03.21 |
---|---|
스프링에서 예외를 처리하기 (0) | 2025.03.21 |
QueryDSL (0) | 2025.03.14 |
AOP? (0) | 2025.03.12 |
테스트 코드 (0) | 2025.03.12 |