E-Commerce Service

여러 사용자가 동시에 하나의 물품을 구매하려고 할 때 발생할 수 있는 문제를 해결하기 위해 다음과 같은 요소들을 고려해야 한다.

고려해야 할 요소

  1. 동시성 제어: 여러 사용자가 동시에 같은 물품을 구매하려 할 때 발생할 수 있는 충돌을 관리해야 한다.
  2. 재고 관리: 실시간으로 정확한 재고 수량을 유지하고 업데이트해야 한다.
  3. 트랜잭션 일관성: 결제 과정과 재고 감소가 일관성 있게 처리되어야 한다.
  4. 사용자 경험: 구매 과정에서 사용자에게 명확한 피드백을 제공해야 한다.

핵심 영역

  1. 상품 관리 시스템
  • 상품 정보 관리 (이름, 가격, 재고, 카테고리, 상품 상태 등)
  • 재고 관리 시스템 (동시성 제어가 매우 중요)
  • 상품 검색 및 필터링 기능
  • 이미지 처리 및 저장
  1. 주문 처리 시스템 (매우 중요)
  • 주문 상태 관리 (결제대기, 결제완료, 배송준비, 배송중, 배송완료 등)
  • 장바구니 기능
  • 동시 주문 처리를 위한 동시성 제어
  • 재고 차감 로직
  • 주문 취소/환불 처리
  1. 결제 시스템
  • 결제 게이트웨이 연동
  • 결제 상태 관리
  • 결제 실패 처리
  • 환불 처리
  • 결제 보안 (매우 중요)
  1. 사용자 관리
  • 회원가입/로그인
  • 권한 관리
  • 개인정보 보호
  • 주소록 관리
  • 구매 이력 관리

구현 방법

데이터베이스 수준의 잠금 (Database-Level Locking)

낙관적 잠금 (Optimistic Locking)

낙관적 잠금은 대부분의 트랜잭션이 충돌하지 않는다는 가정하에 작동한다.

구현 예시:

1
2
3
UPDATE items
SET stock = stock - 1
WHERE item_id = ? AND stock = ?;

이 SQL 문은 재고 값이 변경되지 않은 경우에만 항목을 업데이트한다.

비관적 잠금 (Pessimistic Locking)

비관적 잠금은 트랜잭션 기간 동안 항목을 잠그어 다른 트랜잭션이 업데이트하지 못하게 한다.

구현 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
BEGIN;
SELECT stock
FROM items
WHERE item_id = ?
FOR UPDATE;
-- 재고 확인 및 가능한 경우 업데이트
UPDATE items
SET stock = stock - 1
WHERE item_id = ?;
COMMIT;

애플리케이션 수준의 처리

분산 잠금 (Distributed Locking)

분산 잠금을 사용하면 여러 서버에서 동시에 접근하는 경우에도 일관성을 유지할 수 있다.

구현 방법:

  1. 사용자가 구매를 시도할 때 해당 항목에 대한 분산 잠금을 획득한다.
  2. 잠금을 유지한 상태에서 구매 작업을 수행한다.
  3. 작업 완료 후 잠금을 해제한다.

상태 관리

물품의 상태를 세 가지로 관리할 수 있다: 사용 가능, 보류 중, 판매됨.

  • 사용자가 결제 페이지로 이동할 때 물품 상태를 “보류 중"으로 변경.
  • 일정 시간(예: 10분) 후에 결제가 완료되지 않으면 상태를 다시 “사용 가능"으로 변경.

실시간 재고 동기화

여러 판매 채널을 사용하는 경우, 중앙 집중식 재고 관리 시스템을 구축하여 실시간으로 재고를 동기화해야 한다.

실시간 인벤토리 동기화를 구현하는 방법
데이터 통합 및 실시간 업데이트
  1. 중앙 집중식 인벤토리 관리
    모든 판매 채널의 재고를 단일 플랫폼에서 관리한다.
    이를 통해 여러 채널에서의 재고 수준을 실시간으로 추적하고 동기화할 수 있다.
  2. 자동화된 동기화
    판매, 반품, 재입고 등 재고 변동이 발생할 때마다 자동으로 모든 연결된 채널에서 업데이트가 이루어진다.
    이는 과잉 판매와 재고 부족을 방지하는 데 도움이 된다.
