[과제 과정] 1️⃣다른 서버의 API 데이터 가져오기2️⃣ @RequestParam vs @ModelAttribute

늘보·2025년 3월 20일

Spring

목록 보기
15/24
post-thumbnail

🔎 다른 서버의 API 데이터 가져오기 ( Weather )

전체 코드

@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 객체 생성 
}

💡 RestTemplateBuilderRestTemplate을 구성(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;
    }

🟠 weatherClient.getTodayWeather() 사용

 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 클래스 사용 방법


🔎 @RequestParam 사용 (feat. 날짜 Pattern 변환하기)

🔎 일정의 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);



Pattern 변환 부분

LocalDate formatDateTimeStartAt = LocalDate.parse(startAt, DateTimeFormatter.ISO_LOCAL_DATE); //String -> LocalDate 변환
formatStartAt = formatDateTimeStartAt.atStartOfDay(); //LocalDate -> LocalDateTime으로 변환하기

💡 LocalDate.parse(startAt, DateTimeFormatter.ISO_LOCAL_DATE)

➡︎ 입력된 String 타입 startAtLocalDate의 ISO_LOCAL_DATE (2025-03-20) 형태로 파싱한다는 의미이다.

💡 formatDateTimeStartAt.atStartOfDay()

➡︎ LocalDate 타입의 formatDateTimeStartAt 2025-03-20 00:00:00.00000000 형태의 LocalDateTime 타입으로 만든다는 의미이다.


📌 Reference

LocalDateTime format, pattern으로 변환하여 표현하기

LocalDate to LocalDateTime


🔎 @ModelAttribute 사용 (feat. 날짜 Pattern 변환하기)

🔎 일정의 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

이번 과제에서는 @RequestParam, @ModelAttribute를 이용해서 검색 기능을 구현하였다.

1️⃣ @RequestParam을 통해 값을 입력 받도록 하였을 경우

위의 사진과 같이 조건이 늘어날 때마다 매개변수를 추가해 주어야한다.

이 방식을 사용할 경우 service에 값을 넘겨줄 때 하나라도 순서가 바뀌면 잘못된 값이 전달되어 오류가 발생할 수 있다.

➡︎ 조건이 많아질수록 코드가 길어지고 오류 발생 가능성이 커진다.


2️⃣ @ModelAttribute로 할 경우

  • 조건이 늘어나더라도 Controller에서는 추가해야하는 매개변수가 생기지 않는다.
  • 매개변수의 순서도 신경쓰지 않아도 된다.
  • 내가 원하는 검증 로직을 추가해서 잘못된 값이 입력되는 것을 사전에 차단할 수 있다.

➡︎ 이러한 이유로 검색 기능과 같이 조건이 여러개 들어가는 경우에는 @RequestParam보다 @ModelAttribute를 사용하는 것이 더 좋을 것 같다는 생각을 하였다.

profile
누워만 있지 말고 제발 뭐라도 하자.

0개의 댓글