서버가 여러 대일 때 문제가 생긴다.
서버 1, 서버 2, 서버 3이 동시에 같은 데이터에 접근하면, 각 서버의 메모리는 공유가 안 되기 때문에 여러 서버가 같이 볼 수 있는 공간에 락을 걸어야 한다. 이것이 분산락이다.
방 입장 정원 초과
→ 방에 3명 있고 정원이 4명일 때
→ A, B가 동시에 입장 버튼 클릭
→ 둘 다 "3명이니까 입장 가능" 판단
→ 5명이 되어버림
게임 시작 중복
→ 방장이 start를 동시에 두 번 클릭
→ 게임이 두 번 시작될 수 있음
랭킹 업데이트
→ 여러 게임이 동시에 끝나면서
→ 같은 유저 점수를 동시에 업데이트
1. Redis SETNX ← 가장 많이 씀
2. Redisson ← Redis 기반 라이브러리
3. Zookeeper ← 분산 코디네이터
4. DB 락 ← DB를 락 저장소로 사용
이 글에서는 실무에서 가장 많이 쓰이는 SETNX와 Redisson을 비교한다.
SET if Not eXists - 키가 없을 때만 저장 성공, 키가 이미 있으면 저장 실패
이 특성을 이용해서 락을 구현한다.
public boolean acquireLock(String lockKey) {
Boolean acquired = redisTemplate.opsForValue()
.setIfAbsent(
lockKey, // 락 키
"locked", // 값
5, // TTL
TimeUnit.SECONDS // 5초 후 자동 해제
);
return Boolean.TRUE.equals(acquired);
}
public void releaseLock(String lockKey) {
redisTemplate.delete(lockKey);
}
public void joinRoom(String roomId, String playerId) {
String lockKey = "lock:room:" + roomId;
if (!acquireLock(lockKey)) {
throw new RuntimeException("잠시 후 다시 시도해주세요");
}
try {
int currentCount = roomService.getPlayerCount(roomId);
if (currentCount >= 4) {
throw new RuntimeException("방이 가득 찼습니다");
}
roomService.addPlayer(roomId, playerId);
} finally {
releaseLock(lockKey); // 무조건 락 해제
}
}
A, B 동시 입장 시도
A → lock:room:123 락 획득 성공
B → lock:room:123 락 획득 실패 → 대기 or 실패 반환
A 입장 처리 완료 → 락 해제
B → 락 획득 성공
B → 인원 체크 (이미 4명) → 입장 거부
TTL 없이 락을 걸면
A가 락 획득
A 서버 장애로 락 해제 못 함
→ lock:room:123 키가 영원히 남아있음
→ 아무도 방에 입장 못 하는 상황 발생
TTL 5초 설정하면
A가 락 획득
A 서버 장애
→ 5초 후 자동으로 락 해제
→ B가 입장 가능
기본 구현은 쉽지만, 예외 상황 처리가 생각보다 많다.
문제 1. 재시도 로직
// 락 획득 실패했을 때 재시도 로직 직접 구현해야 함
int retry = 0;
while (retry < 3) {
Thread.sleep(100);
if (acquireLock("lock:room:123")) break;
retry++;
}
문제 2. 내가 건 락을 내가 해제하는지 보장
// A가 락 걸고 처리 중
// A의 락 TTL 만료 → 자동 해제
// B가 락 획득
// A 처리 완료 → 락 해제 시도
// → B가 건 락을 A가 해제해버리는 문제 발생
// 해결: 락에 고유값 넣기
String lockValue = UUID.randomUUID().toString();
redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 5, TimeUnit.SECONDS);
문제 3. Lua 스크립트로 원자적 처리
-- 확인이랑 삭제를 원자적으로 처리해야 함
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
redisTemplate.execute(script,
Collections.singletonList(lockKey), lockValue);
결국 제대로 구현하려면 락 획득 → 고유값 식별 → 재시도 로직 → Lua 스크립트 원자적 해제 → 예외처리 까지 전부 직접 짜야 한다.
Redis를 Java에서 쉽게 쓸 수 있게 만든 라이브러리. Redis 클라이언트인데 분산락, 캐시, 큐 등을 편하게 쓸 수 있도록 기능을 다 만들어둔 것이다.
// RedisTemplate - Redis 명령어 직접 날리기
redisTemplate.opsForValue().set("key", "value");
// Redisson - Java 객체처럼 사용
RLock lock = redissonClient.getLock("lock:room:123");
RMap map = redissonClient.getMap("roomData");
RQueue queue = redissonClient.getQueue("gameQueue");
implementation 'org.redisson:redisson-spring-boot-starter:3.27.0'
RLock lock = redissonClient.getLock("lock:room:123");
try {
// 락 획득 시도 (최대 3초 대기, 5초 후 자동 해제)
if (lock.tryLock(3, 5, TimeUnit.SECONDS)) {
// 작업 처리
}
} finally {
lock.unlock();
// 재시도, 타이밍 관리 자동 처리
}
SETNX로 직접 구현할 때 신경써야 했던 재시도 로직, 고유값 처리, Lua 스크립트가 전부 내부적으로 처리된다.
| 기능 | 클래스 |
|---|---|
| 분산락 | RLock |
| 캐시 | RMap |
| 큐 | RQueue |
| pub/sub | RTopic |
| 순위 | RScoredSortedSet |
| 세마포어 | RSemaphore |
SETNX
장점
→ 외부 라이브러리 없음
→ Redis 명령어 직접 제어
→ 가볍고 단순한 구조
→ 내부 동작 완전히 파악 가능
단점
→ 재시도 로직 직접 구현
→ 고유값 + Lua 스크립트 직접 구현
→ 예외처리 직접 구현
→ 실수할 여지 많음
→ 구현 시간 오래 걸림
Redisson
장점
→ 재시도 로직 자동
→ 락 해제 안전하게 처리
→ 구현 시간 짧음
→ 검증된 라이브러리
→ 분산락 외 부가 기능 풍부
단점
→ 라이브러리 의존성 추가
→ 내부 동작 파악 어려움
→ 문제 생겼을 때 디버깅 어려움
→ 단순한 락에 비해 무거움
| SETNX | Redisson | |
|---|---|---|
| 구현 난이도 | 높음 | 낮음 |
| 안정성 | 직접 보장해야 함 | 라이브러리가 보장 |
| 재시도 로직 | 직접 구현 | 자동 |
| 의존성 | 없음 | 라이브러리 추가 |
| 디버깅 | 쉬움 | 어려움 |
| 부가 기능 | 없음 | 풍부 |
| 개발 속도 | 느림 | 빠름 |
SETNX가 나은 경우
→ 라이브러리 의존성을 최소화해야 할 때
→ 락 동작을 완전히 직접 제어하고 싶을 때
→ 단순한 락 하나만 필요할 때
Redisson이 나은 경우
→ 개발 일정이 빠듯할 때
→ 분산락 외 Redis 기능도 같이 쓸 때
→ 안정성이 중요할 때
→ 팀원이 Redis를 깊게 모를 때
나중에 SETNX로 전환하고 싶다면, 락 코드를 서비스 로직과 분리해서 만들어두면 된다.
// LockService만 바꾸면 나머지 코드는 그대로
public class LockService {
public boolean acquireLock(String lockKey) { ... } // 여기만 수정
public void releaseLock(String lockKey) { ... } // 여기만 수정
}
함수 두 개만 바꾸면 되기 때문에 하루면 전환 가능하다.
SETNX는 제어권, Redisson은 편의성
일정이 빠듯하다면 Redisson으로 빠르게 구현하고,
이후 시간이 생기면 SETNX로 전환 도전해보는 것을 추천한다.
정리를 너무 잘해두셨네요!!!