1편에서는 MySQL의 조건부 UPDATE, 낙관적 락, 비관적 락으로 동시 요청을 다뤘다. 서버가 여러 파드로 늘어나도 이 방법들은 여전히 동작한다. 같은 MySQL을 사용한다면 어느 파드에서 온 요청이든 DB에서 같은 조건을 검사하고 잠글 수 있기 때문이다. 그렇다면 Redis에 분산 락을 따로 두는 이유는 무엇일까?
핵심은 작업을 한 번만 실행하려는 이유에 있다. 어떤 작업은 두 번 실행되면 데이터가 잘못되고, 어떤 작업은 데이터는 그대로지만 계산 비용만 늘어난다. Redis 잠금은 후자처럼 잠시 동안 중복 실행을 줄이고 싶을 때 선택하기 쉽다. 이 차이를 먼저 이해해야 잠금이 해결할 문제와 잠금만으로는 해결되지 않는 문제를 구분할 수 있다.
Redis 분산 락이 보장하려는 것
한 파드의 메모리 잠금은 다른 파드에 보이지 않는다. Redis를 공유하는 파드들이 같은 논리적 대상의 잠금 키를 사용하면, 한 파드가 잠금을 가진 동안 다른 파드는 작업 시작을 기다리거나 건너뛸 수 있다. 잠금 대상은 DB 행일 필요가 없다. 함께 실행하면 안 되는 요청들이 동일하게 만들 수 있는 값을 잠금 키로 쓰면 된다.
하지만 이 잠금에는 유효 시간이 있다. 작업자가 비정상 종료돼도 다음 작업자가 언젠가 실행할 수 있게 하기 위해서다. 따라서 Redis 분산 락이 뜻하는 것은 '이 작업이 영원히 한 번만 실행된다'가 아니라 잠금이 유효한 동안 다른 작업자의 시작을 제한한다는 것이다. Redis 공식 문서도 상호 배제의 범위를 잠금 유효 시간과 함께 설명한다.
어떤 작업에 Redis 잠금이 적합할까?
여러 파드가 같은 MySQL을 쓴다면 DB에서도 한 번에 한 작업만 실행되도록 만들 수 있다. 고유 키로 작업 ID를 선점하거나, 작업 상태 행을 조건부로 변경하거나, 이미 존재하는 행을 잠그는 식이다. 반드시 처리돼야 하는 작업에는 이런 영속적인 상태가 유리하다. 어느 작업이 시작됐고 완료됐는지, 장애 후 무엇을 재처리해야 하는지 남기기 때문이다.
반면 실행 여부를 오래 보존할 필요가 없는 짧은 작업이라면 매번 DB에 작업 실행 상태를 기록할 행을 만들고 완료 여부를 관리하는 일이 과할 수 있다. 중복 실행을 줄이기만 하면 되고, 드물게 중복 실행돼도 결과가 안전하다면 Redis에 만료되는 잠금을 두는 편이 단순하다. MySQL로도 별도의 작업 테이블에 작업 ID를 고유하게 기록해 실행을 선점할 수 있다. 따라서 Redis를 선택하는 이유는 MySQL에서 불가능해서가 아니라, 영속적인 작업 상태를 남기지 않고 중복 실행을 일시적으로 줄이기 위해서다.
이 판단은 작업을 두 번 실행했을 때의 결과로 확인할 수 있다.
| 중복 실행의 결과 | 필요한 설계 |
| 계산이나 조회 비용만 늘어난다. | Redis 잠금으로 중복을 줄일 수 있다. 드문 중복 실행은 허용한다. |
| 동일한 결과를 두 번 저장할 수 있지만, 중복이나 오래된 결과를 저장소에서 거부할 수 있다. | Redis 잠금에 더해 최종 저장 지점의 검증을 둔다. |
| 돈이 두 번 빠지거나 업무 상태가 잘못된다. | 잠금만 믿지 않고 DB 조건, 고유 제약과 멱등성으로 결과를 보호한다. |
| 실패한 작업을 반드시 찾아 재실행해야 한다. | 영속적인 작업 상태나 큐를 둔다. |
만료되면 실제로 어떤 일이 생길까?
잔액이 1억 원인 계좌에서 10만 원을 보내는 요청이 중복 전달됐다고 해보자. 두 작업자는 같은 송금 요청 ID로 Redis 잠금을 시도한다. 송금 요청 ID를 DB에서 고유하게 검사하는 장치는 없고, 두 작업자 모두 자신이 잠금을 얻었다는 사실만 믿고 송금을 처리한다고 가정한다.
| 순서 | 작업자 A | 작업자 B | 송금 건수 | 보내는 계좌 잔액 |
| 초기 | 0건 | 1억 원 | ||
| 1 | 잠금을 얻고 송금을 시작한다. | 0건 | 1억 원 | |
| 2 | DB에 반영하기 전 실행이 오래 멈춘다. 그 사이 잠금이 만료된다. | 0건 | 1억 원 | |
| 3 | 같은 송금 요청 ID의 새 잠금을 얻어 10만 원을 송금한다. | 1건 | 9,990만 원 | |
| 4 | 다시 실행돼 같은 요청의 10만 원을 송금한다. | 2건 | 9,980만 원 |
송금을 요청한 것은 한 번이었지만 10만 원이 두 번 빠졌다. 잠금을 해제할 때 소유자를 확인해도 A가 잠금 만료 뒤 송금하는 일은 막지 못한다. 같은 송금 요청 ID는 한 번만 처리한다는 조건을 DB에서 원자적으로 검사하고, 송금 기록과 잔액 변경을 같은 트랜잭션에 묶어야 한다. 오래된 작업자의 쓰기를 막는 fencing token도 검토할 수 있지만, 그것만으로 같은 송금 요청의 중복 처리가 해결되는 것은 아니다. Redis 공식 문서도 잠금 유효 시간과 fencing token의 필요성을 설명한다.
선택 기준을 사례에 적용해보자
예를 들어 계좌의 최근 1년 입출금 내역을 월별로 합산해 보여주는 화면이 있다고 해보자. 계산 결과를 캐시에 저장했는데 캐시가 만료된 직후 같은 계좌를 조회하는 요청이 여러 파드에 들어오면 각 파드가 동일한 내역을 읽고 합계를 다시 계산할 수 있다. 계좌 ID별 잠금으로 한 파드만 캐시를 채우고 나머지는 잠시 기다리게 하면 중복 조회와 계산을 줄일 수 있다. Redis의 캐시 예제도 이렇게 캐시가 만료됐을 때 여러 요청이 동시에 원본을 조회하는 문제를 다룬다.
여러 파드가 동시에 시작하는 예약 작업에도 잠금을 쓸 수 있다. 다만 예약 작업은 MySQL의 작업 상태 행으로도 선점할 수 있다. 실패한 실행을 추적하고 반드시 복구해야 한다면 DB나 큐가 더 적합하다. 한 번의 실행을 건너뛰어도 다음 실행에서 회복되고, 실행 이력을 별도로 남길 필요가 없다면 만료되는 잠금이 간단할 수 있다. 예약 작업용 잠금인 ShedLock도 Redis와 JDBC를 모두 지원하며, 잠금 시간이 끝난 작업의 중복 실행 가능성을 명시한다.
외부 결제나 정산 API 호출은 신중해야 한다. Redis 잠금으로 동시 호출을 줄일 수 있어도, 호출이 성공한 직후 작업자가 종료되면 결과를 기록하지 못할 수 있다. 이후 재시도에서 같은 호출이 다시 나갈 수 있으므로 요청 ID를 DB에 선점해 기록하고, 외부 API의 멱등성 키를 사용하는 방법을 함께 검토해야 한다. 여러 서비스가 서로 다른 DB를 쓰더라도 Redis 잠금은 같은 작업이 동시에 시작되는 것을 줄일 뿐, 두 DB의 변경을 함께 커밋하지는 않는다.
요청이 아주 많다는 이유만으로 분산 락을 고르는 것도 맞지 않다. 선착순 100명의 입장을 조절하는 일은 잠금보다 정원 관리, 속도 제한, 대기열의 문제다. Redis를 이 용도로 쓸 수는 있지만 Redis 속도 제한은 분산 락과 다른 패턴이다. 실제 혜택 지급 조건은 DB에서 다시 확인해야 한다.
정리
Redis 분산 락은 여러 프로세스의 중복 작업을 일시적으로 줄이는 방법이다. 같은 MySQL을 공유한다면 DB에서도 대부분의 작업을 중복되지 않게 처리할 수 있다. Redis를 선택할 이유는 작업 상태를 영속적으로 관리할 필요가 없고, 드문 중복 실행을 허용하거나 최종 결과에서 안전하게 걸러낼 수 있을 때 분명해진다.
선택 전에 두 가지를 확인해야 한다. 잠금이 만료돼 작업이 겹쳐도 업무 결과가 안전한가? 잠금을 얻지 못하거나 작업자가 죽었을 때 기다릴지, 건너뛸지, 재시도할지 정했는가? 두 질문에 답할 수 없다면 Redis 잠금만 추가하기보다 DB의 조건 검증, 영속적인 작업 상태와 재처리 방법을 먼저 설계해야 한다.
'Knowledge' 카테고리의 다른 글
| 트랜잭션 격리 수준 - 왜 MySQL은 Repeatable Read인가 (0) | 2026.10.15 |
|---|---|
| 분산 트랜잭션과 Saga (1) - 분산 트랜잭션은 왜 필요할까? (0) | 2026.10.14 |
| 동시성 제어 (1) - 낙관적 락과 비관적 락 (0) | 2026.10.13 |