기술적 구현 방법
  1. 웹훅 (Webhooks)
    재고 변경 시 즉각적인 알림을 통해 시스템 전체에 즉시 업데이트를 트리거한다.
  2. API 통합
    다양한 소프트웨어 구성 요소 간의 원활한 통신을 가능하게 하여 모든 접점에서 재고 데이터의 일관성을 보장한다.
  3. 데이터베이스 복제
    재고 데이터베이스의 여러 복사본을 생성하여 다양한 위치에서 빠른 액세스와 업데이트를 가능하게 한다.
고급 기능
  1. 맞춤형 알림 설정
    재고가 낮아질 때 알림을 받아 적시에 공급업체에 주문을 할 수 있도록 한다.
  2. 멀티팩 및 번들 추적
    키트를 구성하는 개별 아이템 수준에서 멀티팩과 번들을 추적한다.
  3. 다중 창고 관리
    여러 창고의 재고를 추적하고, 채널별로 창고 우선순위를 설정할 수 있다.
구현 시 고려사항
  1. 데이터 일관성: 모든 채널에서 재고 정보가 일치하도록 유지해야 한다.
  2. 네트워크 지연: 실시간 업데이트 시 발생할 수 있는 지연을 최소화해야 한다.
  3. 확장성: 비즈니스 성장에 따라 시스템이 확장될 수 있어야 한다.
  4. 오류 처리: 동기화 과정에서 발생할 수 있는 오류를 효과적으로 관리해야 한다.
  • 트랜잭션 관리
    데이터베이스의 ACID 속성을 활용하여 트랜잭션의 일관성을 유지한다.
    이를 통해 하나의 트랜잭션만 성공하고 나머지는 중단되도록 할 수 있다.

구현예제

사용된 주요 기술과 패턴들:

  1. 분산 락(Distributed Lock):
    Redis를 사용하여 분산 락을 구현.
    이는 여러 서버에서 동시에 같은 상품에 대한 구매 요청이 들어올 때도 안전하게 처리할 수 있게 해준다.
    Redis의 SETNX 명령어를 활용하여 락의 획득과 해제를 관리한다.
  2. 데이터베이스 트랜잭션:
    데이터베이스 수준에서 트랜잭션을 사용하여 재고 확인, 결제 처리, 주문 정보 저장이 모두 하나의 원자적 단위로 처리되도록 보장한다.
    만약 중간에 실패가 발생하면 모든 변경사항이 롤백된다.
  3. SELECT FOR UPDATE 재고를 확인하고 수정할 때 데이터베이스의 row-level 락을 사용.
    이를 통해 다른 트랜잭션이 동시에 같은 재고 정보를 수정하는 것을 방지한다.
  4. 멱등성 처리
    각 주문 요청에 고유한 idempotency_key를 할당하고, Redis를 사용하여 이를 관리한다.
    같은 키로 중복 요청이 들어와도 한 번만 처리되도록 보장한다.
  5. 예외 처리
    모든 주요 작업에 대해 적절한 예외 처리를 구현하여, 시스템이 안정적으로 동작하도록 보장한다.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from datetime import datetime, timedelta
import threading
from typing import Optional
from dataclasses import dataclass
import uuid
from enum import Enum
import redis
from sqlalchemy import create_engine, select
from sqlalchemy.orm import Session
from sqlalchemy.exc import IntegrityError

# 상품 상태를 나타내는 열거형
class OrderStatus(Enum):
    PENDING = "pending"
    PAYMENT_PROCESSING = "payment_processing"
    COMPLETED = "completed"
    FAILED = "failed"

@dataclass
class OrderRequest:
    product_id: int
    user_id: int
    quantity: int
    total_amount: float
    idempotency_key: str = str(uuid.uuid4())

