프로젝트 개발을 진행하면서 공공데이터 API를 사용하며 API 통신으로 받아온 Json 데이터를 변환하는 방법을 정리한 글입니다.
현재 날짜의 최고 기온과, 최저 기온, 현재 시각의 습도를 조회하기 위해 기상청 단기예보 조회서비스를 사용한다.
단기예보조회 API의 상세기능명세는 다음과 같다.
상세기능명 단기예보조회
상세기능 설명 | 단기예보 정보를 조회하기 위해 발표일자, 발표시각, 예보지점 X좌표, 예보지점 Y 좌표의 조회 조건으로 발표일자, 발표시각, 자료구분문자, 예보 값, 예보일자, 예보시각, 예보지점 X 좌표, 예보지점 Y 좌표의 정보를 조회하는 기능 |
Call Back URL | http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst |
request 명세
serviceKey | 인증키 | 인증키 | |
(URL Encode) | 공공데이터포털에서 발급받은 인증키 | ||
numOfRows | 한 페이지 결과 수 | 50 | 한 페이지 결과 수 (Default: 10) |
pageNo | 페이지 번호 | 1 | 페이지 번호 (Default: 1) |
dataType | 응답자료형식 | XML | 요청자료형식(XML/JSON) (Default: XML) |
base_date | 발표일자 | 20210628 | ‘21년 6월 28일발표 |
base_time | 발표시각 | 0500 | 05시 발표 |
nx | 예보지점 X 좌표 | 55 | 예보지점의 X 좌표값 |
ny | 예보지점 Y 좌표 | 127 | 예보지점의 Y 좌표값 |
postman에서 request를 보내는 방식이다.
날씨 공공데이터 조회를 위해 오늘의 날씨 데이터를 받기 위해서는 예보 정보를 관측하려는 날짜와 시간을 입력한다.
최고 기온과 최저 기온값은 특정 시간대만 호출했을 때는 받아올 수 없기 때문에 일단 그날 하루의 날씨정보를 받으려면 전날 23시를 base_date와 base_time으로 설정하고 numOfRows를 266개를 하면 해당 날짜의 날씨정보를 모두 받아올뿐더러 최고기온과 최저기온값을 받아올수 있다.
응답메시지의 데이터 형식을 정리해보면 다음과 같다.
{
"response":{
"header":{
"resultCode":"00",
"resultMsg":"NORMAL_SERVICE"
},
"body":{
"dataType":"JSON",
"items":{
"item":[
{
"baseDate":"20230826", //발표 날짜
"baseTime":"2300", //발표 시각
"category":"TMP", //자료 구분 코드
"fcstDate":"20230827", //23년 8월 27일 예보
"fcstTime":"0000", //23시 예보
"fcstValue":"24", //예보 값
"nx":61, //예보 지점 X좌표
"ny":128 //예보 지점 Y좌표
}
]
"pageNo":1,
"numOfRows":400,
"totalCount":882
}
}
}
springboot에서 json 데이터를 java 클래스 객체로 매핑하기 위해 jackson 라이브러리를 사용한다.
jackson 라이브러리 dependency추가
implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.11.2'
응답메시지에서 필요한 데이터를 매핑하기 위한 데이터 클래스 형식은 다음과 같다.
@Getter
@NoArgsConstructor
public class WeatherResult {
@Getter
@NoArgsConstructor
@AllArgsConstructor
@JsonDeserialize(using = WeatherDeserializer.class)
public static class items{
private List<item> item;
}
@Getter
@NoArgsConstructor
public static class item{
private String baseDate;
private String baseTime;
private String category;
private String fcstDate;
private String fcstTime;
private String fcstValue;
private Long nx;
private Long ny;
}
}
items 에서는 역직렬화시에 해당 deserializer을 사용하도록 @JsonDeserialize를 추가해주었다.
역 직렬화를 수행하기 위해 메서드, 필드, 클래스 또는 파라미터에 @JsonDeserialize 어노테이션을 사용할 수 있다.
클래스에 deserializer를 지정하여 이 경우 JSON에서 해당 클래스로 역직렬화가 진행되는 모든 경우에 지정한 deserializer를 사용한다.
Jackson 은 기본적으로 Property로 동작하기 때문에 클래스에 Getter 어노테이션과 생성자 어노테이션인 NoArgsConstructor 를 선언한다.
deserialize는 기본 생성자로 객체를 생성하고, 기본적으로 Property를 사용하기 때문에 public 필드 또는 public의 getter/setter로 필드를 찾아 바인딩 하기 때문에 @NoArgsConstructor 와 @Getter가 필요하다
custum deserializer
ObjectMapper의 기본 역직렬화 참조 클래스가 아닌, 다른 개인이 만든 클래스로 역직렬화를 시키기 위해 ObjectMapper에 모듈을 추가해서 진행할 수 있다.
@RequiredArgsConstructor
public class WeatherDeserializer extends JsonDeserializer<WeatherResult.items> {
private final ObjectMapper objectMapper;
public WeatherDeserializer()
{
this.objectMapper = new ObjectMapper();
}
@Override
public WeatherResult.items deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
JsonNode node = p.getCodec().readTree(p);
JsonNode responseNode = node.findValue("response");
JsonNode itemNode = responseNode.get("body").get("items").get("item");
List<WeatherResult.item> items = Arrays.stream(objectMapper.treeToValue(itemNode, WeatherResult.item[].class)).collect(Collectors.toList());
//TMX(최고온도), TMN(최저온도), REH(시간별 습도)
List<WeatherResult.item> list = new ArrayList<>();
for(WeatherResult.item item : items){
if(item.getCategory().equals("TMX"))
list.add(item);
if(item.getCategory().equals("TMN"))
list.add(item);
if(item.getCategory().equals("REH")) {
list.add(item);
}
}
return new WeatherResult.items(list);
}
}
ObjectMapper를 deserialize() 안에서 반복해서 생성하면 성능에 악영향을 미치므로 WeatherDeserializer 생성 시 한번만 생성해서 재사용하는 것이 좋다.
json 의 tree로 접근한 데이터를 JsonNode를 통해 매핑해 주었다.
JsonNode의 요소에 접근하는 몇가지 방법이있는데 그중에서
get() - 노드의 필드를 찾고 없으면 null return
path() - 노드의 필드를 찾고 없으면 MissingNode return
findValue() - 노드와 자식노드들에서 필드를 찾고 없으면 null return
순차적인 접근을 위해서는 get() 또는 path()를 사용한다. findValue()는 노드 하위 전체에서 필드를 찾아주어 편하지만 동일한 필드명이 존재하는 경우 원치않는 필드를 가져올 수 있다.
List값을 받기 위해서 objectMapper.treeToValue를 사용해 배열로 받아 list로 변환해 주었다.
서비스 단에서 테스트를 해보자
public void openApiTest(){
ObjectMapper objectMapper = new ObjectMapper();
RestTemplate restTemplate = new RestTemplate();
HttpEntity<?> entity = new HttpEntity<>(new HttpHeaders());
WeatherResult.items response;
String url = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst?" +
"serviceKey=[서비스 키]&numOfRows=266&pageNo=1&dataType=JSON&base_date=20230813&base_time=2300&nx=61&ny=128";
try{
ResponseEntity<String> result = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
response = objectMapper.readValue(result.getBody(), WeatherResult.items.class);
}catch (IOException e){
throw new BadRequestException(OBJECT_MAPPER_FAIL);
}
System.out.println(ToStringBuilder.reflectionToString(response));
}
'Backend' 카테고리의 다른 글
Git-Flow 브랜치 전략 (0) | 2023.11.29 |
---|---|
Jackson 라이브러리란? (0) | 2023.09.07 |