이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 기능 요구사항을 API, 상태, 트래픽, 장애 경계로 어떻게 바꾸는가?
- 동기 처리와 비동기 처리를 나누는 기준은 무엇인가?
- 요구사항 번역은 성능, 정합성, 장애 격리, 운영 복잡도를 어떻게 바꾸는가?
개요
System Design의 첫 단계는 구조도를 그리는 것이 아니라 요구사항을 분해하는 것이다.
“이벤트 쿠폰 발급 기능을 만든다”라는 문장은 설계 입력으로 너무 거칠다.
백엔드 개발자는 이 문장을 쓰기 API, 읽기 API, 데이터 불변식, 재고 판단, 트래픽, 실패 응답으로 바꿔야 한다.
이 번역을 건너뛰면 cache, queue, DB, replica 같은 선택이 근거 없는 기술 나열이 된다.
요구사항을 쪼개는 순서
먼저 사용자에게 보이는 결과를 적는다.
- 사용자는 이벤트 페이지에서 쿠폰 발급을 요청한다.
- 사용자는 이벤트 쿠폰을 한 번만 발급받을 수 있다.
- 사용자는 발급 성공, 중복, 소진, 대기 상태를 즉시 알아야 한다.
- 사용자는 내 쿠폰함에서 방금 받은 쿠폰을 확인할 수 있어야 한다.
- 운영자는 재고 소진, 중복 요청, 내부 오류를 구분해 추적해야 한다.
- 푸시 알림과 통계 집계는 늦어져도 발급 성공을 막지 않아야 한다.
이 문장들은 곧 구조 선택으로 이어진다.
쿠폰 발급 원장 기록과 재고 차감은 동기 핵심 경로에 가깝다.
푸시 알림과 이벤트 통계는 비동기 후속 경로로 분리할 수 있다.
내 쿠폰함은 read-after-write 요구가 있으므로 cache나 read model을 쓸 때 주의해야 한다.
구조로 번역하는 표
| 요구사항 문장 | 설계 질문 | 구조 결정 |
|---|---|---|
| 회원당 1회만 발급해야 한다 | 깨지면 안 되는 불변식은 무엇인가? | (event_id, member_id) unique constraint |
| 선착순 재고를 넘기면 안 된다 | 경쟁 요청이 몰리면 무엇이 깨지는가? | 재고 차감 transaction 또는 token 선점 |
| 결과를 즉시 알려야 한다 | 사용자 성공의 응답 상태는 무엇인가? | ISSUED, DUPLICATE, SOLD_OUT, PENDING 분리 |
| 알림은 늦어도 된다 | 사용자 응답 전에 필요한가? | outbox와 queue로 후속 처리 |
| 내 쿠폰함은 바로 보여야 한다 | read-after-write가 필요한가? | command 결과 반환 또는 원본 DB 우선 조회 |
| 운영자가 원인을 알아야 한다 | 어떤 지표가 필요한가? | issue status, reject reason, stock remaining 분리 |
이 표를 만들면 컴포넌트가 요구사항에서 나온다.
반대로 표 없이 구조부터 그리면 나중에 사용자가 기대한 성공 기준과 설계가 어긋난다.
기준 시나리오
쿠폰 발급 peak = 1,000 TPS
쿠폰 상세 조회 peak = 8,000 QPS
발급 가능 수량 = 5,000개
발급 API 목표 p95 = 150ms
푸시 알림 허용 지연 = 10m
내 쿠폰함 read-after-write 허용 지연 = 2s이 숫자에서 바로 보이는 결정이 있다.
발급 API는 재고 경쟁 때문에 느려질 수 있으므로 동시성 제어와 거절 상태가 필요하다.
쿠폰 상세 조회는 발급보다 훨씬 많으므로 cache와 read replica 후보가 된다.
푸시 알림은 발급 성공의 조건이 아니므로 queue와 retry 대상으로 분리한다.
내 쿠폰함은 사용자 신뢰에 직접 닿으므로 stale cache를 그대로 쓰면 안 된다.
흐름 예시
flowchart LR C["Client"] --> A["Coupon Issue API"] A --> S["CouponIssue Service"] S --> I["Inventory Row/Token"] S --> L["Issue Ledger"] S --> O["Outbox"] O --> Q["Notification Queue"] Q --> W["Push Worker"]
이 그림의 핵심은 컴포넌트 수가 아니다.
재고 차감과 발급 원장 기록은 사용자 응답의 일부이고, 푸시 알림은 outbox 이후 비동기 작업이라는 경계가 중요하다.
이 경계가 성능과 장애 격리를 바꾼다.
사용자 응답은 빨라질 수 있지만, 알림은 중복 발송과 재처리 문제가 생긴다.
API 계약 예시
동기 핵심 경로와 비동기 후속 경로를 응답에 드러낸다.
{
"couponIssueId": "ci-1001",
"issueStatus": "ISSUED",
"stockDecision": "DECREASED",
"notificationStatus": "PENDING",
"createdAt": "2026-07-01T10:15:00+09:00"
}이 응답은 “푸시 알림까지 완료됐다”를 약속하지 않는다.
사용자가 쿠폰 발급 성공을 확인하는 데 필요한 정보와 나중에 처리될 정보를 분리한다.
Spring 경계 예시
서비스 계층은 불변식 검증, 재고 판단, outbox 기록의 경계를 잡는다.
@Transactional
public CouponIssueResult issue(IssueCouponCommand command) {
IdempotencyKey key = idempotencyService.reserve(command.requestId());
StockDecision stock = couponRepository.decreaseStock(command.couponId());
CouponIssue issue = issueRepository.save(CouponIssue.issued(command, stock));
outboxRepository.save(OutboxMessage.couponIssued(issue.id(), key.value()));
return CouponIssueResult.issued(issue.id(), stock.status(), NotificationStatus.PENDING);
}이 코드는 완성 예제가 아니라 책임 위치를 보여준다.
재고 차감이 발급 성공의 일부라면 소진, 중복, 내부 오류 상태가 명확해야 한다.
푸시 알림이 후속 작업이라면 같은 트랜잭션에서 발송하지 않고 재처리 가능한 기록을 남긴다.
설계 축의 변화
요구사항을 구조로 번역하면 다음 축이 바뀐다.
| 결정 | 성능 | 정합성 | 장애 격리 | 운영 복잡도 |
|---|---|---|---|---|
| 알림 queue 분리 | 응답 지연 감소 | 알림은 지연 가능 | provider 장애가 발급을 막지 않음 | retry, DLQ 필요 |
| 내 쿠폰함 cache | 조회 지연 감소 | stale 가능 | DB 읽기 부하 감소 | invalidation 필요 |
| 재고 row lock | 초과 발급 방지 | hot row 가능 | 불변식은 강해짐 | lock wait 관측 필요 |
| idempotency key | 중복 부하 감소 | 중복 발급 방지 | retry 안전성 증가 | key 저장소 필요 |
설계 리뷰에서는 이 표가 기술 선택보다 중요하다.
무엇을 얻고 무엇을 새로 운영해야 하는지 드러내기 때문이다.
위험 신호
- 기능 목록만 있고 성공, 지연, 실패 상태가 없다.
- 모든 후속 작업을 사용자 응답 전에 처리한다.
- 재고 소진, 중복 요청, 내부 오류를 같은 실패로 반환한다.
- read-after-write가 필요한 화면을 cache 뒤에 숨긴다.
- 중복 요청과 retry를 고려하지 않고 “한 번만 호출된다”고 가정한다.
- 운영자가 장애 원인을 구분할 상태값과 로그가 없다.
개인 프로젝트 기준
개인 프로젝트에서는 모든 컴포넌트를 실제로 나눌 필요는 없다.
대신 다음 근거는 남긴다.
- 동기 처리와 비동기 처리로 나눈 이유
- 생략한 cache, queue, retry의 전환 조건
- 실패 재현 방법
- 핵심 로그와 상태값
포트폴리오에서는 “queue를 썼다”보다 “쿠폰 발급 성공과 알림 발송 성공을 다른 사용자 결과로 봤다”가 더 강한 설명이다.
기업 운영 기준
기업 운영에서는 요구사항 번역이 runbook과 지표까지 이어져야 한다.
- 쿠폰 발급 p95와 재고 차감 lock wait를 분리한다.
- 알림 queue age와 DLQ count를 본다.
- idempotency conflict count를 본다.
- 소진, 중복, 내부 오류를 다른 상태로 남긴다.
- 배포 중 old/new 응답 계약이 호환되는지 확인한다.
확인 질문
확인 질문
- 확인 질문: 요구사항에서 가장 먼저 구조로 바꿔야 하는 것은 무엇인가?
- 답변: 사용자가 성공으로 보는 결과와 지연되어도 되는 후속 작업을 분리하는 것이다.
- 확인 질문: 알림 발송을 queue로 분리하면 어떤 축이 바뀌는가?
- 답변: 사용자 응답 성능과 장애 격리는 좋아지지만, 발송 정합성은 지연되고 retry와 DLQ 운영 복잡도가 생긴다.
- 확인 질문: 구조도를 그리기 전에 요구사항 표가 필요한 이유는 무엇인가?
- 답변: 컴포넌트가 어떤 요구사항 때문에 필요한지, 없으면 무엇이 깨지는지 검증할 수 있기 때문이다.