[Troubleshooting] - 다사다난했던 기상청 단기 예보 적용기

청주는사과아님·2025년 8월 9일
0

Troubleshooting

목록 보기
7/7
post-thumbnail

최근 프로젝트를 진행하며 기상청 단기 예보 API 를 사용할 일이 생겼습니다.
그런데 예상과 달리 여러 순차적인 난관을 겪었고, 이에 대한 트러블슈팅 회고록을 적고자 합니다.


📢 배경 설명

프로젝트 주제는 레저 여행지 검색 & 여행 계획 관리 로, 아래처럼 장소간 방문 순서 및 일자를 계획하는 기능이 포함되어 있습니다.

이에 더불어 저희는 여행지에 대한 기상 예보 기능을 계획하였고, 이를 위해 기상청 API 를 사용하고자 했습니다.

외부 API 호출 에 사용한 기술은 spring-cloudopenFeign 을 이용했습니다.


1️⃣ 단기 예보 API 의 너무 느리고 많은 데이터

첫번째 문제는 단기 예보 API 를 시험해보며 발생했습니다.

단기 예보 API0 ~ (최대) 5 일 까지의 예보를 1 시간 간격 으로 제공합니다.

문제는 그 개수가 너무 많다는 것입니다.

위 그림을 보시면 totalCount : 1016 으로 예보 정보가 총 1016 개 있음을 알 수 있습니다.

이를 한 요청에 전부 요구할 경우, 아래처럼 응답 길이도 길어질 뿐더러 응답 시간도 느려집니다. (1.28s)

때문에 이를 그대로 사용할 경우 기상 예보 기능 의 응답 시간 (단기 예보 응답 시간 + json parsing 시간) 에 큰 저하가 생길 것이라 예상했습니다.

✅ Sol : @Async 를 활용한 Api Client 구성

결국 문제는 한번에 많은 데이터를 요구 하는 것으로 생각할 수 있습니다.

때문에 저는 이를 해결하고자 "Paging 계획 수립" + @Async 분할 요청 방법을 도입하였습니다.

이를 도식화해 나타내면 다음과 같습니다.

1. 전체 정보 개수를 파악

단기 예보 API예보 기준 시각 에 따라 전체 데이터 개수 가 달라집니다.

(14:00 시 넘겨 요청하면 5 일자 예보를 포함한다던지, 당일 최고 기온은 15:00 시 예보 정보에만 포함된다던지 등)

때문에 분할 요청 계획 을 만들기 위해선 현재 시각에 따른 전체 개수를 파악해야 합니다.


2. 분할 요청 계획 수립

전체 개수를 파악한 후, "알맞은 방법" 으로 분할 요청 계획을 구비합니다.

이 때 알맞은 방법 은 다음을 의미를 내포합니다.

  • 설정된 한 페이지 기본 개수 에 따라 N 개의 요청으로 분할
  • 전체 개수가 분할 요청하기 에는 애매하개 작음. 이 경우 분할하지 않고 단일 요청으로 계획 수립

3. 비동기 분할 요청 요구, 전체 응답 조립

수립한 분할 요청 계획 들을 바탕으로 비동기 요청 을 진행하고, 이들이 모두 성공했을 시 응답된 정보를 단일 List 로 조립합니다.

각 과정을 코드로 나타내면 다음과 같습니다.

[1 & 2] 과정

  • 전체 정보 개수 파악 & 분할 요청 계획 수립

  • 분할 요청 계획 수립 자세한 코드

[3 & 전체] 과정

  • @Async, CompletableFutre 를 이용한 비동기 요청

  • [1 ~ 3] 과정 & 단일 List 조립

이러한 과정을 통해 최소 1400ms 걸리던 응답을 500ms 정도로 단축할 수 있었습니다.


2️⃣ 카테고리별 상이한 정보

단기 예보 API기상 정보 는 카테고리 분류별 다른 의미를 내포합니다.

카테고리 (항목 구분)항목별 의미

다음 응답 예시를 같이 보겠습니다.

각 기상 정보는 (category, fcstValue) 에 따라 의미하는 정보가 다릅니다.

  • TMP : 기온 29 ℃
  • UUU : 풍속 (동서) 0.8 m/s
  • VVV : 풍속 (남서) 0.3 m/s
  • VEC : 249° (SW-W)

