본문 바로가기

Programming/Java

[Java] 공공데이터포털 Open API 사용하기 - 동네예보 (2)

반응형

지난시간에 이어서 공공데이터포털 Open API를 사용하여 특정 지역의 날씨를 얻어오는 방법을 알아보자.

동네예보조회서비스는 xml과 json 형태로 날씨예보 데이터를 제공한다. 이 데이터 내용을 프로그래밍적으로 읽으려면 전송된 데이터의 구조를 이해하는 것이 중요하다.

아쉽게도 공공데이터포털에서 제공하는 사용자활용가이드 문서에 수록된 예시 데이터는 xml 형태로만 나와있다. json 데이터도 xml과 같은 hierarchy 구조이긴 하지만, 처음 사용하는 사람들을위해 json 구조를 보여주며 설명하려고 한다.

 

1. 먼저 알아야 할 것

데이터구조를 보기전에 먼저 알아야 할 것이 있다. 

동네예보조회서비스는 발표시각과 예보시각이 있다.

앞선 포스팅에서 잠깐 언급했지만, 동네예보조회서비스는 오전2시부터 1일 8회, 3시간 간격으로 지역별 날씨를 발표한다. 이때의 발표순간의 시간을 발표시각이라고 한다. 그런데 이 발표된 날씨정보는 발표시각 순간의 날씨가 아니라 발표시각으로부터 4시간 후의 날씨로 예측되는 정보이다. 이 시각을 예보시각이라고 한다. 따라서 예보시각은 무조건 발표시각 + 4시간 이다.

 

정리하면, 동네예보조회서비스는 발표시각으로부터 4시간후의 날씨를 오전 2시 정각부터 3시간 간격으로 1일 8회 발표한다.

 

2. 요청 만들기

API에 적절한 요청을 보내면 API가 적절한 응답을 한다. 반대로 API에 잘못된 요청을 보내면 잘못된 응답이 돌아온다.

endpoint로 GET method를 요청할 때 전달해야 하는 parameter로는 OpenAPI 사용자 활용 가이드에 나온 아래 표와 같다.

GET method 파라미터 전달 형식은 endpoint 뒤에 ?key1=value1&key2=value2 형식을 가진다.

(_type은 json으로 보내기로 한다.)

 

메시지명

http://newsky2.kma.go.kr/service/SecndSrtpdFrcstInfoService2/ForecastSpaceData

항목명

항목명(국문)

항목크기

항목구분

샘플데이터

항목설명

(필수)ServiceKey

서비스 키

255

1

TEST_SERVICE_KEY

서비스 인증

(필수)base_date

발표일자

8

1

20151201

‘15121일발표

(필수)base_time

발표시각

4

1

0500

05시 발표

* 하단 참고자료 참조

(필수)nx

예보지점 X 좌표

2

0

1

예보지점의 X 좌표값

(필수)ny

예보지점 Y 좌표

2

0

1

예보지점의 Y 좌표값

_type

타입

 

 

xml, json

xml(기본값), json

 

발표시각이 중요한 이유는 잘못된 발표시각 값을 입력하여 요청을 전달하면 에러를 반환하기 때문이다.

필자는 현재시간으로부터 가장 최근에 발표된 동네예보를 불러오려고 한다. 그러기 위해서는 현재시각에서 가장 가까운 발표시각을 찾는 알고리즘을 먼저 만들어야한다.

가장 가까운 발표시각과 예보시각을 구하는 알고리즘을 구현해보자. 오전 2시부터 1일 8회이면 3시간 간격으로 발표가 이루어진다고 볼 수 있다. 또한 분단위는 필요 없으므로 시간만 계산하면 된다. 이를 구현하면 다음과 같다.

// t : 현재시간
private int getLastBaseTime(int t) {
    return t - (t + 1) % 3;
}

그런데만약에 현재시간이 예를들어 2017년 10월 20일 1시 20분이라고 생각해보자. 그렇다면 위 메소드는 음수를 반환하게 된다. 2시 이전일경우는 전날 23시를 반환하여야 한다.

그렇다면 극단적인 케이스도 생각해볼 수 있는데, 2018년 1월 1일 00시 00분 1초가 딱 지나가는 순간의 경우에는 '2017년 12월 31일 23시' 를 반환하여야 하므로 연단위 계산까지 필요하다.

이러한 조건을 반영하여 코드를 다음과 같이 수정하여야 한다.

