
@Component
public class WeatherClient {
private final RestTemplate restTemplate;
public WeatherClient(RestTemplateBuilder builder) {
this.restTemplate = builder.build();
}
public String getTodayWeather() {
ResponseEntity<WeatherDto[]> responseEntity =
restTemplate.getForEntity(buildWeatherApiUri(), WeatherDto[].class);
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
}
WeatherDto[] weatherArray = responseEntity.getBody();
if (weatherArray == null || weatherArray.length == 0) {
throw new ServerException("날씨 데이터가 없습니다.");
}
String today = getCurrentDate();
for (WeatherDto weatherDto : weatherArray) {
if (today.equals(weatherDto.getDate())) {
return weatherDto.getWeather();
}
}
throw new ServerException("오늘에 해당하는 날씨 데이터를 찾을 수 없습니다.");
}
private URI buildWeatherApiUri() {
return UriComponentsBuilder
.fromUriString("https://f-api.github.io")
.path("/f-api/weather.json")
.encode()
.build()
.toUri();
}
private String getCurrentDate() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
return LocalDate.now().format(formatter);
}
}

🟢 WeatherClient.java 1️⃣
public WeatherClient(RestTemplateBuilder builder) {
this.restTemplate = builder.build(); //build()로 RestTemplate 객체 생성
}
💡
RestTemplateBuilder는 RestTemplate을 구성(configure) 할 수 있는 빌더 패턴을 제공하는 클래스다.
🟢 WeatherClient.java 2️⃣
public String getTodayWeather )() {
ResponseEntity<WeatherDto[]> responseEntity =
restTemplate.getForEntity(buildWeatherApiUri(), WeatherDto[].class);
if (!HttpStatus.OK.equals(responseEntity.getStatusCode())) {
throw new ServerException("날씨 데이터를 가져오는데 실패했습니다. 상태 코드: " + responseEntity.getStatusCode());
}
WeatherDto[] weatherArray = responseEntity.getBody();
if (weatherArray == null || weatherArray.length == 0) {
throw new ServerException("날씨 데이터가 없습니다.");
}
String today = getCurrentDate(); // 현재 날짜 가져오기
for (WeatherDto weatherDto : weatherArray) {
if (today.equals(weatherDto.getDate())) {
return weatherDto.getWeather(); //현재 날짜와 weatherDto의 날짜가 같다면 해당 날씨 데이터를 가져온다.
}
}
throw new ServerException("오늘에 해당하는 날씨 데이터를 찾을 수 없습니다.");
}
🟢 WeatherClient.java 3️⃣
private URI buildWeatherApiUri() {
return UriComponentsBuilder
.fromUriString("https://f-api.github.io")
.path("/f-api/weather.json")
.encode()
.build()
.toUri();
}
🟢 WeatherClient.java 4️⃣
private String getCurrentDate() {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
return LocalDate.now().format(formatter);
}
🟢 RestTemplateBuilder.class [build() 메소드] 1️⃣
public RestTemplate build() {
return this.configure(new RestTemplate()); //새로운 RestTemplate 생성하는 부분
}
💡
RestTemplate스프링3부터 지원하는 HTTP 통신 기능을 쉽게 사용할 수 있게 설계되어 있는 템플릿이다.
REST API 호출이후 응답을 받을 때까지 기다리는 동기방식으로 처리되며 RESTful 방식을 지키고 있다.
🟢 RestTemplateBuilder.class [configure() 메소드] 2️⃣
/**
* 다양한 configure 설정을 한다.
*/
public <T extends RestTemplate> T configure(T restTemplate) {
ClientHttpRequestFactory requestFactory = this.buildRequestFactory();
if (requestFactory != null) {
restTemplate.setRequestFactory(requestFactory);
}
this.addClientHttpRequestInitializer(restTemplate);
if (!CollectionUtils.isEmpty(this.messageConverters)) {
restTemplate.setMessageConverters(new ArrayList(this.messageConverters)); //메세지 변환기
}
if (this.uriTemplateHandler != null) {
restTemplate.setUriTemplateHandler(this.uriTemplateHandler); //uri 설정
}
if (this.errorHandler != null) {
restTemplate.setErrorHandler(this.errorHandler); //에러 처리
}
if (this.rootUri != null) {
RootUriBuilderFactory.applyTo(restTemplate, this.rootUri);
}
restTemplate.getInterceptors().addAll(this.interceptors);
if (!CollectionUtils.isEmpty(this.customizers)) {
for(RestTemplateCustomizer customizer : this.customizers) {
customizer.customize(restTemplate);
}
}
return restTemplate;
}
public TodoSaveResponse saveTodo(AuthUser authUser, TodoSaveRequest todoSaveRequest) {
User user = User.fromAuthUser(authUser);
String weather = weatherClient.getTodayWeather(); //getTodayWeather() 사용
Todo newTodo = new Todo(
todoSaveRequest.getTitle(),
todoSaveRequest.getContents(),
weather,
user
);
Todo savedTodo = todoRepository.save(newTodo);
return new TodoSaveResponse(
savedTodo.getId(),
savedTodo.getTitle(),
savedTodo.getContents(),
weather,
new UserResponse(user.getId(), user.getEmail())
);
}
일정 저장 시 weatherClient.getTodayWeather()로 현재 날씨 가져오기
📌 [Spring] - 스프링 RestTemplate, RestTemplateBuilder 클래스 사용 방법

