Redis는 싱글 스레드인데, 왜 동시성 제어가 필요할까?
이 프로젝트는 Redis의 싱글 스레드 특성에도 불구하고 분산 환경에서 발생할 수 있는 동시성 문제를 실제로 검증하고, 해결 방법을 비교하는 데모입니다.
자세한 설명은 블로그를 참고해주세요: 레디스는 싱글스레드인데 왜 동시성 제어가 필요할까?
Redis가 싱글 스레드로 동작한다는 것은 각 명령이 원자적 으로 처리된다는 의미입니다. 하지만 애플리케이션 레벨에서 Read → Modify → Write 패턴을 사용하면, 여러 서버 인스턴스가 동시에 작동하는 분산 환경에서 Race Condition이 발생합니다.
이 프로젝트는:
- ❌ Before: RMW 패턴의 동시성 문제를 재현
- ✅ After: Redis 원자 연산을 사용한 올바른 해결 방법 제시
.
├── before/ # 동시성 문제가 있는 버전
│ ├── src/
│ │ └── main/java/com/example/before/
│ │ ├── config/
│ │ ├── controller/
│ │ └── service/
│ ├── Dockerfile
│ └── build.gradle
│
├── after/ # 원자 연산을 사용한 해결 버전
│ ├── src/
│ │ └── main/java/com/example/after/
│ │ ├── config/
│ │ ├── controller/
│ │ └── service/
│ ├── Dockerfile
│ └── build.gradle
│
├── test-scripts/ # 동시성 테스트 스크립트
│ ├── test-before.sh # Before 버전 테스트
│ └── test-after.sh # After 버전 테스트
│
└── docker-compose.yml
// 1️⃣ READ: Redis에서 현재 값 조회
Object currentObject = redisTemplate.opsForHash().get(key, "attemptCount");
int currentAttempts = (currentObject != null) ? Integer.parseInt(currentObject.toString()) : 0;
// ⏸️ 이 사이에 다른 서버가 끼어들 수 있음!
Thread.sleep(100); // Race Window 발생
// 2️⃣ MODIFY: 애플리케이션에서 계산
int nextAttempts = currentAttempts + 1;
// 3️⃣ WRITE: Redis에 저장
redisTemplate.opsForHash().put(key, "attemptCount", String.valueOf(nextAttempts));문제점:
- 3단계가 분리되어 있어 다른 요청이 중간에 끼어들 수 있음
- 서버 A와 B가 동시에
currentAttempts = 3을 읽으면 둘 다4로 저장 - Lost Update 발생: 12번 요청했는데 카운트가 7만 증가하는 등의 문제
// 1️⃣ Redis 내부에서 단일 명령으로 처리
Long currentAttempts = redisTemplate.opsForHash().increment(key, "attemptCount", 1);해결 원리:
HINCRBY명령은 Redis 내부에서 원자적 으로 실행- Redis 싱글 스레드 특성상 다른 명령이 끼어들 수 없음
- 분산 환경(여러 서버)에서도 정확한 카운팅 보장
- Docker & Docker Compose
- curl, jq (테스트 스크립트 실행 시)
# 전체 환경 시작 (Redis + Before 2대 + After 2대)
docker-compose up -d --build
# 상태 확인
docker-compose ps서비스 포트:
- Redis:
16379 - Before 서버 1:
28081 - Before 서버 2:
28082 - After 서버 1:
29081 - After 서버 2:
29082
chmod +x test-scripts/test-before.sh
./test-scripts/test-before.sh예상 결과:
총 요청 수: 12
Redis 카운트: 7
차단 여부: Failed
chmod +x test-scripts/test-after.sh
./test-scripts/test-after.sh예상 결과:
총 요청 수: 12
Redis 카운트: 12
차단 여부: Blocked
사용자가 이메일 인증 코드를 입력할 때:
- 최대 5회까지만 시도 가능
- 초과 시 보안을 위해 차단
- 2대의 서버 (Docker Compose로 분산 환경 시뮬레이션)
- 12번의 동시 요청 (각 서버에 6번씩)
- 잘못된 코드로 시도하여 카운트 증가 검증
- 정합성: Redis에 저장된
attemptCount가 실제 요청 수(12)와 일치하는가? - 보안:
MAX_ATTEMPTS(5)초과 시 정상적으로 차단되는가?