class PurchaseSystem:
    def __init__(self):
        # Redis 연결 설정 (동시성 제어를 위한 분산 락 구현에 사용)
        self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
        
        # 데이터베이스 연결 설정
        self.engine = create_engine('postgresql://user:password@localhost/dbname')
        
        # 결제 시도 횟수 제한 설정
        self.max_retries = 3
        
    def acquire_lock(self, product_id: int, timeout: int = 10) -> bool:
        """분산 락 획득을 시도합니다."""
        lock_key = f"product_lock:{product_id}"
        return self.redis_client.set(
            lock_key,
            str(uuid.uuid4()),
            ex=timeout,
            nx=True
        )
    
    def release_lock(self, product_id: int) -> None:
        """분산 락을 해제합니다."""
        lock_key = f"product_lock:{product_id}"
        self.redis_client.delete(lock_key)
    
    def check_idempotency(self, idempotency_key: str) -> Optional[dict]:
        """요청의 멱등성을 확인합니다."""
        result = self.redis_client.get(f"idempotency:{idempotency_key}")
        return result.decode() if result else None
    
    def save_idempotency_result(self, idempotency_key: str, result: dict) -> None:
        """멱등성 결과를 저장합니다."""
        self.redis_client.setex(
            f"idempotency:{idempotency_key}",
            timedelta(hours=24),
            str(result)
        )
    
    def check_and_reserve_inventory(self, session: Session, product_id: int, quantity: int) -> bool:
        """재고를 확인하고 예약합니다."""
        # SELECT FOR UPDATE를 사용하여 재고 데이터 락
        stmt = select(Product).where(Product.id == product_id).with_for_update()
        product = session.execute(stmt).scalar_one()
        
        if product.inventory >= quantity:
            product.inventory -= quantity
            return True
        return False
    
    def process_payment(self, order_request: OrderRequest) -> bool:
        """결제를 처리합니다."""
        # 실제 결제 처리 로직을 구현합니다
        # 외부 결제 시스템과의 연동이 필요합니다
        return True
    
    def purchase(self, order_request: OrderRequest) -> dict:
        """상품 구매를 처리합니다."""
        # 멱등성 체크
        idempotency_result = self.check_idempotency(order_request.idempotency_key)
        if idempotency_result:
            return idempotency_result
        
        # 분산 락 획득 시도
        if not self.acquire_lock(order_request.product_id):
            return {"status": "error", "message": "다른 거래가 진행 중입니다"}
        
        try:
            with Session(self.engine) as session:
                # 트랜잭션 시작
                with session.begin():
                    # 재고 확인 및 예약
                    if not self.check_and_reserve_inventory(
                        session,
                        order_request.product_id,
                        order_request.quantity
                    ):
                        return {"status": "error", "message": "재고가 부족합니다"}
                    
                    # 결제 처리
                    if not self.process_payment(order_request):
                        # 결제 실패 시 롤백은 자동으로 수행됨
                        return {"status": "error", "message": "결제 처리에 실패했습니다"}
                    
                    # 주문 정보 저장
                    order = Order(
                        user_id=order_request.user_id,
                        product_id=order_request.product_id,
                        quantity=order_request.quantity,
                        total_amount=order_request.total_amount,
                        status=OrderStatus.COMPLETED
                    )
                    session.add(order)
                    
                    result = {
                        "status": "success",
                        "order_id": order.id,
                        "message": "구매가 완료되었습니다"
                    }
                    
                    # 멱등성 결과 저장
                    self.save_idempotency_result(order_request.idempotency_key, result)
                    return result
                    
        except Exception as e:
            return {"status": "error", "message": str(e)}
        
        finally:
            # 락 해제
            self.release_lock(order_request.product_id)

시스템을 실제로 운영할 때 고려해야 할 추가 사항들

  1. 모니터링과 로깅
  • 모든 주문 처리 과정을 로깅하여 문제 발생 시 추적할 수 있어야 합니다.
  • 시스템의 성능과 안정성을 모니터링해야 합니다.
  1. 성능 최적화
  • 캐시를 활용하여 자주 조회되는 상품 정보의 접근 속도를 개선할 수 있습니다.
  • 데이터베이스 인덱스를 적절히 설정하여 조회 성능을 향상시킬 수 있습니다.
  1. 확장성
  • 시스템이 성장함에 따라 수평적 확장이 가능하도록 설계해야 합니다.
  • 마이크로서비스 아키텍처의 도입을 고려할 수 있습니다.

참고 및 출처