국가 백신 예약 및 접종 시스템
Source: Dev.to
몇 년 전, 시스템 설계 인터뷰를 본 적이 있습니다. 면접관이 저에게 다음과 같은 시나리오를 제시했습니다:
전국 백신 예약 시스템을 설계하라.
수백만 명의 시민이 등록하고 슬롯을 예약해야 합니다. 클리닉은 백신을 투여해야 하고, 정부는 감사 로그와 사기 방지 기능을 필요로 합니다.
처음 생각한 해결책은 간단했습니다: 사람들에게 슬롯을 예약하게 하고, 재고를 확인한 뒤 확정하는 것이었습니다. 화이트보드에 기본 흐름을 그려보고 꽤 만족스러웠습니다. 그런데 면접관이 더 어려운 질문을 하기 시작했습니다.
- “두 사람이 동시에 마지막 슬롯을 예약하려 하면 어떻게 할 것인가?”
- “예약이 이미 확정된 뒤에 클리닉에서 백신이 부족해지면 어떻게 할 것인가?”
- “중간에 자격 검사에 실패하면 어떻게 롤백할 것인가?”
좋은 답변이 없었습니다. 저는 행복 경로(happy path)만 설계했을 뿐이었습니다.
그 인터뷰가 머릿속에 남아 있었습니다. 몇 달 후, 인터넷 신용 구매 시스템을 위한 재고 예약 패턴을 조사하던 중, 같은 아이디어가 그 인터뷰에 도움이 될 수 있었음을 깨달았습니다. 그래서 문제로 돌아가 다시 설계했습니다. 제가 만든 설계는 다음과 같습니다.
원래 제안했던 내용 (인터뷰 중)
User selects clinic → slot → vaccine type → system confirms → appointment created
빠르게 나타나는 문제들
| 문제 | 설명 |
|---|---|
| 경쟁 상태 | 두 사람이 마지막 슬롯을 동시에 “예약” 클릭. 두 사람 모두 확인을 받지만, 한 시민은 좌석이 없게 됨. |
| 재고 불일치 | 슬롯은 확인되었지만, 예약일과 진료일 사이에 클리닉의 백신 용량이 소진됨. |
| 늦은 자격 실패 | 시스템이 먼저 진료 예약을 확인하고, 이후에 시민이 연령/보험 요건을 충족하지 못한다는 것을 발견. 슬롯/용량은 이미 할당된 상태. |
| 롤백 없음 | 중간에 무언가 실패하면, 슬롯이나 용량을 풀어 다시 풀(pool)로 되돌릴 방법이 없음. |
이 문제들은 제가 이후에 인터넷 신용 구매 시스템을 설계하면서도 발견한 문제와 동일합니다. 제한된 자원과 다수의 동시 사용자를 다룰 때는 정상 흐름(해피 패스)만으로는 충분하지 않습니다.
핵심 인사이트
모든 것이 검증될 때까지는 어떤 것도 확인하지 마세요.
다단계 프로세스를 사용하세요: 일시 보류 → 검증 → 최종 확인. 어느 단계라도 실패하면 롤백합니다.
콘서트 티켓 판매 방식과 정확히 같습니다: 결제하는 동안 좌석이 보류되고, 시간이 초과하면 좌석이 다시 풀립니다.
Source: …
개선된 설계 전체 흐름
1. 임시 예약 생성
- 사용자가 클리닉, 시간대, 백신 종류를 선택합니다.
- 시스템은 TTL(예: 5분) 을 가진 Redis에 임시 예약을 생성합니다.
- 예약 상태 =
PENDING. - 슬롯 용량과 백신‑투여 수는 일시적으로 감소되어 다른 사용자는 감소된 가용성을 보게 됩니다.
왜 Redis인가?
- 빠른 인‑메모리 저장소이며 TTL을 기본 제공합니다.
- 관계형 DB도 사용할 수 있지만, 만료된 예약을 정리하기 위한 스케줄 작업이 필요합니다. Redis는 키를 자동으로 만료시킵니다.
2. Redis에서 경쟁 조건 처리
- 슬롯 카운터에 원자적
DECR명령을 사용합니다. - 카운터가 0이 되면 다음 요청은 거부됩니다.
- 추가 안전을 위해 Lua 스크립트로 체크‑앤‑디크리먼트를 감싸 하나의 원자 연산으로 만듭니다.
3. 자격 검사 수행 (슬롯이 보류된 동안)
| 검사 | 설명 |
|---|---|
| 연령 | 일부 백신은 60세 이상만 대상입니다. |
| 보험 | 외부 API를 통해 확인합니다. |
| 의료 기록 | 알레르기, 이전 접종 여부 등 |
| 지역 | 시민이 해당 지역에 속해 있어야 합니다. |
검사 중 하나라도 실패하면:
- Redis 예약을 삭제합니다.
- 임시 슬롯 카운터를 증가시켜 슬롯을 해제합니다.
- 명확한 오류 메시지를 반환합니다(예: “귀하는 … 때문에 자격이 없습니다.”).
4. 최종 확인 (모든 검사가 통과된 경우)
- 메인 데이터베이스에서 슬롯 용량과 백신 재고를 영구적으로 감소시킵니다.
- 예약 상태를 업데이트:
PENDING → CONFIRMED. - Redis 예약을 삭제합니다(더 이상 필요 없음).
- 시민에게 확인 메시지를 전송합니다(SMS, 이메일, 푸시).
이것이 돌이킬 수 없는 시점입니다. 이 단계 이전의 모든 작업은 취소할 수 있습니다.
5. 클리닉 도착
- 직원이 시민의 QR 코드를 스캔합니다(예약 ID + 검증 해시 포함).
- 서버가 QR 코드를 예약 레코드와 대조합니다.
- 직원이 백신 배치 번호와 투여 시간을 기록합니다.
- 예약 상태 →
ADMINISTERED. - 분석, 정부 보고, 감사 로그를 위해 이벤트를 발생시킵니다.
Failure‑Handling Scenarios
| Failure | Handling |
|---|---|
| No‑show | 예약된 작업이 시간 창이 지난 CONFIRMED 약속을 스캔합니다. 상태 → NO_SHOW; 재고가 다시 반환됩니다. |
| Citizen cancels | 포털을 통한 취소가 즉시 재고를 반환합니다. |
| Clinic cancels a slot | 영향을 받은 모든 약속에 플래그가 지정되고, 시민에게 알림이 전송되며, 우선순위로 재예약할 수 있습니다. |
| External API down (e.g., insurance) | circuit‑breaker 패턴을 사용합니다. 연속 N번 실패하면 API 호출을 일시 중지합니다. 예약은 재시도를 위해 대기열에 넣거나(지수 백오프), 수동 검토를 위한 플래그와 함께 임시로 허용됩니다. |
| Redis goes down | database‑level reservations 로 폴백하고 정리 작업을 수행합니다. 다소 느리지만 예약은 여전히 작동합니다. |
고수준 아키텍처
+-------------------+ +-------------------+ +-------------------+
| Frontend | ---> | API Gateway | ---> | Auth Service |
| (Booking portal | | (Auth, rate‑limit | | (Login, ID check)|
| & Clinic dashboard) | , routing) | +-------------------+
+-------------------+ +-------------------+ |
+-------------------+
| Booking Service |
| (Reservation, |
| eligibility, |
| confirmation) |
+-------------------+
|
+-------------------+----------------------+-------------------+
| | | |
+-------------------+ +-------------------+ +-------------------+ +-------------------+
| Redis Cache | | Relational DB | | External APIs | | Messaging / |
| (Temp holds, TTL) | | (Appointments, | | (Insurance, | | Event Bus |
| | | Stock, Logs) | | Medical, etc.) | | (Kafka, SNS…) |
+-------------------+ +-------------------+ +-------------------+ +-------------------+
- 프론트엔드 – 시민을 위한 웹/모바일 포털; 클리닉 직원용 대시보드.
- API 게이트웨이 – 인증 처리, 전역 속도 제한(대량 예약 시 중요), 마이크로서비스 라우팅 담당.
- 인증 서비스 – 국가 ID 검증, 토큰 발급.
- 예약 서비스 – 핵심 로직: 임시 예약, 자격 확인, 최종 확인, 취소 처리.
- Redis 캐시 – 빠른 TTL 기반 임시 보류.
- 관계형 DB – 예약, 재고 수준, 감사 로그의 영구 저장소.
- 외부 API – 보험 검증, 의료 기록 조회 등.
- 메시징 / 이벤트 버스 – 분석, 보고 및 서브시스템 간 최종 일관성을 위한 이벤트 발행.
요점
- 행복 경로를 절대 신뢰하지 말라 자원이 부족할 때.
- 원자적이고 일시적인 보류 (Redis
DECR/Lua) 는 경쟁 조건을 방지한다. - 다단계 워크플로우 (PENDING → CONFIRMED → ADMINISTERED) 는 명확한 롤백 지점을 제공한다.
- TTL 기반 예약 은 포기된 시도를 자동으로 정리한다.
- 서킷 브레이커와 폴백 은 하위 종속성이 실패할 때 시스템을 탄력적으로 유지한다.
이러한 재고 예약 패턴을 적용함으로써 순진한 “예약‑확인” 설계를 견고하고 프로덕션에 준비된 국가 백신 예약 시스템으로 바꾸었다.
Services Overview
| Service | Responsibility |
|---|---|
| Patient Service | 의료 기록, 예방접종 이력 |
| Clinic Service | 슬롯 관리, 직원 일정, 용량 |
| Inventory Service | 클리닉별 백신 재고, 배치 추적 |
| Appointment Service | 예약, 확인 및 상태 변경 관리 |
| Eligibility Service | 규칙 엔진 + 외부 API 호출 |
| Notification Service | SMS, 이메일, 푸시; 전송 실패 시 재시도 |
| Audit Service | 모든 상태 변경에 대한 Append‑only 로그 (정부 규정 준수 필요) |
Data Layer
- PostgreSQL – 영구 데이터 저장소
- Redis – 임시 예약 및 캐시
Asynchronous Messaging
- Kafka topics for events:
AppointmentReserved
AppointmentConfirmed
AppointmentAdministered
AppointmentCancelled
These events keep services decoupled and make the system auditable by default.
인터뷰에서 얻은 교훈
“그 인터뷰를 되돌아보면, 내가 놓친 가장 큰 부분은 기술이 아니라 사고방식이었다. 나는 완전해 보였기 때문에 행복 경로(happy path)로 바로 뛰어들었다. 하지만 면접관이 내가 예약 양식을 설계할 수 있는지를 테스트한 것이 아니라, 문제가 발생했을 때 어떻게 생각할 수 있는지를 테스트한 것이다.”
핵심 정리
- 행복 경로가 아니라 실패 시나리오부터 시작하라 – 어떤 설계를 최종 확정하기 전에 “각 단계에서 무엇이 잘못될 수 있을까?” 라는 질문을 스스로에게 해보라.
- 임시 예약은 해킹이 아니라 패턴이다 – 콘서트 티켓, 플래시 세일, 백신 접종 슬롯 등 제한된 재고와 다수의 사용자가 있을 때는 보류‑후‑확인 흐름이 필요하다.
- 롤백을 명시적으로 정의하라 – “오류는 우리가 처리한다”는 설계가 아니다. 오류가 발생했을 때 데이터, 재고, 사용자에게 어떤 일이 일어나는지를 구체적으로 명시하라.
- 외부 서비스 장애에 대비하라 – 보험 API나 알림 서비스가 다운될 수 있다. 서킷 브레이커와 재시도 큐는 선택 사항이 아니라 필수이다.
- 재고 예약 패턴을 공부하라 – 인터넷 신용 구매 시스템 설계에 관한 내 이전 포스트에서는 이러한 패턴을 더 자세히 다루고 코드 예시도 제공한다. 핵심 아이디어 – 먼저 예약하고, 검증한 뒤 커밋 – 은 여러 시스템에서 공통적으로 나타난다.
Call for Feedback
읽어 주셔서 감사합니다. 비슷한 인터뷰 질문을 겪었거나 이 디자인을 개선할 아이디어가 있다면 댓글로 알려 주세요.