OpenFeign을 사용하여 XML 응답 처리하기 (Spring Boot + OpenFeign + Jackson XML)

soyul·2025년 3월 16일

spring

목록 보기
2/2

0. 기술 스택

  • JAVA 21
  • Spring Boot 3.2.9
  • Spring OpenFeign 4.1.1

1. 문제상황

  • 진행하고 있는 사이드 프로젝트에서 공공데이터를 읽어와야 하는 일이 생겼는데, 해당 데이터는 JSON이 아닌 XML 형식의 응답이었다. 기존에는 OpenFeign을 사용하여 JSON을 쉽게 처리했지만, XML을 직접 역직렬화하는 과정에서 여러 문제가 발생했다.
  • 응답 예시
<dbs>
    <db>
        <mt20id>PF254457</mt20id>
        <prfnm>SMTOWN LIVE</prfnm>
        <prfpdfrom>2025.01.11</prfpdfrom>
        <prfpdto>2025.01.12</prfpdto>
        <fcltynm>고척스카이돔</fcltynm>
        <poster>http://www.kopis.or.kr/upload/pfmPoster/PF_PF254457_241125_093822.jfif</poster>
        <area>서울특별시</area>
        <genrenm>대중음악</genrenm>
        <openrun>N</openrun>
        <prfstate>공연완료</prfstate>
    </db>
    <db>
        <mt20id>PF254256</mt20id>
        <prfnm>임영웅 리사이틀, RE: CITAL</prfnm>
        <prfpdfrom>2024.12.27</prfpdfrom>
        <prfpdto>2025.01.04</prfpdto>
        <fcltynm>고척스카이돔</fcltynm>
        <poster>http://www.kopis.or.kr/upload/pfmPoster/PF_PF254256_241121_094857.gif</poster>
        <area>서울특별시</area>
        <genrenm>대중음악</genrenm>
        <openrun>N</openrun>
        <prfstate>공연완료</prfstate>
    </db>
</dbs>

2. 이슈 1) XML 역직렬화 문제 발생

2-1. 문제 상황

  • OpenFeign을 사용하여 공공데이터 API를 호출했으나, 응답이 JSON이 아니라 XML이었다
@FeignClient(name = "kopis-api", url = "${kopis.api.uri}", configuration = FeignLoggerConfig.class)
public interface KopisRepository {

    String SERVICE = "service";
    String START_DATE = "stdate";
    String END_DATE = "eddate";
    String PAGE = "cpage";
    String SIZE = "rows";
    String GENRE_CODE = "shcate";
    String VENUE_CODE = "prfplccd";

    @GetMapping(value = "")
    Object getPerformanceList(@RequestParam(SERVICE) String serviceKey,
                              @RequestParam(START_DATE) String startDate,
                              @RequestParam(END_DATE) String endDate,
                              @RequestParam(PAGE) Integer page,
                              @RequestParam(SIZE) Integer size,
                              @RequestParam(value = GENRE_CODE, required = false) String genreCode,
                              @RequestParam(value = VENUE_CODE, required = false) String venueCode
    );
}
  • XML을 처리할 라이브러리가 없어 아래와 같은 예외 발생
  • 참고 : 테스트를 위해 우선 Object 객체로 받았다.
feign.codec.DecodeException: Could not extract response: no suitable HttpMessageConverter found for response type [class java.util.Objects] and content type [application/xml;charset=UTF-8]


Caused by: org.springframework.web.client.UnknownContentTypeException: Could not extract response: no suitable HttpMessageConverter found for response type [class java.util.Objects] and content type [application/xml;charset=UTF-8]

2-2. 원인 분석

  • OpenFeign은 기본적으로 JSON을 처리하며, XML을 역직렬화할 HttpMessageConverter가 없음
  • XML을 역직렬화하려면 별도의 라이브러리 필요

2-3. 해결) XML 역직렬화 라이브러리 추가

  • XML을 역직렬화 해 줄 수 있는 라이브러리 추가
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'

2-4. 결과

  • 정상적으로 데이터를 받아온 것을 볼 수 있다.

3. 이슈 2) 외부 API 필드와 DTO 필드 매핑 문제

