1. 네임드 락(Named Lock)이란?
네임드 락(Named Lock)은 MySQL에서 제공하는 GET_LOCK() 및 RELEASE_LOCK() 함수를 사용하여 사용자가 지정한 임의의 문자열에 대해 잠금을 획득하는 기능이다. 기존의 레코드 락(X-Lock, S-Lock)이 특정 데이터 행을 잠근다면, 네임드 락은 "유저의 주문", "이벤트 응모"와 같이 특정 '행위'나 '논리적 리소스'를 잠글 수 있다는 점이 특징이다. 이는 데이터베이스 레벨에서 제공하는 락이기에 분산 환경에서도 효과적인 동시성 제어가 가능하다.
- 장점: 별도의 인프라(Redis 등) 없이 분산 환경에서 동시성 제어 가능
- 데이터베이스 레벨의 강력한 정합성 제공
- 특정 리소스 식별자(문자열) 기반의 유연한 관리
- 단점: 락 획득 및 해제를 명시적으로 관리해야 함
- 세션 관리에 신경을 써야 하며, 잘못 사용할 경우 커넥션 풀 고갈 위험이 있음
2. 네임드 락의 구현
2.1. Repository 구현
MySQL의 네이티브 쿼리를 직접 호출하여 락을 획득하고 해제하는 인터페이스를 정의한다.
public interface EventNamedLockRepository extends JpaRepository<EventWithLock, Long> {
// 락 획득: 성공 시 1, 타임아웃 0, 에러 시 NULL 반환
@Query(value = "SELECT GET_LOCK(:lockName, :timeoutSeconds)", nativeQuery = true)
Integer getLock(@Param("lockName") String lockName, @Param("timeoutSeconds") int timeoutSeconds);
// 락 해제: 성공 시 1, 실패 시 0 반환
@Query(value = "SELECT RELEASE_LOCK(:lockName)", nativeQuery = true)
Integer releaseLock(@Param("lockName") String lockName);
}
2.2. Facade 패턴을 이용한 역할 분리
낙관적 락과 마찬가지로, 네임드 락 역시 락을 획득하는 행위와 비즈니스 로직을 분리하기 위해 Facade 패턴을 활용한다.
@Component
@RequiredArgsConstructor
public class NamedLockEventFacade {
private final EventWithLockService eventWithLockService;
private final EventWithLockRepository eventWithLockRepository;
public void joinEvent(Long eventId, Long memberId) throws InterruptedException {
String lockName = String.format("event_%d", eventId);
try {
// 1. 락 획득 시도 (3초 타임아웃)
Integer lockResult = eventWithLockRepository.getLock(lockName, 3);
if (lockResult == null || lockResult <= 0) {
throw new RuntimeException("락 획득 실패");
}
// 2. 비즈니스 로직 실행
eventWithLockService.joinEventWithNamedLock(eventId, memberId);
} finally {
// 3. 반드시 락 해제 수행
eventWithLockRepository.releaseLock(lockName);
}
}
}
3. 직면한 문제: 락 해제와 커밋 시점의 불일치
위와 같이 구현한 뒤 100명의 동시 참가 테스트를 수행하면 예상과 달리 데이터가 꼬이는 현상이 발생한다. 원인은 트랜잭션 커밋과 네임드 락 해제의 타이밍 차이에 있다.
문제 발생 시나리오
- Thread 1: 네임드 락을 획득하고 데이터를 수정한다.
- Thread 1: 비즈니스 로직이 끝난 뒤 finally 블록에서 락을 해제한다.
- Thread 2: 해제된 락을 즉시 획득하여 작업을 시작한다.
- 문제: Thread 1의 트랜잭션은 아직 커밋되지 않은 상태이므로, Thread 2는 Thread 1의 변경 사항이 반영되지 않은 과거 데이터를 읽게 된다.
4. 해결 방법: 트랜잭션 분리 (REQUIRES_NEW)
문제를 해결하려면 트랜잭션이 완전히 커밋된 후에 락이 해제되도록 보장해야 한다. 이를 위해 서비스 로직에 Propagation.REQUIRES_NEW를 적용하여 독립적인 트랜잭션을 실행한다.
public class EventWithLockService {
@Transactional(propagation = Propagation.REQUIRES_NEW) // 새로운 트랜잭션 시작
public void joinEventWithNamedLock(Long eventId, Long memberId) {
// 비즈니스 로직 수행 및 즉시 커밋
}
}
이렇게 하면 joinEventWithNamedLock이 종료되는 시점에 즉시 트랜잭션이 커밋되고, 이후 Facade의 finally 구문에서 락이 해제된다. 따라서 다음 스레드는 항상 최신 데이터를 읽을 수 있게 된다.
5. 새로운 복병: 커넥션 풀(Connection Pool) 고갈
트랜잭션을 분리(REQUIRES_NEW)하자 테스트 중 Connection is not available 에러가 발생한다. 이는 하나의 요청이 2개의 커넥션을 점유하게 되었기 때문이다.
- 커넥션 1: 네임드 락을 유지하기 위한 커넥션 (Facade 범위)
- 커넥션 2: 실제 데이터를 수정하기 위한 새로운 트랜잭션 커넥션 (Service 범위)
최적화 및 해결 방안
커넥션 풀 고갈을 방지하기 위해 HikariCP 설정과 DB 설정을 조정해야 한다.
- 커넥션 풀 크기 확장: 필요한 커넥션 수를 계산하여 maximum-pool-size를 늘려준다.

- DB 설정 동기화: DB의 max_connections 값이 HikariCP 풀 크기보다 충분히 큰지 확인해야 한다.
- 풀 분리(Pool Separation): 네임드 락 전용 풀과 일반 비즈니스용 풀을 분리하면 안정성을 더욱 높일 수 있다.
'Database > MySQL' 카테고리의 다른 글
| [Index-8][Optimization] FullText Index와 n-gram 파서 (1) | 2026.01.20 |
|---|---|
| [Lock-10][Optimization] Lock: 실무 적용(⭐) - 트랜잭션 범위 축소와 이벤트 기반 관심사 분리 (0) | 2025.12.26 |
| [Lock-8][Optimization] 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock) 구현 및 비교 (0) | 2025.12.26 |
| [Lock-7][Optimization] 데드락(Deadlock)의 유령과 갱신 분실(Lost Update)의 함정 (0) | 2025.12.25 |
| [Lock-6][Optimization] 데이터 정합성을 보장하는 락(Lock)의 종류와 전략 (0) | 2025.12.25 |