문제는 category 종류가 14 개가 될 뿐더러, 각 예보 정보를 예보 날짜 + 예보 시각 에 맞춰 "하나의 정보로 묶어 제공" 해야 된다는 점입니다.

즉, 단순 if-else 와 같은 로직으로는 해결할 순 있어도 아주 복잡해지는 상황입니다.

✅ Sol : 전략 패턴을 활용한 composer

결국 중요한건 "카테고리별 정보를 꾸미는 방식이 다르다" 는 점 입니다.

저는 이를 해결하기 위해 전략 패턴 을 활용 하였습니다.

이를 간략히 도식화 하면 다음과 같습니다.

각 전략 (TMP, UUU strategy 등) 에는 주어진 예보 정보로부터 DTO 를 어떻게 꾸밀지 명시되어 있습니다.

위 그림의 TMP strategy 는 TMP 카테고리 정보 를 받아 DTO 의 기온 정보 를 꾸며주는 것을 볼 수 있습니다.

즉, 카테고리별 전략 component 를 구비해두고 Composer 가 알맞은 component 에게 (알맞은 예보 정보를 주면서) DTO 를 꾸며달라 요청하는 방식입니다.

이를 코드로 간략히 확인하면 다음과 같습니다.

  • 카테고리별 전략 component interface

  • (예시) 습도 카테고리 전략

  • Composer 생성자

  • Composer <---> Strategy component

이러한 과정으로 코드 중복을 낮추고 다양한 카테고리의 기상 정보를 (예보 날짜 : 시각) 으로 묶어 응답할 수 있었습니다.


3️⃣ 단기 예보 API 의 잦은 NO_DATA 응답

앞선 1️⃣, 2️⃣ 과정으로 여행지에 대한 기상 예보 기능 자체는 완성 되었습니다.

하지만 1️⃣ 비동기 분할 요청 을 테스트하며 단기 예보 API 의 예상치 못한 응답을 확인했는데, 바로 "NO_DATA 응답" 입니다.

정확한 원인은 알 수 없지만 (요청을 올바르게 보냈음에도 불구하고) 단기 예보 API 가 간혈적으로 기상 정보 를 제공하지 않는 상황을 발견했습니다.

  • 완전히 동일한 요청임에도 NO_DATA 응답을 받는 상황
정상 응답간혈적으로 일어나는 NO_DATA 응답

즉, 기능이 실패하다 얼마 이후에는 정상 작동하는, 기능의 신뢰성이 떨어지는 상황 을 발견했습니다.

⚠️ Sol : 요청 재시도 하드 코딩 + @Cacheable

근본적인 원인은 단기 예보 API 가 불완전하기 때문으로, backend 에서 이를 완전히 해결할 순 없었습니다.

하지만 그렇다고 신뢰성이 낮은 기능을 제공할 순 없으므로, 아쉽게도 요청 재시도 로직을 하드코딩 하였습니다.

위처럼 NO_DATA 응답을 받았을 땐 일정 시간 이후 (최대 N 번) 재시도 하는 로직을 추가하였습니다.

또한 단기 예보 API 의 요청 변수에는 람베르트 정각원추도법 으로 변환된 int nx, ny 좌표와 예보 기준 시각 이 필요한데, 이들을 활용해 기능 성공시 응답을 cache 저장 하도록 구성하였습니다.

이를 통해 기능의 오작동을 하루에 3 ~ 4 번 에서 일주일에 1 ~ 2 번 으로 크게 개선할 수 있었습니다.


4️⃣ 공공데이터 포털의 XML 에러 형식

마지막으로 공공 데이터 포털에러 응답 형식 입니다.

공공 데이터 포털기상청 API 외 수많은 open API 를 중개합니다.

때문에 기상청 API 로 인한 에러와 별개로, (비교적 매우 드물게) 공공 데이터 포털 로 인한 에러도 발생하였습니다.

  • 공공 데이터 포털 에러를 응답 받는 모습

<OpenAPI_ServiceResponse>
	<cmmMsgHeader>
		<errMsg>SERVICE ERROR</errMsg>
		<returnAuthMsg>HTTP ROUTING ERROR</returnAuthMsg>
		<returnReasonCode>04</returnReasonCode>
	</cmmMsgHeader>
