본문 바로가기
TIL

Plmography 프로젝트 작업 로그 #16 - RestTemplate 으로 api 호출하고 DB 에 저장하기 (22.12.28 TIL)

by winteringg 2022. 12. 28.

이전에는 HttpURLConnection 과 JsonParser 를 사용해 api 를 호출하는 방식을 사용했었는데 이번에는 홀맨님의 조언에 따라 RestTemplate 으로 바꿔보는 작업을 진행했다.

이전 코드는 아래와 같다!

// Controller

@GetMapping("/saveApi")
    public String saveApi() throws IOException {
        String result = "";

        try {
            for (int i = 1; i <= 500; i += 1) {
                String apiURL = "https://api.themoviedb.org/3/movie/popular?api_key=" + KEY
                        + "&language=ko&page=" + i;

                URL url = new URL(apiURL);

                HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
                urlConnection.setRequestMethod("GET");
                urlConnection.setRequestProperty("Content-type", "application/json");

                BufferedReader bf = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "UTF-8"));

                result = bf.readLine();

                getApiService.saveApiInfo(result);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return "ok";
    }
    
 // Service
 
 public String saveApiInfo(String result) {

        JsonObject jsonObject = JsonParser.parseString(result).getAsJsonObject();

        JsonArray list = (JsonArray) jsonObject.get("results");

        for (int i = 0; i < list.size(); i += 1) {
            JsonObject contents = (JsonObject) list.get(i);

            String imageUrl = "https://image.tmdb.org/t/p/original";
            String match = "[\"]";

            contentRepository.save(
                    Content.builder()
                            .description(contents.get("overview").toString().replaceAll(match, ""))
                            .korTitle(contents.get("title").toString().replaceAll(match, ""))
                            .engTitle(contents.get("original_title").toString().replaceAll(match, ""))
                            .releaseDate(contents.get("release_date")
                                    == null ? ""
                                    : contents.get("release_date").toString().replaceAll(match, ""))
                            .genres(contents.get("genre_ids").toString().replaceAll(match, ""))
                            .imageUrl(imageUrl + contents.get("poster_path").toString().replaceAll(match, ""))
                            .createdAt(dateTime)
                            .updatedAt(dateTime)
                            .build()
            );
        }
        return "ok";
    }

먼저 RestTemplate 과 WebClient 에 대해서 알아보자. 스프링은 REST 서비스에서 endpoint (API가 서버에서 리소스에 접근할 수 있도록 가능하게 하는 URL)를 호출하는 2가지 방법이 제공하는데, 바로 RestTemplate 과 WebClient 이다. 일단 기존에 많이 사용되던 RestTemplate 은 Spring 5.0부터는 deprecated 될 예정이므로 스프링에서는 WebClient 사용을 권장하고 있다.

RestTemplate 은 api 요청을 하게 되면 한 api 의 요청에 대한 응답이 올 때까지 기다리는 동기 방식이다. 반대로 비동기 방식에는 WebClient 가 있다. WebClient 를 사용하기 위해서는 webflux 와 mono 를 익혀야 한다. 이번 프로젝트에서는 RestTemplate 를 사용했지만 프로젝트 완성 후 WebClient 를 차차 공부해서 리팩토링 하는 작업을 진행해보는 것을 내 2023년 목표로 세웠다.

이제 RestTemplate 으로 바꿔보는 작업을 시작해 보자! 이 방법은 spring boot 를 사용한다는 것을 전제로 한다. 일단 먼저 RestTemplate config 설정 파일을 utills 패키지에 만든다. RestTemplate를 사용할 때마다 객체를 생성해서 사용할 수 도 있지만, bean으로 등록하여 사용하는 방법을 택하였다.

rootUri() 메서드에 호출 할 api 의 url 을 넣어주면 되고, 보통 api 주소의 공통 부분까지 넣어놓으면 편하다. 나는 Tmdb 의 api url 을 넣어놓았다. 그리고 BufferingClientHttpRequestFactory 객체를 통해 아래 코드와 같이 타임아웃을 제어할 수 있다. 만약 타임아웃이 필요 없다면 "RestTemplate resttemplate = new RestTemplate();" 이렇게 생성해도 된다.

에러가 발생할 경우, 에러 응답이 body로 넘어오게 되는데, 어떤 에러를 받을 것 인지, 받고 난 후 어떻게 처리할 것 인지를 따로 정의할 수 있다. 이를 errorHandler() 에 넣어준다.

@Configuration
@RequiredArgsConstructor
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder
                .requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
                .setConnectTimeout(Duration.ofMillis(5000)) // connection-timeout
                .setReadTimeout(Duration.ofMillis(5000)) // read-timeout
                .additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")))
                .rootUri(String.format("https://api.themoviedb.org/3"))
                .errorHandler(new RestTemplateResponseErrorHandler())
                .build();
    }
}

RestTemplateResponseErrorHandler 클래스. 정해진 에러코드만 받아서 처리했다.

public class RestTemplateResponseErrorHandler implements ResponseErrorHandler {