🔎 일정의 modifiedAt 필드는
LocalDateTime 타입으로 설정되어 있다.
🟢 Controller
@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) String startAt,
@RequestParam(required = false) String endAt
) {
return ResponseEntity.ok(todoService.getTodos(page, size, weather, startAt, endAt));
}
🟢 Service
@Transactional(readOnly = true)
public Page<TodoResponse> getTodos(int page, int size, String weather, String startAt, String endAt) {
Pageable pageable = PageRequest.of(page - 1, size);
//입력된 기간의 시작과 끝 날짜 값을 LocalDateTime 타입으로 변환
LocalDateTime formatStartAt = null;
LocalDateTime formatEndAt = null;
//시작 기간이 입력된 경우
if (StringUtils.hasText(startAt)) {
LocalDate formatDateTimeStartAt = LocalDate.parse(startAt, DateTimeFormatter.ISO_LOCAL_DATE);
formatStartAt = formatDateTimeStartAt.atStartOfDay(); //LocalDate -> LocalDateTime으로 변경하기
}
//끝 기간이 입력된 경우
if (StringUtils.hasText(endAt)) {
LocalDate formatDateTimeEndAt = LocalDate.parse(endAt, DateTimeFormatter.ISO_LOCAL_DATE);
formatEndAt = formatDateTimeEndAt.atTime(LocalTime.MAX);
}
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(weather,formatStartAt, formatEndAt, 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()
));
}
@Query 어노테이션을 통해 날짜 조건 설정
🟢 Repository
@EntityGraph(attributePaths = {"user"})
@Query("SELECT t FROM Todo t " +
"WHERE (:weather IS NULL OR t.weather = :weather) " +
"AND (:startAt IS NULL OR :endAt IS NULL OR t.modifiedAt BETWEEN :startAt AND :endAt) " +
"ORDER BY t.modifiedAt DESC")
Page<Todo> findAllByOrderByModifiedAtDesc(
@Param("weather") String weather,
@Param("startAt") LocalDateTime startAt,
@Param("endAt") LocalDateTime endAt,
Pageable pageable);
LocalDate formatDateTimeStartAt = LocalDate.parse(startAt, DateTimeFormatter.ISO_LOCAL_DATE); //String -> LocalDate 변환
formatStartAt = formatDateTimeStartAt.atStartOfDay(); //LocalDate -> LocalDateTime으로 변환하기
💡 LocalDate.parse(startAt, DateTimeFormatter.ISO_LOCAL_DATE)
➡︎ 입력된 String 타입
startAt을 LocalDate의 ISO_LOCAL_DATE (2025-03-20) 형태로 파싱한다는 의미이다.
💡 formatDateTimeStartAt.atStartOfDay()
➡︎ LocalDate 타입의
formatDateTimeStartAt을 2025-03-20 00:00:00.00000000 형태의 LocalDateTime 타입으로 만든다는 의미이다.
📌 Reference
🔎 일정의 modifiedAt 필드는
LocalDateTime 타입으로 설정되어 있다.
🟢 SearchRequest
@Getter
@AllArgsConstructor
public class SearchRequest {
private String weather;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate startAt;
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate endAt;
}
💡
@DateTimeFormat(pattern = "yyyy-MM-dd")를 사용하여 입력 패턴을 지정한다.
🟢 Controller
@GetMapping("/todos")
public ResponseEntity<Page<TodoResponse>> getTodos(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@Valid @ModelAttribute SearchRequest searchRequest
) {
return ResponseEntity.ok(todoService.getTodos(page, size, searchRequest));
}
🟢 Service
@Transactional(readOnly = true)
public Page<TodoResponse> getTodos(int page, int size, SearchRequest searchRequest) {
Pageable pageable = PageRequest.of(page - 1, size);
//입력된 기간의 시작과 끝 날짜 값을 LocalDateTime 타입으로 변환
LocalDateTime formatStartAt = null;
LocalDateTime formatEndAt = null;
//시작 기간이 입력된 경우
if (!ObjectUtils.isEmpty(searchRequest.getStartAt()) ) {
formatStartAt = searchRequest.getStartAt().atStartOfDay();
}
//끝 기간이 입력된 경우
if (!ObjectUtils.isEmpty(searchRequest.getEndAt())) {
formatEndAt = searchRequest.getEndAt().atTime(LocalTime.MAX);
}
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(searchRequest.getWeather(),formatStartAt, formatEndAt, 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()
));
}
LocalDate 타입으로 값을 받기 때문에 LocalDate로 변환하는 부분을 제거해었다.
📌 Reference
[Spring] LocalDateTime 원하는 Format으로 바인딩하기 (feat.@DateTimeFormat, @JsonFormat)
이번 과제에서는 @RequestParam, @ModelAttribute를 이용해서 검색 기능을 구현하였다.
1️⃣ @RequestParam을 통해 값을 입력 받도록 하였을 경우

위의 사진과 같이 조건이 늘어날 때마다 매개변수를 추가해 주어야한다.
이 방식을 사용할 경우 service에 값을 넘겨줄 때 하나라도 순서가 바뀌면 잘못된 값이 전달되어 오류가 발생할 수 있다.
➡︎ 조건이 많아질수록 코드가 길어지고 오류 발생 가능성이 커진다.
2️⃣ @ModelAttribute로 할 경우

검증 로직을 추가해서 잘못된 값이 입력되는 것을 사전에 차단할 수 있다.