1. 개요
내일배움 캠프 [Spring 5기] 플러스 주차 개인 과제 진행 중,
레벨 1단계 3. 코드 개선 퀴즈 - JPA의 이해 문제를 해결 하고 있었습니다.
해당 문제 풀이 과정을 작성하고자 합니다.
(1) 기존 코드
(2) 요구 사항
2. 컨트롤러 계층
[수정 전]
@GetMapping("/todos")
public ResponseEntity<Page<TodoResponse>> getTodos(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size
) {
return ResponseEntity.ok(todoService.getTodos(page, size));
}
(1) 첫 번째 요구사항 : weather 조건으로 검색하기
@RequestParam(required = false) String weather
날씨 정보를 쿼리 파라미터로 받을 수 있게 `@RequestParam`을 이용하였습니다.
weather 조건은 있을 수도 있고, 없을 수도 있기 때문에 속성을 `(required = false)`로 설정하였습니다.
위 설정을 해주지 않는다면, Defalut 값이 `required = true`이기 때문에 파라미터 입력을 필수로 해야 합니다.
🔍 쿼리 파라미터란?
HTTP 요청 시 URL의 `?` 뒤에 key=value 형태로 전달되는 값을 의미합니다.
이때, `@RequestParam`은 쿼리 파라미터를 매핑하는데 사용됩니다.
[HTTP 요청 예시]
GET : http://localhost:8080/todos?weather=Sunny
(2) 두 번째 요구사항 : 수정일 기준으로 기간 조회하기
@RequestParam(required = false) LocalDateTime startDate,
@RequestParam(required = false) LocalDateTime endDate
수정일 기준으로, 특정 기간을 조회하기 위해서
`startDate`와 `endDate`를 파라미터로 받아 필터링 할 수 있도록 구현하였습니다.
해당 값들은 LocalDateTime의 형식을 가지고 있기에,
요청 시 ISO 8601형식 (yyyy-MM-dd'T'HH:mm:ss)으로 요청해야 합니다.
LocalDateTime 형식으로 지정한 이유는
해당 형식이 연도, 월, 일, 시간을 가지고 있어 날짜 설정을 정밀하게 할 수 있기 때문입니다.
(3) 수정된 컨트롤러 클래스 코드
@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));
}
3. 서비스 계층
[수정 전]
public Page<TodoResponse> getTodos(int page, int size) {
Pageable pageable = PageRequest.of(page - 1, size);
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable);
return todos.map(todo -> new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
));
}
(1) 조건 추가하기
if (startDate == null) {
startDate = LocalDateTime.of(1, 1, 1, 0, 0);
}
if (endDate == null) {
endDate = LocalDateTime.of(9999, 12, 31, 23, 59);
}
Page<Todo> todos = todoRepository.findTodos(pageable, weather, startDate, endDate);
startDate와 endDate가 null인 경우, SQL 실행 시 오류 발생 가능성이 있습니다.
따라서, 서비스 계층에서 if 문을 이용하여 기본값을 설정하여 조회 조건을 보장되도록 하였습니다.
시작일과 종료일을 넉넉히 설정하여, 날짜 필터가 없어도 모든 데이터를 정상적으로 조회할 수 있습니다.
Repository에 `findTodos`메소드를 추가하여,
컨트롤러에서 전달 받은 값으로 필터링하여 조회하는 쿼리를 작성하였습니다.
(2) 수정된 서비스 클래스 코드
@Transactional(readOnly = true)
public Page<TodoResponse> getTodos(int page, int size, String weather, LocalDateTime startDate, LocalDateTime endDate) {
Pageable pageable = PageRequest.of(page - 1, size);
if (startDate == null) {
startDate = LocalDateTime.of(1, 1, 1, 0, 0);
}
if (endDate == null) {
endDate = LocalDateTime.of(9999, 12, 31, 23, 59);
}
Page<Todo> todos = todoRepository.findTodos(pageable, weather, startDate, endDate);
return todos.map(todo -> new TodoResponse(
todo.getId(),
todo.getTitle(),
todo.getContents(),
todo.getWeather(),
new UserResponse(todo.getUser().getId(), todo.getUser().getEmail()),
todo.getCreatedAt(),
todo.getModifiedAt()
));
}
4. 레포지토리 계층
(1) 추가한 레포지토리 클래스 코드
@Query("SELECT t FROM Todo t " +
"WHERE (:weather IS NULL OR t.weather = :weather) " +
"AND (t.modifiedAt BETWEEN :startDate AND :endDate) " +
"ORDER BY t.modifiedAt DESC")
Page<Todo> findTodos(
Pageable pageable,
@Param("weather") String weather,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate
);
(2) 코드 설명
🔍 @Query란?
Spring Data JPA에서 JPQL을 직접 정의하여 사용할 때 이용합니다.
@Query안에 엔티티 기준으로 JPQL 쿼리를 작성합니다.
1️⃣ 쿼리 설명
SELECT t FROM Todo t
t는 Todo 엔티티의 별칭(alias)이며,
해당 쿼리는 Todo 엔티티의 모든 데이터를 조회합니다.
WHERE (:weather IS NULL OR t.weather = :weather)
AND (t.modifiedAt BETWEEN :startDate AND :endDate)
WHERE 절을 사용하여 조건을 지정하였습니다.
`:weather`가 Null이면, 모든 weahter 값을 조회합니다.
값이 존재하면 `t.weather = :weather` 조건을 만족하는 데이터만 조회합니다.
이때, `t.weather`는 Todo 엔티티에 저장된 날씨 값이며,
`:weather`은 컨트롤러에 사용자가 조회를 위해 전달한 값입니다.
AND로 추가 조건을 작성하였습니다.
수정일 기준으로 특정 기간의 todo를 조회하는 것이므로,
특정 기간을 나타내기 위해 BETWEEN 를 이용하였습니다.
todo에 저장된 `t.modifiedAt`이 전달 받은 `:startDate`와 `:endDate` 사이에 있는 값을 조회합니다.
만약 startDate와 endDate가 null 값이라면,
서비스 계층에서 초기값으로 지정한 날짜를 전달 받기 때문에, 오류가 발생하지 않습니다.
ORDER BY t.modifiedAt DESC
수정일 기준으로 내림차순 정렬을 위해, ORDER BY DESC(내림차) 를 사용하였습니다.
2️⃣ 메소드 분석
Page<Todo> findTodos(
Pageable pageable,
@Param("weather") String weather,
@Param("startDate") LocalDateTime startDate,
@Param("endDate") LocalDateTime endDate
);
`@Param`을 사용하여 쿼리 파라미터로 전달 받은 값을 바인딩하여 쿼리 작성에 사용할 수 있습니다.
예시
JPQL 변수 `:weather`에 weather 값을 전달
JPQL 변수 `:startDate`에 startDate 값을 전달
JPQL 변수 `:endDate`에 endDate 값을 전달
🔍 @Param이란?
JPQL 또는 네이티브 쿼리에서, SQL파라미터와 매개변수를 연결하는 역할을 합니다.
즉, 쿼리의 바인딩변수(:변수명)과 메소드의 매개변수를 매핑합니다.