  @Override
  public boolean hasError(ClientHttpResponse response) throws IOException {
    return response.getStatusCode() == HttpStatus.BAD_REQUEST
      || response.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR;
  }

  @Override
  public void handleError(ClientHttpResponse response) throws IOException {

  }
}

본격적으로 데이터를 호출해보자. (LocalDateTime 과 api key 는 내 로직에 필요한 코드라서 api key 가 필요 없는 api 의 경우 생략해도 된다.)

HttpHeaders 로 header 를 정의해 주고, url 을 정의해 주고, restTemplate.exchange() 메서드로 api 를 호출한다.

ResponseEntity<Map> resultMap = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);

이 한 줄의 코드로 api 를 호출해 map 으로 전달받는다. 전달받는 값은 json 형태이므로 String 자체로 받아서 파싱해도 된다. restTemplate 의 경우 map 이나 사용자가 정의한 class 등 다양한 형태로 데이터를 파싱해서 받을 수 있다. 예시는 아래와 같다.

ResponseEntity<원하는 클래스 타입> result = restTemplate.exchange(url, HttpMethod.GET, entity, 원하는 클래스 타입.class);

내 실제 코드에는 없지만 호출 결과로 http status code, 헤더 정보, 실제 데이터가 존재하는 body정보를 확인할 수 있다. 데이터를 제대로 전달받았는지 확인하려면 이 api 호출 코드와 아래 코드들을 더 추가해주면 된다.

//api 호출 코드
ResponseEntity<Map> resultMap = restTemplate.exchange(uri.toString(), HttpMethod.GET, entity, Map.class);
    
    result.put("statusCode", resultMap.getStatusCodeValue()); //http status code를 확인
    result.put("header", resultMap.getHeaders()); //헤더 정보 확인
    result.put("body", resultMap.getBody()); //실제 데이터 정보 확인

    //데이터를 제대로 전달 받았는지 확인 string형태로 파싱해줌
    ObjectMapper mapper = new ObjectMapper();
    String jsonInString = mapper.writeValueAsString(resultMap.getBody());


    return jsonInString;

원하는 데이터만 뽑아내기 위해서는 resetTemplate.exchange() 메서드 호출 이후 코드를 아래와 같이 추가해주면 된다. 

ArrayList<Map> list = (ArrayList<Map>) resultMap.getBody().get("results");

내가 db 에 저장하려는 json 형태는 다음과 같은데, body 에서 "results" 라는 이름이 가진 value 에 접근하겠다는 의미이다.

그리고 그 저장된 list 를 forEach 문으로 반복하면서 하나씩 레포지토리에 저장해주면 완성! db 에 잘 들어가 있는 것을 확인할 수 있다.

아래는 전체 코드이다. 참고로, 원하는 값만 뽑아서 저장하기 위해 builder 가 사용되었는데, 이렇게 빌드를 해주기 위해서는 Content 라는 엔티티 클래스에다 @Builder 어노테이션을 꼭 붙여주어야 한다.

@RestController
@RequestMapping("/api")
public class BackdoorController {
    private final RestTemplate restTemplate;
    private final ContentRepository contentRepository;
    private final PerformerRepository performerRepository;

    @Value("${tmdb.api-key}")
    private String apiKey;

    LocalDateTime dateTime = LocalDateTime.now();

    public BackdoorController(RestTemplate restTemplate,
                              ContentRepository contentRepository,
                              PerformerRepository performerRepository) {
        this.restTemplate = restTemplate;
        this.contentRepository = contentRepository;
        this.performerRepository = performerRepository;
    }

    @GetMapping("/setupContents")
    public String setupContents() throws IOException {
        HttpHeaders headers = new HttpHeaders();
        HttpEntity<?> entity = new HttpEntity<>(headers);

        for (int i = 1; i <= 10; i += 1) {
            String url = "/movie/popular?"
                    + "api_key=" + apiKey + "&language=ko&page=" + i;

            ResponseEntity<Map> resultMap = restTemplate.exchange(url, HttpMethod.GET, entity, Map.class);

            ArrayList<Map> list = (ArrayList<Map>) resultMap.getBody().get("results");

            for (Map data : list) {
                String imageUrl = "https://image.tmdb.org/t/p/original";
                String match = "[\"]";

                contentRepository.save(
                        Content.builder()
                                .tmdbContentId(data.get("id").toString().replaceAll(match, ""))
                                .tmdbGenreId(data.get("genre_ids").toString().replaceAll(match, ""))
                                .imageUrl(imageUrl + data.get("poster_path").toString().replaceAll(match, ""))
                                .korTitle(data.get("title").toString().replaceAll(match, ""))
                                .engTitle(data.get("original_title").toString().replaceAll(match, ""))
                                .releaseDate(data.get("release_date")
                                        == null ? ""
                                        : data.get("release_date").toString().replaceAll(match, ""))
                                .popularity(data.get("popularity").toString().replaceAll(match, ""))
                                .description(data.get("overview").toString().replaceAll(match, ""))
                                .createdAt(dateTime)
                                .build()
                );
            }
        }

        return "Contents completely saved!";
    }
}

 

 

댓글