동시성 제어 구현(코드 위주)

김신영·2025년 7월 11일
post-thumbnail

1. 동시성 문제 개요

동시성 문제의 본질

결국 같은 자원에 여러 스레드(메서드)가 접근해서 데이터를 수정하려고 해서 생기는 일

이 동시성 문제를 해결하기 위해:

  • 트랜잭션 격리 수준이 나뉘고
  • 선정된 수준에 따라 트랜잭션을 격리하기 위한 방법들 중
  • Lock이라는 개념을 채택한 것

핵심 해결 과제: 데이터가 수정된 것이 확정되기 전에 또 다른 스레드가 데이터를 수정하지 못하도록 해야한다


2. 테스트 시나리오

2.1 테스트 환경

  • 재고: 1,000개
  • 동시 요청: 1,000개 스레드
  • 스레드 풀: CPU 코어 수 × 2 (I/O-bound 작업 특성 고려)

2.2 실패 테스트 (Lock 미적용)

결과: 동시성 문제로 재고 불일치 발생

2.3 스레드 풀 크기 설정

ExecutorService pool = Executors.newFixedThreadPool(
    Runtime.getRuntime().availableProcessors() * 2
);

이유:

  • 재고 차감은 I/O-bound 작업 (DB/Redis/네트워크)
  • 스레드가 자주 대기 상태에 빠짐
  • CPU-bound: 코어 수 + 1
  • I/O-bound: 코어 수 × 2~4

3. 구현한 Lock 방식들

3.1 Lettuce + Spin Lock + Lua Script (직접 구현)

  • 락 연장 기능 없음
  • 기본 타임아웃: 100ms
  • 락 획득 재시도 간격: 50ms
  • 최대 재시도 시간: 4초
  • 최대 시도 횟수: 80회

deadline과 retrial time을 둘 다 설정해 준 이유:
서로 보완하는 효과가 있음

deadline → 최대 총 대기 시간을 제어
: 시스템 응답성을 보장 예) 4초 이상 블록되지 않음.

MAX_RETRY_COUNT → 과도한 재시도를 방지
: Redis에 불필요한 요청 폭주를 막고, 로드나 대기 부하를 줄임

3.2 JPA 비관적 락

  • MySQL Exclusive 행락 사용

3.3 Redisson의 tryLock()

  • 락 유지 시간 100ms
  • lock 획득 최대 대기시간 4초
  • Watch-dog 없이 구현

4. Lettuce Lock 구현 상세

4.1 논리 흐름

┌─────────────────┐
│   Lock 요청      │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ setIfAbsent()   │ 
└────────┬────────┘
         │
    ┌────┴────┐
    │  성공?   │
    └────┬────┘
         │
    Yes  │  No
    ┌────┴────┐
    │         │
    ▼         ▼
┌───────┐ ┌────────┐
│ 작업   │ │재시도    │
│ 수행   │ │(50ms)  │
└───┬───┘ └────────┘
    │
    ▼
 ┌───────┐
 │ 락해제  │
 └───────┘

4.2 락 획득 핵심 로직

매개변수:

  • lockKey: 락 키 (예: "stock:lock:1")
  • value: 락의 식별자 (UUID-랜덤값)
  • duration: 락이 자동으로 해제될 시간 (100ms)

4.3 Spin Lock 구현

4.4 락 해제 - Lua Script


5. 시스템 최적화

5.1 락 타이밍 파라미터 계산

DEFAULT_LOCK_TIMEOUT = 100ms

DB_Processing_Time (10ms) + 
Network_Latency (3ms) + 
Buffer_Time (87ms) = 100ms

MAX_RETRY_DURATION = 4초

Active_Threads (24) × 
Avg_Processing_Time (15ms) × 
Safety_Factor (2.0) = 1440ms ≈ 4초

5.2 아키텍처 구조

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│StockService │ --> │ LockService  │ --> │LockRedis    │
│             │     │              │     │Repository   │
└─────────────┘     └──────────────┘     └─────────────┘

5.3 트랜잭션 분리 전략


6. Lettuce vs Redisson 비교

6.1 동작 방식 차이

Lettuce (Spin Lock - Polling 방식)

┌─────────┐  락 요청   ┌─────────┐
│Thread 1 │ ────────> │  Redis  │
└─────────┘           └─────────┘
     │                     │
     │   실패 시 Sleep     │
     │<─────────────────── 
                          
        재시도 (50ms 후)  
     │────────────────────>

Redisson (Pub/Sub + Polling 혼합)

┌─────────┐  락 요청   ┌─────────┐
│Thread 1 │ ────────> │  Redis  │
└─────────┘           └─────────┘
     │                     │
     │   구독 & 대기       │
     │<─────────────────── 
                          
         해제 알림      
     │<═══════════════════ 
                          
        즉시 재시도       
     │────────────────────>

6.2 Redisson 구현

  1. 비동기 락 처리
  2. 락 획득 실패 시 정교한 후속 처리
  3. Watch-dog 메커니즘 (lock() 사용 시)
  4. 멀티 스레드 환경 최적화

7. MySQL Lock vs Redis Lock

7.1 MySQL 비관적 락

7.2 비교표

구분MySQL LockRedis Lock
인프라별도 불필요Redis 필요
분산 환경제한적완벽 지원
성능DB 부하 증가메모리 기반 고속
확장성단일 DB 한계수평 확장 용이

8. 프로젝트 아키텍처와 Redis Lock 선택 이유

8.1 현재 아키텍처

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Domain A  │────>│ RestTemplate │────>│  Domain B   │
└─────────────┘     └──────────────┘     └─────────────┘
       │                                         │
       └──────────── Event System ───────────────┘

8.2 Redis Lock 선택 이유

  1. MSA 전환 고려

    • 도메인 간 완전 분리
    • RestTemplate & Event 통신
    • 향후 서비스 분리 시 Lock 메커니즘 유지
  2. 확장성

    • 분산 환경에서도 동일하게 동작
    • 다중 인스턴스 배포 지원
  3. 성능

    • 메모리 기반 빠른 응답
    • DB 부하 분산

9. 최종 구현: Redisson 채택 이유

  1. Pub/Sub 기반으로 효율적인 리소스 사용
  2. 정교한 예외 처리로 높은 안정성
  3. Watch-dog 등 추가 기능으로 확장 가능성
  4. 비동기 처리 지원으로 성능 최적화

10. 성공 테스트 결과


참고: 백오프 전략

백오프 전략이란 재시도 간격을 점점 늘려가는 방법

  • 첫 번째 실패 후: 100ms 대기
  • 두 번째 실패 후: 150ms 대기
  • 세 번째 실패 후: 225ms 대기

Redis나 네트워크 부하를 줄여 시스템 안정성 향상

0개의 댓글