3-1. 문제 상황

  • XML 응답의 필드명이 mt20id, prfnm, fcltynm 등으로 제공된다.
  • 데이터를 정상적으로 매핑하려면 외부 API에서 정의한 값으로 받아야 했다.
  • 아래처럼 mt20id, prfnm, fcltynm 등의 값을 정의해두면 해당 값이 어떤 데이터를 의미하는지 알 수 없고, 주석으로 기록해둔다 하더라도 추후 getter메서드를 사용할 때 헷갈릴 수 있다.
  • getId() { return mt20id; } 같은 메서드를 정의할 수도 있겠지만, 보통 lombok 을 사용하기 때문에 하나하나 정의해주는 게 여간 귀찮은 일이 아니다.
@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class KospiResponse<T> {

    T db;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class PerformanceInfoResDto {

    String mt20id;

    String prfnm;

    String fcltynm;
}

3-2. 해결) @JacksonXmlProperty 사용하여 데이터 매핑

@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class KospiResponse<T> {

    @JacksonXmlProperty(localName = "db")
    List<T> value;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class PerformanceInfoResDto {

    @JacksonXmlProperty(localName = "mt20id")
    String id;

    @JacksonXmlProperty(localName = "prfnm")
    String name;

    @JacksonXmlProperty(localName = "fcltynm")
    String venue;
}

4. 이슈 3) XML 리스트 매핑 문제

4-1. 문제 상황

  • xml이 정상적으로 dto에 매핑되지 않았다.
  • 아래처럼 내가 원하는 필드명으로 선언하고, @JacksonXmlProperty를 사용하여 매핑될 데이터 필드를 지정해주었지만, 또다시 에러가 발생하였다.
@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class KospiResponse<T> {

    @JacksonXmlProperty(localName = "db")
    List<T> value;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class PerformanceInfoResDto {

    @JacksonXmlProperty(localName = "mt20id")
    String id;

    @JacksonXmlProperty(localName = "prfnm")
    String name;

    @JacksonXmlProperty(localName = "fcltynm")
    String venue;
}
Caused by: com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.puremadeleine.viewith.dto.client.kopis.PerformanceInfoResDto` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('PF254457')
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 4, column: 25] (through reference chain: com.puremadeleine.viewith.dto.client.kopis.KospiResponse["db"]->java.util.ArrayList[0])

4-2. 원인 분석

  • Jackson이 리스트를 감싸는 태그가 없다고 가정하여 오류 발생

4-3. 해결) @JacksonXmlElementWrapper 설정 추가

  • @JacksonXmlElementWrapper(useWrapping = false) 설정을 추가하였다.
    • default 값 : true
    • XML 리스트를 감싸는 태그가 없다는 것을 명시

4-3-1. @JacksonXmlElementWrapper 이란

  • @JacksonXmlElementWrapper : List 및 Map 속성을 감싸는 XML 요소를 지정할 수 있도록 허용하는 어노테이션
  • 기본적으로 Jackson은 XML을 파싱할 때, 리스트(List)를 감싸는 별도의 XML 태그가 있어야 한다고 가정한다.
  • 예를 들어, 내가 아래와 같이 dto를 정의했다고 해보자.
@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class KospiResponse<T> {

    @JacksonXmlProperty(localName = "db")
    List<T> value;
}
@NoArgsConstructor
@AllArgsConstructor
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class PerformanceInfoResDto {

    @JacksonXmlProperty(localName = "mt20id")
    String id;
}
  • 그럼 jacskon은 아래와 같은 데이터 구조를 기대한다.
<KospiResponse>
  <db>
      <Wrapper>
          <mt20id>PF254457</mt20id>
          <mt20id>PF123456</mt20id>
      </Wrapper>
  </db>
</KospiResponse>
  • 그치만 우리 데이터는 아래와 같은 데이터이기 때문에 리스트를 감싸는 태그가 없어도 여러 개의 요소를 리스트로 변환 가능하도록 설정한다.
<KospiResponse>
  <db>
    <mt20id>PF254457</mt20id>
    <mt20id>PF123456</mt20id>
  </db>
</KospiResponse>

0개의 댓글