Framework/Spring

[SpringBoot] 외부 API 호출하기(1)

jyjyjy25 2024. 4. 18. 20:21

WebClient 선정 이유

최근 Spring 3.2부터 추가된 RestClient를 사용하면 기존의 RestTemplate의 직관적이지 못한 사용성과 WebClient의 의존성 문제를 해결할 수 있다. 하지만 외부 여러 API를 통해 대량의 데이터를 조회해야 하므로 비동기적 수행이 필수적이다. RestClient에서는 비동기 기능을 찾아볼 수 없었으므로 비동기 방식을 지원하는 WebClient를 사용하려고 한다.

 

WebClientConfig

@Configuration
public class WebClientConfig {

    @Value("${open-api.base-url}")
    private String BASE_URL;

    @Bean
    public WebClient pharmacyWebClient() {
        HttpClient httpClient = HttpClient.create()
            .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
            .responseTimeout(Duration.ofMillis(10000))
            .doOnConnected(conn ->
                conn.addHandlerLast(new ReadTimeoutHandler(50000))
                    .addHandlerLast(new WriteTimeoutHandler(10000)));

        return WebClient.builder()
            .baseUrl(BASE_URL)
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
    }
}

  • timeout과 baseurl을 지정한다.

 

WebClient를 통해 Get 요청 보내기

PharmacyItems pharmacyItems = webClient.get()
      .uri(uriBuilder -> uriBuilder
          .path(PHARMACY_ENDPOINT)
          .queryParam("serviceKey", PHARMACY_API_KEY)
          .build())
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(PharmacyItems.class)
      .retry(3)
      .block();

try {
    pharmacyItems.getPharmacyItems().forEach(item -> {
        Pharmacy pharmacy = item.toEntity();
        pharmacyRepository.save(pharmacy);
    });
} catch (DataIntegrityViolationException e) {
}

조회한 데이터를 pharmacyItems 형식으로 받아오고, 이를 엔티티로 변환하여 DB에 저장하는 로직이다.

  • url(): 엔드포인트와 쿼리 파라미터로 serviceKey를 추가하여 요청 URI를 설정한다.
  • accept(): 요청 헤더에서 Accept 헤더를 설정한다.
  • retrieve(): HTTP의 호출 결과를 가져오는 방법 중 하나로, responseBody를 바로 처리하기 위해 사용한다.
  • bodyToMono(): 응답 body를 Mono로 변환하고, 인자로 전달하는 PharmacyItems에 데이터를 매핑한다.
  • retry(3): 오류 발생 시 주어진 횟수만큼 요청을 재시도한다.
  • block(): Mono의 결과를 블로킹으로 동기적으로 받아온다.

 

XML → JSON 매핑하기

@Data
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class PharmacyItems {
    @JsonProperty("item")
    private List<PharmacyItem> pharmacyItems;

    @JsonCreator
    public PharmacyItems(@JsonProperty("response") JsonNode node) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();

        JsonNode itemNode = node.findValue("item");
        this.pharmacyItems = Arrays.stream(objectMapper.treeToValue(itemNode, PharmacyItem[].class)).toList();
    }

}

공공데이터포털에서 제공하는 데이터의 형식은 text/xml이므로 이를 json으로 매핑하여 사용하려고 한다.

응답 데이터 형식을 분석하고 데이터가 있는 위치를 찾아 파싱한다.

 

이슈 1: UnsupportedMediaTypeException

Caused by: org.springframework.web.reactive.function.UnsupportedMediaTypeException: Content type 'text/xml;charset=UTF-8' not supported for bodyType=dac2dac.doctect.agency.vo.pharmacyItems at org.springframework.web.reactive.function.BodyExtractors.readWithMessageReaders(BodyExtractors.java:205) ~[spring-webflux-6.1.4.jar:6.1.4]

공공데이터포털에서 제공하는 데이터의 형식은 text/xml이다. 이를 json으로 매핑하는 과정에서 발생하는 예외이다. 이를 해결하기 위해 다음과 같은 방법을 사용한다.

 

방법 1: retry(3)

HTTP 요청은 네트워크 지연, 일시적인 서버 오류, 잘못된 요청 등 다양한 이유로 실패할 수 있기 때문에 에러 발생 시 주어진 횟수만큼 요청을 재시도한다. 이를 통해 일시적인 오류로 인한 요청 실패를 해결한다.

 

방법 2: accept(MediaType.*APPLICATION_JSON*)

요청 헤더에서 Accept 헤더로 MediaType.APPLICATION_JSON을 설정하여 json 형식의 데이터만 응답하도록 한다.

 

이슈 2: DataIntegrityViolationException

응답으로 받은 데이터를 살펴보니 병원명과 주소가 같은데 전화번호만 다른 데이터가 몇 개 있었다. 이를 같은 데이터로 봐도 무방하다고 판단하여 테이블에 (병원명, 주소)로 유니크 제약 조건을 걸었다.

따라서 이미 저장된 데이터가 있는 상황에서 같은 데이터를 또 저장하려고 할 때 DataIntegrityViolationException이 발생한다. 이를 방지하고자 try~catch()문을 통해 예외처리를 수행했다.

 

 

지금까지는 한 번 조회한 데이터(10개)를 저장하는 로직을 구현했다.

다음 포스팅에서는 페이지 별로 요청하여 모든 데이터를 DB에 저장하는 방법을 소개하려고 한다.