System Design
마켓컬리를 설계해보자 - 1️⃣ 주문처리
마켓컬리를 설계한다면 어떻게 할까?
일반 커머스랑 다르게 마켓컬리는 특수한 특성이 있다.
1. 자정 주문 마감 → 다음날 새벽 7시 배송
2. 신선식품 → 재고 정확성 매우 중요
3. 자정에 수십만 명 동시 접속
이 특성을 고려하는게 중요!하다.
주문 마감 직전에 수십만 명이 몰린다면?
처음엔 두 가지 방식을 고민했다.
1. Redis + 동기
장점
→ 주문 성공/실패 즉각 응답
→ 구현 단순
→ Redis가 게이트키퍼로 DB 부하 제어
단점
→ Redis 장애 시 주문 자체가 불가
→ 언더셀링 가능성
2. Kafka + 아웃박스 + 비동기
장점
→ 트래픽 버퍼링으로 DB 부하 분산
→ 장애 복구 용이 (아웃박스 재시도)
단점
→ 주문 결과 즉각 응답 불가
→ 자정 마감이라 실패 알았을 때 재주문 불가 💀
→ 구현 복잡도 높음
마켓컬리는 신선식품 + 자정 마감 특성상 주문 성공/실패를 그 순간 바로 알아야 한다. 나중에 실패를 알면 재주문 자체가 불가능하기 때문이다.
그래서 Kafka 비동기가 아닌 Redis + 동기 처리를 선택했다.
만약 23:58에 주문했는데 비동기 응답이 늦어져서 00:00을 넘기면 유저는 다른 상품 주문을 못하고, 기대했던 신선상품 배송을 받지 못하게 된다. 그렇다고 동기 방식일 경우에도 네트워크 지연이나 DB 부하로 응답이 늦어져 00:00을 넘기면 같은 문제가 발생할 수 있다.
그럼에도 동기 방식을 선택한 이유는, 비동기보다 처리 단계가 적고 응답 지연 가능성이 더 낮기 때문이다. Kafka 큐잉 대기 없이 바로 처리하니까 edge case가 발생할 확률 자체가 줄어든다고 판단했다.
Redis가 게이트키퍼
주문할 때 가장 먼저 확인해야 하는 게 재고 여부이다.
Redis에 저장할 것
→ 재고 수량
DB에 저장할 것
→ 실제 주문 데이터 (상세 정보)
처음엔 Redis 메모리 부하를 걱정했는데, 주문 데이터 전체가 아니라 재고 숫자만 저장하는 거라 메모리 부담이 거의 없다.
이러면 수십만 명이 몰려도 DB까지 오는 요청이 재고 수만큼만 도달한다.
(신선식품 특성상 재고가 수천~수만 개 수준일 것이다!)
변수가 발생한다면?
DB에 주문 저장이 실패하여 Redis 재고 복구를 시도했는데, Redis 재고 복구가 실패하면 언더셀링이 발생한다.
오버셀링 (재고보다 많이 팔림)
→ 절대 안 됨 💀
→ 없는 상품을 배송해야 함
언더셀링 (재고 있는데 못 팖)
→ 일시적으로 감수 가능
→ 주기적 배치로 DB ↔ Redis 동기화로 복구 😎
커머스에서는 오버셀링은 절대 발생하면 안되므로, 오버셀링 보다는 언더셀링을 감수하는 게 맞다고 판단했다.
그치만 물론 언더셀링도 발생하면 안되므로! 이것도 최대한 막아버리자.
Redis를 견고하게
레디스가 재고 복구를 실패하는 대표적인 이유로 아래 4가지가 떠오른다. (물론 더 있을 수 있음)
1. Redis 프로세스 장애
2. 네트워크 순간 지연
3. 메모리 부족 (OOM)
4. 연결 풀 고갈
하나씩 막아보자!
1. Redis 프로세스 장애
Master-Slave 이중화로 해결 → Master 다운 시 Slave가 자동 승격 → Slave는 Master의 데이터를 실시간으로 복제하고 있어서, Master가 죽어도 Slave가 즉시 Master 역할을 이어받아 서비스 중단 없이 INCR 재시도 가능
1-1. Split Brain 발생 가능 (Master, Slave 둘 다 자신이 Master라고 판단)
Redis Sentinel이 Master 상태를 지속적으로 모니터링하다가, 통신 단절 감지 시 자동으로 Failover 처리해서 Split Brain 방지
(그럼에도 불안하다면 물리적으로 분리하는 멀티 AZ 방식으로,,,)
2. 네트워크 순간 지연
재시도 로직으로 해결 → 타임아웃은 일시적인 현상인 경우가 많아서, 잠깐 기다렸다가 재시도하면 대부분 성공함 → N회 전부 실패 시 알람 발송 및 후처리
3. 메모리 부족 (OOM)
메모리 모니터링 + 임계치 알람으로 사전 감지 → OOM은 갑자기 터지는 게 아니라 메모리가 서서히 차오르는 거라, N% 도달 시 알람을 걸어둬서 문제 발생 전에 대응 가능
4. 연결 풀 고갈
연결 풀 사이즈 조정으로 해결 → 연결 풀이 부족한 건 동시 요청 수 대비 풀 사이즈가 작아서인데, 트래픽 패턴을 분석해서 적절한 사이즈로 늘려두면 고갈 방지 가능
주문 처리 흐름
여기까지의 주문 처리 흐름을 적어보면 아래와 같다.
유저 주문
↓
Redis DECR (재고 차감)
0 이상 → 통과
0 미만 → 즉시 품절 반환
↓
DB 주문 저장
↓
성공 → 주문 완료 응답 😎
실패 → Redis INCR (재고 복구 시도)
복구 성공 → 주문 실패 응답
복구 실패 → 재시도 N회
그래도 실패 → Slave가 Master 승격 후 재시도 (1번 - Master-Slave)
최종 실패 → 언더셀링 감수
이러면 수십만 명이 몰려도 DB까지 오는 요청이 재고 수만큼만 도달한다.
(신선식품 특성상 재고가 수천~수만 개 수준일 것이다!)
꽤 길어질 것 같아서 여기는 주문만 작성하고, 다른 글에서 배송, 추천/검색, 정산을 다뤄볼 예정 !