주문 생성과 결제 실패를 어떻게 설계할 것인가

·2026년 1월 8일
post-thumbnail

1. 문제 상황 — 주문이 사라지는 시스템

이커머스에서 주문은 단순 데이터가 아니다.
주문은 다음 모든 흐름의 출발점이다.

주문 → 결제 → 참여 수량 집계 → 목표 달성 → 정산 → 판매자 수익

즉 주문은 비즈니스의 기준 데이터다.

실제 문제

초기 구조에서는 주문 생성 과정에서 결제 모듈을 동기 호출(openfeign)했다.

주문 생성 → 포인트 차감 → 성공 시 주문 저장

문제는 여기서 발생했다.

포인트 모듈 장애 발생 → 트랜잭션 롤백 → 주문 자체가 저장되지 않음

이 구조에서는 다음 문제가 생긴다.

  • 주문 시도 이력 유실
  • CS 대응 불가
  • 실패 원인 분석 불가
  • 결제 재시도 불가

핵심 요구사항 정의

팀 논의를 통해 주문 도메인의 핵심 요구사항을 다음과 같이 정의했다.

"결제가 실패하더라도 주문 의도는 반드시 기록되어야 한다."

이 요구사항을 만족시키는 구조가 필요했다.

2. 설계 대안 비교

아키텍처 논의 과정에서 3가지 구조를 비교했다.

방식 1 — 주문 모듈이 결제까지 처리 (동기 구조)

프론트 → 주문 → 결제

장점

  • 단순함
  • 구현 쉬움

치명적 단점

  • 결제 장애 → 주문 생성 실패
  • 주문 이력 유실

→ 장애 전파 구조


방식 2 — 프론트가 흐름 제어

1. 프론트 → 주문 생성
2. 프론트 → 결제 호출

장점

  • 주문은 항상 저장됨

문제

  • 프론트가 비즈니스 흐름 알게 됨
  • 상태 동기화 책임 불명확
  • 중복 요청 제어 어려움

프론트가 오케스트레이터가 되는 구조


방식 3 — 이벤트 기반 구조 (최종 선택)

1. 프론트 → 주문 생성 → 저장(PENDING)
2. 프론트 → 결제 수행 → 이벤트 발행(카프카)
3. 카프카 → 주문 상태 업데이트

이 구조는 다음 특성을 가진다:

  • 주문은 항상 저장됨
  • 결제 실패해도 주문 남음
  • 장애 격리 가능
  • 결제 수단 확장 가능

3. 왜 이벤트 기반 구조를 선택했는가

초기에는 단순 동기 구조도 후보였다.
하지만 장애 시나리오를 기준으로 검토하면서 문제가 명확해졌다.

동기 구조 문제

결제 서버 다운
→ 주문 생성 실패
→ 주문 기록 없음

반면 이벤트 구조는

결제 서버 다운
→ 주문 생성 성공
→ 상태만 PENDING 유지

즉 차이는 이것이다.

구조장애 시 주문
동기사라짐
이벤트남음

주문을 "트랜잭션 결과"가 아니라 "고객 의도 기록"으로 정의하면 정답은 명확했다.

4. Saga 패턴을 사용하지 않은 이유

Saga도 검토했지만 선택하지 않았다.

이유는 도메인 특성 때문이다.

Saga는 기본적으로

  • 실패 시 이전 상태로 되돌리는 패턴

하지만 주문 도메인은 다르다.

주문은 되돌릴 대상이 아니라 남겨야 할 기록이다.

또한 결제 방식이 다양했다.

  • 포인트
  • 외부 PG

Saga는 선형 트랜잭션 흐름에 적합하지만
이 구조는 분기 흐름이 많았다.

그래서 Saga 대신 이벤트 기반 상태 전이 모델을 선택했다.

주문 생성 (PENDING)
    ↓
결제 요청
    ↓
[PAYMENT_CHANGED 이벤트]
    ↓
주문 상태 업데이트 (COMPLETED / FAILED)

5. 최종 아키텍처

이벤트 흐름

sequenceDiagram
    participant F as Client
    participant O as Order
    participant P as Payment
    participant K as Kafka

    F->>O: 주문 생성
    O->>F: orderId 반환 (PENDING)

    F->>P: 결제 요청
    P->>K: PAYMENT_CHANGED
    K->>O: 이벤트 전달
    O->>O: 상태 업데이트

    loop 폴링 (주기적 상태 확인)
        F->>O: GET /orders/{id}
        O->>F: status 응답
    end

주문 상태 흐름

PENDING → PAYMENT_PROCESSING → COMPLETED
                             ↘ FAILED

6. 이 설계를 통해 배운 점

이번 설계를 통해 깨달은 가장 중요한 사실은 이것이다.

좋은 아키텍처는 기술이 아니라 도메인에서 나온다.

이벤트 기반 구조를 선택한 이유도
기술 트렌드 때문이 아니라

"주문은 기록이어야 한다" 는 도메인 정의 때문이었다.


7. 결론

최종적으로 선택한 구조는

이벤트 기반 주문 상태 전이 아키텍처

이 구조는 다음 요구사항을 만족한다.

  • 주문 이력 보존
  • 장애 격리
  • 확장성 확보
  • 결제 수단 추가 가능

즉 이 설계는 단순 기능 구현이 아니라

장애 상황에서도 비즈니스 데이터 정합성을 유지하는 구조 설계 경험이었다.


8. 구현 참고

주요 Kafka 토픽

  • ORDER_CREATED: 주문 생성 이벤트
  • PAYMENT_CHANGED: 결제 상태 변경 이벤트
  • ORDER_CONFIRMED: 주문 확정 이벤트
  • ORDER_CANCELED: 주문 취소 이벤트

코드 위치

  • Commerce 서비스: /commerce/src/main/java/store/_0982/commerce/
  • 주문 도메인: domain/order/Order.java
  • 주문 이벤트 리스너: application/order/OrderEventListener.java
  • Kafka 이벤트: /common/src/main/java/store/_0982/common/kafka/event/

0개의 댓글