본문 바로가기
회고

[이상청] 기상청 단기예보 API를 받아와서 처리하기

by 선의 2022. 8. 8.

개요

https://www.data.go.kr/data/15084084/openapi.do

 

기상청_단기예보 ((구)_동네예보) 조회서비스

초단기실황, 초단기예보, 단기((구)동네)예보, 예보버전 정보를 조회하는 서비스입니다. 초단기실황정보는 예보 구역에 대한 대표 AWS 관측값을, 초단기예보는 예보시점부터 6시간까지의 예보를,

www.data.go.kr

  이상청은 현재 날씨 및 날씨 예보 정보를 제공하는 프로젝트다. 날씨 관련 정보를 제공하는 많고 많은 API 중에서, (1)무료다 (2)기상청 공식이다라는 두가지 이유로 위의 API에서 정보를 받아와서 원하는 형태로 가공하기로 했다.

  받아오는 데 특별한 문제가 발생하진 않았고 솔직히 그렇게 어렵지도 않았지만, 구글링하면 해당 API를 활용하는 자료가 그닥 없다...는 점이 가장 막막한 점이었다. 위의 링크로 들어가면 활용 가이드 문서와 샘플 자바 코드를 제공하긴 하는데, 원하는 형식으로 바꾸려면 이외의 프레임워크가 더 필요했다. 하루 정도 구글링하다가 이 작업은 맨땅에 헤딩하는 심정으로 해야한다는 걸 깨달았고, 다음날엔 스프링에 맞는 자바 코드로 API를 받아오는 데 성공했다. 그 다음날쯤에 json으로 받아온 item들을 하나씩 보면서 왜 취업할 때 코딩 테스트를 보는가(...)를 깨닫게 되었다.

  코딩을 하면서 삼일 정도 되는 시간 동안 (1)방법을 구글에서 찾는다 (2)나오지 않음을 깨닫고 일단 코드를 짠다 (3)에러가 나면 에러 메시지를 읽으면서 뭐가 필요한지 생각한다 (4)운동이나 런닝을 나갔다가 들어오면서 아 이렇게 하면 되지 않을까? 생각한다 (5)해결 후에 박수치면서 좋아하다가 더 효율적인 방법이 있지 않을까? 생각한다. 위의 다섯 단계를 반복했다. 전공 공부를 시작한 이후에 가장 구글링의 도움을 받지 못한 작업이라... 회고도 회고지만 혹시라도 나와 같은 삽질을 하는 사람이 있을까봐 최대한 글을 상세하게 적어보려고 한다.

  해당 작업을 할 당시, 내가 아는 제일 재수없고 연차 높은 개발자인 아빠가 내가 키보드를 두드리고 있을 때마다 나를 비웃으면서 지나가곤 했는데(아직도 하냐?^^) 덕분에 집중이 끊기지 않고 반드시 해결하겠다는 마음가짐으로 임할 수 있었다. 또한 동생이 로보틱스 클럽에 들어가면서 한두번정도 보호자로 같이 가서 11살쯤부터 자바로 코딩을 하는 인도계 아가들을 봤는데, 숨쉬듯이... 걍 놀면서... 자바에 대해 줄줄 읊는 아이들을 보면서...? 아,,, 미래에 저 아이들이 커서 구글과 유튜브에서 온갖 에러를 해결해주는 멋진 개발자들이 되는거구나... 나도 발전해야겠다... 그런 생각을 하면서 작업했다. 전공이 컴퓨터라고 하니까 이것저것 물어보는데 자신있게 대답할 수 없는 내가 좀 부끄러웠다. 나보다 열살 가까이 어린 그 아이들이 그다지 구글링 안 하고 걍 뚝딱 만들어내는 모습을 보면서 할 수 있다... 해야 한다... 그런 생각을 했다.

 

 

API를 우선 그대로 받아와 보기

1. 우선 build.gradle의 dependencies에 json-simple 라이브러리를 추가해준다.

implementation 'com.googlecode.json-simple:json-simple:1.1.1'

2. API 문서를 찬찬히 읽고, 샘플 코드를 뜯어보자

/* Java 1.8 샘플 코드 */


import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.io.BufferedReader;
import java.io.IOException;

