티스토리 뷰
[Spring Boot] 공공데이터 포털 Open API로 데이터 가져오기 (w. RestTemplate, json-simple)
mopil 2022. 4. 30. 16:342024.01.03 업데이트
# 서론
개발을 하다 보니 서버-서버로 HTTP 요청을 보내야 하는 상황이 생겼다.
그래서 몇 가지 조사를 해본 결과 스프링에서 기본적으로 제공하는 HTTP 전송용 모듈이 몇 개 있는 걸 발견했다.
그중 기본 내장되어 있는 게 RestTemplate이라는 모듈인데, 동기방식으로 요청을 처리하고 JSON 객체 변환하는 걸 도와주는 모듈이다. (그런데 지금은 WebClient라는 새로운 모듈에 자리를 뺏겨 더 이상 쓰이지 않을 예정이라고 한다.)
WebClient는 비동기 방식으로 뭐 non-blocking 도 지원한다고 하는데, Spring WebFlux라는 라이브러리를 도입해야 해서 이는 찾아보니까 Spring MVC 구조랑 조금 다른 것 같았다. (Mono, Flux 어지러워서 이해하는걸 포기했다..)
그래서 조금 구시대적인 기술이지만, 도입하기 쉬운 RestTemplate으로 스프링 서버에서 오픈 API를 가져오는 걸 구현해 볼 예정이다.
# RestTemplate 기본 스펙
- Spring 3부터 지원, REST API 호출이후 응답을 받을 때까지 기다리는 동기 방식
동기 방식이 좀 걸리는데, 그래서 AsyncRestTemplate이라는 비동기 형식으로 처리하는 모듈이 Spring 5에 도입되었지만 얘 또한 deprecated(더 이상 쓸모 없어져서 안 쓰이는) 매우 안습적인 상황인 것 같다...
일단 그냥 RestTemplate으로 구현하고 나중에 비동기 방식이 필요할 거 같으면 마이그레이션을 고려할 예정이다.
뭐 여튼 자바에서 지원하는 URLConnect를 쌩으로 쓰는 것보다 코드 중복이 줄고, JSON이랑 XML 응답을 쉽게 받을 수 있으니 사용할 가치는 있다.
자세한 설명은 이 블로그를 첨부한다.
# Open API - 한국관광공사 문화관광 데이터
Open API로 가져올 데이터는 한국관광공사에서 제공하는 문화관광 데이터이다.
https://www.data.go.kr/index.do
여기에 회원 가입하고
https://www.data.go.kr/iim/api/selectAPIAcountView.do
이 API를 사용할 예정이다. 활용 신청을 해준다. 빠르면 30분 이내로 사용가능하다.
Open API 매뉴얼은 해당 사이트에 아주 상세히 명시되어 있으니 참조한다.
인증키가 서비스 키인데, 보안관련해서 가려놨다.
XML을 기본으로 응답하지만, 쿼리 파라미터에 &_type=json을 붙이면 JSON으로도 받을 수 있다.
주의!! 웹브라우저나 포스트맨은 인코딩 된 인증키를 서비스키에 넣어야 하고, 스프링 서버에서 요청을 날릴 땐 디코딩된 서비스키를 넣어야 한다. (이걸로 삽질을 좀 했다... 왜 다르게 해 놓은 걸까...)
여하튼, Open API 사용 예시는 다음과 같다.
http://api.visitkorea.or.kr/openapi/service/rest/KorService/areaBasedList?ServiceKey=서비스 키&MobileOS=ETC&MobileApp=AppTest&_type=json&numOfRows=100
초록색 부분은 API endpoint이다.
파란색은 사용하고자 하는 서비스의 URI이다. 위치기반 정보, 지역기반 정보 등 원하는 URI를 넣으면 된다.
빨간색 부분은 요청을 날리기 위해서 필수적으로 포함되어야 하는 쿼리 파라미터다. (MobileApp은 통계를 위해서 넣어놓은 거라는데... 암튼 필수로 넣어달란다.)
검은색은 추가 사항이다.
다음을 입력하고 GET 요청을 날리면 다음과 같은 응답이 온다.
# 스프링 서버에서 요청 날리기
@RestController
@RequiredArgsConstructor
@Slf4j
public class OpenApiController {
private final OpenApiManager openApiManager;
@GetMapping("open-api")
public ResponseEntity<?> fetch() throws UnsupportedEncodingException {
return success(openApiManager.fetch().getBody());
}
}
@Component
public class OpenApiManager {
private final String BASE_URL = "http://api.visitkorea.or.kr/openapi/service/rest/KorService";
private final String apiUri = "/areaBasedList";
private final String serviceKey = "?ServiceKey=디코딩 서비스 키";
private final String defaultQueryParam = "&MobileOS=ETC&MobileApp=AppTest&_type=json";
private final String numOfRows = "&numOfRows=100";
private final String areaCode = "&areaCode=1";
private final String contentTypeId = "&contentTypeId=12";
private String makeUrl() throws UnsupportedEncodingException {
return BASE_URL +
apiUri +
serviceKey +
defaultQueryParam +
numOfRows +
areaCode +
contentTypeId;
}
public ResponseEntity<?> fetch() throws UnsupportedEncodingException {
System.out.println(makeUrl());
RestTemplate restTemplate = new RestTemplate();
HttpEntity<?> entity = new HttpEntity<>(new HttpHeaders());
ResponseEntity<Map> resultMap = restTemplate.exchange(makeUrl(), HttpMethod.GET, entity, Map.class);
System.out.println(resultMap.getBody());
return resultMap;
}
}
fetch 메서드가 좀 지저분하지만 그냥 테스트를 위해서 일단 대충 작성했다. (이런 식으로 작동된다는 것만 확인하고 리팩토링)
중요한 건, 스프링서버에서 요청을 날릴 때 꼭 디코딩된 서비스 키를 이용해서 날려야 한다는 점이다. (이것만 명심하면 된다.)
아니면 SERVICE KEY IS NOT REGISTERED ERROR를 만나게 될 것이다.
이걸 이제 서버단에서 잘 가공해서 디비에 넣고 클라이언트에 내려주거나 하면 된다.
# 데이터 가공하기 (JSON 파싱)
사실 이 부분에서 상당한 삽질과 뻘짓을 했다.... 흑흑
{
"header": {
"resultCode": "0000",
"resultMsg": "OK"
},
"body": {
"pageNo": 1,
"totalCount": 1440,
"items": {
"item": [
{
"readcount": 29942,
"addr2": "(장흥면)",
"addr1": "경기도 양주시 장흥면 권율로 117",
"contentid": 129194,
"firstimage2": "http://tong.visitkorea.or.kr/cms/resource/46/2010746_image3_1.jpg",
"title": "가나아트파크",
"areacode": 31,
"createdtime": 20060807000000,
"mapy": 37.7254519094,
"contenttypeid": 12,
"mapx": 126.9497496852,
"zipcode": 11520,
"cat2": "A0202",
"cat3": "A02020700",
"modifiedtime": 20211221165153,
"cat1": "A02",
"mlevel": 6,
"sigungucode": 18,
"firstimage": "http://tong.visitkorea.or.kr/cms/resource/46/2010746_image2_1.jpg"
},
{
"readcount": 0,
"addr1": "경기도 여주시 가남읍 대명산길 98",
"contentid": 2777865,
"firstimage2": "http://tong.visitkorea.or.kr/cms/resource/71/2777971_image2_1.jpg",
"title": "가남체육공원",
"areacode": 31,
"createdtime": 20211123092854,
"mapy": 37.2017093544,
"contenttypeid": 12,
"mapx": 127.5349142281,
"zipcode": 12662,
"cat2": "A0202",
"cat3": "A02020600",
"modifiedtime": 20211125170324,
"cat1": "A02",
"mlevel": 6,
"sigungucode": 20,
"firstimage": "http://tong.visitkorea.or.kr/cms/resource/71/2777971_image2_1.jpg"
},
{
"readcount": 25446,
"addr2": ", 강원도 춘천시 서면",
"addr1": "경기 가평군 북면",
"contentid": 125462,
"firstimage2": "http://tong.visitkorea.or.kr/cms/resource/23/1900123_image3_1.jpg",
"title": "가덕산",
"areacode": 31,
"createdtime": 20030331000000,
"mapy": 37.9202321907,
"contenttypeid": 12,
"mapx": 127.5683098007,
"zipcode": "477-840",
"cat2": "A0101",
"cat3": "A01010400",
"modifiedtime": 20210906135931,
"cat1": "A01",
"mlevel": 6,
"sigungucode": 1,
"firstimage": "http://tong.visitkorea.or.kr/cms/resource/23/1900123_image2_1.jpg"
}
]
},
"numOfRows": 3
}
}
일단 이따구로 생겼다..
RestTemplate을 사용해서 한 번에 객체로 받을 수 있지만, 내가 요청한 데이터가 좀 복잡한 JSON 형태라 불가능했다. 그래서 손 수 파싱 작업을 해줘야 했는데... json-simple 라이브러리로 수행했다.
implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1'
먼저 Gradle 의존성을 추가해 준다.
public List<OpenApiDto> fetch() throws ParseException {
RestTemplate restTemplate = new RestTemplate();
String jsonString = restTemplate.getForObject(makeUrl(), String.class);
JSONParser jsonParser = new JSONParser();
JSONObject jsonObject = (JSONObject) jsonParser.parse(jsonString);
// 가장 큰 JSON 객체 response 가져오기
JSONObject jsonResponse = (JSONObject) jsonObject.get("response");
// 그 다음 body 부분 파싱
JSONObject jsonBody = (JSONObject) jsonResponse.get("body");
// 그 다음 위치 정보를 배열로 담은 items 파싱
JSONObject jsonItems = (JSONObject) jsonBody.get("items");
// items는 JSON임, 이제 그걸 또 배열로 가져온다
JSONArray jsonItemList = (JSONArray) jsonItems.get("item");
List<OpenApiDto> result = new ArrayList<>();
for (Object o : jsonItemList) {
JSONObject item = (JSONObject) o;
result.add(makeLocationDto(item));
}
return result;
}
일단 JSON 문자열로 받아와서 json-simple에서 제공해 주는 JSONParser 객체를 이용해서 파싱을 진행한다.
// 콘텐츠 정보 JSON을 DTO로 변환
private OpenApiDto makeLocationDto(JSONObject item) {
OpenApiDto dto = OpenApiDto.builder().
title((String) item.get("title")).
address((String) item.get("addr1")).
areaCode((Long) item.get("areacode")).
contentTypeId((Long) item.get("contenttypeid")).
firstImage((String) item.get("firstimage")).
mapX((double) item.get("mapx")).
mapY((double) item.get("mapy")).
build();
return dto;
}
makeLocationDto는 그냥 변환용 편의 메서드이다.
public class OpenApiDto {
private String title;
private String address;
private Long areaCode;
private Long contentTypeId;
private String firstImage;
private double mapX;
private double mapY;
}
DTO는 이렇게 생겼다.
이제 컨트롤러에서 가져온 위치정보를 리스트에 담아서 내려주면 된다.
위 로직의 디테일한 소스코드는 아래에서 확인할 수 있다.