이메일 인증 동시성 문제 해결
Redis Hash + 원자적 연산(HINCRBY)을 활용하여
동시 요청 환경에서도 인증 시도 횟수 정확성 보장
0. 개요
문제 상황
- 이메일 인증 코드 최대 5회 시도 제한 요구사항
- 동시에 여러 인증 요청이 들어올 수 있는 환경
- 기존 구조에서 Race Condition 발생
- 시도 횟수가 정확히 카운팅되지 않는 문제 발생
해결 방향
- Redis Hash 구조로 인증 정보 통합 관리
HINCRBY를 활용한 원자적 시도 횟수 증가- TTL 적용으로 자동 만료 및 메모리 관리
1. 문제 분석
AS-IS: 분리된 Key 구조
code:{email} → 인증 코드
attempt:{email} → 시도 횟수
인증 로직 흐름 (문제 발생)
- GET code
- 코드 비교
- GET attemptCount
- SET attemptCount + 1
❌ 문제점
- 두 Key 간 데이터 일관성 깨짐
- code는 존재하지만 attemptCount가 없는 경우 발생
- GET → SET 사이 Race Condition
- 동시에 요청 시 시도 횟수 누락
- 동시 요청이 많을수록 제한 횟수 초과 허용 가능성 증가
2. 해결 과정
Step 1: Redis Hash 구조 설계
verification:{type}:{email}
├─ code: 인증 코드
└─ attemptCount: 시도 횟수
(TTL: 5분)
- 인증 코드와 시도 횟수를 하나의 Key로 묶어 관리
- 데이터 정합성 문제 근본적 해결
Step 2: 인증 코드 저장
public void saveCode(String email, String code, VerificationType type) {
String key = buildKey(email, type);
Map<String, String> data = new HashMap<>();
data.put("code", code);
data.put("attemptCount", "0");
stringRedisTemplate.opsForHash().putAll(key, data);
stringRedisTemplate.expire(key, Duration.ofMinutes(5)); // TTL 5분
}
포인트
- Hash 구조로 인증 정보 통합
- TTL 적용으로 만료 자동 처리
Step 3: 인증 검증 + 시도 횟수 원자적 증가
public void verifyCode(String email, String inputCode, VerificationType type) {
String key = buildKey(email, type);
// 1. 코드 조회
String savedCode = (String) stringRedisTemplate.opsForHash().get(key, "code");
if (savedCode.equals(inputCode)) {
stringRedisTemplate.delete(key);
return; // 인증 성공
}
// 2. 시도 횟수 원자적 증가
Long newAttemptCount = stringRedisTemplate.opsForHash()
.increment(key, "attemptCount", 1);
if (newAttemptCount > 5) {
stringRedisTemplate.delete(key);
throw new InvalidCodeException("인증 시도 횟수 초과");
}
int remaining = 5 - newAttemptCount.intValue();
throw new InvalidCodeException(
"인증코드 불일치 (남은 시도: " + remaining + "회)"
);
}
핵심 포인트
- HINCRBY → 원자적 연산
- 동시 요청에서도 시도 횟수 정확히 증가
- 별도의 Lock 없이 동시성 문제 해결
3. TO-BE 아키텍처
[Client 요청]
↓
[VerificationService]
↓
[Redis Hash 조회]
├─ code 비교
└─ HINCRBY(attemptCount) ← 원자적 처리
↓
[성공 → Key 삭제]
[실패 → 남은 횟수 반환]
4. 핵심 개선 포인트
1️⃣ 데이터 일관성 확보
- 코드 / 시도 횟수를 하나의 Hash Key로 관리
- 부분 데이터 유실 문제 제거
2️⃣ 동시성 안전성 확보
- HINCRBY 사용으로 Race Condition 제거
- 동시 요청 상황에서도 정확한 횟수 제한 보장
3️⃣ Redis 메모리 효율 개선
- TTL 자동 만료 적용
- 불필요한 인증 데이터 잔존 방지
5. 성과
성과
- 동시 인증 요청 환경에서도 시도 횟수 정확성 100% 보장
- Race Condition 완전 제거
- 인증 실패/성공 흐름 단순화
- Redis Key 구조 간결화 및 유지보수성 향상
배운 점
- 동시성 문제는 로직이 아닌 자료구조 선택에서 해결 가능
- Redis 원자적 연산의 중요성 체감
- 분산 환경에서는 “읽고-수정”보다 “한 번에 처리”가 핵심