FastAPI route를 얇게 둔 API 설계

화면용 응답 조립과 저장소 접근을 분리해 FastAPI route를 얇게 유지한 구조

한강자리 백엔드는 FastAPI 프로세스와 별도 worker로 구성된다.

API는 앱과 위젯이 바로 읽을 수 있는 화면용 응답을 내려준다. 외부 데이터 수집, 예측 계산, 푸시 후보 고르기와 발송은 worker로 빠져 있다.

사용자가 앱을 열 때 API는 빠르게 응답해야 한다. 외부 공공데이터 수집은 느리거나 실패할 수 있고 출처마다 갱신 주기도 다르므로 요청 경로 밖에서 처리한다.

초기에는 route 함수 안에서 필요한 값을 읽고 조합해도 충분해 보였다. 공원별 첫 화면, 위젯, 예측과 알림 설정이 붙자 route가 외부 출처, Redis, DB schema와 DTO 모양을 함께 알아야 했고 작은 변경도 여러 계층으로 번졌다.

그래서 FastAPI route에는 HTTP 요청과 응답 처리를 남겼다. Application query/use case는 공원의 첫 화면을 만들고 repository는 필요한 row를 읽는다.

조립만 맡는 HTTP 라우트

백엔드는 clean architecture에 가까운 형태로 나눴다.

  • domain: entity와 policy.
  • application: query, command, use case, port.
  • infrastructure: SQLAlchemy, Redis, APNs, 외부 출처 adapter.
  • interfaces: HTTP route와 job entrypoint.
flowchart TB
  Route["FastAPI 라우트"] --> Dependency["의존성 계층"]
  Dependency --> UseCase["애플리케이션 조회 / 명령"]
  UseCase --> Port["저장소 / 캐시 포트"]
  Port --> Infra["SQLAlchemy / Redis / APNs / 출처 클라이언트"]
  Infra --> Store["Postgres / Redis / 외부 서비스"]
  UseCase --> DTO["Pydantic DTO 직렬화"]
  DTO --> Client["iOS 앱 / 위젯"]

FastAPI route는 얇게 둔다. route가 SQL query나 출처 adapter를 직접 알기 시작하면 화면에서 필요한 일과 인프라 세부사항이 섞인다.

파일을 나눌 때는 다음 세 동작이 섞이지 않게 했다.

route는 HTTP parameter와 response shape를 다루고 query/use case는 화면용 데이터를 조립한다. repository/cache adapter는 저장소의 세부 구현을 맡는다. 이 정도만 지켜도 테스트와 장애 분석이 훨씬 쉬워졌다.

앱이 호출하는 API 묶음

앱이 읽는 v2 앱 API 영역은 다음과 같다.

  • Parks/Parking: 공원, 주차장, parking overview, lot status.
  • Home Summary: 첫 화면용 주차, 나들이, 예측, 갱신 상태를 묶은 화면용 응답.
  • Outing: 공원별 일반 화면 overview와 signal detail.
  • Forecast: 주차 예측, 공원 혼잡 예측, horizon timeline.
  • Telemetry: client performance event와 product event batch.
  • Push: APNs subscription 등록/해제.
  • App access bootstrap: 앱 접근 검증과 request proof 발급.

관리용 route는 사용자용 API와 따로 둔다. 관리용 세부사항은 화면용 응답에 섞지 않는다.

주차 화면을 만들 때 서버가 읽는 데이터

주차 overview는 Redis와 Postgres를 함께 사용한다. master data는 Postgres에서 읽고, 최신 status는 Redis를 먼저 본다.

sequenceDiagram
  autonumber
  participant Client as iOS 앱/위젯
  participant Route as FastAPI 라우트
  participant Query as 주차 조회
  participant Cache as 상태 캐시
  participant Repo as 주차 저장소
  participant DB as Postgres

  Client->>Route: 주차 화면 요청
  Route->>Query: 주차 화면 값 만들기 요청
  Query->>Repo: 공원과 주차장 기준 데이터 읽기
  Repo->>DB: 기준 데이터 조회
  Query->>Cache: 최신 주차 상태 확인
  alt 캐시 미스 또는 형식 오류
    Query->>Repo: 저장소의 마지막 상태 확인
    Repo->>DB: 상태 이력 조회
  end
  Query-->>Route: 화면용 읽기 모델 전달
  Route-->>Client: 신선도 포함 응답 반환

