한강자리 시스템 개요

외부 데이터 수집부터 API, iOS 앱과 위젯까지 한강자리의 전체 데이터 흐름

한강자리는 한강공원에 가기 전과 한강 안에서 필요한 정보를 빠르게 확인하는 iOS 앱이다. 사용자는 공원 목록, 주차장 상태, 위젯과 알림에서 “지금 가도 괜찮을까”, “차를 가져가도 될까”, “다른 공원이 나을까”, “근처에서 무엇을 찾을까”를 확인한다.

이 질문을 앱 화면 하나에서 매번 새로 계산하기는 어렵다. 주차 현황은 자주 바뀌고, 행사와 공지는 다른 주기로 갱신되며, 예측은 미리 계산해야 한다. 위젯은 앱이 열려 있지 않아도 바로 읽을 수 있어야 한다.

worker는 외부 데이터를 미리 정리하고 API는 앱과 위젯이 바로 읽을 응답을 만든다. 클라이언트는 값의 표시와 상호작용을 맡는다.

한 줄로 본 전체 구조

한강자리는 느리거나 비어 있거나 늦게 바뀌는 공공·공식 데이터를 화면에서 상태와 함께 읽을 수 있는 값으로 바꾼다.

수집, 정리, 응답과 표시에는 서로 다른 실행 경로를 두었다. 외부 출처는 실패할 수 있고 갱신 주기도 제각각이지만 앱과 위젯은 사용자가 보는 순간 빠르게 응답해야 한다.

사용자 요청을 처리하는 API에는 화면을 만들기 위한 읽기 작업을 남겼다. 수집, 예측, 알림, 번역처럼 시간이 걸리거나 실패 가능성이 큰 일은 worker와 CronJob이 처리한다.

먼저 읽히는 앱과 위젯

사용자가 처음 보는 곳은 앱과 위젯이다. 사용자는 앱과 위젯에서 상황을 확인하고 더 필요하면 지도 앱으로 이동한다. 위젯은 별도 실행 환경에서 앱이 미리 저장해 둔 값을 읽는다.

flowchart LR
  User["사용자<br/>방문 전후 확인"] --> Widget["위젯<br/>짧게 확인"]
  User --> App["앱<br/>비교 · 상세 · 설정"]
  Widget --> App
  App --> Maps["지도 앱<br/>이동 시작"]

  subgraph Device["iOS 단말"]
    App
    Widget
    Local["앱 내부 캐시<br/>최근 화면 값"]
    Saved["저장된 표시값<br/>앱과 위젯이 공유"]
  end

  App <--> Local
  App --> Saved
  Widget --> Saved
  App --> API["화면용 API 응답"]
  Widget -.-> API

앱이 화면을 그릴 때마다 모든 외부 출처를 다시 확인하면 사용자는 기다리게 된다. 서버는 앱이 바로 표시할 수 있는 응답을 미리 준비하고 클라이언트는 표시와 상호작용을 처리한다.

위젯은 앱 프로세스와 다르게 움직인다. 그래서 저장된 표시값을 먼저 읽고, 필요한 경우에만 API에서 새 값을 가져온다.

미리 정리하는 외부 데이터

사용자 요청이 들어온 뒤 외부 출처를 차례로 부르면 앱은 출처의 속도와 실패를 그대로 떠안게 된다. 한강자리에서는 worker가 먼저 데이터를 수집하고 정리한다. Postgres는 기준 데이터와 이력을 남기고, Redis는 빠르게 읽을 수 있는 현재 상태와 요약을 맡는다.

flowchart LR
  Sources["공개/공식 출처<br/>주차 · 행사 · 공지 · 시설"] --> Workers["워커 / CronJob<br/>수집 · 정리 · 검사"]
  Workers --> Records[("Postgres<br/>기준 데이터 · 이력")]
  Workers --> Fast[("Redis<br/>빠른 조회값")]

  Records --> Prepared["예측 · 알림 후보<br/>미리 계산"]
  Prepared --> Records
  Prepared --> Fast

  Records --> API["화면용 API 응답"]
  Fast --> API
  API --> Client["앱 · 위젯"]

