- Published on
- •👁️
락⭐
- Authors

- Name
- River
상세 설명
동시성 제어 매커니즘
- Locking (락 기반 제어, 락 메커니즘)
- Time-stamping (타임스탬프)
- Optimistic Concurrency Control (낙관적 제어, OCC)
락 메커니즘
- 다중 사용자 환경에서 데이터의 일관성과 무결성을 보호하기 위해 사용되는 동시성 제어 메커니즘
- 여러 프로세스나 스레드가 공유 자원에 접근할 때 발생할 수 있는 충돌을 방지하기 위해 사용
주요 락 메커니즘
- 접근 권한에 따른 분류
- 공유 락 (S-Lock)과 배타 락 (X-Lock)
- 적용 범위(Granularity)에 따른 분류
- 데이터베이스 락, 테이블 락, 페이지 락
- 레코드 락, 갭 락, 넥스트키 락
- 구현 방식에 따른 분류
- 비관적 락 (Pessimistic Locking)과 낙관적 락 (Optimistic Locking)
- 특수 락 메커니즘
- 2단계 락킹 (Two-Phase Locking)
- MVCC (Multi-Version Concurrency Control)
- 인텐션 락 (Intention Lock)
- 타임스탬프 기반 동시성 제어
- 분산 락 (Distributed Lock)
공유 락 (Shared Lock, S-Lock)
여러 트랜잭션이 동시에 데이터를 읽을 수 있도록 허용하지만, 데이터를 수정할 수는 없다
읽기 전용 작업에 사용되며,
SELECT쿼리 실행 시 획득여러 트랜잭션이 동시에 같은 데이터에 공유 락을 획득할 수 있다.
공유 락이 설정된 데이터에 대해서는 다른 트랜잭션이 배타 락을 획득할 수 없다.
MySQL에서는
SELECT ... LOCK IN SHARE MODE로 명시적으로 공유 락을 설정할 수 있다.전통적인 DBMS는
SELECT문에 공유 락을 자동으로 사용하지만, 오늘날 DBMS는 락을 사용하지 않고 MVCC를 사용하여 읽기 작업이 쓰기 작업을 차단하지 않도록 설계되어 있다.⇒ 오늘날 DBMS는
select쿼리에 공유 락을 쓰지 않고 MVCC를 사용한다.
배타 락 (Excluesive Lock, X-Lock)
- 특정 트랜잭션이 데이터를 수정할 수 있지만, 이 동안 다른 트랜잭션은 해당 데이터에 접근할 수 없다
- 데이터 변경 작업(
INSERT,UPDATE,DELETE)에 사용 - 배타 락이 설정된 데이터에는 다른 트랜잭션이 어떠한 락(공유/배타)도 획득할 수 없다.
- MySQL에서는
SELECT ... FOR UPDATE로 명시적으로 배타 락을 설정할 수 있다. - DBMS에서 쓰기 작업은 기본적으로 배타 락을 자동으로 설정한다.
사용 예시
- 은행 시스템에서 계좌 잔액을 조회할 때는 공유 락을 사용하여 여러 사용자가 동시에 조회할 수 있게 하지만, 잔액을 수정할 때는 배타 락을 사용하여 데이터의 일관성을 보장한다.
락 호환성
| 기존 락 / 요청 락 | 공유 락(S) | 배타 락(X) |
|---|---|---|
| 공유 락(S) | 가능 | 불가능 |
| 배타 락(X) | 불가능 | 불가능 |
- 즉, 여러 트랜잭션이 동시에 읽기만 가능하다.
- 하지만, 읽고 읽는 중이거나 쓰고 있는 중에 다른 트랜잭션이 쓰기 작업을 할 수 없다.
락 에스컬레이션 (Lock Escalation)
레코드 락과 같은 세밀한 락(row-level)에서 테이블 락처럼 더 넓은 범위의 락(table-level)으로 전환하는 과정
작은 범위의 세밀한 락이 많아지면 각각의 락을 유지, 관리하는 데 메모리와 CPU 리소스를 많이 사용한다.
다만, 테이블 락이 설정되면 해당 테이블의 모든 행이 잠기게 된다.
⇒ 많은 트랜잭션이 하나의 락을 기다리게 되어 전체 시스템 처리량이 감소한다.
락 에스컬레이션은 일반적으로 DBMS가 자동으로 수행한다.
- MySQL은 기본적으로 행 수준 락과 넥스트키 락을 사용하며, 락 에스컬레이션 보다는 인덱스를 통한 최적화를 활용한다.
- PostgreSQL 또한 행 수준 락을 사용하며 MVCC를 통해 락 경합을 최소화한다.
락 에스컬레이션의 trade-off는 락 에스컬레이션이 시스템 리소스를 절약하기 위해 사용되지만, 동시성을 저하시킬 수 있다는 점이다.
상세 설명
경쟁 상태 (race condition)
두 개 이상의 프로세스나 스레드가 동시에 공유 자원에 접근할 때 발생할 수 있는 문제로
실행 순서에 따라 결과가 달라지는 비결정적 상황을 의미한다.
int counter = 0; void thread1() { counter += 1; } void thread2() { counter += 1; }- 스레드가 동시에 실행되었을 때 경쟁 상태가 발생하면
counter = 1이 나올 수 있다.
- 스레드가 동시에 실행되었을 때 경쟁 상태가 발생하면
공유 자원에 대한 읽기-수정-쓰기(read-modify-write) 연산이 원자적으로 수행되지 않을 때 주로 발생
이를 해결하기 위해서는 주로 '상호 배제'가 보장되어야 한다.
공유 자원은 여러 프로세스나 스레드가 동시에 접근할 수 있는 데이터나 장치이다.
- 데이터베이스의 테이블, 레코드, 메모리의 특정 변수, 파일 등
상호 배제 (mutual exclusion)
- 한 번에 하나의 프로세스나 스레드만 공유 자원에 접근할 수 있도록 하는 것
- 상호 배제 구현 방법
락 (Lock)
lock.lock(); // 락 걸기 (자원 사용 시작) try { // 자원 사용 } finally { lock.unlock(); // 락 해제 }- 가장 기본적인 상호 배제 메커니즘
- 임계 영역에 진입하기 전에 락을 획득하고, 작업이 끝난 후 해제하여 하나의 자원에 대한 동시 접근을 막는다.
세마포어 (Semaphore)
Semaphore sem = new Semaphore(3); sem.acquire(); // 자원 사용 전 - 카운터 감소 // 자원 사용 sem.release(); // 자원 사용 후 - 카운터 증가- 정수 값을 기반으로 여러 스레드의 접근을 제어하며, 지정된 수만큼 동시 접근을 허용할 수 있다.
- 바이너리 세마포어(값이 0 또는 1인 세마포어)는 락과 유사하게 동작한다.
모니터 (Monitor)
synchronized(공유자원) { // 해당 자원을 사용하는 코드 }- 언어 수준에서 제공하는 동기화 구조
- Lock과 조건 변수를 함께 포함한 고수준 동기화 구조
- Java의 synchronized 블록이 이에 해당
원자성(Atomicity) 보장 방법
- 락 없이도 동시성 문제를 해결할 수 있는 대안적 기법
- 원자적 연산 (Atomic Operations)
- CPU 수준에서 지원하는 원자적 명령어(CAS, Compare-And-Swap 등)를 사용하여 락 없이도 안전한 연산이 가능하다.
- 트랜잭션 메모리 (Transactional Memory)
- 메모리 접근을 트랜잭션으로 관리하여 원자성을 보장한다.
- AtomicInteger 등의 사용은 Lock-Free 프로그래밍의 예시로, 성능 병목을 줄이는 데 유리하다
데드락(Deadlock) 방지
- 데드락이란, 여러 개의 프로세스 또는 스레드가 서로가 가진 자원을 기다리면서 영원히 작업을 끝내지 못하는 상태이다.
- Thread 1 : 자원 1 보유 ⇒ 자원 2 기다리는 중
- Thread 2 : 자원 2 보유 ⇒ 자원 1 기다리는 중
- 이 상태로 영원히 멈추는 것을 데드락(교착 상태)이라고 한다.
- 경쟁 상태를 해결하기 위해 락을 사용할 때는 데드락 발생 가능성을 고려해야 한다.
- 데드락이 발생하는 조건은 아래 4가지 조건이 모두 만족할 때 발생한다.
- 상호 배제 : 자원은 한 번에 하나의 프로세스(스레드)만 사용 가능
- 점유 대기 : 자원을 가진 채로 다른 자원을 기다린다.
- 비선점 : 자원을 강제로 뺏을 수 없음
- 환형 대기 : 서로가 서로의 자원을 기다림
- 락 획득 순서를 일관되게 유지하거나, 타임아웃을 설정하여 데드락을 방지할 수 있다.
언어 및 프레임워크 수준의 지원
- Java
synchronized,volatile,java.util.concurrent패키지(ReentrantLock, AtomicInteger 등)- Java에서는 'synchronized' 키워드를 사용하여 메서드나 블록을 동기화할 수 있다.
- 또한, 'ReentrantLock' 클래스를 사용하여 보다 세밀한 제어를 할 수도 있다.
- C++
std::mutex,std::atomic,std::lock_guard
- 데이터베이스
- 트랜잭션 격리 수준, 락(SELECT FOR UPDATE 등)
분산 시스템에서의 경쟁 상태 해결
- 경쟁 상태는 MSA 환경에서도 발생한다.
- 분산 락 (Distributed Lock)
- Redis, ZooKeeper 등을 사용하여 여러 서버 간 상호 배제를 구현하는 것
- 최종 일관성 (Eventual Consistency)
- 분산 환경에서 일시적인 데이터 불일치를 허용하되, 시간이 지나면 결국 일관된 상태로 수렴하도록 보장하는 일관성 모델
- CRDT (Conflict-free Replicated Data Type)
- 충돌 없이 분산 데이터를 병합할 수 있는 자료구조를 사용하는 것
- 경쟁 상태를 해결하려면 단순히 락만 사용하는 것에 그치지 않고, 원자적 연산, 언어 및 프레임워크 수준의 동기화 메커니즘, 그리고 분산 환경에서의 일관성 유지 전략까지 폭넓게 고려해야 한다.
상세 설명
데이터베이스 시스템에서 동시성 제어란?
- 데이터베이스 시스템에서 동시성 제어는 여러 트랜잭션이 동시에 데이터베이스에 접근할 때 데이터의 일관성과 무결성을 보장하기 위한 메커니즘이다.
- 동시성 제어 메커니즘은 크게 3가지 방식이 존재한다.
- 잠금 (Locking)
- 타임스탬프 (Time-stamping)
- 낙관적 동시성 제어(Optimistic Concurrency Control, OCC)
- 실무에서는 DBMS이 이러한 동시성 제어 메커니즘을 자동으로 처리하지만, 개발자는 트랜잭션 격리 수준을 설정하여 동시성 제어의 강도를 조절할 수 있다.
락 메커니즘 (Locking)
- 가장 일반적인 방법으로, 트랜잭션이 데이터에 접근할 때 잠금을 설정하여 다른 트랜잭션이 해당 데이터에 접근하지 못하게 하는 것
- 잠금(락 메커니즘)은 공유 락(읽기)과 배타 락(쓰기)으로 나뉜다.
타임스탬프 (Time-stamping)
- 각 트랜잭션에 타임스탬프를 부여하여 트랜잭션의 순서를 결정한다.
- 트랜잭션 실행 순서를 미리 결정하고, 충돌 여부를 타임스탬프 기반으로 판단한다.
- 트랜잭션이 데이터에 접근할 때 타임스탬프를 비교하여 일관성을 유지한다.
낙관적 동시성 제어 (Optimistic Concurrency Control, OCC)
- 트랜잭션이 데이터에 접근할 때 잠금을 사용하지 않고, 커밋 시점에 충돌을 검사하여 문제가 있을 경우 롤백한다.
- 트랜잭션 간 충돌이 거의 없는 시스템(예: 읽기 위주 시스템)에서 성능상 유리
특수 락 메커니즘
- 2단계 락킹 (Two-Phase Locking)
- MVCC (Multi-Version Concurrency Control)
- 인텐션 락 (Intention Lock)
- 타임스탬프 기반 동시성 제어
- 분산 락 (Distributed Lock)
2단계 락킹(2PL, Two-Phase Locking)
- 트랜잭션은 확장 단계와 수축 단계의 2단계를 거친다.
- 확장 단계에서는 락을 획득만 하고 해제하지 않으며, 수축 단계에서는 락을 해제만 하고 획득하지 않는다.
- 이는 직렬화 가능성(serializability)을 보장한다.
MVCC (Multi-Version Concurrency Control)
데이터의 각 버전을 트랜잭션마다 별도로 관리함으로써 서로 다른 버전을 볼 수 있어 읽기 트랜잭션이 쓰기 트랜잭션을 차단하지 않는다.
PostgreSQL, Oracle, MySQL의 InnoDB에서 사용된다.
(MySQL의 InnoDB는 MySQL에서 사용하는 저장 엔진 중 가장 널리 쓰이는 기본 엔진)
인텐션 락 (Intention Lock)
- 인덱스나 테이블처럼 계층적인 자원에 락을 적용할 때 상위 레벨에 락 충돌 없이 하위 레벨 락이 가능하도록 돕는 메타 정보용 락
- Ex. 특정 레코드에 배타 락을 걸려면 그 테이블에 먼저 Intention Exclusive Lock(IX Lock)을 요청해야 한다.
- 주로 InnoDB에서 사용되며, 락 충돌 여부를 빠르게 판단할 수 있도록 도와준다.
분산 락 (Distributed Lock)
- 여러 노드/서버에서 공유 자원에 동시에 접근하지 못하도록 제어하는 락
- 단일 메모리 공간이 아닌 분산 환경에서의 동기화 문제를 해결한다.
- 보통 Redis, ZooKeeper, etcd 등을 활용해서 구현
분산 데이터베이스 동시성 제어
- 2단계 커밋(2PC), 3단계 커밋(3PC) 프로토콜을 사용하여 여러 노드에 걸친 트랜잭션의 일관성과 원장성을 보장하기 위한 메커니즘
- 트랜잭션 전체 흐름을 조율하는 프로토콜
- 2PC : Coordinator가 참여자들 모두에게 커밋 여부를 확인 → 모두 OK면 커밋, 아니면 롤백
- 3PC : 2PC의 단점을 보완해서 중간 단계 추가 (네트워크 장애 대비)
상세 설명
- 낙관적 락과 비관적 락은 데이터베이스에서 동시성 제어를 위한 두 가지 접근 방식으로 구현 방식에 따른 분류한 것이다.
낙관적 락 (Optimistic Lock)
낙관적 락은 데이터 충돌이 드물다고 가정하고, 트랜잭션이 끝날 때까지 데이터에 대한 락을 걸지 않는다. 대신 트랜잭션이 커밋될 때 데이터가 변경되지 않았는지 확인한다.
예를 들어, 사용자가 데이터를 읽고 수정한 후 저장할 때, 저장 시점에 데이터의 버전이 변경되지 않았는지 확인하여 충돌을 감지한다.
- 구현 방식
일반적으로 버전 필드(version), 타임스탬프(timestamp), 해시값(hash) 등을 사용하여 구현
JPA 구현 예시 :
@Version어노테이션을 사용하여 엔티티의 버전 필드를 관리한다.@Entity public class Product { @Id @GeneratedValue private Long id; private String name; private int price; @Version private Long version; }
- 장점
- 락을 획득하지 않기 때문에 동시성이 높고, 교착 상태(Deadlock)가 발생하지 않는다.
- 단점점
- 충돌 발생 시 롤백과 재시도 로직이 필요하며, 충돌이 자주 발생하면 성능이 저하된다.
- 사용 시나리오
- 읽기 작업이 많고 충돌이 적은 환경, 웹 애플리케이션의 동시 편집, 분산 시스템 등에 적합하다
비관적 락 (Pessimistic Lock)
비관적 락은 데이터 충돌이 자주 발생한다고 가정하고, 데이터에 접근할 때마다 락을 걸어 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 한다.
- 구현 방식
데이터베이스의 락 메커니즘(shared lock, exclusive lock)을 직접 사용한다.
JPA 구현 예시 :
LockModeType을 사용하여 다양한 종류의 락을 적용할 수 있다.// 읽기 락(shared lock) Product product = entityManager.find(Product.class, id, LockModeType.PESSIMISTIC_READ); // 쓰기 락(exclusive lock) Product product = entityManager.find(Product.class, id, LockModeType.PESSIMISTIC_WRITE);
- 장점
- 트랜잭션이 시작될 때 데이터 일관성을 보장하기 때문에 충돌 처리 로직이 필요 없다.
- 단점
- 락이 오래 유지되면 동시성이 저하되고, 교착 상태가 발생할 수 있다.
- 사용 시나리오
- 충돌이 자주 발생하는 환경, 데이터 일관성이 매우 중요한 금융 시스템, 재고 관리 등에 적합하다
성능 비교
- 낙관적 락은 락을 사용하지 않아 시스템 자원 소모가 적지만, 충돌 시 롤백 비용이 발생한다.
- 비관적 락은 락을 유지하는 오버헤드가 있지만, 충돌이 자주 발생하는 경우 롤백 비용이 없어 유리할 수 있다.
상세 설명
팬텀 리드 (Phantom Read)
-- 트랜잭션 T1 시작
BEGIN;
SELECT * FROM users WHERE age BETWEEN 20 AND 30; -- (결과: 10건)
-- 이 시점에 다른 트랜잭션 T2가 아래 쿼리를 실행하고 COMMIT한다.
-- 트랜잭션 T2 : INSERT INTO users (name, age) VALUES( 'phantom', 25 );
-- T1에서 다시 조회
SELECT * FROM users WHERE age BETWEEN 20 AND 30;
-- (결과: 11건) 새롭게 추가된 'phantom'도 조회된다.
COMMIT;
- Phantom Read는 대표적인 데이터베이스의 3가지 동시성 문제 중 하나로, 트랜잭션 격리 수준과 관련되어 있다.
- Phantom Read는 동일 조건으로 두 번이상 쿼리를 실행했을 때, 첫 번째 결과에는 없던 유령(Phantom) 같은 행이 두 번째 결과에 나타나는 현상이다.
- 이는 다른 트랜잭션이 해당 조건에 만족하는 새로운 행을
INSERT하고 커밋했을 때 발생하며, REPEATABLE READ 격리 수준의 표준 정의에 따르면 허용되는 현상이다. - 하지만 MySQL의 InnoDB는 이 현상을 허용하지 않는다
MySQL InnoDB에서 갭 락과 넥스트키 락이란 무엇인가?
MySQL의 InnoDB 저장 엔진에서, **갭 락(Gap Lock)**과 **넥스트키 락(Next-Key Lock)**은 Phantom Read를 방지하기 위한 락 메커니즘이다.
MySQl InnoDB는 트랜잭션 격리 수준 중 Repeatable Read 격리 수준이지만 넥스트키 락을 사용하여 Serializable 처럼 Phantom Read도 방지할 수 있는 것이다.
핵심 원칙 : "잠금은 인덱스에 걸린다”
- InnoDB의 모든 잠금은 데이터 로우(Row)가 아닌, 인덱스 레코드(Index Record)에 적용된다.
- 쿼리가 실행될 때 MySQL 옵티마이저는 가장 효율적인 실행 계획을 위해 인덱스를 하나 선택
UPDATE,DELETE,SELECT ... FOR UPDATE등의 쿼리가 실행되면, InnoDB는 옵티마이저가 선택한 인덱스를 스캔하면서 지나가는 모든 곳에 잠금을 설정한다.- 즉, 잠금의 범위는
WHERE절 조건 자체가 아니라, 그 조건을 만족시키기 위해 스캔한 인덱스의 범위에 의해 결정- 스캔 시작 지점 탐색 (where 조건에 해당하는 위치)
- 해당 레코드에 대해 레코드 락, 해당 레코드 바로 앞의 레코드에 갭 락 적용
- 계속해서 뒤쪽으로 조건에 해당하는 레코드들을 레코드 락하고 앞 간격에 갭 락 적용된다.
- 마지막 해당 레코드의 경우 바로 다음 레코드에 갭 락을 적용하여 사이 공간을 막는다.
락의 종류
레코드 락 (Record Lock)
- 인덱스 레코드에 대한 락으로, 특정 Row에만 락을 설정한다.
- 락의 적용 범위는 사용되는 인덱스의 구조에 따라 달라진다.
- 적절한 인덱스 조건
- PK 또는 UK를 통해 단 하나의 레코드를 정확히 특정할 때 사용된다. (
WHERE id = 10) - 다른 레코드나 간격에는 영향을 주지 않아 가장 효율적입니다.
- PK 또는 UK를 통해 단 하나의 레코드를 정확히 특정할 때 사용된다. (
- 인덱스가 없거나 적절하지 않은 인덱스
- 옵티마이저는 조건을 만족하는 데이터를 찾기 위해 테이블의 모든 데이터를 처음부터 끝까지 다 뒤져봐야 합니다 (Full Table Scan)
- 아주 넓은 범위의 락이 설정되는 최악의 상황이 발생
갭 락 (Gap Lock)
인덱스 레코드 사이의 간격에 대한 락으로, 다른 트랜잭션이 해당 간격에 새로운 레코드가 삽입하는 것을 방지한다.
⇒ 즉, 조건에 따른 결과 레코드 사이의 부분을 잠금하여 이 간격 내에 새로운 레코드 삽입을 방지
넥스트키 락 (Next-Key Lock)
- 레코드 락과 갭 락을 결합한 형태(레코드 락 + 갭 락)로, InnoDB의 기본 락 방식
- 넥스트키 락은 인덱스 레코드에 대한 레코드 락과 그 이전 간격에 대한 갭 락의 조합
팬텀 리드 방지 작동 방식
위의 잠금들을 조합하여 InnoDB는 팬텀 리드를 차단한다.- 쿼리 실행 :
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;실행 - 인덱스 스캔 : 옵티마이저는
age컬럼의 인덱스를 스캔하기 시작 - 넥스트키 락 적용
age가 20 이상인 첫 번째 레코드부터 30 이하인 마지막 레코드까지 스캔하면서, 마주치는 모든 인덱스 레코드와 그 사이의 간격에 넥스트키 락을 설정- 예를 들어
age인덱스에 19, 22, 28, 32 값이 있다면, 쿼리는age=22와age=28레코드를 잠글 뿐만 아니라,(19, 22],(22, 28],(28, 32]구간의 간격까지 모두 잠근다.
- 삽입 차단
- 이 상태에서 다른 트랜잭션이
INSERT INTO users (..., age) VALUES (..., 25);를 시도하면,age=25는(22, 28]간격에 해당한다. 이 간격은 이미 첫 번째 트랜잭션의 갭 락에 의해 잠겨 있으므로,INSERT트랜잭션은 대기 상태에 빠진다.
- 이 상태에서 다른 트랜잭션이
- 정합성 보장
- 첫 번째 트랜잭션이 커밋되거나 롤백될 때까지
age20~30 범위에는 어떤 새로운 데이터도 들어올 수 없다. - 이로써 팬텀 리드가 원천적으로 방지된다.
- 첫 번째 트랜잭션이 커밋되거나 롤백될 때까지
실무적 관점
성능 영향
- 넥스트키 락은 높은 수준의 일관성을 제공하지만, 동시성을 저하시킬 수 있다.
- 데드락의 주된 원인 될 수 있다.
락 모드 조정
- MySQL 5.7부터
SELECT ... FOR SHARE와SELECT ... FOR UPDATE의 옵션으로NOWAIT또는SKIP LOCKED를 사용하여 락 대기 동작을 조정할 수 있다.
- MySQL 5.7부터
데드락은 왜 발생하는가?
- 데드락은 둘 이상의 트랜잭션이 서로가 점유한 리소스(잠금)를 기다리면서 무한 대기 상태에 빠지는 현상이다. InnoDB의 넥스트키 락 환경에서는 다음과 같은 상황에서 주로 발생한다.
- 상호 배타적인 잠금 경로
- 두 트랜잭션이 서로 다른 순서로 잠금을 요청할 때 발생합니다.
- 복수의 인덱스 동시 수정
UPSERT와 같은 작업은 UK(Unique Key)로 중복을 체크하고, PK로 데이터를 삽입하며, 다른 세컨더리 인덱스들도 동시에 수정한다. 이 과정에서 여러 인덱스에 동시다발적으로 잠금을 걸다가 다른 트랜잭션과 경로가 꼬이기 쉽다.
데드락 발생 시나리오 예시
두 스레드가 병렬로 UPSERT(INSERT ... ON DUPLICATE KEY UPDATE)와 DELETE를 실행한다고 가정해봅시다.트랜잭션 A (DELETE)
DELETE FROM holidays WHERE year = 2025 AND country = 'US';를 실행idx_country_year인덱스를 넓게 스캔하며 조건에 맞는 범위에넥스트키 락 을 설정
트랜잭션 B (UPSERT)
INSERT INTO holidays (date, name, country, year) VALUES ('2025-12-25', 'Christmas', 'US', 2025) ON DUPLICATE KEY UPDATE ...;를 실행- 먼저 중복 체크를 위해
uk_date_name(유니크 키)에 잠금을 시도 - 동시에, 삽입될 위치의 PK 인덱스와
idx_country_year인덱스에도 잠금이 필요
교착 상태
- 트랜잭션 A가
idx_country_year의 넓은 범위를 잠근 상태에서, 트랜잭션 B가uk_date_name을 잠그고idx_country_year의 특정 지점에 잠금을 걸려고 한다. - 이때, 트랜잭션 A가 트랜잭션 B가 잡고 있는
uk_date_name또는 PK와 관련된 다른 리소스를 필요로 하게 되면순환 대기 가 발생하여 InnoDB가 한쪽 트랜잭션을 롤백시키며 데드락 에러를 발생시킨다.
- 트랜잭션 A가
데드락 해결 및 예방을 위한 아키텍처 전략
데드락은 일시적인 현상이므로 재시도만으로 해결할 수도 있지만, 근본적인 발생 원인을 제거하는 것이 중요하다.데이터 정렬
- 가장 간단하고 효과적인 방법이지만, 완벽한 해결은 아니다.
- 대량의 데이터를
INSERT하거나UPDATE할 때, PK나 UK 같은 고유 키 기준으로 데이터를 미리 정렬한 후 처리하는 것 - 모든 트랜잭션이 항상 동일한 순서(ex. 오름차순)로 인덱스 레코드를 접근하게 되므로, 잠금 획득 순서가 꼬일 확률이 줄어든다.
작업 분리 (Separating Workloads)
데드락의 원인이 복잡한 작업들의 동시 실행에 있다면, 작업을 분리하는 것이 좋습니다.
- 1단계: 병렬 조회 및 준비: 외부 API 호출, 데이터 파싱 등 DB와 무관한 작업들은 병렬로 최대한 처리하여 메모리에 준비합니다.
- 2단계: 순차 쓰기: 모든 데이터가 준비되면, 단일 스레드, 단일 트랜잭션으로 DB에 쓰는 작업을 순차적으로 실행합니다. (예: 모든
UPSERT실행 후, 마지막에DELETE한 번 실행) - 효과: DB에 쓰기 작업을 하는 스레드가 오직 하나뿐이므로, 트랜잭션 간의 잠금 경합 자체가 사라져 데드락이 원천적으로 발생하지 않습니다.
- 최종 안전망: 재시도 로직 (@Retryable)
- 아키텍처를 완벽하게 구축했더라도, 네트워크 문제나 예상치 못한 데드락이 발생할 수 있다.
- 데드락은 재시도 시 성공할 가능성이 높은 대표적인 일시적 오류이므로, Spring의
@Retryable애노테이션 같은 메커니즘을 활용하여, 데드락이나 관련 예외 발생 시 자동으로 작업을 재시도하는 최종 안전망을 구축하는 것이 좋다.
JDBC 드라이버 최적화
- 순차 쓰기 전략의 성능을 극대화하기 위해, MySQL JDBC 연결 URL에
rewriteBatchedStatements=true옵션을 추가하는 것을 권장된다. - 이 옵션은 여러 개의
INSERT쿼리를 하나의 거대한 multi-valueINSERT쿼리로 재작성하여 DB에 전송함으로써, 네트워크 통신 오버헤드를 획기적으로 줄여줄 수 있다.