Spring에서 외부 API 요청(RestClient & Object Mapping)

Rookedsysc·2024년 12월 4일
0

Spring Boot

목록 보기
1/1
post-thumbnail

RestTemplate deprecated?

Stack Overflow의 글을 참조해보면 Deprecated가 되었고 버그나 간단한 변경사항 정도에 대해서만 유지보수가 이루어질 것이라고 한다.
RestTemplate에 대한 Docs에도 다음과 같이 적혀있는 것으로 볼 때, RestTemplateRestClient로 대체될 것으로 보인다.

NOTE: As of 6.1, RestClient offers a more modern API for synchronous HTTP access. For asynchronous and streaming scenarios, consider the reactive WebClient.

그래서 이번 포스팅에서는 RestTemplate을 대체해서 사용할 수 있는 RestClient와 WebClient에 대해서 알아볼려고 한다.

RestClient

RestClientRestClient 객체를 생성할 때 BaseUrl을 넣고 호출시에 Uri를 통해서 자원을 명시한다.

XML with RestClient

호출한 API는 건강보험심사평가원_질병정보서비스의 데이터이다. XML 변환을 위해서 RestClient를 build할 때 messageConverters(List.of(new MappingJackson2XmlHttpMessageConverter()))를 지정해준다.

@Service
@RequiredArgsConstructor
public class XmlRestClient {

    private final ApiKey apiKey;

    public List<Item> getDiseaseInfoList() {
        RestClient restClient = RestClient.builder()
            .requestFactory(new HttpComponentsClientHttpRequestFactory())
            .baseUrl("https://apis.data.go.kr")
            // 지정 안해주면 04 http route error 발생
            .defaultHeader("Content-Type", MediaType.APPLICATION_XML_VALUE)
            .defaultHeader("Accept", MediaType.APPLICATION_XML_VALUE)
            .defaultHeader("Accept-Charset", StandardCharsets.UTF_8.name())
            .messageConverters(List.of(new MappingJackson2XmlHttpMessageConverter()))
            .build();

        DiseaseResponse diseaseResponse = restClient.get()
            .uri("/B551182/diseaseInfoService1/getDissNameCodeList1?serviceKey=" + apiKey.disease()
                + "&numOfRows=10&pageNo=1&sickType=1&medTp=1&diseaseType=SICK_CD")
            .retrieve()
            .body(DiseaseResponse.class);

        return diseaseResponse.getBody().getItems().getItemList();
    }
}

위와 같이 코딩하고 retrieve().body(Class.class)로 데이터를 Object로 맵핑한다.
만약 위 코드에서 Raw Response를 보고 싶다면, Response Interceptor같은 기능이 없기 때문에 아래와 같이 exchange를 사용하면 된다.

        DiseaseResponse diseaseResponse = restClient.get()
            .uri("/B551182/diseaseInfoService1/getDissNameCodeList1?serviceKey=" + apiKey.disease() 
                + "&numOfRows=10&pageNo=1&sickType=1&medTp=1&diseaseType=SICK_CD")
            .accept(MediaType.TEXT_XML)
            .acceptCharset(StandardCharsets.UTF_8)
            .exchange((request, response) -> {
                String rawResponse = new String(response.getBody().readAllBytes());
                log.info("Raw Response: {}", rawResponse);
                return response.body(DiseaseResponse.class);
            });

XML 맵핑

라이브러리는 jackson-dataformat-xml를 활용했다. XML 파싱을 코드로 일일이 구현하는 방식이 아니라면 거의 대부분 Annotation을 활용하는 방법 밖에 없어서 비교할 것도 없이 가장 많이 쓰여지는 라이브러리인 jackson을 선택했다.
JacksonXmlProperty는 Xml의 태그를 지정해주는 역할을 하는데 변수명과 동일하다면 굳이 지정해주지 않아도 된다.

@Getter
@Setter
@JacksonXmlRootElement(localName = "response")
public class DiseaseResponse {
    @JacksonXmlProperty(localName = "script")
    private String script;
    @JacksonXmlProperty(localName = "header")
    private Header header;
    @JacksonXmlProperty(localName = "body")
    private Body body;
}
@Getter
@Setter
public class Header {
    @JacksonXmlProperty(localName = "resultCode")
    private String resultCode;
    @JacksonXmlProperty(localName = "resultMsg")
    private String resultMsg;
}
@Getter
@Setter
public class Body {
    @JacksonXmlProperty(localName = "items")
    private Items items;
    @JacksonXmlProperty(localName = "numOfRows")
    private int numOfRows;
    @JacksonXmlProperty(localName = "pageNo")
    private int pageNo;
    @JacksonXmlProperty(localName = "totalCount")
    private int totalCount;
}
@Getter
@Setter
@JacksonXmlRootElement(localName = "items")
public class Items {
    @JacksonXmlElementWrapper(useWrapping = false)
    @JacksonXmlProperty(localName = "item")
    private List<Item> itemList;
}
@Getter
@Setter
public class Item {
    @JacksonXmlProperty(localName = "sickCd")
    private String sickCd;
    @JacksonXmlProperty(localName = "sickEngNm")
    private String sickEngNm;
    @JacksonXmlProperty(localName = "sickNm")
    private String sickNm;
}

Json

이번에 활용해볼 데이터는 신호제어기 신호현시 및 잔여시간 정보 제공 API이다. 이 API 호출을 통해서 가져오는 데이터 중 dataId, trsmUtcTime, ntPdsgRmdrCs, etPdsgRmdrCs, stPdsgRmdrCs, wtPdsgRmdrCs, nePdsgRmdrCs, sePdsgRmdrCs, swPdsgRmdrCs, nwPdsgRmdrCs 등 보행 신호 관련 데이터만 가져올 생각이다.