public class ApiExplorer {
    public static void main(String[] args) throws IOException {
        StringBuilder urlBuilder = new StringBuilder("http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getUltraSrtNcst"); /*URL*/
        urlBuilder.append("?" + URLEncoder.encode("serviceKey","UTF-8") + "=서비스키"); /*Service Key*/
        urlBuilder.append("&" + URLEncoder.encode("pageNo","UTF-8") + "=" + URLEncoder.encode("1", "UTF-8")); /*페이지번호*/
        urlBuilder.append("&" + URLEncoder.encode("numOfRows","UTF-8") + "=" + URLEncoder.encode("1000", "UTF-8")); /*한 페이지 결과 수*/
        urlBuilder.append("&" + URLEncoder.encode("dataType","UTF-8") + "=" + URLEncoder.encode("XML", "UTF-8")); /*요청자료형식(XML/JSON) Default: XML*/
        urlBuilder.append("&" + URLEncoder.encode("base_date","UTF-8") + "=" + URLEncoder.encode("20210628", "UTF-8")); /*‘21년 6월 28일 발표*/
        urlBuilder.append("&" + URLEncoder.encode("base_time","UTF-8") + "=" + URLEncoder.encode("0600", "UTF-8")); /*06시 발표(정시단위) */
        urlBuilder.append("&" + URLEncoder.encode("nx","UTF-8") + "=" + URLEncoder.encode("55", "UTF-8")); /*예보지점의 X 좌표값*/
        urlBuilder.append("&" + URLEncoder.encode("ny","UTF-8") + "=" + URLEncoder.encode("127", "UTF-8")); /*예보지점의 Y 좌표값*/
        URL url = new URL(urlBuilder.toString());
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Content-type", "application/json");
        System.out.println("Response code: " + conn.getResponseCode());
        BufferedReader rd;
        if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300) {
            rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        } else {
            rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
        }
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = rd.readLine()) != null) {
            sb.append(line);
        }
        rd.close();
        conn.disconnect();
        System.out.println(sb.toString());
    }
}

- StringBuilder로 url을 만들어 준다. 후에 선언한 StringBuilder urlBuilder에 append 함수를 사용하여 서비스 키, UTF, 한 페이지 당 결과 수, 요청 자료 형식(JSON), 기준 날짜, 기준 시간, 좌표값을 넣어 GET을 요청할 url을 완성한다.

- String으로 완성된 urlBuilder를 URL로 형변환을 시킨다(여기서는 형변환이라기 보단 String으로 URL을 새로 생성한다는 쪽이 좀 더 말이 맞긴 하다)

- HttpURLConnection으로 요청할 준비를 한다. Method는 "GET", 프로퍼티에는 "application/json"을 넣어준다. 요청 후에는 ResponseCode를 받을 수 있다. 200이면 성공. 실패하면 보통 500이 뜬다. 500은 좌표값(nx, ny) 자료형을 잘못 선언했을 때 만났다(다른 건 다 String인데 좌표값만 Long이다).

- BufferedReader를 선언한다(rd). rd에 받아온 json을 넣는다. 후에 다시 StringBuilder를 선언하여 한줄한줄 읽어 준다.

 

위 코드를 복사+붙여넣기 하면 콘솔창에 성공적으로 출력되는 걸 볼 수 있다. 하지만 우리는 콘솔창에 출력되는 게 필요한 게 아니고, 객체로 받아와서 곰돌이나 날씨예보를 담당하는 서비스에서 사용할 수 있도록 만들어야 한다.

 

3. 우선은 그대로 받아오는 걸 목표로 한다. 이제 위에서 implement한 simple-json을 활용할 때다. 우선 DTO를 만든다.

OpenWeatherResponseDto.java

import lombok.Builder;
import lombok.Getter;

@Getter
public class OpenWeatherResponseDto {
    /** 발표일자 */
    private String baseDate;

    /** 발표시각 */
    private String baseTime;

    /** 예보일자 */
    private String fcstDate;

    /** 예보시각 */
    private String fcstTime;

    /** 자료구분문자 */
    private String category;

    /** 예보 값 */
    private String fcstValue;

    /** 예보지점 X 좌표 */
    private Long nx;

    /** 예보지점 Y 좌표 */
    private Long ny;

    @Builder
    public OpenWeatherResponseDto(String baseDate, String baseTime, String fcstDate, String fcstTime, String category, String fcstValue, Long nx, Long ny) {
        this.baseDate = baseDate;
        this.baseTime = baseTime;
        this.fcstDate = fcstDate;
        this.fcstTime = fcstTime;
        this.category = category;
        this.fcstValue = fcstValue;
        this.nx = nx;
        this.ny = ny;
    }
}

