Today I Learned
Spring 플러스 주차 개인 과제
조건 분기 방식으로 할 일 검색 기능 개선하기
이번 과제에서는 할 일 목록을 조회할 때 다양한 조건을 적용할 수 있도록 코드를 개선하는 문제를 풀어보았다.

먼저 `@RequestParam(required = false)`를 활용해 조건들을 필수값이 아니게 설정했다.
컨트롤러는 아래와 같이 구성했다.
@GetMapping("/todos")
public ResponseEntity<Page<TodoResponse>> getTodos(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String weather,
@RequestParam(required = false) LocalDateTime startDate,
@RequestParam(required = false) LocalDateTime endDate
) {
return ResponseEntity.ok(todoService.getTodos(page, size, weather, startDate, endDate));
}
커스텀 쿼리 메서드를 만들기 전에, 위 조건들을 `if`문으로 분기 처리하면 어떻게 되는지 직접 구현해봤다.
@Transactional(readOnly = true)
public Page<TodoResponse> getTodos(int page, int size, String weather, LocalDateTime startDate, LocalDateTime endDate) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Todo> todos;
if (weather != null && startDate != null && endDate != null) {
todos = todoRepository.findByWeatherAndModifiedAtBetweenOrderByModifiedAtDesc(weather, startDate, endDate, pageable);
} else if (weather != null && startDate != null) {
todos = todoRepository.findByWeatherAndModifiedAtAfterOrderByModifiedAtDesc(weather, startDate, pageable);
} else if (weather != null && endDate != null) {
todos = todoRepository.findByWeatherAndModifiedAtBeforeOrderByModifiedAtDesc(weather, endDate, pageable);
} else if (weather != null) {
todos = todoRepository.findByWeatherOrderByModifiedAtDesc(weather, pageable);
} else if (startDate != null && endDate != null) {
todos = todoRepository.findByModifiedAtBetweenOrderByModifiedAtDesc(startDate, endDate, pageable);
} else if (startDate != null) {
todos = todoRepository.findByModifiedAtAfterOrderByModifiedAtDesc(startDate, pageable);
} else if (endDate != null) {
todos = todoRepository.findByModifiedAtBeforeOrderByModifiedAtDesc(endDate, pageable);
} else {
todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);
}
return todos.map(todo -> new TodoResponse(
...
}
그 후 서비스에서 호출할 수 있도록 `TodoRepository`에 필요한 메서드들을 모두 정의했다.
모든 쿼리에는 `fetch join`을 적용해 `User` 정보를 함께 가져오도록 구성했다.
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user WHERE t.weather = :weather ORDER BY t.modifiedAt DESC")
Page<Todo> findByWeatherOrderByModifiedAtDesc(@Param("weather") String weather, Pageable pageable);
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user WHERE t.weather = :weather AND t.modifiedAt BETWEEN :start AND :end ORDER BY t.modifiedAt DESC")
Page<Todo> findByWeatherAndModifiedAtBetweenOrderByModifiedAtDesc(@Param("weather") String weather, @Param("start") LocalDateTime start, @Param("end") LocalDateTime end, Pageable pageable);
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user WHERE t.weather = :weather AND t.modifiedAt >= :start ORDER BY t.modifiedAt DESC")
Page<Todo> findByWeatherAndModifiedAtAfterOrderByModifiedAtDesc(@Param("weather") String weather, @Param("start") LocalDateTime start, Pageable pageable);
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user WHERE t.weather = :weather AND t.modifiedAt <= :end ORDER BY t.modifiedAt DESC")
Page<Todo> findByWeatherAndModifiedAtBeforeOrderByModifiedAtDesc(@Param("weather") String weather, @Param("end") LocalDateTime end, Pageable pageable);
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user WHERE t.modifiedAt BETWEEN :start AND :end ORDER BY t.modifiedAt DESC")
Page<Todo> findByModifiedAtBetweenOrderByModifiedAtDesc(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end, Pageable pageable);
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user WHERE t.modifiedAt >= :start ORDER BY t.modifiedAt DESC")
Page<Todo> findByModifiedAtAfterOrderByModifiedAtDesc(@Param("start") LocalDateTime start, Pageable pageable);
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user WHERE t.modifiedAt <= :end ORDER BY t.modifiedAt DESC")
Page<Todo> findByModifiedAtBeforeOrderByModifiedAtDesc(@Param("end") LocalDateTime end, Pageable pageable);
@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
코드량이 어마어마해졌다!
`if` 분기 방식은 직관적이지만, 메서드 수가 폭발적으로 늘어나고, 유지보수 측면에서 부담이 커진다는 걸 체감할 수 있었다.
결국 조건이 많아질수록 JPQL의 유연한 조건 처리나 QueryDSL을 활용한 동적 쿼리 방식으로 넘어가는 것이 자연스러운 과정이라는 걸 깨달았다.
그래도 이렇게 직접 분기 방식으로 구현해보는 경험을 가져보는 것 좋았다고 생각한다.
조건 분기 → 한방 쿼리로 리팩토링하기!
이제는 위에서 분기했던 모든 조건을 하나의 JPQL 쿼리로 통합해보려 한다.
`(:param IS NULL OR 조건)` 패턴을 활용하면, 조건이 있을 때만 필터링되고 없으면 무시되기 때문에 훨씬 깔끔한 구조가 됐다.
@Query("""
SELECT t FROM Todo t
LEFT JOIN FETCH t.user
WHERE (:weather IS NULL OR t.weather = :weather)
AND (:startDate IS NULL OR t.modifiedAt >= :startDate)
AND (:endDate IS NULL OR t.modifiedAt <= :endDate)
ORDER BY t.modifiedAt DESC
""")
Page<Todo> searchTodos(
@Param("weather") String weather,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate,
Pageable pageable
);
이제 조건이 `null`일 경우 자동으로 무시되고, 쿼리 하나로 모든 경우를 처리할 수 있게 되었다.
@Transactional(readOnly = true)
public Page<TodoResponse> getTodos(int page, int size, String weather, LocalDateTime startDate, LocalDateTime endDate) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Todo> todos = todoRepository.searchTodos(weather, startDate, endDate, pageable);
return todos.map(todo -> new TodoResponse(
...
}
코드가 훨씬 깔끔해져서 눈에 잘 들어오는 구조가 되었다.
이제 새로운 조건이 추가되더라도 로직을 손대지 않고 쿼리만 확장하면 되니 유지보수도 한결 수월해졌다.
마치며
직접 if 문으로 모든 조건을 분기해보면서, 그 방식의 한계와 복잡함을 몸소 느낄 수 있었다.
이번 리팩토링을 통해, 쿼리를 하나로 통합하면서도 유연하게 조건을 처리할 수 있다는 걸 배웠고,
결국 실무에서는 이런 구조가 유지보수와 확장성 모두를 만족시킨다는 걸 깨달았다.
앞으로 조건이 많아지는 검색 기능을 구현할 때는, 처음부터 이런 구조를 염두에 두고 설계하는 습관을 가져야겠다고 느꼈다.