[Spring] 분산 환경 동시성 제어 — Redisson 락 병목과 MySQL Named Lock 전환기
암호화폐 거래소 다중 서버 환경에서 1분 단위 차트 배치를 처리할 때 Redisson 분산 락에서 겪은 커넥션 풀 병목 현상을 분석하고, MySQL Named Lock(GET_LOCK/RELEASE_LOCK) 및 REQUIRES_NEW 트랜잭션 격리로 전환해 해결한 실무 경험을 정리합니다.
1. 다중 서버 환경에서의 병행 제어(Concurrency Control)
실무에서 동시성과 데이터 정합성 관리는 매우 중요하며, 특히 금융·가상자산 거래소 도메인에서는 작은 중복 처리나 Race Condition도 큰 이슈로 이어집니다.

당시 제가 담당하던 거래소 차트 시스템은 여러 대의 서버 인스턴스가 1분 간격 스케줄러로 동작하며 BTC, ETH 등 코인 페어별 시세 데이터를 DB에 갱신하고 WebSocket으로 실시간 전달하는 구조였습니다.
문제는 여러 서버가 동일한 시점의 1분 차트 배치를 동시에 중복 실행해서는 안 된다는 점이었습니다.
- 자바의
synchronized나Atomic타입은 단일 JVM 프로세스 내부의 스레드 간 경쟁만 막아줄 뿐, 서로 다른 서버 인스턴스 간의 중복 실행은 제어할 수 없습니다. - 따라서 여러 서버가 공통으로 참조할 수 있는 외부 저장소 기반의 **분산 락(Distributed Lock)**이 반드시 필요했습니다.
2. 기존 방식: Redisson 분산 락과 발생한 병목 문제
기존에는 Redisson 클라이언트를 이용한 Redis 분산 락으로 배타 제어를 수행하고 있었습니다.

원하는 Key로 락을 획득하면 일정 시간 또는 작업이 끝날 때까지 Redis에 락 키가 유지되어, 다른 서버 인스턴스가 중복 작업을 수행하지 않도록 막아줍니다.

왜 Redisson 락을 교체하게 되었을까?
당시 전체 아키텍처에서는 수많은 원격 호출 등으로 인해 Redis 서버 자체의 부하가 매우 높은 상태였고, Redisson 분산 락에서도 두 가지 문제가 겹쳐 발생했습니다.
- Redis 트랜잭션과 커넥션 점유: Redis의 분산 락 제어(
WATCH등)가 진행되는 동안 커넥션을 점유하며 부하를 가중시킵니다. - 커넥션 풀 경합으로 인한 락 획득 실패: 다른 대량 트래픽 작업들과 한정된 Redis 커넥션 풀을 공유하다 보니, 정작 차트 스케줄러가 락을 획득하기 위한 커넥션 할당 자체가 실패하는 현상이 빈번했습니다.

차트 서버가 4대 이상인 상황에서 한정된 connectionPoolSize를 두고 다른 작업과 경합이 벌어지자 락 획득 실패율이 높아졌고, Redis 의존도와 부하를 낮추기 위해 다른 분산 락 대안을 찾게 되었습니다.
3. 대안 적용: MySQL Named Lock 전환
마침 메인 DB로 MySQL 5.7 이상 버전을 사용하고 있었고, 우아한형제들 기술블로그(MySQL Named Lock을 활용한 분산 락)의 사례를 참고해 추가 인프라 비용 없이 MySQL Named Lock으로 전환하기로 결정했습니다.



Native Query 기반 ChartLockRepository 구현
JPA는 MySQL의 Named Lock 함수를 기본 문법으로 제공하지 않으므로 Native Query로 직접 호출하도록 구현했습니다.

사용한 MySQL 5.7+ 내장 락 함수는 다음 3가지입니다. (MySQL Locking Functions 공식 문서)
SELECT IS_USED_LOCK(:key): 해당 이름의 락이 현재 점유 중인지 확인SELECT GET_LOCK(:key, :timeout): 지정된 키 이름으로 락 획득 시도 (대기 타임아웃 초 단위 지정)SELECT RELEASE_LOCK(:key): 작업 완료 후 락 명시적 해제
적용 결과 및 트랜잭션 격리 (REQUIRES_NEW)
락 상태 확인 → 락 획득 → 비즈니스 트랜잭션 수행 → finally 블록에서 락 해제 흐름으로 전환한 뒤에는 더 이상 락 획득 실패 장애가 발생하지 않았으며, Redis 서버의 부하도 눈에 띄게 줄일 수 있었습니다.
또한 배치 작업 중 특정 코인 페어의 처리가 실패하더라도 다른 정상 코인 차트 작업까지 함께 롤백되지 않도록, 개별 처리 메서드의 트랜잭션 전파 옵션을 **@Transactional(propagation = Propagation.REQUIRES_NEW)**로 분리해 장애 전파를 차단했습니다.
4. 트러블슈팅 리마인드 (Named Lock의 Timeout 오해)
당시 작업을 진행하며 GPT와 함께 점검했던 락 동작 특성 및 주의사항 기록입니다.



💡 회고: "GET_LOCK에 1분(60초)을 넘겼는데 왜 작업 시간이 1분을 초과해도 락이 풀리지 않았을까?"
당시 가장 궁금했던 부분인데, **MySQLGET_LOCK(str, timeout)의timeout은 Redis의 TTL(유지 시간)이 아니라 "락을 획득하기 위해 기다리는 대기 타임아웃(Wait Timeout)"**입니다!
MySQL Named Lock은 한 번 획득하면RELEASE_LOCK()을 호출하거나 해당 DB 커넥션 세션이 끊어질 때까지 시간 제한 없이 계속 유지됩니다. 또한 락을 쥐고 있는 동안 DB 커넥션을 점유하므로, 실무에서는 Named Lock 전용 별도 DataSource(커넥션 풀)를 일반 비즈니스 DataSource와 분리해 커넥션 고갈(Deadlock)을 예방하는 것이 안전합니다.
[Spring] PRG 패턴에서 redirect:/가 동작하지 않는 이유 (@ResponseStatus 함정)
NEXT LOG →[Spring] Redisson Remote Service 내부 동작 원리와 한계 분석