baseDate는 발표 일자, baseTime은 발표 시간, fcstDate는 예보 일자, fcstTime은 예보 시간을 의미한다. Category에는 14개의 값이 있는데, 하늘 상태/강수 확률/강수 형태/온도 등등이 있고, 프로젝트에서 필요한 카테고리는 6개 정도였다. 위에도 언급했듯 예보지점 좌표는 Long이다. Long으로 요청하고 Long으로 받아와야 한다(삽질을 엄청 했다).

 

서비스를 보자(핵심이다)

OpenWeatehrAPI.java

    private final String BASE_URL = "http://apis.data.go.kr/1360000/VilageFcstInfoService_2.0/getVilageFcst";
    private final String serviceKey = "*";
    // 요청 고정갑
    private String pageNo = "1";
    private String numOfRows = "310";
    private String dataType = "JSON";// api 제공시각
    private String nx = "59";
    private String ny = "126"; // nx, ny는 서대문구 신촌동 좌표값

  메소드를 작성하기 전에, 고정값들은 선언하고 시작한다. serviceKey는 공공데이터포털에서 회원가입하고 API 사용신청하면 받을 수 있다. pageNo는 받아올 페이지 수, numOfRows는 몇 개 받아올지(22시간정도 한 시간당 온도, 하늘, 강수확률 받아올 때 필요한 최소값 + 넉넉하게 10개 더 해서 310), dataType은 JSON으로(기본값 XML이다 주의하자) 고정한다.

  nx, ny값은 위에서 제공하는 가이드 문서의 엑셀 파일에서 서대문구 신촌동 좌표값으로 고정했다.

    @Transactional
    public List<OpenWeatherResponseDto> findWeather() throws IOException, ParseException {
        URL url = buildRequestUrl(); // url 생성
        System.out.println(url);  // url 어떻게 되는지 확인
        String stringResult = httpURLConnect(url);
        JSONArray jsonResult = getItems(stringResult);
        return buildResponse(jsonResult);
    }

다음은 API를 요청해서 값을 받아오고, JSON형태로 가공한 다음 ResponseDto로 만드는 메소드다. 코드가 길어지면 이해하기도 읽기도 귀찮아지고 여러가지 형태로 가공해야 하기 때문에 여러 함수로 쪼개서 사용했다. 값 받아오는 부분은 위의 샘플 코드를 참고했고, JSONArray로 처리하는 부분은 simple-json 라이브러리 사용 예시를 구글링해서(공식문서 열심히 읽음) 작성했다. 쭉 보면

 

- buildRequestURL(날짜, 시간 받아오는 부분은 아래쪽에 후술)

public URL buildRequestUrl() throws IOException {
        StringBuilder sb = new StringBuilder(BASE_URL);
        String baseDate = getCurrentDate();
        String baseTime = getBaseTime();
        //System.out.println(baseDate + " " + baseTime);
        if(Objects.equals(baseTime, "2300")){
            Date dDate = new Date();
            dDate = new Date(dDate.getTime()+(1000*60*60*24*-1));
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
            baseDate = sdf.format(dDate);
        }
        
        //System.out.println(baseDate + " " + baseTime);
        sb.append("?").append(URLEncoder.encode("serviceKey", "UTF-8")).append("=").append(serviceKey);
        sb.append("&").append(URLEncoder.encode("pageNo", "UTF-8")).append("=").append(URLEncoder.encode(pageNo, "UTF-8"));
        sb.append("&").append(URLEncoder.encode("numOfRows", "UTF-8")).append("=").append(URLEncoder.encode(numOfRows, "UTF-8")); /* 한 페이지 결과 수 */
        sb.append("&").append(URLEncoder.encode("dataType", "UTF-8")).append("=").append(URLEncoder.encode(dataType, "UTF-8")); /* 타입 */
        sb.append("&").append(URLEncoder.encode("base_date", "UTF-8")).append("=").append(URLEncoder.encode(baseDate, "UTF-8")); /* 조회하고싶은 날짜 */
        sb.append("&").append(URLEncoder.encode("base_time", "UTF-8")).append("=").append(URLEncoder.encode(baseTime, "UTF-8")); /* 조회하고싶은 시간 AM 02시부터 3시간 단위 */
        sb.append("&").append(URLEncoder.encode("nx", "UTF-8")).append("=").append(URLEncoder.encode(nx, "UTF-8")); // 경도
        sb.append("&").append(URLEncoder.encode("ny", "UTF-8")).append("=").append(URLEncoder.encode(ny, "UTF-8")); // 위도

        return new URL(sb.toString());
    }

 

