Search
🎴

[프로젝트] 내일의 날씨 기능 개발

태그
python
System Design
데이터베이스
알고리즘
⭐️
날짜
2024/07/20
4 more properties

기획 배경

프로젝트에서 내일 날씨 정보를 제공하는 신규 기능을 추가하면서 고민했던 내용을 정리한다.
User Retention을 높이기 위한 방법을 고민 하다가 날씨 알림과 같이 사용자가 원할만한 정보를 제공하여 지표를 개선할 수 있는지 실험을 하였다.
이 글에서는 FastAPI 서버에서 날씨 기능을 개발했던 여정을 간략하게 기록해보려 한다.
화면 구성에서 확인할 수 있듯, 사용자는 자신의 위치를 기반으로 앞으로 24시간 이내의 날씨 정보를 전달받을 수 있다.
날씨 정보는 기상청 단기 예보 API를 통해서 받아왔다.

API 기획

날씨 기능을 제공하기 위해서 서버에서는 세 가지 API가 필요하다

1. 날씨 조회 API

사용자의 위치를 기반으로 가장 가까운 관측소의 날씨 데이터를 제공한다.
위치 정보 제공을 허용하지 않은 경우에는 서울특별시를 기준으로 전송한다.
# Request GET /api/v2/weather/current # Query Params longitude: float (경도) latitude: float (위도) # Response { "area_1": "string", // 관측소 위치 (대분류) "area_2": "string", // 관측소 위치 (소분류) "weather_info": [ // 1시간 단위 날씨 정보 { "forecast_date": "date", "forecast_time": "time", "pop": "int", // 강수 확률 "pty": "int", // 강수 형태 (1=, 2=/, 3=, 4=소나기) "reh": "int", // 습도 "sky": "int", // 하늘 상태 (1=맑음, 3=흐림, 4=구름 많음) "tmp": "int", // 기온 "wsd": "float" // 풍속 (m/s) } ] }
Python
복사

2. 날씨 알림 설정 설정 API

저녁에 내일 날씨 정보를 받을 수 있는 Push Notification을 켤 수 있는 APi이다. 알림을 설정하면 저녁마다 다음 날 날씨 정보를 받을 수 있다.
우리 서비스는 유저의 수면 관리를 위한 앱으로 유저의 자러가기 시간 정보를 필수로 받는다. 아래 noti_type은 설정된 자러가기 몇 시간 전에 알림을 전송할 지 정하는 타입이다.
# Request PUT /api/v2/weather/notification # Request Body { "noti_type": "string" // PREV_1, PREV_2, PREV_3, OFF } # Response { "noti_type": "string" // 설정된 알림 타입 }
Python
복사

2. 날씨 알림 설정 조회 API

UI에서 선택된 값을 검토하고 수정할 수 있어야 하기 때문에 설정된 알림 정보를 조회할 수도 있어야 한다.

데이터베이스 설계

날씨 기능을 제공하기 위해서 세 개의 DB 테이블이 추가 되었고, 서버에서 관리하는 하나의 static file이 필요했다. 참고로 아래는 예시일 뿐이고 모든 데이터베이스 테이블은 SQLAlchemy ORM을 통해서 관리되고 있다.

Static File: 관측소 위치

수백개의 관측소의 위치 정보를 static 파일로 저장하고 관리한다. 나중에 날씨 정보를 수집할 때는 각각의 관측소마다 정보를 저장한다. 너무 많아서 일부 가까운 관측소들의 중복은 수동으로 제거했다.
관측소 위치별로 날씨 정보를 저장하는 이유
사용자는 자신의 위치를 기반으로 날씨 정보를 조회한다. 서버에 관측소 위치마다 날씨 정보를 미리 관리하고 있다면 API응답 시간을 크게 줄일 수 있다.
[{"region_code":1100000000,"area_1":"서울특별시","area_2":"","nx":60,"ny":127,"longitude":126.980008333333,"latitude":37.5635694444444}, {"region_code":1111000000,"area_1":"서울특별시","area_2":"종로구","nx":60,"ny":127,"longitude":126.981641666666,"latitude":37.5703777777777}, ... {"region_code":5182000000,"area_1":"강원도","area_2":"고성군","nx":85,"ny":145,"longitude":128.470163888888,"latitude":38.3779611111111}, {"region_code":5183000000,"area_1":"강원도","area_2":"양양군","nx":88,"ny":138,"longitude":128.621355555555,"latitude":38.0728333333333}]
Python
복사

Table 1: 날씨 데이터 테이블

날씨 데이터 수집 요청을 관리하는 테이블.
날씨 정보 수집을 진행하는 스케쥴링 프로세스가 진행한 수집 요청 정보를 담고 있다. nx, ny는 서버에서 관측소의 위치를 나타낸다.
CREATE TABLE weather_short_request ( id BIGINT PRIMARY KEY, created_at TIMESTAMP, nx INTEGER, -- 예보 지점 X 좌표 ny INTEGER, -- 예보 지점 Y 좌표 base_date VARCHAR(8), -- 예보 기준 일자 base_time VARCHAR(4), -- 예보 기준 시각 status VARCHAR(16), -- 상태: READY, FINISHED, FAILED description TEXT -- 실패 사유 등 특이사항 );
SQL
복사

