springboot

springboot(kotlin) 환경에서의 RestTemplate을 사용 및 xml 정보 파싱

bakerlee 2022. 11. 21. 20:32

토이프로젝트 개발 중 발생한 문제와 그 해결방법에 대하여 기술합니다. 

오늘을 기준으로 가장 빠른 휴일을 알려주는 위젯을 만들기 위해 휴일정보 API 를 작성하려 한다. 

데이터 소스는 공공기관 API를 통해 하루에 한번씩 업데이트 하도록 합니다.  

 

공공기관 API 를 사용하여 데이터를 가져온다. (server to server 통신)

https://www.data.go.kr/data/15012690/openapi.do

 

한국천문연구원_특일 정보

(천문우주정보)국경일정보, 공휴일정보, 기념일정보, 24절기정보, 잡절정보를 조회하는 서비스 입니다. 활용시 날짜, 순번, 특일정보의 분류, 공공기관 휴일 여부, 명칭을 확인할 수 있습니다.

www.data.go.kr

해당 API 는 get방식으로 요청하며 반환값은 xml 이다. 

 

위 기능의 개발에서 발생한 문제는 크게 3가지였다. 

 

1. RestTemplate을 사용한 GET요청 시 Encoding 으로 인한 오류 

2. xml파싱 과정에서의 parser 관련오류 

3. XmlMapper 사용 중 kotlin환경으로 인한 오류 

 

 

1. RestTemplate을 사용한 GET요청 시 Encoding 으로 인한 오류 

RestTemplate 을 통해 호출할 시 기본 URL 및 전달 인자를 셋팅하여야 한다. 

Url 호출 시 UriComponentsBuilder 를 사용하여 URI 를 생성하여야 하며 이 과정에서 Url 을 인코딩하는데 

Get 방식으로 호출하는 과정에서 포함되어있는 API 키가 인코딩되는 문제가 발생하였다. 

따라서 build 메소드 호출 시 true 값을 인자로 추가하여 불필요한 인코딩이 발생하지 않도록 한다. 

 

private fun generateUri(request: HolidayHttpRequest): URI{
    val httpBody: MultiValueMap<String, String> = LinkedMultiValueMap()

    httpBody["serviceKey"] = API_KEY
    httpBody["solYear"] = request.solYear
    httpBody["solMonth"] = request.solMonth

    return UriComponentsBuilder.fromHttpUrl(END_POINT_URL).queryParams(httpBody).build(true).toUri()
}

프레임워크 내부 구현부에는 아래와 같이 기술되어 있다. 

[이미 인코딩되어 있는지 여부를 인자로 받는다.] 라고 기술되어 있다. 

/**
 * Variant of {@link #build()} to create a {@link UriComponents} instance
 * when components are already fully encoded. This is useful for example if
 * the builder was created via {@link UriComponentsBuilder#fromUri(URI)}.
 * @param encoded whether the components in this builder are already encoded
 * @return the URI components
 * @throws IllegalArgumentException if any of the components contain illegal
 * characters that should have been encoded.
 */
public UriComponents build(boolean encoded) {
   return buildInternal(encoded ? EncodingHint.FULLY_ENCODED :
         (this.encodeTemplate ? EncodingHint.ENCODE_TEMPLATE : EncodingHint.NONE));
}

또한 restTemplate 을 호출할때 주의사항으로 인자를 반드시 URI 로 주어야 한다. 

url으로 해당 메서드를 호출하는 경우 이 단계에서도 인코딩이 추가로 발생한다. 

return restTemplate.exchange(uri, HttpMethod.GET, httpEntity, String::class.java)

 

마지막으로 restTemplate 인스턴스에 인코딩을 설정 해 주어야 한다. 

restTemplate.messageConverters.add(0,StringHttpMessageConverter(StandardCharsets.UTF_8))

 

 

2. xml파싱 과정에서의 parser 관련오류 

기본적으로 json 타입의 반환값을 처리하는 함수만 제공되며 xml 데이터 처리를 통해서는 라이브러리를 추가해야 한다. 

 

gradle 에 의존성을 추가하도록 한다. 

https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-xml

implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.14.0")

 

 

이후 XmlMapper 클래스를 사용하여 인코딩을 한다.

 

 

3. XmlMapper 사용 중 kotlin환경으로 인한 오류  

해당 오류는 kotlin 환경으로 인해 발생한 오류이다. 해당 오류는 2가지의 원인으로 인하여 발생한다.  

그 중 첫번째인 호출 단계의 오류이며 원인은 다음과 같다. 

    public <T> T readValue(String content, Class<T> valueType)
        throws JsonProcessingException, JsonMappingException
    {
        _assertNotNull("content", content);
        return readValue(content, _typeFactory.constructType(valueType));
    }

호출 시 인자로 xml형식의 문자열, 해당 타입을 받을 데이터 클래스 를 받는다. 

문제는 코들린에서의 데이터 클래스는 보통 data class Foo( ... ) 와 같이 data 타입을 생성하는데 이 data class 는 일반 클래스와 구조가 조금 다르다. 

 

xml parser, json parser 는 기본 생성자를 사용하여 동작하는 절차가 있는데 data 클래스는 기본 생성자가 생성되지 않는다. 

그리고 뭔가 더 이유가 있다고 하는데 정확하지 않아 거기까지는 기술하지 않는다. 

해당 문제에 대한 해결 방법은 아래의 stackoverflow를 참조한다. 

 

 

https://stackoverflow.com/questions/71458854/kotlin-deserializing-with-jackson-xmlmapper

 

Kotlin - Deserializing with Jackson XmlMapper

I'm having some trouble to parse correctly an XML file with Jackson XML Mapper. Suppose I have this simple XML: <Test> <Simple> <x>I am x</x> <y>I ...

stackoverflow.com

 

 

 

작성 중인 전체 코드이다. 

https://github.com/BowonLee/HolidayServer