Blaybus MVP 개발 해커톤 TypeScript Rewrite
Case 01 · auth / authorization boundary
1. Fastify Auth Plugin 기반 인증/인가 구조 설계
라우트마다 흩어질 수 있는 인증 검증을 Fastify auth plugin으로 공통 처리하고, Repository 계층에서 사용자 리소스 접근 조건을 강제했습니다.
Fig. 1 — Fastify auth plugin 으로 인증 경계 격리
문제
- 라우트별 인증 검증이 흩어지면 중복 코드와 누락 위험이 발생
- 사용자별 리소스 접근 제어가 일관되지 않으면 타 사용자 데이터 노출 가능성 존재
해결
- Public route와 Protected route를 분리
- JWT 검증을 Fastify plugin으로 공통 처리
- 검증된 사용자 정보를 request.user에 주입
- Repository 계층에서 user_id 조건을 강제 적용
결과
- 인증 로직 중복 감소
- 사용자 소유 리소스 접근 정책 일관화
- 보호 API의 보안 경계 명확화
Blaybus MVP 개발 해커톤 TypeScript Rewrite
Case 02 · route-service-repository-driver
2. 기능별 API 정책을 Route-Service-Repository-Driver 구조로 분리
사용자 기능별로 달라지는 API 정책을 Route Handler, Service Layer, Repository Layer, Postgres Driver로 분리하여 라우트 비대화와 정책 누락 위험을 줄였습니다.
Fig. 2 — 사용자 흐름과 API 계약 · 정책 매핑
문제
- Route Handler에 정책과 DB 접근이 섞이면 라우트가 비대해지고 정책 누락 위험 발생
- ownership check, answer hidden 같은 보안성 정책은 일관된 위치에서 관리할 필요가 있었음
해결
- API 서버를 Route Handler, Service Layer, Repository Layer, Postgres Driver의 4계층으로 분리
- 사용자 흐름별 정책을 API 계층에 매핑하고 소유권 검사와 정답 은닉 정책을 일관된 위치에서 관리
결과
- 기능별 정책의 위치가 명확해짐
- Memo 소유권 검사와 Exam 정답 은닉 정책을 일관되게 적용
- 기능 추가 시 변경 영향 범위 감소
Blaybus MVP 개발 해커톤 TypeScript Rewrite
Case 03 · migration / delete simplification
3. Strangler Fig 마이그레이션과 ON DELETE CASCADE
v1 API와 v2 rewrite를 병행 운영하면서 기존 다운로드 URL 호환성을 유지하고, 삭제 정합성 문제는 DB cascade로 단순화했습니다.
Fig. 3 — Strangler Fig 이전과 ON DELETE CASCADE 기반 단순화
문제
- 기존 v1 API는 X-User-ID header 기반 인증을 사용
- 기존 클라이언트와 다운로드 URL 호환성을 유지해야 했음
- 노드 삭제 시 연결 파일, 연결 관계, 노드 row를 애플리케이션에서 순차 삭제해야 했음
- 삭제 순서 오류 또는 중간 실패 시 일부 데이터가 남는 정합성 문제 발생 가능
해결
- Strangler Fig 패턴을 적용해 v1과 v2를 병행 운영
- GET 응답의 download URL 규격을 유지하여 클라이언트 변경 범위 최소화
- 인증 방식은 v2에서 JWT Authorization Bearer token으로 전환
- 하위 테이블에 ON DELETE CASCADE 적용
결과
- 기존 API를 즉시 제거하지 않고 안정적으로 v2 전환 가능
- 클라이언트 호환성 유지
- 인증 정책 개선
- 삭제 로직 단계 감소
- 데이터 정합성과 유지보수성 개선
PeakPass 예약·결제 API 서버
Case 01 · command / query separation
1. 상태 변경 API와 조회 API를 분리한 예약·결제 서버 구조
쓰기 API와 조회 API의 책임을 나누고, PostgreSQL은 source of truth로, Redis는 운영 보조 상태로 분리했습니다.
Fig. 1 — 쓰기/읽기 API 분리 및 Source of Truth 배치
문제
- 예약 생성, 결제 요청, 정산 웹훅은 주문·결제·티켓 상태를 변경하는 작업이라 정합성 관리가 중요했음
- 쓰기와 읽기 요청이 섞이면 상태 변경 흐름을 추적하기 어렵고 API 책임이 모호해질 수 있었음
해결
- PostgreSQL은 주문·티켓·결제 기록의 source of truth로 사용
- Redis는 hold TTL, rate-limit, idempotency cache 용도로 분리
- 상태 변경 기능은 REST 명령 API로 구성, 조회 기능은 GraphQL 조회 API로 분리
결과
- 멱등성, rate-limit, 트랜잭션 처리를 쓰기 API에 집중할 수 있었음
- 조회 API는 화면 요구사항에 맞게 필요한 데이터만 선택적으로 제공 가능
PeakPass 예약·결제 API 서버
Case 02 · reserve / checkout / settlement / issue
2. 미결제 티켓 발급을 방지하기 위한 예약·결제 상태 분리
미결제 주문으로 티켓이 생성되는 문제를 방지하고자, 결제 요청이 생성되더라도 정산완료 전까지 티켓을 발급하지 않도록 설계했습니다.
Fig. 2 — 예약 → 체크아웃 → 정산 완료 후 티켓 발급
문제
- 결제 요청 생성만으로 티켓을 발급하면 미결제 주문에 티켓이 생성될 수 있음
- 클라이언트 응답만으로 결제를 확정하면 실제 상태와 서버 상태가 불일치할 수 있음
- 예약, 결제 대기, 결제 확정, 티켓 발급 상태가 섞이면 주문 추적이 어려움
해결
- 예약 생성 시 좌석을 임시 확보하고 reserved 상태로 관리
- 체크아웃 요청 시 order를 payment_pending 상태로 생성
- Payment Provider의 settlement webhook 수신 후 order를 paid로 변경
- paid 상태가 된 주문에 대해서만 ticket issuance 실행
결과
- 결제 확정 기준을 settlement webhook으로 일원화
- 주문 상태 추적이 명확해짐
- 예약 → 결제 → 정산 → 티켓 발급 흐름의 데이터 정합성 개선
PeakPass 예약·결제 API 서버
Case 03 · concurrency / row-level lock
3. 동시 체크아웃과 row-level lock
동일 이벤트에 대한 동시 체크아웃 요청에서도 좌석 수가 중복 차감되지 않도록 PostgreSQL row-level lock으로 재고 변경을 직렬화했습니다.
Fig. 3 — 동시 체크아웃에서 row-level lock 으로 oversell 방지
문제
- 동일 이벤트에 대해 여러 사용자가 동시에 체크아웃할 경우 available_seats가 중복 차감될 수 있음
- 단순 조회 후 업데이트 방식은 oversell 위험이 있음
해결
- checkout transaction을 SERIALIZABLE하게 처리
- SELECT ... FOR UPDATE로 이벤트 row에 lock을 획득
- lock을 획득한 트랜잭션만 좌석 수를 검사하고 차감
- 좌석 부족 시 주문 생성 없이 실패 처리
결과
- 동시에 요청이 들어와도 좌석 수가 음수가 되는 상황 방지
- PostgreSQL을 source of truth로 두고 정합성을 보장
PeakPass 예약·결제 API 서버
Case 04 · idempotent settlement webhook
4. settlement webhook 멱등성 처리
Payment Provider의 settlement webhook이 중복 전송되어도 같은 결제에 대해 티켓이 한 번만 발급되도록 멱등성 방어선을 구성했습니다.
Fig. 4 — settlement webhook 멱등성 구조와 중복 발급 방지
문제
- Payment Provider의 settlement webhook은 네트워크 문제로 중복 전송될 수 있음
- 동일 결제에 대해 티켓이 중복 발급되면 치명적인 데이터 정합성 문제가 발생
해결
- 1차 방어: Redis idempotency lock으로 동일 key 요청 중복 처리
- 2차 방어: pg_advisory_xact_lock으로 같은 결제 키의 DB 처리 직렬화
- 3차 방어: payment_records.provider_transaction_id UNIQUE 제약 적용
- 4차 방어: 이미 paid order 또는 existing ticket 여부 확인
결과
- 동일 settlement webhook이 반복 호출되어도 티켓은 한 번만 발급
- 중복 요청은 결과 캐시 또는 no-op 처리