// calBase : 현재시간의 Calendar 객체
private Calendar getLastBaseTime(Calendar calBase) {
    int t = calBase.get(Calendar.HOUR_OF_DAY);
    if (t < 2) {
        calBase.add(Calendar.DATE, -1);
        calBase.set(Calendar.HOUR_OF_DAY, 23);
    } else {
        calBase.set(Calendar.HOUR_OF_DAY, t - (t + 1) % 3);
    }
    return calBase;
}

Calendar 클래스를 사용하면 날짜단위 연산이 훨씬 간편하다. 해가 넘어가는 시점에 일단위 연산을 하면 자동으로 연단위까지 맞춰준다.

위 코드를 사용하여 발표시각과 예보시각을 구할 수 있다.

 

지역코드를 구하는 코드는 앞 포스팅에서 소개하였으므로, 요청 파라미터를 구성하는 값들은 이제 모두 산출할 수 있게 되었다. 이를 목록으로 작성하자.

 

endpoint : http://newsky2.kma.go.kr/service/SecndSrtpdFrcstInfoService2/ForecastSpaceData

ServiceKey : xxxxxxxxxxxxxxxxxxxxxxx

base_date : 20171019

base_time : 2300

nx : 60

ny : 125

_type : json

 

이를 URL로 변경하면,

 

http://newsky2.kma.go.kr/service/SecndSrtpdFrcstInfoService2/ForecastSpaceData?ServiceKey=xxxxxxxxxxxxxxxxxxxxxxx&base_date=20171019&base_time=2300&nx=60&ny=125&_type=json

 

이렇게 된다.

위 주소의 서비스 키 부분만 자신이 신청한 서비스키로 바꿔넣어준 후 웹브라우저에 입력하면 10월 19일 23시에 발표된 서울시 서초구 반포1동의 날씨 데이터를 json 형태로 얻을 수 있다!

이제는 이러한 요청을 보냈을 때 돌아올 응답 데이터의 샘플을 통해 어떤 데이터를 얻을 것인지 미리 확인해보자.

 

 

3. 응답 데이터 샘플 보기

json에는 key:value의 형태, {} 형태와 [] 형태의 데이터 구조가 있다.

key:value 형식의 데이터를 pair라고 한다. 그리고 {}는 object라고 하고 []는 array라고 한다.

샘플을 보면서 함께 설명하겠다.

{"response":{"header":{"resultCode":"0000","resultMsg":"OK"},"body":{"items":{"item":[{"baseDate":20171106,"baseTime":1400,"category":"POP","fcstDate":20171106,"fcstTime":1800,"fcstValue":10,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"PTY","fcstDate":20171106,"fcstTime":1800,"fcstValue":0,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"R06","fcstDate":20171106,"fcstTime":1800,"fcstValue":0,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"REH","fcstDate":20171106,"fcstTime":1800,"fcstValue":65,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"S06","fcstDate":20171106,"fcstTime":1800,"fcstValue":0,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"SKY","fcstDate":20171106,"fcstTime":1800,"fcstValue":2,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"T3H","fcstDate":20171106,"fcstTime":1800,"fcstValue":13,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"UUU","fcstDate":20171106,"fcstTime":1800,"fcstValue":1.7,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"VEC","fcstDate":20171106,"fcstTime":1800,"fcstValue":300,"nx":63,"ny":111},{"baseDate":20171106,"baseTime":1400,"category":"VVV","fcstDate":20171106,"fcstTime":1800,"fcstValue":-1,"nx":63,"ny":111}]},"numOfRows":10,"pageNo":1,"totalCount":195}}}

 

이렇게 보니 구조가 눈에 잘 안들어온다.

엔터와 들여쓰기를 활용하여 구조적으로 살펴보자.

{
  "response":{
    "header":{
      "resultCode":"0000",
      "resultMsg":"OK"
    },
    "body":{
      "items":{
        "item":[
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"POP",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":10,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"PTY",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":0,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"R06",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":0,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"REH",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":65,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"S06",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":0,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"SKY",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":2,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"T3H",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":13,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"UUU",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":1.7,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"VEC",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":300,
            "nx":63,
            "ny":111
          },
          {
            "baseDate":20171106,
            "baseTime":1400,
            "category":"VVV",
            "fcstDate":20171106,
            "fcstTime":1800,
            "fcstValue":-1,
            "nx":63,
            "ny":111
          }
        ]
      },
      "numOfRows":10,
      "pageNo":1,
      "totalCount":195
    }
  }
}

이 구조를 그림으로 그려보았다.

 

 

 

