앱 밖에서 준비한 공공데이터
공공데이터 수집을 worker로 옮기고 API에는 준비된 값과 최신성만 남긴 이유
iOS 앱과 위젯이 여러 화면과 접점으로 나뉘면서 백엔드는 외부 데이터를 미리 가져와 앱이 바로 읽을 수 있게 해두는 일을 맡게 됐다.
초기에는 앱이 공공데이터 API를 직접 호출해도 된다고 생각했다. 작게 보면 그게 더 간단하다. 앱에서 주차 데이터 API를 호출하고 응답을 화면에 표시하면 된다.
그 방식에는 이유도 있었다. 서버에서 할 일이 줄고, 처음 화면을 빨리 만들 수 있고, 문제가 생겨도 코드가 눈앞에 보인다. 하지만 한강자리에서 다루는 데이터는 출처도 다르고 속도도 달랐다. 앱이 그 차이를 모두 떠안으면 화면은 빨리 만들 수 있어도, 사용자는 느린 출처와 빈 응답을 그대로 보게 된다.
기능을 붙일수록 앱 직접 호출 방식에는 한계가 뚜렷해졌다.
외부 출처마다 필드 이름, 갱신 방식, 상태 표현이 달랐다. 어떤 데이터는 자주 바뀌고, 어떤 데이터는 하루에 한 번만 바뀐다. 외부 출처가 느리거나 실패할 때도 마지막으로 확인된 값, 갱신 시각, 출처 상태를 같이 들고 있어야 했다.
예측도 서버가 더 적합했다. 시간대별 주차 변화나 공원 혼잡 변화는 누적 데이터와 백그라운드 작업이 필요했다. 알림도 마찬가지였다. 사용자가 앱을 열지 않아도 상태 변화나 혼잡 신호를 알려주려면, 서버가 구독 설정과 APNs 전송을 맡아야 했다.
이 때문에 백엔드는 FastAPI로 두고, 요청에 답하는 API와 뒤에서 값을 준비하는 worker를 분리했다.
flowchart LR
Sources["공식 공개 출처<br/>주차 · 행사 · 시설 · 실시간 신호"] --> Workers["워커<br/>수집 · 정리 · 검사"]
Workers --> PG[("PostgreSQL / PostGIS<br/>기준 데이터 · 이력 · 위치")]
Workers --> Redis[("Redis<br/>현재 상태 · 예측 요약")]
PG --> Forecast["예측<br/>시간대별 변화"]
Forecast --> Redis
PG --> Notify["알림 후보<br/>조건 변화"]
Redis --> Notify
Notify --> APNs["APNs"]
PG --> API["FastAPI<br/>준비된 응답"]
Redis --> API
API --> Client["iOS 앱 · 위젯"]
APNs --> Client
이 구성에서 API는 가능한 한 읽는 일에만 집중한다. 외부 출처를 직접 긁거나, 예측을 새로 만들거나, 알림 후보를 계산하는 일은 요청을 받는 API 안에 넣지 않았다. 사용자가 앱을 여는 순간에 외부 출처가 느리거나 실패하면, 그 느림과 실패가 그대로 화면에 드러나기 때문이다.
현재 백엔드는 크게 이런 역할로 나뉜다.
- API: 앱과 위젯이 바로 읽는 응답
- 주차 worker: 주차장 목록과 현재 상태 수집
- 나들이 worker: 행사, 시설, 공지, 실시간 도시데이터 수집
- 예측 worker: 주차와 공원 혼잡 변화 계산
- 알림 worker: 알림 후보를 고르고 APNs로 전송
데이터는 PostgreSQL/PostGIS에 저장하고 자주 읽는 상태와 예측 응답은 Redis에 캐시한다. Postgres에는 기준 데이터와 이력을 남기고 Redis에는 빠르게 꺼내 쓸 짧은 수명의 응답을 둔다.
장애가 나면 API가 읽는 값, worker의 마지막 실행과 외부 출처 상태를 차례로 확인했다.
읽기만 남긴 API
초기에는 API와 배치 작업이 더 가까이 붙어 있었다. 수집, 예측, 알림과 API 응답의 실행 주기가 달라 worker를 분리했고, 관측 지표와 헬스 체크도 컴포넌트별로 나눴다.
공공데이터 출처가 실패하면 앱에는 빈 값이나 오래된 값이 나타날 수 있다. API는 마지막 성공 값과 갱신 시각, 출처 상태를 함께 반환해 화면이 현재 수집 상태를 설명할 수 있게 했다.
예측도 같은 맥락이었다. 예측은 시간대별 변화와 불확실성을 함께 보여 주는 보조 신호로 설계했다. 그래서 예측값은 현재값과 분리되어야 했고, 캐시 수명과 갱신 경로도 달라야 했다.
요청 중에는 하지 않는 작업
작은 앱이라면 API가 요청을 받은 자리에서 데이터를 가져오고 가공해 바로 응답해도 될 것 같다. 한강자리의 요청은 사용자가 이동을 결정하는 순간에 온다. 그 순간에 외부 출처가 느리거나 실패하면 앱은 같이 느려지고 위젯은 비어 보인다.
API 요청 경로에서는 다음 작업을 뺐다.
- 외부 출처를 직접 호출하는 일.
- 오래 걸리는 정규화와 좌표 보정.
- 예측을 새로 만드는 일.
- 알림 후보를 계산하는 일.
- 실패한 출처를 계속 재시도하는 일.
이 일들은 worker가 주기적으로 맡고, API는 이미 정리된 값을 빠르게 읽어 응답하게 했다. API는 사용자 요청에 답하는 쪽이고, worker는 뒤에서 값을 준비하는 쪽이다. 둘은 느려지는 이유도, 확인해야 할 지점도 달랐다.
서버를 이렇게 나눈 뒤에는 실패를 보는 방법도 달라졌다. API가 느린 것인지, worker가 값을 못 준비한 것인지, 출처가 늦은 것인지 나눠서 볼 수 있었다.
오래된 값의 표시 방식
마지막 성공 값은 출처가 실패한 뒤에도 참고가 될 수 있다. 한강자리는 그 값을 최신으로 표시하지 않고 갱신 시각과 함께 반환했다.
예를 들어 주차장 잔여 대수가 5분 전 값이면 참고할 수 있다. 하지만 40분 전 값이라면 현장 차이를 감안해야 한다. 행사 정보는 몇 시간 전 값이어도 큰 문제가 없을 수 있지만 실시간 주차 정보는 다르게 봐야 한다. 데이터마다 허용할 수 있는 신선도가 다르기 때문이다.
그래서 응답에는 값과 함께 갱신 시각, 마지막 성공 시각, 출처 실패 여부를 담았다.
현재값과 분리한 예측
주차 예측은 행사, 날씨, 통제와 돌발 상황을 모두 반영하지 못한다. 그래서 화면에는 예측 시각의 여유 가능성을 현재 주차장 상태와 구분해 표시했다.
예측은 현재값보다 한 단계 낮은 신호로 두었다. 지금 남은 자리와 갱신 시각이 먼저이고, 예측은 시간대별 변화를 이해하는 보조 자료다. 사용자가 “지금 바로 가도 되나”를 판단할 때는 현재값이 중심이고, “조금 있다가 가면 나을까”를 볼 때 예측이 도움이 된다.
worker는 외부 데이터를 미리 수집하고 출처 상태와 갱신 시각을 함께 저장했다. API는 사용자가 앱이나 위젯을 여는 순간 그 값을 바로 읽어 반환했다.
Comments
아직 댓글이 없습니다. 첫 댓글을 남겨주세요.
검토 대기 중