최근 데이터 활용 공모전에 참여하게 되면서 Open API를 활용해서 데이터를 수집하고 이를 가공하는 작업을 맡게 되었다. 서버 어플리케이션에서 Open API를 써본 경험이 많지 않아서 미리 연습을 해보자🔥
다양한 기업 및 공공기관에서 API를 제공하고 있다. 필자의 경우, 공공데이터 포털에서 제공하고 있는 Open API로 통신한 데이터를 사용해볼 것이다!
위 목표를 위해 오늘도 뚝딱여보자.
공공데이터란 공공기관이 만들어내는 모든 자료나 정보, 국민 모두의 소통과 협력을 이끌어내는 공적인 정보를 말한다.
각 공공기관이 공유한 공공데이터 목록과 국민에게 개방할 수 있는 공공데이터를 포털에 등록하면 모두가 공유할 수 있는 양질의 공공데이터로 재탄생하게 된다.
-공공데이터 포털 개요 참고-
공공데이터 포털을 접속해보면 개방 데이터를 파일 형태 혹은 OpenAPI 형태로 사용할 수 있다. 나는 Spring에서 API를 활용해 JSON 형식으로 데이터를 받아올 것이다.
우선 자신이 활용할 데이터를 서치한다. 기상청전국 해수욕장 날씨 조회서비스 OpenAPI를 활용하기 위해 '활용 신청'을 했다.
마이페이지에 들어가면 내가 신청한 건에 대한 리스트도 확인할 수 있다.
신청한 API를 누르면 개발계정 상세보기 페이지가 나오는데 여기서 출력되는 일반 인증키를 사용하면 된다.
일일 가능 트래픽 규모도 확인해준다. 내가 사용하는 API는 하루에 10,000번의 트래픽을 사용할 수 있다.
'미리보기'를 통해서 요청에 따른 응답을 테스트해볼 수 있다.
service key와 적절한 request 파라미터를 넣어주면 아래처럼 새로운 탭에 응답값이 찍히는 것을 확인할 수 있다.
참고 문서도 다운로드 받을 수 있다. 참고 문서에 더 친절한 설명이 적혀있다.
요청 URL과 요청에 필요한 파라미터를 확인해준다.
응답 메시지 명세도 확인해준다. 요청을 하면 이런 응답 파라미터가 온다는 것!
친절하게 어떻게 요청과 응답이 이루어지는지 예제도 나와있다.
XML 형식으로 오는 예제이다.
request에서 dataType을 JSON으로 요청하면 JSON 데이터가 반환된다.
해당 가이드라인 하단에는 참고 문서 별첨이 있었다. response 중 특정 코드로 표시되는 값들이 있어서 이에 대한 해석이 나와있었다. 활용 가이드를 꼼꼼하게 읽어보면 자세하게 안내가 되어 있다.
Spring 프로젝트를 생성하자
https://start.spring.io/
Datasource 세팅을 마치고 이제 본격적으로 연동을 해보자.
우선 controller에서 Open API와 통신하여 데이터를 받아오자.
자세한 코드 설명은 아래에 적어놓겠다.
@RestController
@RequestMapping("/api")
public class ForecastController {
@Value("${openApi.serviceKey}")
private String serviceKey;
@Value("${openApi.callBackUrl}")
private String callBackUrl;
@Value("${openApi.dataType}")
private String dataType;
@GetMapping("/forecast")
public ResponseEntity<String> callForecastApi(
@RequestParam(value="base_time") String baseTime,
@RequestParam(value="base_date") String baseDate,
@RequestParam(value="beach_num") String beachNum
){
HttpURLConnection urlConnection = null;
InputStream stream = null;
String result = null;
String urlStr = callBackUrl +
"serviceKey=" + serviceKey +
"&dataType=" + dataType +
"&base_date=" + baseDate +
"&base_time=" + baseTime +
"&beach_num=" + beachNum;
try {
URL url = new URL(urlStr);
urlConnection = (HttpURLConnection) url.openConnection();
stream = getNetworkConnection(urlConnection);
result = readStreamToString(stream);
if (stream != null) stream.close();
} catch(IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
return new ResponseEntity<>(result, HttpStatus.OK);
}
/* URLConnection 을 전달받아 연결정보 설정 후 연결, 연결 후 수신한 InputStream 반환 */
private InputStream getNetworkConnection(HttpURLConnection urlConnection) throws IOException {
urlConnection.setConnectTimeout(3000);
urlConnection.setReadTimeout(3000);
urlConnection.setRequestMethod("GET");
urlConnection.setDoInput(true);
if(urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("HTTP error code : " + urlConnection.getResponseCode());
}
return urlConnection.getInputStream();
}
/* InputStream을 전달받아 문자열로 변환 후 반환 */
private String readStreamToString(InputStream stream) throws IOException{
StringBuilder result = new StringBuilder();
BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
String readLine;
while((readLine = br.readLine()) != null) {
result.append(readLine + "\n\r");
}
br.close();
return result.toString();
}
}
configuration 값
serviceKey
같은 경우 외부에 유출되어서 안되기 때문에 property로 관리한다.callBackUrl
이나 dataType
같이 변하지 않는 값들도 함께 관리한다.RequestParam
baseTime
이나 baseDate
, beachNum
같이 요청에 따라 변해야 하는 값들은 GET 요청의 파라미터로 구성했다.요청 파라미터
serviceKey
: 공공데이터 포털에서 발급받은 API KEY(Encoding)dataType
: JSON/XMLbase_date
: 요청 날짜base_time
: 요청 시각beach_num
: 요청 해수욕장 number (해수욕장 number 표가 별첨되어 있다.)URLConnection
클래스의 서브 클래스로, HTTP 고유 기능에 대한 추가 지원을 제공한다.URLConnection
인스턴스를 얻는다.URL
객체를 생성한다.URL url = new URL(urlStr);
해당 생성자는 URL 형식이 잘못된 경우 MalformedURLException
을 throw한다.
해당 예외는 IOException
의 하위 클래스이므로 try-catch 문으로 예외처리를 해주었다.
URLConnection
인스턴스는 URL 객체의 openConnection()
메소드 호출에 의해 얻어진다.
나는 프로토콜이 http://
이므로 반환된 객체를 캐스팅해주었다.
urlConnection = (HttpURLConnection) url.openConnection();
URL의 openConnection()
메서드는 I/O 오류가 발생하면 IOException
을 발생시킨다.
openConnection()
메서드는 실제 네트워크 연결을 설정하지 않고,URLConnection
클래스의 인스턴스만 반환한다.
네트워크 연결은connect()
메서드가 호출 될 때 명시적으로 이루어지거나, 헤더 필드를 읽거나 입력 스트림/출력 스트림을 가져올 때 암시적으로 이루어진다.
연결을 설정하기 전에 타임아웃, 캐시, HTTP 요청 방법 등과 같이 클라이언트와 서버 간의 옵션을 설정할 수 있다. 연결이 이미 설정된 이후 메서드를 호출하면 일부는IllegalStateException
을 발생시킨다.
urlConnection.setConnectTimeout(3000);
urlConnection.setReadTimeout(3000);
urlConnection.setRequestMethod("GET");
urlConnection.setDoInput(true);
setConnectTimeout (int timeout)
: 연결 타임아웃 값을 설정한다. (단위:ms)java.net.SocketTimeoutException
이 발생한다.setReadTimeout (int timeout)
: 읽기 타임아웃 값을 설정한다. (단위:ms)SocketTimeoutException
이 발생한다.setDefaultUserCaches (boolean default)
: URLConnection
이 기본적으로 캐시를 사용하는지 여부를 사용한다.(기본값은 true)setUseCaches (boolean useCaches)
: 연결이 캐시를 사용하는지 여부를 설정한다. (기본값은 true)setDoInput (boolean doInput)
: URLConnetion
을 서버에서 콘텐츠를 읽는 데 사용할 수 있는지 여부를 설정한다. (기본값은 true)setDoOutput (boolean doOutput)
: URLConnection
이 서버에 데이터를 보내는 데 사용할 수 있는 여부를 설정한다. (기본값은 false)setIfModifiedSince (long time)
: 주로 HTTP 프로토콜에 대해 클라이언트가 검색한 콘텐츠의 마지막 수정 시간을 새로 설정한다. setAllowUserInteraction (boolean allow)
: 사용자 상호 작용을 활성화 또는 비활성화환다.setDefaultAllowUserInteraction (boolean default)
: 이후의 모든 URLConnection
객체에 대한 사용자 상호 작용의 기본값을 설정한다.setRequestProperty (String key, String value)
: key=value 쌍으로 지정된 일반 요청 속성을 설정한다.HttpURLConnetion
연결 구성setRequestMethod (String method)
: HTTP 메서드 GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE 중 하나를 URL 요청에 대한 메소드로 설정한다. (기본값은 GET)setChunkedStreamingMode (int chunkLength)
: 콘텐츠 길이를 미리 알 수 없는 경우 내부 버퍼링 없이 HTTP 요청 본문을 스트리밍할 수 있다.setFixedLengthStreamingMode (long contentLength)
: 콘텐츠 길이를 미리 알고 있는 경우 내부 버퍼링 없이 HTTP 요청 본문을 스트리밍할 수 있다.setFollowRedirects (boolean follow)
: 클래스의 미래 개체가 자동으로 따라야 하는지 여부를 설정한다. (기본값은 true)SecurityException
을 발생시킨다. setInstanceFollowRedirects (boolean follow)
: HttpURLConnection
객체가 리다이렉트를 따라가게 만든다. (기본값은 true)getResponseCode()
를 이용해 서버에서 보낸 HTTP 상태 코드를 체크했다. if(urlConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
throw new IOException("HTTP error code : " + urlConnection.getResponseCode());
}
InputStream
인스턴스를 얻어온다.getInputStream()
메서드는 다음과 같은 예외가 발생한다.IOException
: 입력 스트림을 생성하는 동안 I/O 오류 발생SocketTimeoutException
: 데이터를 읽을 수 있기 전 timeout 발생UnknownServiceException
: 프로토콜이 입력을 지원하지 않는 경우return urlConnection.getInputStream();
InputStream
을 InputStreamReader
로 wrapping한다.InputStream
을 BufferedReader
로 wrapping한다.BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
BufferedReader
의 문자열을 한줄씩 읽으며 result에 붙여준다. String readLine;
while((readLine = br.readLine()) != null) {
result.append(readLine + "\n\r");
}
if (urlConnection != null) {
urlConnection.disconnect();
}
@RunWith(SpringRunner.class)
@WebMvcTest(ForecastController.class)
@AutoConfigureMockMvc
public class ForecastControllerTest {
@Autowired
private MockMvc mvc;
@Test
@DisplayName("Open API 통신 테스트")
public void callOpenApi() throws Exception {
String baseTime = "1100";
String baseDate = "20230525";
String beachNum = "1";
MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
param.add("base_time", baseTime);
param.add("base_date", baseDate);
param.add("beach_num", beachNum);
this.mvc.perform(get("/api/forecast").params(param))
.andExpect(status().isOk())
.andDo(print());
}
}
test 코드를 작성하고 결과값을 확인해보니
{
"response": {
"header": {
"resultCode": "00",
"resultMsg": "NORMAL_SERVICE"
},
"body": {
"dataType": "JSON",
"items": {
"item": [
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "TMP",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "20",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "UUU",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "-0.1",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "VVV",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "2.7",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "VEC",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "176",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "WSD",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "2.7",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "SKY",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "1",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "PTY",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "0",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "POP",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "0",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "WAV",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "0.5",
"nx": 49,
"ny": 124
},
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "PCP",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "강수없음",
"nx": 49,
"ny": 124
}
]
},
"pageNo": 1,
"numOfRows": 10,
"totalCount": 737
}
}
}
잘 받아와진다!
참고로 지금은 콘솔에 출력된 String으로 반환된 미친 json 데이터를 json formatter를 이용해서 예쁘게 만들어보았다.
이제 데이터를 사용하려면 적절하게 가공을 해주어야 한다. JSON deserialize 작업을 해주어 데이터를 가공해 Java Object에 mapping하여 값을 반환하자.
참고로 추후에는 Service 단에서 DB에 가공 값을 저장해줄 예정이라 JSON 가공 작업은 Service 단에서 해줄 것이다.
JSON 라이브러리는 정말 많은데 나는 그중 Jackson 라이브러리를 사용해볼 것이다.
스프링부트는 spring-boot-starter-web
에 Jackson
라이브러리를 제공하고 있어서 Json의 직렬/역직렬화에는 Jackson
을 사용한다.
deserialize(Json > VO)는 아래와 같은 순서를 따른다.
- 기본 생성자로 객체를 생성한다.
- public 필드 또는 public의 getter/setter로 필드를 찾아 binding한다.
response JSON 응답을 적절하게 binding 해주어야 하는데 이 과정이 몹시 몹 시 귀찮다!
우선 response JSON의 구조를 파악해서 depth를 구분한다.
{
"response": {
"header": {
"resultCode": "00",
"resultMsg": "NORMAL_SERVICE"
},
"body": {
"dataType": "JSON",
"items": {
"item": [
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "TMP",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "20",
"nx": 49,
"ny": 124
},
(...)
{
"beachNum": "1",
"baseDate": "20230525",
"baseTime": "1100",
"category": "PCP",
"fcstDate": "20230525",
"fcstTime": "1200",
"fcstValue": "강수없음",
"nx": 49,
"ny": 124
}
]
},
"pageNo": 1,
"numOfRows": 10,
"totalCount": 737
}
}
}
depth 1 : response
depth 2 : header, body
depth 3(header) : resultCode, resultMsg
depth 3(body) : dataType, items, pageNo, numOfRows, totalCount
depth 4(items) : item(array)
depth 5(item) : beachNum, baseDate, baseTime, category, fcstDate, fcstTime, fcstValue, nx, ny
미친 중첩을 보여준다.
우리는 이 중 depth 4에 있는 item array를 가져올 것이기 때문에 직접 mapping을 해주는 작업이 필요하다.
Map과 VO의 논쟁이 뜨거운데
담겨있는 필드가 많아지는 경우 대체로 VO 객체를 사용하는 추세라고..
우선 응답 데이터를 mapping 시킬 VO를 생성한다.
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FcstItems {
@JsonProperty("item")
private List<FcstItem> fcstItems;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FcstItem {
// 해변코드
@JsonProperty("beachNum")
private int beachNum;
// 발표일자
@JsonProperty("baseDate")
private String baseDate;
// 발표시각
@JsonProperty("baseTime")
private String baseTime;
// 자료구분코드
@JsonProperty("category")
private String category;
// 예보일자
@JsonProperty("fcstDate")
private String fcstDate;
// 예보시간
@JsonProperty("fcstTime")
private String fcstTime;
// 예보 값
@JsonProperty("fcstValue")
private String fcstValue;
// X좌표
@JsonProperty("nx")
private int nx;
// Y좌표
@JsonProperty("ny")
private int ny;
}
binding하는 방법은 두 가지가 있다고
1. Custom deserializer 작성하기
2. 어노테이션 사용하기
Deserializer를 별도의 클래스에 코드로 작성하면 DTO 내부 코드가 깔끔하고 재사용면에서 장점을 가진다. 그러나 dto마다 deserializer를 작성해주어야 하는 경우와 같이 클래스의 수가 많아지는 일이 발생할 수 있다.
public class FcstItemDeserializer extends JsonDeserializer<FcstItems> {
private final ObjectMapper objectMapper;
public FcstItemDeserializer() {
this.objectMapper = new ObjectMapper();
}
@Override
public FcstItems deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
JsonNode node = p.getCodec().readTree(p);
JsonNode itemNode = node.findValue("item");
List<FcstItem> items = Arrays.stream(objectMapper.treeToValue(itemNode, FcstItem[].class)).toList();
return new FcstItems(items);
}
}
com.fasterxml.jackson.databind.JsonDeserializer
를 구현하는 Deserializer 객체를 생성한다.
JsonNode
를 통해 mapping 해주었다.
get()
: 노드의 필드를 찾고 없으면 null을 반환한다.
- e.g.
node.get("body").get("totalCount").asInt();
path()
: 노드의 필드를 찾고 없으면 MissingNode를 반환한다.findValue()
: 노드와 자식 노드들에서 필드를 찾고 없으면 null을 반환한다.
get()
, path()
findValue()
item
은 동일 필드명이 없기 때문에 findValue()
방식으로 접근했다.List 값을 받으려면 objectMapper.treetoValue()
를 활용하여 배열로 받아 toList()
해주어야 한다.
@Data
@JsonDeserialize(using = FcstItemDeserializer.class)
public class FcstItems {
@JsonProperty("item")
private List<FcstItem> fcstItems;
public FcstItems(List<FcstItem> fcstItems) {
this.fcstItems = fcstItems;
}
}
FcstItems에 Deserialize할 때, 어떤 Deserializer를 사용할지 명시해주어야 한다.
@JsonDeserializer
어노테이션을 추가하여 class를 설정해주었다.
Deserializer를 재사용할 일이 많이 없거나, DTO마다 별도의 Deserializer가 필요한 경우 annotation을 사용해주는 방법도 있다.
@Data
@AllArgsConstructor
public class FcstItems {
@JsonProperty("item")
private List<FcstItem> fcstItems;
@JsonCreator
public FcstItems(@JsonProperty("response")JsonNode node) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode itemNode = node.findValue("item");
this.fcstItems = Arrays.stream(objectMapper.treeToValue(itemNode, FcstItem[].class)).toList();
}
}
@JsonCreator
과 @JsonProperty
어노테이션을 VO 안에서 사용해주었다.
@JsonCreator
은 기본생성자, setter 조합을 대체 하기때문에 @NoArgsConstructor
가 필요없다.
객체를 생성하고 필드를 생성과 동시에 채워 setter없이 immutable한 객체를 얻을 수 있다는 장점이 있다.
@JsonProperty
로 depth 1의 response를 가져와 주었다.
@Service
public class ForecastService {
public FcstItems parsingJsonObject(String json) {
FcstItems items = null;
try {
ObjectMapper mapper = new ObjectMapper();
items = mapper.readValue(json, FcstItems.class);
} catch(Exception e) {
e.printStackTrace();
}
return items;
}
}
원하는 값이 mapping된 것을 확인할 수 있다.
{
"item": [
{
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "TMP",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "20",
"nx": 49,
"ny": 124
},
(...)
{
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "PCP",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "강수없음",
"nx": 49,
"ny": 124
}
]
}
이제 json > object로의 mapping 작업은 모두 종료됐다. 추가적으로 Open API로 가져온 코드 값들을 해석해줄 일이 남았다.
category의 경우 별첨 자료에 코드 해석에 대한 내용이 나와있다. 클라이언트측에 값을 줄 때 해석한 값을 주는 것이 좋을 것 같아서 enum 클래스를 생성했다.
public enum CategoryCode {
POP("강수확률", "%"),
PTY("강수형태", ""),
PCP("1시간 강수량", "mm"),
REH("습도", "%"),
SNO("1시간 신적설", "cm"),
SKY("하늘상태", ""),
TMP("1시간 기온", "℃"),
TMN("아침 최저기온", "℃"),
TMX("낮 최고기운", "℃"),
UUU("풍속(동서성분)", "m/s"),
VVV("풍속(남북성분)", "m/s"),
WAV("파고", "M"),
VEC("풍향", "deg"),
WSD("풍속", "m/s");
>
private final String name;
private final String unit;
CategoryCode(String name, String unit) {
this.name = name;
this.unit = unit;
}
public String getName() { return name; }
public String getUnit() { return unit; }
>
public static String getCodeInfo(String name, String value) {
CategoryCode c = CategoryCode.valueOf(name);
if(c == CategoryCode.PTY) {
switch (value) {
case "0":
return "없음";
case "1":
return "비";
case "2":
return "비/눈";
case "3":
return "눈";
case "4":
return "소나기";
}
} else if(c == CategoryCode.SKY) {
switch(value) {
case "1":
return "맑음";
case "3":
return "구름많음";
case "4":
return "흐림";
}
}
return value;
}
}
category 코드를 해석하면 담아줄 필드를 하나 추가했다.
json과 mapping 될 때 json에는 없는 값이므로 @jsonIgnoreProperties(ignoreUnknown = true)
를 추가해주었다.
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class FcstItem {
// 해변코드
@JsonProperty("beachNum")
private int beachNum;
(...)
// 예보 값
@JsonProperty("fcstValue")
private String fcstValue;
private String categoryName;
}
CategoryCode
를 활용하는 코드를 추가한다.
@Service
public class ForecastService {
public FcstItems parsingJsonObject(String json) {
FcstItems result = new FcstItems(new ArrayList<>());
try {
ObjectMapper mapper = new ObjectMapper();
FcstItems items = mapper.readValue(json, FcstItems.class);
for(FcstItem item : items.getFcstItems()) {
result.getFcstItems().add(decodeCategory(item));
}
} catch(Exception e) {
e.printStackTrace();
}
return result;
}
private FcstItem decodeCategory(FcstItem item) {
String name = CategoryCode.valueOf(item.getCategory()).getName();
String value = CategoryCode.getCodeValue(item.getCategory(), item.getFcstValue());
String unit = CategoryCode.valueOf(item.getCategory()).getUnit();
item.setCategoryName(name);
item.setFcstValue(value + unit);
return item;
}
}
{
"item": [
{
"categoryName": "1시간 기온",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "TMP",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "20℃",
"nx": 49,
"ny": 124
},
{
"categoryName": "풍속(동서성분)",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "UUU",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "0.6m/s",
"nx": 49,
"ny": 124
},
{
"categoryName": "풍속(남북성분)",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "VVV",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "2.3m/s",
"nx": 49,
"ny": 124
},
{
"categoryName": "풍향",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "VEC",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "195deg",
"nx": 49,
"ny": 124
},
{
"categoryName": "풍속",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "WSD",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "2.4m/s",
"nx": 49,
"ny": 124
},
{
"categoryName": "하늘상태",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "SKY",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "구름많음",
"nx": 49,
"ny": 124
},
{
"categoryName": "강수형태",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "PTY",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "없음",
"nx": 49,
"ny": 124
},
{
"categoryName": "강수확률",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "POP",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "20%",
"nx": 49,
"ny": 124
},
{
"categoryName": "파고",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "WAV",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "0.5M",
"nx": 49,
"ny": 124
},
{
"categoryName": "1시간 강수량",
"beachNum": 1,
"baseDate": "20230524",
"baseTime": "1100",
"category": "PCP",
"fcstDate": "20230524",
"fcstTime": "1200",
"fcstValue": "강수없음mm",
"nx": 49,
"ny": 124
}
]
}
전체 코드가 궁금하다면?
https://github.com/Jeongminyooa/open-api
자신의 목적에 맞게 Open API를 활용한다면 분명 강력한 도구가 될 것 같다. 오늘은 간단하게 통신해와서 반환하는 코드만 작성했지만 필요에 따라 Database에 저장하는 작업도 추가해주어도 된다. (어쩌면 다음 포스팅이 될지도 모르는..)
Open API와 연동하여 통신하는 작업보다 Json parsing 작업이 시간이 더 소요된 것 같다. 그래도 json mapping 하는 여러가지 방법이나 URLConnection 클래스에 대해 정확하게 공부해볼 수 있어서 유익했다.
ref
https://www.codejava.net/java-se/networking/how-to-use-java-urlconnection-and-httpurlconnection
https://tjdans.tistory.com/6
https://thalals.tistory.com/273
https://shinsunyoung.tistory.com/52
https://hianna.tistory.com/631
https://blog.naver.com/PostView.naver?blogId=occidere&logNo=222512549848&redirect=Dlog&widgetTypeCall=true&directAccess=false
https://homoefficio.github.io/2016/11/19/%EC%A1%B0%EA%B8%88%EC%9D%80-%EC%8B%A0%EA%B2%BD%EC%8D%A8%EC%A4%98%EC%95%BC-%ED%95%98%EB%8A%94-Jackson-Custom-Deserialization/
https://velog.io/@yevini118/SpringBoot-Json-Deserialize-%ED%95%98%EA%B8%B0
잘 봤습니다. 감사합니다!