Table 2: 실제 날씨 데이터를 저장하는 테이블

CREATE TABLE weather_short ( id BIGINT PRIMARY KEY, request_id BIGINT, -- WeatherShortRequest 참조 forecast_date DATE, -- 예보 날짜 forecast_time TIME, -- 예보 시각 pop VARCHAR(16), -- 강수확률(%) pty VARCHAR(8), -- 강수형태 reh VARCHAR(16), -- 습도(%) sky VARCHAR(8), -- 하늘상태 tmp VARCHAR(20), -- 기온 wsd VARCHAR(20), -- 풍속(m/s) -- 기타 날씨 관련 필드들... status VARCHAR(16), -- 데이터 상태 description TEXT );
SQL
복사

사용자 관련 테이블

Table 3: 사용자별 알림 설정을 관리하는 테이블

CREATE TABLE user_notification_allow ( id BIGINT PRIMARY KEY, user_id BIGINT, -- User 테이블 참조 created_at TIMESTAMP, noti_type VARCHAR(16), -- 알림 종류 (ex: WEATHER) detail_type VARCHAR(16), -- 세부 설정 (ex: PREV_1, PREV_2...) allow BOOLEAN -- 알림 허용 여부 );
Python
복사
날씨 데이터 조회를 위한 복합 인덱스 설계도 필요하다. 몇가지 인덱스가 설정 되었지만 가장 중요한 인덱스는 사용자의 요청을 빠르게 처리하기 위한 인덱스이다.
nx, ny 좌표로 1차 필터링을 진행하고 status로 완료된 요청 데이터만 가져온다. created_at desc를 통해 최신 데이터를 우선 조회하도록 한다.
CREATE INDEX idx_short_weather_request_base ON weather_short_request (nx, ny, status, created_at DESC);
SQL
복사

날씨 정보 처리 로직

날씨 데이터를 처리하기 위해 WeatherHandler를 구현해서 십분 활용했다. 핸들러 클래스의 역할은 다음과 같다.
1.
위치 기반 날씨 정보 매핑
2.
날씨 예보 데이터 수집
3.
캐싱 전략
4.
공공 API 통신 처리
5.
Push Notification 전송을 위한 메시지 생성
6.
기타 등등 …

1. 위치 기반 날씨 정보 매핑

핸들러 클래스의 가장 핵심 기능으로 위도 경도 값이 주어졌을 때 가장 가까운 관측소를 찾고 해당 관측소의 날씨 데이터를 반환한다. KD 트리를 활용하고 있기 때문에 O(logN)O(log N)시간 내에 검색이 가능하다.
def query_region(self, latitude, longitude): dist, idx = self.kd_tree.query([latitude, longitude], k=1) nearest_region = self.regions[idx] return nearest_region
Python
복사
KD-Tree는 2차원 공간에서 최근접 이웃 검색 최적화를 위해 도입했다. 자료구조 및 알고리즘 선택에 몇 가지 옵션이 있었는데 기회가 되면 따로 정리할 예정이다.

2. 날씨 예보 데이터 수집

단기 예측 정보를 수집하는 함수이다. 외부 스케쥴링 프로세스가 호출한다. 내부에 추가된 기능은 다음과 같다.
관측소마다 단기 예보 데이터를 수집한다. 응답에 따라 Retry 등의 로직이 구현 돼 있다.
수집 이력을 관리한다
시간별 날씨 데이터를 정규화하고 저장한다
에러 발생 시 상태를 추적하고 로깅한다
def collect_forecast_short(self, session: Session, base_date: str, base_time: str): for nx, ny in positions: data = self._fetch_forecast_short(nx=nx, ny=ny, base_date=base_date, base_time=base_time)
Python
복사

3. 캐싱 전략

현재는 관측소의 위치, 그리고 날짜별로 데이터를 캐싱해서 불필요한 연산을 제거했다.
cache_key = (target_date, nx, ny) if cached_obj := self.daily_cache.get(cache_key): return cached_obj
Python
복사

마치며

공공 데이터 포털에서 제공하는 데이터를 파악하고 수동으로 관측소 등의 정보를 필터링하는 작업이 생각보다 귀찮았다. 또한 HTTP 응답 에러가 굉장히 불친절하여 초기 구현에서 디버깅이 쉽지 않았다.
작업을 하면 더욱 고도화할 수 있는 부분을 고려하게 돼서 재밌는 것 같다. 기능 추가는 요새 10만명을 감당할 수 있다를 기준으로 구현하고 있다. 하지만 100만이라면? 1억이라면? 이런 고민을 하며 궁금한 것은 찾고 배우는 과정이 즐거운 것 같다.