재고가 틀리면 고객이 떠난다: 이벤트 드리븐 재고 관리 시스템 설계하기

커머스

재고 관리이벤트 드리븐CQRS분산 시스템이커머스

이 글은 누구를 위한 것인가

  • 온라인과 오프라인 재고가 자주 맞지 않아 고객 불만을 겪는 운영자
  • 주문 가능으로 표시됐는데 실제로 품절이어서 취소해야 했던 경험이 있는 팀
  • 분산된 판매 채널(자사몰, 스마트스토어, 쿠팡 등)의 재고를 통합 관리하고 싶은 담당자

들어가며

"주문 완료됐다고 해서 기다렸더니 품절이라고 취소됐어요."

이커머스 불만 후기에서 가장 자주 보이는 말 중 하나다. 재고 불일치는 단순한 기술 문제가 아니다. 고객의 신뢰를 잃는 일이고, 취소 처리, 환불, 대체 상품 제안까지 운영팀의 시간을 대거 잡아먹는다.

문제는 생각보다 흔하다는 것이다. 멀티채널로 판매하는 커머스라면 거의 반드시 경험한다. 자사몰에서 주문이 들어왔는데 그 직전 스마트스토어에서 같은 재고가 팔렸거나, 창고에서 실사 결과 시스템 재고와 실물 재고가 다르다거나.

이 문제의 근본 원인과 현대적인 해결 방법을 살펴보자.


1. 재고 불일치가 생기는 이유

이유 1: 단순 차감 방식의 한계

가장 기본적인 재고 관리는 이렇게 생겼다.

주문 들어오면 → DB에서 재고 숫자 -1 → 끝

이 방식은 트래픽이 적을 때는 문제없지만, 동시에 여러 주문이 들어오면 문제가 생긴다.

재고: 1개

사용자 A: 재고 확인 (1개 있음) → 주문 처리 중...
사용자 B: 재고 확인 (1개 있음) → 주문 처리 중...

사용자 A: 재고 -1 = 0개 → 주문 완료
사용자 B: 재고 -1 = -1개 → ??? 재고 마이너스 발생

이 상황을 **레이스 컨디션(Race Condition)**이라고 한다.

이유 2: 멀티채널 동기화 지연

자사몰, 스마트스토어, 쿠팡, 오프라인 매장에서 같은 상품을 팔 때, 각 채널의 재고를 동기화하는 데 시간이 걸린다. 이 1~5초의 지연 사이에 같은 재고가 두 채널에서 동시에 팔릴 수 있다.

이유 3: 시스템 간 오류 전파

주문이 완료됐는데 재고 차감 API 호출이 실패하면? 또는 재고는 차감했는데 주문이 실패하면? 두 시스템 간의 불일치가 생긴다.


2. 이벤트 드리븐 방식으로 전환하기

**이벤트 드리븐 아키텍처(EDA)**는 모든 상태 변화를 "이벤트"로 기록하는 방식이다. 재고가 줄어든 것, 주문이 들어온 것, 반품이 처리된 것 — 모든 사건을 빠짐없이 순서대로 기록한다.

[기존 방식]
주문 완료 → DB 재고 -1 → 끝 (중간에 실패하면 불일치 발생)

[이벤트 드리븐 방식]
주문 완료 → 이벤트 발행: {type: "ORDER_PLACED", productId: 123, qty: 1}
            이벤트 큐에 순서대로 쌓임
            재고 서비스가 이벤트 읽어서 처리 → DB 재고 -1
            실패해도 이벤트는 남아있음 → 재처리 가능

비유하자면, 기존 방식은 기억에 의존하는 구두 보고이고, 이벤트 드리븐은 모든 것을 장부에 기록하는 방식이다. 장부가 있으면 언제든 현재 상태를 재계산할 수 있다.


3. CQRS — 읽기와 쓰기를 분리하는 패턴

**CQRS(Command Query Responsibility Segregation)**는 명령(Command, 데이터 변경)과 조회(Query, 데이터 읽기)를 분리하는 패턴이다.

왜 분리할까? 재고 시스템에서 "재고를 차감한다"와 "재고를 확인한다"는 매우 다른 요구사항을 가진다.

구분재고 차감 (Command)재고 조회 (Query)
빈도상대적으로 적음매우 높음 (상품 페이지마다)
정확도100% 정확해야 함약간의 지연 허용 가능
속도안전이 우선빠른 응답 필요
[CQRS 재고 구조]

쓰기 경로 (Command):
주문 → Command Handler → 이벤트 저장 → DB (정확성 우선)

읽기 경로 (Query):
상품 페이지 → Query Handler → 읽기 전용 DB (속도 우선)
                               (이벤트 기반으로 비동기 업데이트)