- httpURLConnect

    public String httpURLConnect(URL url) throws IOException{
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("GET");
        conn.setRequestProperty("Content-type", "application/json");
        //System.out.println("Response code: " + conn.getResponseCode());

        BufferedReader rd;

        if(conn.getResponseCode() >= 200 && conn.getResponseCode() <= 300){
            rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        } else{
            rd = new BufferedReader(new InputStreamReader(conn.getErrorStream()));
        }

        StringBuilder sb = new StringBuilder();
        String line;

        while((line = rd.readLine()) != null){
            sb.append(line);
        }

        rd.close();;
        conn.disconnect();
        return sb.toString();
    }

(지금 다시 보니까 위의 함수 두개 그냥 하나로 합칠 걸 그랬다)

 

- getItems

    public JSONArray getItems(String stringResult) throws ParseException {

        JSONParser jsonParser = new JSONParser();
        JSONObject jsonObject = (JSONObject) jsonParser.parse(stringResult);
        JSONObject parseResponse = (JSONObject) jsonObject.get("response");
        JSONObject parseBody = (JSONObject) parseResponse.get("body");
        JSONObject parseItems = (JSONObject) parseBody.get("items");
        JSONArray parseItem = (JSONArray) parseItems.get("item");

        return parseItem;
    }

이렇게 세 함수를 보면, 차례대로 1)URL을 생성 2)URL에 GET으로 값 요청해서 String으로 받아옴 3)String을 JSON으로 파싱하여 item을 얻음. 이런 식으로 수행된다. 이제 responseDto를 만든다.

 

- buildResponse

    public List<OpenWeatherResponseDto> buildResponse(JSONArray parse_item){
        List<OpenWeatherResponseDto> dataList = new ArrayList<OpenWeatherResponseDto>();

        JSONObject obj;

        for(int i = 0; i < parse_item.size(); i++){
            obj = (JSONObject) parse_item.get(i);
            String baseDate = (String) obj.get("baseDate");
            String baseTime = (String) obj.get("baseTime");
            String category = (String) obj.get("category");
            String fcstDate = (String) obj.get("fcstDate");
            String fcstTime = (String) obj.get("fcstTime");
            String fcstValue = (String) obj.get("fcstValue");
            Long nx = (Long) obj.get("nx");
            Long ny = (Long) obj.get("ny");


            OpenWeatherResponseDto weatherResponseDto = new OpenWeatherResponseDto(
                    baseDate, baseTime, fcstDate, fcstTime, category, fcstValue, nx, ny
            );

            dataList.add(weatherResponseDto);

        }

        return dataList;
    }

JSONArray를 돌면서 JSONObject들을 하나씩 처리하고, dto 형태로 만들어서 List에 넣고 List를 return한다. 제대로 나오는지 포스트맨으로 테스트하기 위해서 컨트롤러도 하나 만들어 준다.

 

OpenWeatehrApiController.java

@RestController
@RequestMapping("api/v1/weather")
public class OpenWeatherApiController {

    @Autowired
    private final OpenWeatherAPI openWeatherAPI = new OpenWeatherAPI();

    @GetMapping
    public List<OpenWeatherResponseDto> loadAllWeather() throws IOException, ParseException {
        return openWeatherAPI.findWeather();
    }
}

그럼 어떻게 나오냐면

 

- GET localhost:8080/api/v1/weather

{
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "category": "TMP",
        "fcstValue": "27",
        "nx": 59,
        "ny": 126
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "category": "UUU",
        "fcstValue": "-0.3",
        "nx": 59,
        "ny": 126
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "category": "VVV",
        "fcstValue": "1",
        "nx": 59,
        "ny": 126
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "category": "VEC",
        "fcstValue": "158",
        "nx": 59,
        "ny": 126
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "category": "WSD",
        "fcstValue": "1.1",
        "nx": 59,
        "ny": 126
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "category": "SKY",
        "fcstValue": "4",
        "nx": 59,
        "ny": 126
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "category": "PTY",
        "fcstValue": "0",
        "nx": 59,
        "ny": 126
    },
    .
    .
    .

이렇게 310개가 뽑힌다. 그대로 받아오는 건 성공했으니 다음은 가공을 할 차례다.

 

 

원하는 형태로 가공하기: 현재 날씨

