Info-Tech

[실전 예제로 보는 Race Condition] 중복 보상 지급, 어떻게 막을까? 본문

프로그래밍/파이썬 & 장고

[실전 예제로 보는 Race Condition] 중복 보상 지급, 어떻게 막을까?

개발 로그를 쌓고 싶은 블로거 2025. 3. 17. 14:54

Race Condition이란?

  • Race Condition(경쟁 상태)은 둘 이상의 프로세스나 스레드가 동시에 공통 자원에 접근할 때, 실행 순서에 따라 결과가 달라지는 상황입니다.
  • 쉽게 말해, 누가 먼저 처리하느냐의 '경쟁' 상태에서 발생하는 버그로, 특히 데이터 무결성이 중요한 서비스에서 치명적일 수 있습니다.

REST API에서 Race Condition?

  • REST API에서도 동시 요청이 들어와 동일 리소스를 처리할 때, 순서가 꼬이면 의도치 않은 결과가 발생할 수 있습니다.
  • 예) 중복 결제, 포인트 중복 지급, 중복 예약 등이 대표적인 사례입니다.

실전 예시 – 미션 완료 후 보상 지급

상황 설명

사용자가 미션 완료 후 보상 지급 요청을 보내는 API가 있습니다.
정상적으로는 1회만 지급돼야 하지만, 동시 요청이 발생하면 어떻게 될까요?

  • API Endpoint (예시용):
    POST /api/v1/missions/complete
  • Request Body:
    {"type":"investment_complete"}

서버 처리 흐름 (예시 코드)

1. API View

@router.post('/missions/complete', auth=AuthBearer())
def complete_mission(request, request_data: MissionCompleteRequest):
    user = user_repo.get_by_id(request.user_id)
    mission_service.complete(user, request_data.type)
    return {"result": "success"}

2. 비즈니스 로직

def complete(self, user: User, mission_type: str):
    mission = self.get_mission_by_type(mission_type)
    
    if not mission:
        raise ValueError("미션 정보 없음")

    if self.is_already_completed(user.id, mission_type):
        raise ConflictError("이미 완료된 미션입니다.")

    with transaction.atomic():
        # 완료 기록 저장
        self.log_repository.add_completion(user.id, mission.id)

        # 보상 지급
        if mission.reward_type == "point":
            user.add_points(mission.reward_amount)
        elif mission.reward_type == "credit":
            self.credit_service.grant(user.id, mission.reward_amount)
        else:
            raise ValueError("알 수 없는 리워드 타입")

정상 요청 흐름

  • 단일 요청 시 정상적으로 보상 지급 후 완료 처리됩니다.
[Client]  → POST /missions/complete  → [Server] 200 OK
  • 같은 요청을 다시 보내면, 이미 완료된 상태이므로 오류 반환:
[Client]  → POST /missions/complete  → [Server] 409 Conflict ("이미 완료된 미션입니다.")

비정상 요청 흐름 (Race Condition 발생)

  • 악의적 사용자 또는 실수로 동시에 여러 요청을 보내면?

시나리오

[Client]  → 30개의 동시 요청 발생
   ↓
[Server]  → 중복 요청 모두 처리됨
   ↓
보상 30회 지급 😱

테스트 스크립트 (예시)

간단한 Python 스크립트를 통해 동시 요청 테스트를 할 수 있습니다.

from concurrent.futures import ThreadPoolExecutor
import requests

def send_request():
    headers = {
        "Authorization": "Bearer <token>",
        "Content-Type": "application/json"
    }
    payload = {"type": "investment_complete"}
    response = requests.post("http://localhost:8000/api/v1/missions/complete", json=payload, headers=headers)
    print(f"Response: {response.status_code}")

if __name__ == "__main__":
    with ThreadPoolExecutor(max_workers=30) as executor:
        for _ in range(30):
            executor.submit(send_request)

결과

  • 요청 30회 중 모두 200 OK 응답, 보상 30회 지급 → Race Condition 발생.

Race Condition 방지법

1. Redis Lock 사용

  • 분산 락 메커니즘을 통해 하나의 요청만 처리하도록 제어합니다.
import redis
import redis_lock

redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)

def complete(self, user: User, mission_type: str):
    lock = redis_lock.Lock(redis_client, f"lock:mission:{user.id}:{mission_type}")
    
    with lock:
        if self.is_already_completed(user.id, mission_type):
            raise ConflictError("이미 완료됨")
        ...

2. 데이터베이스 락 (select_for_update)

  • DB 수준에서 레코드 락을 걸어 동시 처리 방지.
from django.db import transaction

def complete(self, user: User, mission_type: str):
    with transaction.atomic():
        user = User.objects.select_for_update().get(id=user.id)
        
        if self.is_already_completed(user.id, mission_type):
            raise ConflictError("이미 완료됨")
        ...

3. 단순 트랜잭션 (불충분할 수 있음)

  • 트랜잭션만 사용할 경우, 완전한 Race Condition 방지 어렵습니다.
from django.db import transaction

def complete(self, user: User, mission_type: str):
    with transaction.atomic():
        if self.is_already_completed(user.id, mission_type):
            raise ConflictError("이미 완료됨")
        ...

마무리

  • 실제 서비스에서는 Race Condition으로 인해 데이터 오류, 과금 문제 등이 발생할 수 있습니다.
  • 특히 포인트/현금과 관련된 로직에서는 반드시 동시성 처리를 고려해야 합니다.
  • Redis 락, DB 락 등 상황에 맞는 방식을 선택해 안정적인 서비스 운영을 해봅시다!

💡 참고

  • 테스트 도구로는 Locust, JMeter, 간단한 Python 멀티스레드 활용 가능.
  • Redis 락 구현 시, 락 타임아웃 설정도 함께 고려하면 더 안전합니다.

🎯 정리 포인트

  • Race Condition은 실제 서비스에서 빈번히 발생
  • 특히 보상, 결제, 예약한정 리소스 처리에서 중요
  • 상황에 맞는 락 메커니즘으로 미리 방지 → 나중에 고통 줄이기!
Comments