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에 저장하는 방법을 소개하려고 한다.
'Framework > Spring' 카테고리의 다른 글
[SpringBoot] 다중 DB 구성하기 (0) | 2024.05.11 |
---|---|
[SpringBoot] @Async로 비동기 처리하기 (0) | 2024.04.18 |
[SpringBoot] 외부 API 호출하기(2) (0) | 2024.04.18 |
[SpringBoot] MockMvc로 Multipart() 테스트 코드 작성하기 (1) | 2024.02.15 |