타원은 pair, 둥근사각형은 object이고 네모는 array이다.

결론부터 이야기하면 우리는 전체 데이터를 json object로 받고, 이 object 아래 body라는 object 아래 items 라는 object 아래 item이라는 array 아래 object들에 접근해서 데이터를 얻어야한다!

이렇게 얻어진 최종 object에는 8개의 pair가 들어있다. 각각의 의미는 OpenAPI사용자활용가이드에 자세히 나와있으므로 여기서는 간략하게 설명한다.

 

baseDate : 발표시각의 날짜

baseTime : 발표시각의 시분

category : 데이터 종류

fcstDate : 예보시각의 날짜

fcstTime : 예보시각의 시분

fcstValue : 예보 값

nx, ny : 기상청 지역코드

 

category에 따라 fcstValue가 나타내는 의미는 다르다. 따라서 OpenAPI 사용자활용가이드의 '동네예보조회서비스' 페이지 설명을 잘 읽어보고 json 에서 원하는 데이터만 파싱하여 사용자에게 제공하면 된다.

category에 따른 예보 값 목록은 다음과 같다.

 

예보구분

항목값

항목명

단위

Missing

압축bit

동네예보

POP

강수확률

%

-1 %

8

PTY

강수형태

코드값

-1

4

R06

6시간 강수량

범주 (1 mm)

-1 mm

8

REH

습도

%

-1 %

8

S06

6시간 신적설

범주(1 cm)

-1 cm

8

SKY

하늘상태

코드값

-1

4

T3H

3시간 기온

-50

10

TMN

아침 최저기온

-50

10

TMX

낮 최고기온

-50

10

UUU

풍속(동서성분)

m/s

-100 m/s

12

VVV

풍속(남북성분)

m/s

-100 m/s

12

WAV

파고

M

-1 m

8

VEC

풍향

m/s

-1

10

WSD

풍속

1

-1

10

 

 

 

== 2019.06.29 ==

본 포스팅은 학부생 때 OpenAPI 이용한 날씨 모니터링 프로그램 개발 프로젝트를 수행하면서 작성한 것인데, Java로 작성해야하는 제약사항이 있어서 부득이하게 Java를 사용해야했다. 그런데 Java는 json 파싱이 상당히 불편하고 코드 길이도 길어져서 난해한 점 등 여러가지 문제가 있어 포스팅을 끝까지 이어가지 못했다. 사실 블로그를 본 지인들과 외부 독자분들에게도 너무 어렵다는 피드백을 많이 받았다. 지금 코드를 보면 내가 봐도 어지럽고 개선해야할 부분들이 많이 보여서 공개하기 부끄럽기까지 하다. 하지만 학부생 때 만든 프로그램이기 때문에 공개할꺼면 딱 학부생 수준으로 가감없이 공개하는 것이 좋다고 생각했다. 우선 그때 만든 프로그램 코드 전체를 올리지는 못하고 참고가 될 부분은 참고할 수 있도록 핵심 클래스와 테스트코드만 올린다. 1편에 수록된 코드와 함께 동작한다.

 

/* WeatherSet.java */

package pkgWeatherMonitor;

import java.util.Calendar;
import java.util.Date;

public class WeatherSet {
    private int pop;
    private int sky;
    private Date baseDate = null;
    private Date fcstDate = null;

    public WeatherSet(int p, int s, Date bd) {
        pop = p;
        sky = s;
        baseDate = bd;
        Calendar calBase = Calendar.getInstance();
        calBase.setTime(baseDate);
        calBase.add(Calendar.HOUR_OF_DAY, 4);
        fcstDate = calBase.getTime();
    }

    public int getPop() {
        return pop;
    }

    public void setPop(int p) {
        pop = p;
    }
    
    public int getSkyValue(){
        return sky;
    }

    public String getSky() {
        String retMsg = null;
        switch (sky) {
        case 1:
            retMsg = "맑음";
            break;
        case 2:
            retMsg = "구름 조금";
            break;
        case 3:
            retMsg = "구름 많음";
            break;
        case 4:
            retMsg = "흐림";
            break;
        default:
            retMsg = "Error";
            break;
        }
        return retMsg;
    }

    public Date getBaseDate() {
        return baseDate;
    }
    
    public Date getFcstDate() {
        return fcstDate;
    }

    public void setSky(int s) {
        sky = s;
    }
}
/* WeatherFetcher.java */

