[댕댕워크] 날씨 API 모듈 구현하기

acho·2024년 8월 25일
2

배경

댕댕워크는 홈 화면에 위와 같이 날씨 정보를 보여주고 있습니다.
이로 인해 홈 화면에 접속할 때마다 기상청의 날씨 API로 해당 지역의 날씨 정보를 가져오게 됩니다.
그런데 기상청 API의 응답 속도가 너무 느려서 렌더링이 몇초 이상 느려지는 현상이 발생했습니다.

문제 원인

댕댕워크 날씨 컴포넌트에서 필요한 정보는 아래와 같습니다.

  • 일출 / 일몰 정보
  • 하늘 상태 (구름)
  • 강수
  • 최고 / 최저 기온

이 중 일출 / 일몰 정보를 제외한 정보들을 기상청의 단기예보 서비스에서 조회하는데, 이 API가 매우 느렸습니다.

단기예보 서비스는 총 세 가지의 API를 제공합니다.

  1. 단기 예보 API: 정보를 얻고자 하는 지역의 좌표값과 관측 시간대를 보내면 관측 시간대로부터 70시간 이후, 모레까지의 데이터를 조회할 수 있습니다.
    이 중 필요한 시간대의 정보만 요청하기 위해 url의 파라미터로 주어지는 numOfRows과 PageNo 항목을 조정할 수 있습니다.
    세 API중 가장 많은 항목을 제공합니다.

  2. 초단기 예보 API: 위와 비슷한데, 관측 시간대로부터 6시간 이후까지의 데이터만 제공합니다. 하루 전체의 데이터를 얻고 싶다면 요청을 두 번 보내야합니다.

  3. 초단기 실황 API : 현재 실제 관측 데이터를 보내줍니다. 한시간 단위로 요청이 가능합니다.
    가장 적은 갯수의 데이터를 제공합니다.

1 -> 3번으로 갈수록 정확도는 높아지지만, 제공되는 데이터의 갯수가 적어집니다.

필요로 하는 정보 중 최고 / 최저 기온이 1번 단기 예보 API에만 존재해서 기존에는 단기 예보 API를 사용하고 있었습니다.
그런데 하루의 최고 / 최저 기온을 조회하기 위해서는 해당 지역의 하루치 날씨 데이터를 모두 요청해야 합니다. 이 데이터의 양이 상당하기 때문에 요청이 느려졌던 것이었습니다.

서버에서 요청을 한다면 하루가 시작될 때 미리 하루치 예보 데이터를 요청해서 저장해두고 사용할 수 있어 이 문제를 해결할 수 있습니다.
또한 요청 횟수에 대한 부담이 없으니 보다 정확한 날씨 정보 제공을 위해 한시간 마다 초단기 실황 API에 요청을 보내 정보를 업데이트 할 수 있습니다.

구현 방향

아키텍처

날씨 정보는 기존 서버에 저장된 유저의 데이터와 아무런 관련성이 없고, 인증 인가도 필요하지 않아 별도의 모듈로 구현해 관심사를 분리했습니다.
이 모듈을 어디에 올릴지가 고민되었습니다. 기존 백엔드 서버는 AWS EC2에 올라가 있었는데 프리티어 요금제를 사용하다 보니 서버를 구동시키기만해도 램이 부족해서 스왑 메모리를 끌어다 쓰는 상황이었습니다.
예전에 만들어둔 오라클 계정의 평생 무료 인스턴스에 올리기로 했습니다.

스택

서버 엔드포인트가 하나밖에 없는 간단한 모듈이니 굳이 프레임워크를 사용하지 않고 Node.js를 사용했습니다.
하루치 날씨 데이터만 필요해 데이터의 크기가 크지 않고, 빈번하게 조회되는 데이터이기에 인메모리 DB인 Redis를 사용해 조회 성능을 높였습니다.

날씨 정보 저장

기상청 날씨 API는 우리나라 행정구역을 표현하기 위한 좌표값을 따로 정의하고 있습니다.
GPS의 위도, 경도를 특정한 공식에 따라 nx, ny로 변환하고 nx와 ny의 조합으로 각 행정구역을 나타내게 됩니다.
이 구역의 갯수가 총 3831개입니다.
실제 사용자가 없는 지역도 많을텐데 매일 모든 지역의 날씨 데이터를 요청하는 건 심한 낭비라는 생각이 들었습니다.
또 기상청 API는 기본적으로 하루에 10000번까지만 요청이 가능합니다. 그 이상 사용이 필요하면 자료를 제출해서 운영 계정으로 업그레이드가 필요합니다.

그래서 한번이라도 요청이 왔던 지역에 대해서만
1) 매일밤 11시에 다음날 하루치 예보 데이터 요청
2) 이후 한시간마다 실황 데이터 요청해서 업데이트
이렇게 구성하기로 했습니다.

구현

    async scheduleTodayWeatherPredicate() {
        const keys = await this.getKeys();
        try {
            cron.schedule('0 0 23 * * *', async () => {
                const start = Date.now();
                await Promise.all(
                    keys.map(async (key) => {
                        const [nx, ny] = key.split(':');
                        this.weatherService.saveTodayWeatherPredicate(parseInt(nx), parseInt(ny));
                    }),
                );
                const end = Date.now();
                this.logger.cronJobFinished('predicateDay', keys.length, end - start);
            });
            this.logger.cronJobAdded('predicateDay', keys.length);
        } catch (error) {
            this.logger.error('CRON JOB FAILED | 하루 예보 저장에 실패했습니다.', error.stack);
        }
    }
  • 데이터 저장 시 key를 nx:ny로 지정해 이 데이터가 어느 지역의 날씨 데이터인지 알 수 있도록 하였습니다.
  • 이후 스케줄러에서 저장된 모든 데이터의 key를 불러와 루프를 돌며 날씨 데이터를 요청하도록 구현했습니다.

결과

100번 요청시 평균 19512ms -> 46ms로 날씨 정보 요청 속도를 약 99% 개선할 수 있었습니다.
홈 화면 날씨 컴포넌트 렌더링도 전혀 거슬리지 않을 정도로 빨라졌습니다.

한계

날씨 정보 수집을 한번이라도 요청이 왔던 지역에 대해서만 하기 때문에, 해당 지역에서 첫 요청을 하는 사용자는 느린 로딩을 경험하게 됩니다.
서버에서 이를 방지하려면 매일 모든 지역(3831개)에 대한 요청을 하는 방법밖에는 없어서 감수하기로 했습니다.

0개의 댓글

관련 채용 정보