Json Annotation 사용방식

  • DTO
    - JsonIgnoreProperties : 매칭되지 않는 데이터가 있을 때 Exception이 발생할건지 아니면 무시할건지
    • JsonNaming : NamingStrategy 설정 (☢️, PropertyNamingStrategies가 아닌 PropertyNamingStrategy는 Deprecated됨)
    • JsonProperty : Naming 직접 지정
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonNaming(PropertyNamingStrategies.LowerCamelCaseStrategy.class)
public class SignalJsonAnnotation {
    private String dataId;
    private String trsmUtcTime;
    @JsonProperty("nt_pdsg_rmdr_cs")  // 이 필드만 snake_case로 변환
    private Long ntPdsgRmdrCs;
    private Long etPdsgRmdrCs;
    private Long stPdsgRmdrCs;
    private Long wtPdsgRmdrCs;
    private Long nePdsgRmdrCs;
    private Long sePdsgRmdrCs;
    private Long swPdsgRmdrCs;
    private Long nwPdsgRmdrCs;
}
@Service
@RequiredArgsConstructor
public class JsonAnnotationRestClient {

    private final ApiKey apiKey;

    public List<SignalJsonAnnotation> getSignalJsonAnnotation() {
        RestClient restClient = RestClient.builder()
            .baseUrl("https://t-data.seoul.go.kr")
            .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader("Accept-Charset", StandardCharsets.UTF_8.name())
            .build();

        String encodedApiKey = URLEncoder.encode(apiKey.signal(), StandardCharsets.UTF_8);

        List<SignalJsonAnnotation> signalJsonAnnotation = restClient.get()
            .uri("/apig/apiman-gateway/tapi/v2xSignalPhaseTimingInformation/1.0?apikey=" + apiKey.signal() + "&pageNo=1&numOfRows=10"
            )
            .retrieve()
            .body(new ParameterizedTypeReference<List<SignalJsonAnnotation>>() {
            });

        return signalJsonAnnotation;
    }
	...
}

Serializable 사용방식

Serializable을 implements하는 방식은 내부적으로 Serializable을 직렬화할 때 ObjectMapper를 가지고 와서 한다. 그래서 네이밍 Strategy를 변경하고 싶다면 기본 ObjectMapper Config을 Bean으로 등록해줘야 한다.

  • ObjectMapper Config 등록 예시 👇
@Configuration
public class ObjectMapperConfig {
    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
        return objectMapper;
    }
}
  • Serializable을 사용한 전체코드
public class SignalJsonSerializable implements Serializable {
    private String dataId;
    private String trsmUtcTime;
    private Long ntPdsgRmdrCs;
    private Long etPdsgRmdrCs;
    private Long stPdsgRmdrCs;
    private Long wtPdsgRmdrCs;
    private Long nePdsgRmdrCs;
    private Long sePdsgRmdrCs;
    private Long swPdsgRmdrCs;
    private Long nwPdsgRmdrCs;
}
@Service
@RequiredArgsConstructor
public class JsonAnnotationRestClient {

    private final ApiKey apiKey;

    public List<SignalJsonSerializable> getSignalJsonSerializable() {
        RestClient restClient = RestClient.builder()
            .baseUrl("https://t-data.seoul.go.kr")
            .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader("Accept", MediaType.APPLICATION_JSON_VALUE)
            .defaultHeader("Accept-Charset", StandardCharsets.UTF_8.name())
            .build();

        String encodedApiKey = URLEncoder.encode(apiKey.signal(), StandardCharsets.UTF_8);

        List<SignalJsonSerializable> signalJsonAnnotation = restClient.get()
            .uri("/apig/apiman-gateway/tapi/v2xSignalPhaseTimingInformation/1.0?apikey=" + apiKey.signal() + "&pageNo=1&numOfRows=10"
            )
            .retrieve()
            .body(new ParameterizedTypeReference<List<SignalJsonSerializable>>() {
            });

        return signalJsonAnnotation;
    }
}

HttpComponentsClientHttpRequestFactory

Timeout, Keep Alive, Connection Pool 등을 세부적으로 설정하기 위해서는 HttpComponentsClientHttpRequestFactory를 사용해서 HttpClient 같은 다른 라이브러리를 활용해서 Request를 생성해야 한다.

	.requestFactory(new HttpComponentsClientHttpRequestFactory())

이런식으로 HttpComponentsClientHttpRequestFactory를 생성하는게 기본적인 사용 방법인데 위에서 설명한 값들을 커스텀 하기 위해서는 아래와 같이 사용한다.

PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setMaxTotal(100); // 전체 커넥션 풀 사이즈
connectionManager.setDefaultMaxPerRoute(20); // 호스트당 커넥션 풀 사이즈

HttpClient httpClient = HttpClients.custom()
    .setConnectionManager(connectionManager)
    .build();

HttpComponentsClientHttpRequestFactory requestFactory = 
    new HttpComponentsClientHttpRequestFactory(httpClient);

HttpClient가 아닌 다른 라이브러리를 활용해서도 구현할 수 있지만 HttpClient를 사용하는 경우,

	implementation 'org.apache.httpcomponents.client5:httpclient5:{version}'

HttpClient 라이브러리를 dependency로 추가해서 사용해야 한다.

Reference

0개의 댓글