상품 상세 페이지에서 재고를 보여줄 때는 "수십 밀리초 전 재고"를 보여줘도 괜찮다. 하지만 실제로 주문을 확정할 때는 정확한 재고를 실시간으로 확인해야 한다. CQRS는 이 두 요구사항을 각각 최적화할 수 있게 해준다.


4. Saga 패턴 — 분산 환경의 트랜잭션 처리

온라인 주문 하나가 완료되려면 여러 시스템이 함께 작동해야 한다.

주문 완료 프로세스:
1. 재고 차감 (재고 서비스)
2. 결제 처리 (결제 서비스)
3. 배송 준비 (물류 서비스)
4. 주문 확인 이메일 발송 (알림 서비스)

문제는 2단계에서 결제가 실패하면 어떻게 되는가다. 이미 1단계에서 재고를 차감했는데, 이걸 어떻게 돌릴까?

Saga 패턴은 이 문제의 해답이다. 각 단계가 성공하면 다음 단계로, 실패하면 이전 단계를 되돌리는 보상 트랜잭션을 자동으로 실행한다.

[정상 흐름]
재고 차감 → 결제 처리 → 배송 준비 → 이메일 발송

[결제 실패 시 보상 흐름]
재고 차감 → 결제 실패 → 재고 복원 (보상) → 주문 취소 안내

마치 체스에서 "무르기"처럼, 실패했을 때 이전 상태로 되돌리는 방법을 미리 정의해두는 것이다.


5. 온·오프라인 통합 재고 관리

멀티채널 판매에서 가장 어려운 부분이 온라인과 오프라인 재고의 실시간 동기화다.

[통합 재고 구조]

중앙 재고 서비스
     │
     ├── 자사몰 (즉시 반영)
     ├── 스마트스토어 API (최대 1분 지연)
     ├── 쿠팡 로켓 API (최대 30분 지연)
     └── 오프라인 POS → 이벤트 발행 → 중앙 재고 업데이트

각 채널은 업데이트 속도가 다르다. 이런 상황에서 현실적인 전략은 채널별 안전 재고 버퍼를 설정하는 것이다.

예를 들어, 실제 재고가 100개라면:

  • 자사몰: 100개 표시 (가장 빠르게 동기화 가능)
  • 스마트스토어: 90개 표시 (10개 버퍼)
  • 쿠팡: 80개 표시 (20개 버퍼)

이렇게 하면 동기화 지연으로 인한 초과 주문을 예방할 수 있다.


6. 실시간 재고 대시보드 구성

재고 관리의 마지막은 가시성이다. 담당자가 언제든 재고 현황을 한눈에 볼 수 있어야 한다.

필수 지표

지표설명알림 조건
실재고창고에 실제 있는 수량-
가용 재고실재고 - 예약 주문 수량0 이하 시 알림
채널별 재고각 판매 채널에 표시 중인 수량불일치 시 알림
재고 회전율평균 판매 속도 대비 재고과잉/부족 예측
안전 재고 임박설정 수량 이하로 줄어들 때재발주 알림

7. 국내 커머스 재고 실패 사례와 교훈

사례 1: 명절 선물 세트 초과 판매 A 식품 쇼핑몰에서 명절 선물 세트 500개를 준비했는데, 설 연휴 직전 자사몰과 스마트스토어에서 동시에 주문이 폭주해 700건이 접수됐다. 200건을 취소해야 했고, 환불 처리와 고객 응대에 3일을 소비했다. 재고 동기화 주기를 10분에서 실시간으로 바꾸고, 채널별 안전 버퍼를 설정한 후 해결됐다.

사례 2: 오프라인 POS 장애로 재고 불일치 B 의류 브랜드에서 오프라인 매장 POS 시스템이 5시간 오프라인 상태가 됐다. 그 사이 온라인에서는 재고가 있다고 표시됐고 주문이 들어왔지만, 실제로 매장에서 팔린 물건이었다. 이후 POS 오프라인 시에는 해당 매장의 재고를 가용 재고에서 자동으로 제외하는 로직을 추가했다.


맺으며

재고 관리는 커머스의 물류 기반이다. 화려한 UI나 개인화보다 기본 중의 기본이지만, 가장 고객 만족에 직결되는 부분이기도 하다.

이벤트 드리븐, CQRS, Saga 같은 개념들이 처음에는 복잡하게 들릴 수 있다. 하지만 핵심은 단순하다. 모든 재고 변화를 빠짐없이 기록하고, 실패했을 때 자동으로 되돌릴 수 있는 구조를 만드는 것이다.

처음부터 완벽한 시스템을 만들 필요는 없다. 지금 가장 자주 생기는 불일치 케이스 하나를 찾아서, 그것부터 해결하는 것이 시작이다.