첫 화면은 home-summary가 맡는다. 여러 데이터를 하나의 payload로 묶는 endpoint다.

sequenceDiagram
  autonumber
  participant Client as 앱/위젯
  participant Route as home-summary 라우트
  participant Cache as 핫/보관 캐시
  participant Parking as 주차 조회
  participant Outing as 나들이 조회
  participant Forecast as 예측 조회
  participant DB as Postgres

  Client->>Route: 첫 화면 요청
  Route->>Cache: 바로 쓸 첫 화면 캐시 확인
  alt 핫 캐시 적중
    Cache-->>Route: 캐시된 첫 화면 응답
  else 캐시 미스
    Route->>Parking: 주차 요약 만들기
    Route->>Outing: 나들이 신호와 출처 상태 모으기
    Route->>Forecast: 예측 요약 확인
    Parking->>DB: 주차 기준 데이터 읽기
    Outing->>DB: 나들이 기준 데이터 읽기
    Forecast->>Cache: 예측 응답 읽기
    Route->>Cache: 빠른 응답과 마지막 성공값 저장
  end
  Route-->>Client: 첫 화면 응답 반환

이 API는 앱의 첫 화면과 일반 공원 위젯을 단순하게 만든다. 클라이언트가 여러 API를 순서대로 조합하지 않아도 된다.

home-summary를 둔 뒤에는 호출 수가 줄었고 앱과 위젯이 같은 값을 읽게 됐다.

예전처럼 클라이언트가 여러 API를 조합하면 주차는 최신인데 행사 정보는 오래되었거나, 위젯은 일부 값만 받아 화면 의미가 달라지는 일이 생긴다. 서버 응답은 이런 어긋남을 한 번에 맞춰준다.

값과 갱신 상태를 담은 응답

iOS 앱의 Swift decoding은 서버 응답 변화의 영향을 바로 받는다. DTO가 내부 DB schema와 앱의 응답 형식 사이에서 변경 범위를 조절한다.

DTO는 다음 일을 맡는다.

  • Optional field와 default policy를 명확히 한다.
  • datetime serialization을 일관되게 한다.
  • fresh, stale, unavailable을 구분할 수 있게 한다.
  • 출처 갱신 상태와 generated/observed/fetched 계열 시각을 담는다.
  • raw payload와 내부 정책 값을 숨긴다.
  • 앱 버전이 섞여 있어도 약속을 쉽게 깨지 않게 한다.

읽기 요청에도 필요한 앱 확인

현재 v2 앱 route는 앱 접근 검증을 지난다. 구체적인 플랫폼 검증 구현과 request proof 발급은 bootstrap 쪽으로 분리되어 있다.

플랫폼 검증 절차를 정하기 전에 다음 실행 정책을 먼저 정했다.

  • 읽기 API도 남용 가능성을 고려한다.
  • 쓰기 API는 request proof를 더 엄격하게 확인한다.
  • 인증 실패 metric은 남기되, 사용자 식별 정보를 로그에 직접 남기지 않는다.
  • 운영 metric endpoint는 일반 사용자 트래픽에서 보이지 않게 둔다.

얇은 route 이후의 변화

FastAPI route를 얇게 두자 화면에서 필요한 일과 저장소 세부사항이 갈라졌다. 앱용 API는 사용자가 고르는 화면 단위로 설계했고, 수집과 계산은 worker에 남겼다.

공공데이터 앱의 응답에는 값과 출처, 시각, 갱신 상태가 함께 들어가야 한다. 첫 화면이 복잡해질수록 서버가 화면용 응답을 묶어 주는 편이 클라이언트 안정성에도 도움이 됐다.

route는 HTTP 형식을 처리하고 use case는 화면용 데이터를 조립한다. 두 작업을 분리해 화면 응답과 저장소 구현을 따로 변경할 수 있었다.

Comments

댓글

    이미지 확대