</OpenAPI_ServiceResponse>

문제는 이 에러 응답 "만" XML 형식이며, 무조건 200 status 코드로 제공된다는 것입니다.

이것이 문제되는 이유는 다음과 같습니다.

A. XML 에러 응답

앞서 공공 데이터 포털기상청 API 를 포함한 수많은 API 를 중개한다 했습니다.

그래서인지 몰라도 공공 데이터 포털 로 인한 에러는 XML, 기상청 API 로 인한 에러 응답 은 JSON 으로 제공되고 있었습니다.

때문에 @FeignClient 에서 응답 객체를 만들 때 parsing error 가 발생하였습니다.

물론 @FeignClient 에서 XML 형식을 parsing 하도록 만들 순 있습니다.
하지만 그러기 위해선 기존에 구성한 Deserializer, 외부 API 응답 form class 등을 재구성해야 되었으며, 이들이 정상 작동하는지도 검증해야 되었습니다.

즉, XML parsing 을 가능케 하는 방식은 refactor 비용이 높았습니다.

B. 에러 응답임에도 200 status code

OpenFeign 에는 "잘못된 응답을 받았을 때 작동" 하는 ErrorDecoder 가 존재하며, 이는 응답 code400 ~ 500 일때만 작동합니다.

하지만 API 가 에러 응답임에도 200 code 로 응답하므로 이를 사용할 수 없고 해결할 다른 방식을 찾아야 합니다.

✅ Sol : "XML 재시도 Decoder" 구성

결국 문제가 되는 부분은 "XML 형식으로 인한 parsing error" 라 할 수 있고, 이를 해결하기 위해 XML 재시도 Decoder 를 구성하였습니다.

OpenFeign 에는 RetryableException 이 존재하며, 해당 exception 을 throw 하여 동일한 API call 을 재시도할 수 있습니다.

코드에서 알 수 있듯 API 응답의 content type 을 확인, XML (에러 응답) 일 시 RetryableException 을 발생시켜 올바른 응답을 다시 받을 수 있도록 구성하였습니다.

이 과정을 log 로 확인하면 아래와 같습니다.

--> RETRYING 와 같이 OpenFeign 이 API 를 재시도 하는 것을 볼 수 있습니다.

(+ RetryableException 으로 API 재시도를 활성화 하기 위해선 별도의 Retryer 설정이 구성되어야 합니다.)


📝 Summary

사실 이전에 진행했던 프로젝트 중, 이번처럼 외부 API 를 적극적으로 사용해본 경험은 없었습니다.

OpenFeign 의 여러 기능을 새로 학습하면서 비동기 처리, 전략 패턴 적용 등 다양한 해결책은 구상한 것은 참 재밌는 경험이었습니다. 이게 개발하는 맛이지

하지만 기술적 시도와 별개로 공공 데이터 포털기상청 API 자체의 신뢰성 부족은 매우 아쉬운 부분 이었던 것 같습니다.

폭우 등 특수 상황을 감안하더라도, 빈번한 HTTP ROUTING ERROR 와 간헐적인 NO_DATA 응답은 서비스 품질을 담보하기 어려운 수준이었습니다.

또한 API 사용성 역시 문제였습니다.
응답 데이터가 불필요하게 방대하거나, 카테고리별 의미 파악이 어려운 구조, 그리고 부실한 API 명세는 체감 가능할 정도로 개발 효율을 떨어뜨렸습니다.

이딴걸 사용하라고 만들어둔건가?

이번 경험을 통해 "외부 API 선택" 이 단순히 기능 지원 여부만이 아니라 성능·안정성·문서 품질까지 종합적으로 고려해야 한다는 점을 다시 느꼈습니다.
기존 구현해둔 코드만 아니었어도 이미 버렸을텐데...

때문에 만약 같은 기능을 다시 구현해야 한다면, 아마 다른 데이터 소스나 API를 사용하지 않을까 싶습니다.

OpenWeather, Google Weather API 쓰세요. 이딴거 쓰지 말고...


profile
나 같은게... 취준?!

0개의 댓글