Postgres와 Redis를 나눈 기준
이력과 감사 기록은 남기고, 다시 만들 수 있는 응답은 cache로 둔 판단
주차장 잔여 대수가 화면에 보이는 순간, 사용자는 그 숫자를 보고 움직인다. “아직 20대 남았다”는 표현은 단순한 데이터가 아니라 한강으로 갈지 말지를 바꾸는 신호가 된다.
그래서 한강자리에서 Postgres와 Redis를 나눈 이유는 속도만이 아니었다. 사용자가 보는 숫자의 근거를 어디에 남길지, 다시 만들 수 있는 조회 결과를 어디까지 cache로 볼지 먼저 정해야 했다.
개인 프로젝트에서는 “DB에서 바로 읽으면 되지 않을까”와 “Redis에 다 넣으면 빠르지 않을까” 사이를 오가기 쉽다. 한강자리에서는 둘 다 위험했다. 모든 요청을 Postgres로만 받으면 자주 읽는 첫 화면과 예측 응답에서 부담이 커진다. 반대로 Redis에 너무 많은 의미를 넣으면 cache 장애가 데이터 손실처럼 번진다.
Postgres와 Redis가 맡은 데이터
먼저 요청 경로에서 두 저장소가 어떻게 쓰이는지 보면 차이가 보인다. Postgres는 나중에 설명해야 하는 기록을 남기고, Redis는 다시 만들 수 있는 조회 결과를 빠르게 꺼내는 쪽에 선다.
flowchart LR
Workers["수집 · 예측 · 알림 워커"] --> PG[("PostgreSQL / PostGIS<br/>기준 데이터 · 이력 · 위치 · 감사 기록")]
Workers --> Redis[("Redis<br/>현재 상태 · 응답 캐시 · 보조 인덱스")]
API["FastAPI 읽기 경로"] --> Redis
Redis -->|적중| API
Redis -->|미스 · 형식 오류 · 오래됨| PG
PG --> API
Postgres는 나중에 다시 설명해야 하는 값을 맡는다. 언제 어떤 출처에서 값이 들어왔는지, 어떤 예측 run이 어떤 결과를 만들었는지, 알림을 보냈거나 보내지 않은 이유가 무엇이었는지 같은 기록은 Postgres에 남아야 한다.
Redis는 사용자 요청 시간을 짧게 만드는 데 쓴다. 최신 주차 상태, forecast 응답, home-summary hot/stale 응답, 출처 상태 요약, push 후보 조회용 index처럼 다시 만들
수 있는 값을 둔다.
이렇게 나눠두면 장애를 볼 때도 말이 정확해진다. Redis가 비었다면 화면용 응답을 다시 만들면 된다. Postgres의 기준 데이터나 이력이 손상됐다면 복구 방법이 완전히 달라진다. 두 저장소를 같은 무게로 보면 대응도 늦어진다.
앱 기능별로 보는 데이터 묶음
DB schema를 전부 펼치는 대신, 앱 기능이 기대는 데이터 묶음부터 봐야 한다. 주차, 나들이 출처, 예측, push, 교통/번역은 같은 DB 안에서도 서로 다른 속도와 실패 방식을 가진다.
erDiagram
PARK ||--o{ PARKING_LOT : 보유
PARKING_LOT ||--o{ PARKING_STATUS : 기록
PARKING_STATUS ||--o{ FORECAST_INPUT : 전환
FORECAST_RUN ||--o{ FORECAST_RESULT : 생성
DATA_SOURCE ||--o{ INGESTION_RUN : 보고
DATA_SOURCE ||--o{ OUTING_SIGNAL : 발행
PUSH_SUBSCRIPTION ||--o{ DELIVERY_DECISION : 평가
DELIVERY_DECISION ||--o{ DELIVERY_ATTEMPT : 감사
TRANSIT_DATASET ||--o{ TRANSIT_ROUTE : 포함
TRANSLATION_SOURCE ||--o{ TRANSLATION_CACHE : 렌더링
이 ERD는 전체 schema의 사본이 아니라, 운영자가 먼저 나눠 보는 지도에 가깝다. 같은 DB 안에 있더라도 “어떤 기능의 기준 데이터와 이력인가”를 알아야 장애 대응도 정확해진다.
실제 schema에는 보조 테이블과 index가 더 많다. 이 그림은 사용자가 보는 기능과 운영자가 확인하는 기록이 어떻게 맞물리는지만 남긴다.
알림 기록은 Postgres에 남긴다
push 영역에서는 이 차이가 더 잘 보인다. 알림 시스템에서 남겨야 하는 것은 “보냈다” 하나가 아니다. 왜 보냈는지, 왜 보내지 않았는지, 전송을 시도했는지, 외부 전송자가 어떻게 응답했는지가 나뉜다.
이 기록을 하나의 로그 문자열로만 남기면 나중에 알림을 얼마나 조용하게 보낼지 조절할 수 없다. 조용한 시간이라 억제한 알림과, 전송 실패로 도착하지 않은 알림은 사용자에게는 둘 다 “알림이 안 왔다”처럼 보일 수 있다. 하지만 운영자가 해야 할 일은 다르다.
그래서 알림을 보낼지 말지와 전송 감사 기록은 Postgres에 둔다. Redis는 후보를 빠르게 찾는 데 도움을 줄 수 있지만, 마지막 이유가 되지는 않는다. 이렇게 해야 “안 보낸 것”도 앱의 선택으로 설명할 수 있다.
값에는 확인한 시각도 따라다닌다
공공데이터 앱에서는 값만 저장하면 부족하다.
예를 들어 “여의도 주차 가능 20대”라는 값이 있어도 다음 질문이 남는다.
- 이 값은 언제 확인됐나?
- 서버는 언제 가져왔나?
- 어느 source에서 왔나?
- source가 지금 정상인가?
- 현재값인가, 예측값인가?
그래서 status, forecast, outing, home summary에는 시각과 갱신 상태가 같이 들어간다. 앱은 이 정보를 바탕으로 “최신”, “지연”, “정보 없음”을 다르게 말한다.
이 구분은 화면 안내까지 바꾼다. 오래된 값을 최신처럼 보여주면 cache는 성공했을지 몰라도 앱은 사용자를 속이게 된다. 반대로 오래된 값이라고 분명히 말하면 사용자는 참고 신호로 받아들일 수 있다.
캐시는 다시 만들 수 있게 지운다
Redis cache는 빠르지만 진실은 아니다. 그래서 다음처럼 두었다.
- Redis 값은 다시 만들 수 있어야 한다.
- Redis miss는 장애가 아니라 정상적인 길이어야 한다.
- malformed cache entry는 무시하고 Postgres 기록으로 fallback한다.
- forecast run이 새로 만들어지면 관련 forecast/home cache를 비운다.
home-summary는 hot cache와 stale backup cache를 나눠 장애 시 마지막으로 믿을 수 있는 값을 보여준다.- push 후보 조회는 Redis-first가 가능하지만, 최종 발송과 audit은 Postgres를 따른다.
운영하면서 이 선택이 왜 필요한지 더 분명해졌다. Redis를 붙이면 latency는 좋아진다. 하지만 사용자가 보는 정보가 오래됐는지, 어느 출처에서 왔는지, 다시 만들 수 있는 값인지 말할 수 없다면 빠른 응답만으로는 믿기 어렵다.
화면에 남긴 판단
Postgres와 Redis를 나눈 뒤 UI에서 달라진 점은 단순했다. 앱은 값만 받지 않고 그 값이 최신인지, 마지막 성공 응답인지, 아예 정보가 없는 상태인지 같이 받게 됐다.
그래서 “정보 없음”과 “한가함”을 같은 화면으로 만들지 않을 수 있었다. timestamp도 보조 문구가 아니라 값을 어디까지 믿어도 되는지 말해주는 일부가 됐다. 위젯처럼 오래된 값을 보여줄 수 있는 자리에서는 이 차이가 더 크게 드러났다.
결국 Postgres와 Redis를 나눈 이유는 인프라 취향이 아니었다. 사용자가 보는 숫자를 어디까지 믿게 할 것인가의 문제였다. 근거가 되는 기록은 Postgres에 남기고, cache는 다시 만들 수 있게 둔다는 단순한 선이 전체 방향을 잡아줬다.