우선 위의 컨트롤러 응답을 보자. baseDate, baseTime은 고정이다. fcstDate, fcstTime은 baseTime을 기준으로 1시간 뒤부터(5시 발표값이면 6시부터) 값을 준다. category는 가이드 문서에 따라 14개의 값이 반복된다. fcstValue는 해당 날짜, 해당 시간의 카테고리에 따른 값이다. 좌표는 고정(이후 관련 설명 생략).

현재 날씨는 현재 날짜, 현재 시간, 최고 온도, 최저 온도, 현재 온도, 강수 확률, 하늘 상태, 강수 형태를 받아 와야 한다. 이 때 카테고리 코드값이 일정하게 반복되므로 12개를 기준으로 반복문을 돌려주면 된 다고 생각했다... 만, 위의 '최고 온도'와 '최저 온도'를 처리하는 게 문제가 되었다.

최고 온도와 최저 온도는 받아온 값에서 딱 하나의 item에만 있다. 카테고리 종류는 14개인데, 12개 단위로 반복된다. 최고 온도와 최저 온도는 불규칙하게 준다. 하루의 어느 시간대가 제일 춥고 더운지 모르기 때문이다. 여기서 일단 첫번째 문제가 생겼다만, 자세히 응답을 들여다 보면 최고온도(tmx)와 최저온도(tmx)는 12개 반복 후 맨 마지막에만 있다. "TMP"로 다음 fcstTime의 값 나열을 시작하기 바로 직전에 존재한다. 이를 이용하여 현재 날씨를 탐색한다.

 

CalendarWeatherResponseDto.java

import lombok.Builder;
import lombok.Getter;

@Getter
public class CalendarWeatherResponseDto {

    private String baseDate;
    private String baseTime;
    private String fcstDate;
    private String fcstTime;
    private String tmp;
    private String tmx;
    private String tmn;
    private String sky;
    private String pop;
    private String pty;

    @Builder
    public CalendarWeatherResponseDto(String baseDate, String baseTime, String fcstDate, String fcstTime, String tmp, String tmx, String tmn, String sky, String pop, String pty) {
        this.baseDate = baseDate;
        this.baseTime = baseTime;
        this.fcstDate = fcstDate;
        this.fcstTime = fcstTime;
        this.tmp = tmp;
        this.tmx = tmx;
        this.tmn = tmn;
        this.sky = sky;
        this.pop = pop;
        this.pty = pty;
    }
}

 

OpenWeatherAPI.java

    @Transactional
    public CalendarWeatherResponseDto findCalendarWeather() throws IOException, ParseException{
        URL url = buildRequestUrl(); // url 생성
        System.out.println(url);  // url 어떻게 되는지 확인
        String stringResult = httpURLConnect(url);
        JSONArray jsonResult = getItems(stringResult);
        return buildCalendarData(jsonResult);
    }

