ABOUT ME

-

  • [ Java ] 기상청 Api 완벽 활용하기 - 1 ( 실시간 날씨, 미세먼지 조회 )
    BackEnd/Spring 2025. 4. 5. 17:39
    반응형

    기상청 api를 사용하여 날씨 정보를 가져올때 

    각 api별로 보내야하는 파라미터가 달라서 사용하기가 까다롭다.

     

    간단하게 설명을 위해 자세한 코드 실행 결과보다는 과정으로 설명 진행 ( 예외처리 작업 및 DB 처리 과정은 생략했다. )

    Front 에서 지오코딩을 활용하여 현재 위도 경도 값을 찾고 해당 위도 경도값을 Back으로 보내는 방식으로 진행

     

    우선 기상청의 데이터를 활용하기 위해 총 4개의 api를 사용했다.

    날씨 정보를 가져오기 위한 중기예보, 단기예보 api

    미세먼지 정보를 가져오기 위한 에어코리아 api


    미세먼지 Api 활용하기 - 에어코리아 Api

    기상청의 미세먼지 정보를 보면 측정소 기준으로 미세먼지 정보를 가져오는 것을 알 수 있다.

    에어코리아의 대기오염 api를 보면 측정소별 실시간 측정정보를 조회할 수 있다.

    그럼 이제 측정소 조회 api를 보면 tmX, tmY 좌표값을 통해 해당 근처의 측정소를 가져올 수 있다.

    이 두가지를 활용하여 특정 측정소의 미세먼지 정보를 가져와서 활용하면 기상청의 미세먼지와 똑같은 결과를 볼 수 있다.

    1. 전달받은 위도, 경도 tmX,tmY 좌표값으로 변경하기

     

    백에서 좌표변환 메서드를 만들어서 사용했다.

    dto에는 위도값과 경도값을 받는다.

     

    중심 좌표계의 값을 어떻게 하느냐에 따라서 tmX,tmY 좌표값이 달라진다.

    현재 여러 중심 좌표계를 해보았지만 이게 근처 측정소 목록 조회 api에서 현재 접속한 위도 경도 데이터와 제일 동일한 결과가 나왔다.

     public static CoordinateDto convertToMm(WeatherApiRequestDto location) {
            CRSFactory crsFactory = new CRSFactory();
    
            String wgs84Name = "WGS84";
            String wgs84Proj = "+proj=longlat +datum=WGS84 +no_defs";
            CoordinateReferenceSystem wgs84System = crsFactory.createFromParameters(wgs84Name, wgs84Proj);
    
            /* 중심 좌표계 */
            String epsg5181Name = "EPSG:5181";
            String epsg5181Proj = "+proj=tmerc +lat_0=38 +lon_0=127.0028902777778 +k=1.0 +x_0=200000 +y_0=500000 +ellps=GRS80 +units=m +no_defs";
            CoordinateReferenceSystem epsg5181System = crsFactory.createFromParameters(epsg5181Name, epsg5181Proj);
    
            ProjCoordinate wgs84Coord = new ProjCoordinate(location.getLongitude(), location.getLatitude());
            ProjCoordinate epsg5181Coord = new ProjCoordinate();
    
            CoordinateTransformFactory ctFactory = new CoordinateTransformFactory();
            CoordinateTransform transform = ctFactory.createTransform(wgs84System, epsg5181System);
            transform.transform(wgs84Coord, epsg5181Coord);
    
            CoordinateDto coordinateDto = new CoordinateDto();
    
            coordinateDto.setTmX(epsg5181Coord.x);
            coordinateDto.setTmY(epsg5181Coord.y);
    
            coordinateDto.setLng(location.getLongitude());
            coordinateDto.setLng(location.getLatitude());
    
            return coordinateDto;
        }

     

    이제 여기서 나온 tmX,tmY 좌표값을 측정소 목록 조회 api를 호출하게 되면 이런식으로 해당 tmX,tmY좌표를 기준으로 거리가 가장 가까운 측정소의 목록을 가져온다. 이때 제일 가까운 값을 사용할 것이므로, 첫번째 값만 가져온다.

    여기서 tm은 거리이며

    addr은 측정소 주소, stationName은 측정소 이름이다. 이때 측정소 미세먼지 조회시에는 이 stationName을 활용한다.

     

    근처 측정소의 첫번째 값의 stationName값을 측정소별 실시간 측정정보 조회 api의 statioinName값에 넣어주면 해당 측정소의 실시간 미세먼지 정보를 가져올 수 있다.

     

    service 에서 tmX, tmY 좌표계로 변환한 위도 경도 값을 받아 근처 관측소 목록 api를 가져온다.

    이때 가져온 측정소의 stationName값을 다시 측정소별 미세전지 조회 api에 보내 해당 측정소의 미세먼지 정보를 가져온다.

    /* 대기 오염 */
        public WeatherDustResponseDto getWeatherDust(GeoLocationDto param) {
            String todayDate = getCurrentDate();
    
            String finalTime = String.format("%02d00", getCurrentHour());
    
            /* 근처 관측소 API */
            WeatherDustResponseDto weatherStation = weatherStationApi(param);
    
            /* 요염 지수 API */
            WeatherDustResponseDto weatherDustData = weatherDustApi(weatherStation.getStation());
    
            WeatherDustRequestDto weatherDustRequest = new WeatherDustRequestDto();
            weatherDustRequest.setAddr(weatherDustRequestDto.getAddr());
            weatherDustRequest.setStation(weatherDustRequestDto.getStation());
    
            weatherDustRequest.setBaseDt(weatherDustRequestDto.getBaseDt());
            weatherDustRequest.setBaseTm(weatherDustRequestDto.getBaseTm());
    
            weatherDustRequest.setSo2(weatherDustData.getSo2());
            weatherDustRequest.setCo(weatherDustData.getCo());
            weatherDustRequest.setO3(weatherDustData.getO3());
            weatherDustRequest.setNo2(weatherDustData.getNo2());
    
            weatherDustRequest.setPm10(weatherDustData.getPm10());
            weatherDustRequest.setPm25(weatherDustData.getPm25());
    
            weatherApiMapper.upsertWeatherDust(weatherDustRequest);
    
            return weatherApiMapper.selectWeatherDust(weatherDustRequestDto);
        }

     


    실시간 날씨 - 단기예보 api

    실시간 날씨 정보를 가져오기 위해서 단기예보의 초단기실황 api를 활용한다.

     

    여기서 nx,ny 좌표는 그리드 좌표계를 사용한다.

    따라서 위도 경도를 그리드 좌표계인 x,y좌표로 변환을 시켜주어야한다.

    이는 구글링을 좀만하다보면 쉽게 찾을 수 있고, 혹은 단기예보 문서에도 나와있다.

        static double RE = 6371.00877; // 지구 반경(km)
        static double GRID = 5.0; // 격자 간격(km)
        static double SLAT1 = 30.0; // 투영 위도1(degree)
        static double SLAT2 = 60.0; // 투영 위도2(degree)
        static double OLON = 126.0; // 기준점 경도(degree)
        static double OLAT = 38.0; // 기준점 위도(degree)
        static double XO = 43; // 기준점 X좌표(GRID)
        static double YO = 136; // 기준점 Y좌표(GRID)
    
        public static CoordinateDto convertToGrid(WeatherApiRequestDto location) {
            double DEGRAD = Math.PI / 180.0;
            double RADDEG = 180.0 / Math.PI;
    
            double re = RE / GRID;
            double slat1 = SLAT1 * DEGRAD;
            double slat2 = SLAT2 * DEGRAD;
            double olon = OLON * DEGRAD;
            double olat = OLAT * DEGRAD;
    
            double sn = Math.tan(Math.PI * 0.25 + slat2 * 0.5) / Math.tan(Math.PI * 0.25 + slat1 * 0.5);
            sn = Math.log(Math.cos(slat1) / Math.cos(slat2)) / Math.log(sn);
            double sf = Math.tan(Math.PI * 0.25 + slat1 * 0.5);
            sf = Math.pow(sf, sn) * Math.cos(slat1) / sn;
            double ro = Math.tan(Math.PI * 0.25 + olat * 0.5);
            ro = re * sf / Math.pow(ro, sn);
    
            CoordinateDto rs = new CoordinateDto();
    
            rs.setLat(location.getLatitude());
            rs.setLng(location.getLongitude());
    
            double ra = Math.tan(Math.PI * 0.25 + (location.getLatitude()) * DEGRAD * 0.5);
            ra = re * sf / Math.pow(ra, sn);
            double theta = location.getLongitude() * DEGRAD - olon;
            if (theta > Math.PI) theta -= 2.0 * Math.PI;
            if (theta < -Math.PI) theta += 2.0 * Math.PI;
            theta *= sn;
    
            int x = (int) Math.floor(ra * Math.sin(theta) + XO + 0.5);
            int y = (int) Math.floor(ro - ra * Math.cos(theta) + YO + 0.5);
    
            rs.setX(x);
            rs.setY(y);
    
            return rs;
        }

     

    이제 이 메서드를 활용하여 위도 경도 값을 그리드 좌표계로 변환시켜 api를 호출한다.

     

    초단기 실황 예보 조회의 결과를 보면 baseTime이 현재 해당 시간의 날씨 정보이다.

    초단기 실황 예보는 10분마다 업데이트가 되는데 이 경우에도 baseTime은 분은 따로 나오지않고 시간으로 나와서
    이를 고려해야한다.

     

    직접 테스트해본 결과 5분쯤에는 해단 분의 정보가 업데이트가 되는 것 같았다.

    20분기준의 정보는 25분쯤에 데이터가 나온다.

    기상청의 경우에는 20분 ~ 25분 사이에는 새로고침을 하면 아직 데이터가 없기때문에 업데이트가 되지 않는다.

    우선 이 부분의 코드는 좀 더 개선이 필요하지만

    ( 현재 기상청처럼 새로고침시에 데이터가 없기때문에 업데이트가 되지 않는 부분이 개선 필요 )

    요청했을때 시간을 가져와 10분 단위로 맞춰주어 이를 요청시간으로 만들어준다.

     

    기상청 api의 경우 1600 1700 이런식으로 baseTime이 지정되어있으므로 10분단위는 요청했을 때를 체크하여 처리해준다.

     

    이후 전날 날씨와 비교를 위해 초단기 실황 api를 전날을 기준으로 다시 조회를 해준다.

    이때 초단기 실황은 24시간이 지나면 api를 호출 할 수 없으므로 최대한 오류가 안나도록 요청 시간의 10분을 추가하고 하루 전날을 기준으로해서 전날의 날씨 정보를 가져온다.

     

    그러면 어제보다 지금 온도가 얼만큼 차이가 있는지 확인 할 수 있다.

     

    추가적으로 네이버 날씨를 기준으로 보면 네이버 날씨의 경우에는 현제 구름 상태도 보여준다.

    초단기 실황 api의 경우에는 구름 상태는 보여주지 않아서 이를 다른 방식으로 활용한다.

     

    구름 상태는 초단기 예보 조회를 통해서 가져올 수 있는데 초단기 예보 조회는 요청시간을 기준으로 6시간의 데이터를 보내준다.

    이를 활용하여 실시간 날씨 api 요청 시간과 동일하게 요청하여 해당 시각의 구름 정보 및 다른 정보들을 가져와서 합쳐서 실시간 날씨를 보여준다.

    /* 실시간 날씨 조회 */
        public WeatherRealTimeResponseDto getWeatherLive(GeoLocationDto param) {
            // api 요청 시각 세팅 ( 어제 온도 비교를 위해 어제 날씨도 조회 ( + 10분 )
            String todayDate = getCurrentDate("0");
            String prevDate = getAddDate("0",-1);
    
            int updateMinute = (getCurrentMinute() / 10) * 10;
    
            String finalTime = String.format("%02d00", getCurrentHour());
    
            if(updateMinute == 0) {
                finalTime = String.format("%02d00",getAddHour(-1));
                updateMinute = 50;
            }
    
            int prevMinute = updateMinute + 10;
    
    
            WeatherRequestDto weatherRealTimeRequestDto = new WeatherRequestDto();
    
            weatherRealTimeRequestDto.setBaseDt(todayDate);
            weatherRealTimeRequestDto.setBaseTm(finalTime.substring(0,2) + String.format("%02d",updateMinute));
            weatherRealTimeRequestDto.setX(param.getX());
            weatherRealTimeRequestDto.setY(param.getY());
    
            // 전날 날씨 조회
            if(prevMinute >= 60) {
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HHmm");
                LocalTime time = LocalTime.parse(finalTime, formatter);
    
                LocalTime newTime = time.plusHours(1);
                finalTime =  newTime.format(formatter);
                prevMinute = 10;
            }
    
            weatherRealTimeRequestDto.setBaseDt(prevDate);
            weatherRealTimeRequestDto.setBaseTm(finalTime.substring(0,2) + String.format("%02d",prevMinute));
    
            WeatherRealTimeResponseDto prevWeahterLive = upsertWeatherLive(param,prevDate,finalTime,prevMinute);
    
            weatherRealTimeResponseDto.setPtyGrowth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getPty()) - Double.valueOf(prevRealTime.getPty())));
            weatherRealTimeResponseDto.setRehGrowth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getReh()) - Double.valueOf(prevRealTime.getReh())));
            weatherRealTimeResponseDto.setRn1Growth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getRn1()) - Double.valueOf(prevRealTime.getRn1())));
            weatherRealTimeResponseDto.setT1hGrowth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getT1h()) - Double.valueOf(prevRealTime.getT1h())));
            weatherRealTimeResponseDto.setUuuGrowth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getUuu()) - Double.valueOf(prevRealTime.getUuu())));
            weatherRealTimeResponseDto.setVecGrowth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getVec()) - Double.valueOf(prevRealTime.getVec())));
            weatherRealTimeResponseDto.setVvvGrowth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getVvv()) - Double.valueOf(prevRealTime.getVvv())));
            weatherRealTimeResponseDto.setWsdGrowth(String.format("%.1f",Double.valueOf(weatherRealTimeResponseDto.getWsd()) - Double.valueOf(prevRealTime.getWsd())));
    
            WeatherRequestDto weatherDaysRequestDto = new WeatherRequestDto();
            weatherDaysRequestDto.setBaseDt(todayDate);
            weatherDaysRequestDto.setBaseTm(String.format("%02d00", getCurrentHour()));
            weatherDaysRequestDto.setX(param.getX());
            weatherDaysRequestDto.setY(param.getY());
    
            List<WeatherDaysResponseDto> weatherDays = getUltraShortWeather(param);
    
            weatherRealTimeCloud = weatherApiMapper.selectWeatherCloud(weatherDaysRequestDto);
    
            weatherRealTimeResponseDto.setSky(weatherRealTimeCloud.getSky());
            weatherRealTimeResponseDto.setFcstTm(weatherRealTimeCloud.getFcstTm());
    
            return ResultUtils.success(weatherRealTimeResponseDto);
        }

     

     

    728x90
    반응형