package pkgWeatherMonitor;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import org.json.simple.JSONArray;
import org.json.simple.JSONObject;

public class WeatherFetcher {
    private final int BASE_DATE = 3;
    private final int BASE_TIME = 5;
    private final int NX = 7;
    private final int NY = 9;
    private final String[] uri = {
            "http://newsky2.kma.go.kr/service/SecndSrtpdFrcstInfoService2/ForecastSpaceData?ServiceKey=",
            "xxxx", //service key
            "&base_date=", "", "&base_time=", "", "&nx=", "", "&ny=", "", "&_type=json"
    };
    
    private Calendar calBase = null;
    private int hour;
    private int lastBaseTime;

    private JakeJsonParser jjp = null;

    public WeatherFetcher() {
        jjp = JakeJsonParser.getInstance();
        calBase = Calendar.getInstance(); // 현재시간 가져옴
        calBase.set(Calendar.MINUTE, 0); // 분, 초 필요없음
        calBase.set(Calendar.SECOND, 0);
        hour = calBase.get(Calendar.HOUR_OF_DAY);
        lastBaseTime = getLastBaseTime(hour);
    }

    private int getLastBaseTime(int t) {
        if (t >= 0) {
            if (t < 2) {
                calBase.add(Calendar.DATE, -1);
                calBase.set(Calendar.HOUR_OF_DAY, 23);
                return 23;
            } else {
                calBase.set(Calendar.HOUR_OF_DAY, t - (t + 1) % 3);
                return t - (t + 1) % 3;
            }
        } else
            return -1;
    }

    private String getBaseTime() {
        if (lastBaseTime / 10 > 0) // 두자리수이면
            return lastBaseTime + "00";
        else // 한자리수이면
            return "0" + lastBaseTime + "00";
    }

    public WeatherSet fetchWeather(String nx, String ny) {
        WeatherSet ws = null;
        String sUrl = new String();
        int pop = -1, sky = -1;
        uri[BASE_DATE] = new SimpleDateFormat("yyyyMMdd").format(calBase.getTime());
        uri[BASE_TIME] = getBaseTime();
        uri[NX] = nx;
        uri[NY] = ny;
        for (int i = 0; i < uri.length; i++)
            sUrl += uri[i];
        
        JSONArray jsonArr = jjp.getWeatherJSONArray(sUrl);

        for (int i = 0; i < jsonArr.size(); i++) {
            JSONObject jobj = (JSONObject) jsonArr.get(i);
            if (((String) jobj.get("category")).equals("POP"))
                pop = (int) (long) jobj.get("fcstValue"); // JSON에서 ""로 감싸지지않은 값은 long 형이므로 casting 필수!
            else if (((String) jobj.get("category")).equals("SKY"))
                sky = (int) (long) jobj.get("fcstValue");
        }
        if (pop != -1 && sky != -1){
            Date bd = calBase.getTime();
            ws = new WeatherSet(pop, sky, bd);
        }
        return ws;
    }
}
/* WthMonitorMain.java */

package pkgWeatherMonitor;

import java.text.SimpleDateFormat;

public class WthMonitorMain {
    public static void main(String[] args) {
        String[] location = { "충청남도", "천안시서북구", "부성동" };
        Coord coLocationCode = null;
        WeatherSet weather = null;
        LocationCodeFetcher lcf = new LocationCodeFetcher();
        WeatherFetcher wf = new WeatherFetcher();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy년 MM월 dd일 HH시 정각");
        
        coLocationCode = lcf.fetchLocationCode(location);
        System.out.println("location code : " + coLocationCode.getSx() + ", " + coLocationCode.getSy());
        weather = wf.fetchWeather(coLocationCode.getSx(), coLocationCode.getSy());
        System.out.println("발표시각 : " + sdf.format(weather.getBaseDate()));
        System.out.println(sdf.format(weather.getFcstDate()) + "의 강수확률은 " + weather.getPop() + "%, 하늘은 " + weather.getSky() + "입니당ㅎ");
    }
}

 

동일 패키지에 모두 넣은 후 WeatherFetcher.java의 service key 부분을 현재 유효한 동네예보 api 서비스키로 입력한 뒤  WthMonitorMain 클래스를 실행하면 다음과 같이 실행된다.

location code : 63, 111
{"response":{"header":{"resultCode":"0000" ...(생략)

발표시각 : 2019년 06월 29일 02시 정각
2019년 06월 29일 06시 정각의 강수확률은 60%, 하늘은 흐림입니당ㅎ

 

반응형