위의 코드와 거의 동일하게 메서드를 작성한다. 핵심은 buildCalendarData이고,

   private CalendarWeatherResponseDto buildCalendarData(JSONArray jsonResult) {
        // 현재 시간 받아서 예상 시간 조회
        String fcstTime = getCurrentTime();
        if(fcstTime.length() == 3){
            fcstTime = "0" + fcstTime;
        }
        String baseDate = getCurrentDate();
        String tmp = "";
        String tmx = "";
        String tmn = "";
        String sky = "";
        String pop = "";
        String pty = "";

        // jsonResult를 조회하며 필요한 데이터 받기
        for(int i = 0; i < jsonResult.size(); i++){
            JSONObject obj = (JSONObject) jsonResult.get(i);

            String TimeTemp = (String) obj.get("fcstTime");
            String CategoryTemp = (String) obj.get("category");

            if(CategoryTemp.equals("TMN")){
                tmn = (String) obj.get("fcstValue");
            }
            else if(CategoryTemp.equals("TMX")){
                tmx = (String) obj.get("fcstValue");
            }
            else if(!fcstTime.equals(TimeTemp)){
                continue;
            }
            else{
                if(CategoryTemp.equals("TMP")){
                    tmp = (String) obj.get("fcstValue");
                }
                if(CategoryTemp.equals("SKY")){
                    sky = (String) obj.get("fcstValue");
                }
                if(CategoryTemp.equals("POP")){
                    pop = (String) obj.get("fcstValue");
                }
                if(CategoryTemp.equals("PTY")){
                    pty = (String) obj.get("fcstValue");
                }
            }

            /*
                        if((tmn != null) && (tmx != null) && (tmp != null) && (sky != null) && (pop != null)){
                break;
            }
             */
        }

fcstDate는 현재 시간에 가장 가끼운 시간대이다. 만약 지금이 7시 2분이면, 7시의 기상예보값을 조회한다. 최고 기온과 최저 기온은 어디 있을 지 모르기 때문에 모든 item들을 조회해야 한다. 또한 최고기온, 최저기온은 전체 시간대를 확인해야 하지만, 다른 값들은 그럴 필요가 없으므로 조건문을 달아서 시간대에 따른 값을 처리해준다.

 

마찬가지로 잘 받아오는지 확인하기 위해서 컨트롤러르 작성해 준다.

OpenWeatehrApiController.java

    @GetMapping("/calendar")
    public CalendarWeatherResponseDto loadCalendarWeather() throws IOException, ParseException{
        return openWeatherAPI.findCalendarWeather();
    }

 

- GET localhost:8080/api/v1/weather/calendar

{
    "baseDate": "20220807",
    "baseTime": "0500",
    "fcstDate": "20220807",
    "fcstTime": "1900",
    "tmp": "30",
    "tmx": "32.0",
    "tmn": "27.0",
    "sky": "4",
    "pop": "30",
    "pty": "0"
}

잘 받아와지는 걸 확인할 수 있다.

 

 

원하는 형태로 가공하기: 날씨 예보

단순히 현재 날씨를 알려주는 API가 아닌 예보를 알려주는 API를 사용하는 이유는 프로젝트에서 날씨 예보 역시 알려주어야 하기 때문이다. 위의 과정과 거의 동일하되 서비스단의 코드만 조금씩 변하므로, 이하 상세한 설명을 생략한다.

 

ForcastResponseDto.java

import lombok.Builder;
import lombok.Getter;

@Getter
public class ForcastResponseDto {

    private String baseDate;
    private String baseTime;
    private String fcstDate;
    private String fcstTime;
    private String sky;
    private String tmp;

    @Builder
    public ForcastResponseDto(String baseDate, String baseTime, String fcstDate, String fcstTime, String sky, String tmp) {
        this.baseDate = baseDate;
        this.baseTime = baseTime;
        this.fcstDate = fcstDate;
        this.fcstTime = fcstTime;
        this.sky = sky;
        this.tmp = tmp;
    }
}

 

OpenWeatherAPI.java

    // 예상 날씨
    @Transactional
    public List<ForcastResponseDto> findForcastWeather() throws IOException, ParseException{
        URL url = buildRequestUrl(); // url 생성
        System.out.println(url);  // url 어떻게 되는지 확인
        String stringResult = httpURLConnect(url);
        JSONArray jsonResult = getItems(stringResult);
        return buildForcastResponse(jsonResult);
    }
    public List<ForcastResponseDto> buildForcastResponse(JSONArray parseItem){
        List<ForcastResponseDto> dtoList = new ArrayList<>();
        JSONObject obj;

        String fcstDate;
        String fcstTime;
        String sky;
        String tmp;

        Integer idx = 0;
        while(dtoList.size() < 22){
            obj = (JSONObject) parseItem.get(idx);
            fcstDate = (String) obj.get("fcstDate");
            fcstTime = (String) obj.get("fcstTime");
            tmp = (String) obj.get("fcstValue");

            obj = (JSONObject) parseItem.get(idx+5);
            sky = (String) obj.get("fcstValue");
            ForcastResponseDto dto = ForcastResponseDto.builder()
                    .baseDate(getCurrentDate())
                    .baseTime(baseTime)
                    .fcstDate(fcstDate)
                    .fcstTime(fcstTime)
                    .tmp(tmp)
                    .sky(sky).build();

            dtoList.add(dto);
            idx += 12;
            obj = (JSONObject) parseItem.get(idx);
            String s = (String) obj.get("category");
            if((Objects.equals(s, "TMN")) || (Objects.equals(s, "TMX"))){
                idx++;
            }
        }

        return dtoList;
    }

해당 서비스단 작성이 제일 귀찮고 어려웠다. TMN, TMX 값이 필요하지 않기 때문에 전체 아이템들을 탐색할 필요가 없고, 필요한 카테고리가 고정되어 있기 때문에 필요한 index만 담아야 하기 때문이다. 공식문서에 따르면 정확도를 보장하는 시간대가 발표 후 22시간까지였기 때문에 기준 시각으로부터 22시간까지의 날씨예보까지만 반환하도록 했다.

 

OpenWeatehrApiController.java

    @GetMapping("/forcast")
    public List<ForcastResponseDto> loadForcastWeather() throws IOException, ParseException{
        return openWeatherAPI.findForcastWeather();
    }

- GET localhost:8080/api/v1/weather/forcast

{
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0600",
        "sky": "4",
        "tmp": "27"
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0700",
        "sky": "4",
        "tmp": "27"
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0800",
        "sky": "4",
        "tmp": "28"
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "0900",
        "sky": "4",
        "tmp": "29"
    },
    {
        "baseDate": "20220807",
        "baseTime": "0500",
        "fcstDate": "20220807",
        "fcstTime": "1000",
        "sky": "4",
        "tmp": "29"
    },

(지금 한국 비오나? sky 코드가 심상치 않다) 이렇게 위처럼 22개 값이 반환된다.

 

 

더 정밀하게: 시간을 관리해보자

  위의 코드는 완성된 상태이기 때문에 시간 관련 코드가 들어가 있지만, 원래 baseTime을 0500으로 맞춰두고 작업했었다. 하지만 그러면 정밀도가 떨어진다. 해당 API는 '예보' 데이터다. 즉, 현재 날씨를 최대한 정확하게 맞추려면 시간 관련 작업이 더 필요하다. 가이드 문서를 뜯어보면서 시간을 더 정확하게 관리해야 겠다는 생각을 했다.

  자바에는 java.text.DateFormat, java.test.SimpleDateFormat 라이브러리가 있다.

https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html

 

SimpleDateFormat (Java Platform SE 8 )

Parses text from a string to produce a Date. The method attempts to parse text starting at the index given by pos. If parsing succeeds, then the index of pos is updated to the index after the last character used (parsing does not necessarily use all charac

docs.oracle.com

  위의 링크에 들어가면 원하는 방식으로 현재 시간 관련 정보를 받아올 수 있다. 관련 에러를 어떻게 처리했는지 자세히 설명하기 전에, 관련 코드부터 다 적어놓고 시작하겠다.

 

날짜

    public String getCurrentDate(){
        DateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
        Date currentDate = new Date();
        return simpleDateFormat.format(currentDate);
    }

현재시간

    public String getFcstTime(){
        DateFormat simpleDateFormat = new SimpleDateFormat("k");
        Date currentTime = new Date();
        return simpleDateFormat.format(currentTime) + "00";
    }

발표시간

    public String getBaseTime(){
        DateFormat simpleDateFormat = new SimpleDateFormat("k");
        Date currentHour = new Date();
        String ApiTime = simpleDateFormat.format(currentHour);
        //System.out.println(ApiTime);
        return switch (ApiTime) {
            case "24", "1", "2" -> "2300";
            case "3", "4", "5" -> "0200";
            case "6", "7", "8" -> "0500";
            case "9", "10", "11" -> "0800";
            case "12", "13", "14" -> "1100";
            case "15", "16", "17" -> "1400";
            case "18", "19", "20" -> "1700";
            case "21", "22", "23" -> "2000";
            default -> "";
        };

 

  해당 세 개의 함수로 시간 관련 모든 것들을 관리한다.

- 날짜의 경우, 요청과 응답 시 20220804 이런 식으로 값이 온다. 이건 쉽게 처리했다.

- 현재 시간의 경우, 현재 날씨를 받아올 때 json을 파싱하면서 사용한다. "k"로 데이터를 포맷하면 현재 시간이 0, 1, 2, ...23, 24 이런 식으로 나온다. 받아오는 값은 0000, 0100, ... 2300 이런 식이므로 해당 형식으로 바꾼다. 더 위로 올라가서 서비스 함수들을 보면 시간을 받아서 300 이런식으로 반환될 경우 앞에 0 하나 더 붙여서 반복문 돌리도록 짜 놨다.

- 발표 시간은 가이드 문서에 따라 작성했다. 2시부터 3시간 단위로 24시간에 8번 예보가 나온다. 현재 시간대에서 최대한 가까운 값을 반환하도록 처리했다(그래야 정확하니까). 또 만약 2시 예보면 2시 15분부터 예보를 확인할 수 있고, 한시간 뒤인 3시부터의 날씨를 알려주기 때문에 이것도 고려해서 switch문으로 ApiTime을 처리했다.

 

  그리고 두 가지 예상치 못한 문제가 발생한다.

 

1) 23시 예보를 받아오는 시간대는 0시, 1시, 2시다. 즉 8월 7일 1시의 정보를 받아온다고 치면, 8월 6일 23시 예보를 받아온다. 그래서 0시부터 2시 사이에 api가 작동을 안 한다. => buildRequestUrl에 baseDate관련 처리 로직 추가

        if(Objects.equals(baseTime, "2300")){
            Date dDate = new Date();
            dDate = new Date(dDate.getTime()+(1000*60*60*24*-1));
            SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
            baseDate = sdf.format(dDate);
        }

2) 자바에서 0시는 24로 반환된다. 하지만 API에서 0시는 0000으로 반환된다. 맞춰주지 않아 에러가 났다.

        if(fcstTime.equals("2400")){
            fcstTime = "0000";
        }

=> 서비스 로직 처리할 때 해당 조건문을 달아첫 처리

 

이 에러들은 해당 시간대가 아니면 확인도 어려울 뿐더러 에러 메세지도... 안 주기 때문에... 찾기가 좀 힘들었다.

 

이후 개선점 및 배운 것들

  코드를 다시 보면서 글을 적다 보니 고칠 점이 눈에 너무 많이 띈다. 첫째, 시간 처리하는 로직들을 따로 함수로 만들어서 처리하면 더 좋았을 것 같다. 둘째, url build해서 json으로 변환하는 과정까지 굳이 함수 세 개가 아니고 하나로 합치는 게 덜 귀찮았을 것 같다. 셋째, 서비스 로직이 마음에 안 든다. 반복문 돌면서 dto 만드는 과정을 좀 더 효율적으로 처리할 수 있을 것 같다.

  위의 세번째 아쉬운 점에서 왜 알고리즘을 공부하고 코딩테스트를 보는지 알게 되었다. 더 효율적으로 짤 수 있을 것 같은데 방법이 생각이 잘 안 난다. 이때 작업하면서 백준 문제를 열심히 풀기 시작했다. 작업하면서도 뭔가 배열 문제 푸는 것 같은 느낌을 받았다.

  이 작업의 가장 뿌듯한 점은 첫째, 다른 블로그 글을 참고하지 못했다는 것이다. 참고할 글이 없다. 자바 코드로 받아오는 건 있는데 그뿐이다. 스프링부트에 맞는 코드는 없다. 때문에 사용한 라이브러리와 API의 공식문서를 보고 스스로 생각하면서 모든 코드를 작성했다. 모든 로직을 스스로 짠 것과 다름없다(...라고 하고 싶다)는 점에서 굉장히 뿌듯했다. 둘째, 에러가 어디서 발생하는지 구글링을 못하기 때문에 문제가 생기면 스스로 생각해서 고쳐야 했다. 시간 관련된 문제는 에러 메세지도 안 떠서 도대체 뭐가 문제인지 한참을 고민해야 했다만 해결하니 그보다 뿌듯할 수가 없었다. 셋째, 라이브러리 공식문서를 읽으면서 코딩했다는 점이다. 코드를 복붙할 수가 없으니 공식문서밖에 참조할 수 밖에 없었는데, 아무래도 미국에 있다 보니 미국 스벅이나 카페에서 작업하는데... 뭔가 영어로 문서 읽고 카페에서 코딩하는 내가 좀 멋있었다. 카페에서 체육관 같이 다니는 친구를 이 때 만났는데 나보고 멋있다고 해 줬다. 너무 좋았다.

 

  이외에도 자료형이나 라이브러리 관련해서 자바 언어 자체에 대한 공부의 필요성을 느꼈다. 로그인 구현하면서도 Map, Claim, 이런 자료형들이 너무 낯설었다. 또 자료형이나 라이브러리를 좀 더 잘 활용한다면 위의 코드가 좀 더 효율적이지 않았을까? 하는 생각도 든다. 어쨌든 너무 재밌고 뿌듯했다.

 

https://github.com/EFUB-TEAM4/backend_e-weather/blob/main/src/main/java/efub/team4/backend_eweather/domain/weather/service/OpenWeatherAPI.java

 

GitHub - EFUB-TEAM4/backend_e-weather: 🌈이상청: 이화인을 위한 기상 정보 및 옷차림 정보 제공 서비스

🌈이상청: 이화인을 위한 기상 정보 및 옷차림 정보 제공 서비스. Contribute to EFUB-TEAM4/backend_e-weather development by creating an account on GitHub.

github.com