프로젝트를 하나 진행 중에 공공데이터(data.go.kr)에서 제공하는 한국천문연구원 특일 정보와 연동을 해야할 상황이 생겼습니다.
이런 공공데이터 같은 경우에는 자체적으로 예제를 제공하기도하고 인터넷에도 자료가 많은데요. 그런데 이 공공데이터는 특이한 부분이 있어서 정보를 공유해보고자 합니다.
Request Parameter에 Service Key를 넣어서 인증을 진행하게 되는데, 이게 UriComponentsBuilder를 통해서 Build하여 Request를 날리게 되면, 이 Key가 인코딩되어 전송되어 Service Key가 변형되는 문제가 있습니다.
그런다고, API에서 제공하는 Decoded Key를 넣어도 일부 문자에 대한 인코딩이 이상하게 되어 API가 Service Key를 올바르게 인식하지 못하는 문제가 있습니다. 그래서 저는 아래와 같이 해결하였습니다.
@Log4j2
@Service
@RequiredArgsConstructor
public class ApiService {
private WebClient webClient;
@Value("${api-token:null}")
private String apiToken;
@PostConstruct
public void initWebClient() {
final DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory();
factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
webClient = WebClient.builder()
.uriBuilderFactory(factory)
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
}
상기와 같이 DefaultUriBuilderFactory 를 별도로 정의 한뒤, setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE)를 호출하여 인코딩을 사용하지 않도록 설정하였습니다. 그리고, WebClient 객체를 만들 때, DefaultUriBuilderFactory를 지정하여 인코딩을 사용하지 않도록 옵션을 추가하였습니다.

데이터 명세를 보면 요청을 할 때, Request Parameter로 _type이라는 Key에 'json'이라고 value를 기재하여 요청을 전송하면, 요청 결과를 Json 형태로 전달 받을 수 있게 되는데요.
여기서 문제는 모든 상황에서 Result를 Json으로 전송하지 않습니다. 요청이 유효해서 데이터가 올바르게 전달 됐을 경우에만 Json 형태로 전달이 되구요. 만약에 Service Key가 유효하지 않아서 오류가 생긴다던지 등의 대한 예외 상황이 발생하면 기존 처럼 XML 형태로 데이터가 전달된다는 점입니다. 그래서 Exception이 발생하게 됩니다.
이 부분이 좀 의아한 부분 중 하나인데요. 요청한 달에 특일이 단 하나라면 dict({}) 처리되어 있고, 여러 개라면 array([])처리가 되어 있습니다. 이거 때문에 Jackson으로 파싱할 때 별도의 알고리즘이 필요했습니다.

사진을 보시면 6월에는 현충일 하나 밖에 없으니 item이 {}으로 감싸져있는데요.

5월을 보시면, 이렇게 []으로 Array 처리가 되어있습니다. 너무나도 황당합니다.
@Data
public class HolidayApiResponseVo {
@JsonProperty("response")
private ApiResponse response;
public Header getHeader() {
return response.getHeader();
}
public Body getBody() {
return response.getBody();
}
@Data
public static class ApiResponse {
@JsonProperty("header")
private Header header;
@JsonProperty("body")
private Body body;
}
@Data
@NoArgsConstructor
public static class Header {
@JsonProperty("resultCode")
private String resultCode;
@JsonProperty("resultMsg")
private String resultMsg;
}
@Data
@NoArgsConstructor
public static class Body {
@JsonDeserialize(using = ItemsDeserializer.class)
private List<Item> items;
@JsonProperty("numOfRows")
private int numOfRows;
@JsonProperty("pageNo")
private int pageNo;
@JsonProperty("totalCount")
private int totalCount;
}
@Data
@NoArgsConstructor
public static class Item {
@JsonProperty("dateKind")
private String dateKind;
@JsonProperty("dateName")
private String dateName;
@JsonProperty("isHoliday")
private String isHoliday;
@JsonProperty("locdate")
private int locdate;
@JsonProperty("seq")
private int seq;
}
/**
* items 필드의 JSON 배열을 List<Item>으로 변환하는 Deserializer<br>
* API에서 결과 값을 반환할 떄, 공휴일이 1개라면 '{}'로 반환하고, 2개 이상이면 '[]'로 반환하기에 별도의 Deserializer가 필요함.
*/
public static class ItemsDeserializer extends JsonDeserializer<List<Item>> {
@Override
public List<Item> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonNode node = p.getCodec().readTree(p);
List<Item> items = new ArrayList<>();
ObjectMapper mapper = (ObjectMapper) p.getCodec();
if (node instanceof ObjectNode) {
// 단일 객체 처리
items.add(mapper.treeToValue(node.get("item"), Item.class));
} else if (node instanceof ArrayNode) {
// 배열 처리
for (JsonNode itemNode : (ArrayNode) node.get("item")) {
items.add(mapper.treeToValue(itemNode, Item.class));
}
}
return items;
}
}
}
그래서 이렇게 ItemsDeserializer을 이용해서 처리 과정을 만들어주었습니다. 적지않게 당황스럽습니다.
404가 아니라면, 대부분 API 결과는 200 OK로 Return 됩니다. 예상이 가능한 오류도 200으로 Return 됩니다. 그래서, 프로그래밍을 하실 때 HTTP Status에 대한 핸들링을 하지 않아도 됩니다. Body에 있는 데이터를 토대로 Error 여부를 판단해야합니다.
API Response를 Json으로 받아오는거라서 대단히 쉬울 줄 알았는데 이상하게 만들어서 힘들게 되었습니다.
글은 Json으로 받아올 수 있게 기똥차게 적었지만, 답은 XML로 받아오는 것이 아닌가 싶습니다. 변수가 이렇게 많으면 추후 다른 오류 핸들링도 힘들 것 같습니다.