이렇게 분리하면 “값이 없다”를 한 가지 실패로 뭉개지 않을 수 있다. 외부 출처 실패, worker 중단, 빠른 조회값 누락과 Postgres 기준 데이터 누락은 각각 확인할 위치와 복구 방법이 다르다. 화면에서는 모두 비슷하게 보일 수 있다.

장애 때 보여줄 마지막 확인값

장애나 지연이 생기면 fallback은 마지막 성공값과 정보 없음을 구분해 표시한다. 마지막 성공값에는 확인 시각과 지연 상태가 따라붙는다.

flowchart TD
  Open["앱 또는 위젯 열기"] --> Saved{"저장된 표시값이<br/>있는가?"}
  Saved -->|예| ShowSaved["마지막으로 확인한 값 표시<br/>갱신 시각 함께 표시"]
  Saved -->|아니오| Request["새 응답 요청"]

  Request --> Result{"응답을 만들 수 있는가?"}
  Result -->|예| Fresh["새 값 표시<br/>갱신 상태 함께 표시"]
  Result -->|마지막 성공 값| Last["마지막 성공 값 표시<br/>지연 상태 표시"]
  Result -->|아니오| Empty["정보 없음 표시<br/>과신하지 않게 표시"]

주차 앱에서는 빠른 응답만큼 정직한 응답도 필요하다. 40분 전 값을 최신처럼 보여주면 화면은 빠를 수 있지만 사용자는 잘못 움직일 수 있다.

home-summary, forecast와 widget snapshot에는 값과 함께 갱신 상태와 출처 상태가 들어간다.

주차에서 확장된 역할

시작은 “한강공원 주차장 잔여 대수를 매번 웹에서 확인하기 불편하다”는 작은 문제였다.

직접 앱을 쓰면서 주차 확인 뒤에도 공원 혼잡도, 행사, 시설, 공지, 날씨와 대중교통 정보가 필요했다. 즐겨찾기, 위젯과 알림은 이 정보를 방문 전후의 짧은 확인 흐름에 연결했다.

그래서 역할도 제품에서 필요한 순서대로 나뉘었다.

  • 앱은 사용자가 지금 결정을 내리는 화면을 만든다.
  • 위젯은 앱을 열기 전에 짧게 확인할 값을 보여준다.
  • API는 화면에서 바로 읽을 수 있는 응답을 내려준다.
  • worker는 외부 출처의 느림, 실패, 갱신 주기 차이를 사용자 요청 시간 밖에서 처리한다.
  • Postgres는 기준 데이터, 이력, 감사 기록을 보존한다.
  • Redis는 사라져도 다시 만들 수 있는 캐시와 보조 인덱스를 맡는다.

Redis에는 Postgres에서 다시 조합할 수 있는 조회 결과만 저장했다. worker가 외부 출처를 미리 읽게 해 앱 요청 시간이 수집 지연의 영향을 받지 않도록 했다.

한 응답으로 받는 첫 화면

현재 앱과 일반 공원 위젯에서 가장 중요한 응답은 home-summary다. 주차, 나들이 정보, 예측, 출처별 갱신 상태를 한 번에 묶어 첫 화면이 읽기 쉬운 모양으로 내려준다.

home-summary는 호출 수를 줄이면서 앱과 위젯이 같은 첫 화면 구조를 읽게 하는 응답 형식이다.

클라이언트가 주차, 나들이, 예측 API를 각각 부르고 자기 쪽에서 조합하면 화면마다 해석이 달라지기 쉽다. 서버가 조합한 응답을 내려주면 앱은 표시와 상호작용에 더 집중할 수 있다.

이 응답 형식은 주차, 행사와 예측이 서로 다른 갱신 상태를 말하는 상황을 한곳에서 조정한다.

이 구조에서 worker가 외부 출처의 데이터와 상태를 남기고 API가 화면용 응답으로 조립한다. 앱과 위젯은 값과 최신성을 함께 표시한다.

Comments

댓글

    이미지 확대