트랜잭션이 안전하게 동작하려면 동시에 실행되는 다른 트랜잭션으로부터 데이터를 보호하는 장치가 필요합니다. MySQL은 이를 두 계층으로 구현합니다.
- MySQL 엔진 락: MySQL 서버 계층에서 제공하는 락. 스토리지 엔진과 무관하게 동작합니다.
- InnoDB 스토리지 엔진 락: InnoDB 내부에서 행(row) 단위로 동작하는 락입니다.
이번 포스트에서는 두 계층의 락 구조와, 락 경합이 발생했을 때 어떤 일이 생기는지를 살펴봅니다.
1. MySQL 엔진 락 (서버 레벨)
MySQL 엔진 락은 InnoDB가 기본 엔진이 된 이후로는 실무에서 직접 마주칠 일이 많지 않습니다. 그러나 MySQL 락 체계의 전체 그림을 이해하는 데 필요한 배경 지식입니다.
글로벌 락 (Global Lock)
MySQL 서버 전체에 걸리는 가장 범위가 넓은 락입니다. 글로벌 락이 걸리면 다른 세션은 DDL과 DML을 모두 실행할 수 없습니다. 주로 전체 데이터베이스를 백업할 때 사용합니다.
FLUSH TABLES WITH READ LOCK;
다만 InnoDB를 사용하는 경우, mysqldump에 --single-transaction 옵션을 지정하면 글로벌 락 없이도 일관된 스냅샷으로 백업할 수 있습니다. MVCC를 활용해 백업 시작 시점의 스냅샷을 읽기 때문입니다.
테이블 락 (Table Lock)
특정 테이블 전체에 락을 거는 방식입니다. 명시적으로 걸 수도 있고, 일부 DDL 작업에서 묵시적으로 걸리기도 합니다.
LOCK TABLES employees READ; -- 읽기 락: 다른 세션의 쓰기 차단
LOCK TABLES employees WRITE; -- 쓰기 락: 다른 세션의 읽기·쓰기 모두 차단
MyISAM은 행 레벨 락을 지원하지 않기 때문에 테이블 락이 동시성 제어의 전부였습니다. InnoDB에서는 트랜잭션과 행 레벨 락이 있기 때문에, 명시적으로 테이블 락을 걸어야 하는 경우는 거의 없습니다.
메타데이터 락 (Metadata Lock, MDL)
MDL은 테이블의 구조(스키마)를 보호하는 락입니다. 명시적으로 걸 수 없고, 테이블에 접근할 때 자동으로 걸립니다.
SELECT,INSERT,UPDATE,DELETE실행 시 → MDL 읽기 락 획득ALTER TABLE,DROP TABLE실행 시 → MDL 쓰기 락 획득
MDL 읽기 락은 여러 세션이 동시에 보유할 수 있지만, MDL 쓰기 락은 배타적입니다. 따라서 장시간 실행 중인 트랜잭션이 있는 상태에서 ALTER TABLE을 실행하면, DDL이 MDL 쓰기 락을 대기하며 블로킹됩니다. 이 상태는 SHOW PROCESSLIST에서 Waiting for table metadata lock 메시지로 확인할 수 있습니다.
MDL은 트랜잭션이 종료될 때까지 유지됩니다. 커밋이나 롤백 없이 연결만 열어 둔 세션이 있으면 DDL 전체가 멈출 수 있으니 주의해야 합니다.

2. InnoDB 스토리지 엔진 락 (행 레벨)
InnoDB는 테이블 전체를 잠그는 대신, 실제로 접근하는 행에만 락을 걸어 동시성을 높입니다.
행 레벨 락을 살펴보기 전에 먼저 알아야 할 개념이 있습니다. InnoDB의 모든 행 레벨 락은 공유 락(S-Lock)과 배타 락(X-Lock) 중 하나로 동작합니다.
공유 락과 배타 락 (S-Lock / X-Lock)
공유 락(S-Lock)은 읽기 목적의 락입니다. FOR SHARE로 획득하며, 여러 트랜잭션이 동시에 보유할 수 있습니다.
배타 락(X-Lock)은 쓰기 목적의 락입니다. FOR UPDATE로 획득하며, 다른 트랜잭션의 S-Lock과 X-Lock 모두를 차단합니다.
SELECT * FROM employees WHERE id = 1 FOR SHARE; -- S-Lock 획득
SELECT * FROM employees WHERE id = 1 FOR UPDATE; -- X-Lock 획득
| S-Lock 요청 | X-Lock 요청 | |
| S-Lock 보유 중 | 허용 | 차단 |
| X-Lock 보유 중 | 차단 | 차단 |
S-Lock끼리는 공존할 수 있지만, X-Lock은 어떤 락과도 공존할 수 없습니다. UPDATE, DELETE, INSERT는 명시적 지정 없이도 항상 X-Lock을 획득합니다.
레코드 락 (Record Lock)
인덱스의 특정 레코드 하나에 거는 락입니다. InnoDB의 레코드 락은 실제 행이 아니라 인덱스 레코드에 걸립니다.
순수한 레코드 락은 = 조건 + PK 또는 유니크 인덱스 + 해당 레코드가 존재할 때만 걸립니다.
-- id가 PK일 때: id=1 레코드에만 X-Lock
SELECT * FROM employees WHERE id = 1 FOR UPDATE;
이 세 조건 중 하나라도 벗어나면 갭 락이 함께 걸리는 넥스트 키 락으로 동작합니다.

갭 락 (Gap Lock)
갭 락은 레코드 자체가 아니라 레코드와 레코드 사이의 빈 구간에 거는 락입니다. 다른 트랜잭션이 그 구간에 새로운 행을 INSERT하는 것을 막습니다.
id가 10, 20, 30인 레코드가 있을 때, id=20 레코드를 넥스트 키 락으로 잠그면 (10, 20] 구간이 잠깁니다. 이 상태에서 다른 트랜잭션이 id = 15를 INSERT하려 하면 갭 락에 의해 블로킹됩니다. 갭 락은 이렇게 팬텀 리드를 방지하는 역할을 합니다.
갭 락은 단독으로 걸리는 경우보다, 아래의 넥스트 키 락의 일부로 함께 걸리는 경우가 대부분입니다.

넥스트 키 락 (Next-Key Lock)
넥스트 키 락은 레코드 락 + 갭 락을 결합한 형태로, InnoDB가 범위 스캔 시 기본적으로 사용하는 락 방식입니다. 스캔한 레코드 자체와 그 레코드 앞의 갭을 함께 잠가, 다른 트랜잭션이 그 사이에 끼어드는 것을 원천 차단합니다.
id가 10, 20, 30인 레코드가 있을 때, id > 10 조건으로 조회하면 다음 구간에 락이 걸립니다.
SELECT * FROM employees WHERE id > 10 FOR UPDATE;
(10, 20] ← id=20 레코드 + 10~20 사이 갭
(20, 30] ← id=30 레코드 + 20~30 사이 갭
(30, +∞) ← 30 이후의 갭 (갭 락만, 레코드 없음)
오른쪽이 닫힌 구간 ]인 이유는 해당 레코드 자체를 포함하기 때문입니다. 마지막 구간 (30, +∞)처럼 레코드가 없는 경우에는 갭 락만 적용됩니다.
어떤 락이 걸리는지는 연산자가 아니라 실제로 스캔하는 인덱스 범위가 결정합니다.
| 조건 | 인덱스 종류 | 걸리는 락 |
= |
PK / 유니크 인덱스, 레코드 존재 | 레코드 락만 |
= |
일반 인덱스, 또는 레코드 없음 | 넥스트 키 락 |
>, <, BETWEEN 등 |
모든 인덱스 | 넥스트 키 락 (범위 전체) |
| 인덱스 없는 컬럼 | — | 전체 레코드에 넥스트 키 락 |
인덱스 설계가 락 범위에 직접 영향을 주는 이유가 바로 여기에 있습니다.
MySQL의 기본 격리 수준인 REPEATABLE READ에서 팬텀 리드를 방지할 수 있는 이유도 이 넥스트 키 락 덕분입니다. 다음 포스트에서 격리 수준과 연결해서 자세히 설명합니다.

자동 증가 락 (Auto-Increment Lock)
AUTO_INCREMENT 컬럼을 가진 테이블에 INSERT할 때 자동으로 걸리는 특수한 락입니다. 여러 트랜잭션이 동시에 INSERT해도 AUTO_INCREMENT 값이 중복 없이 채번되도록 보장합니다.
일반 행 레벨 락과 달리 INSERT가 완료되는 즉시 해제됩니다. 트랜잭션이 롤백되더라도 이미 채번된 값은 반환되지 않으므로, AUTO_INCREMENT 값이 중간에 건너뛰는 현상은 정상입니다.
3. 락 대기와 데드락
락 대기 (Lock Wait)
한 트랜잭션이 다른 트랜잭션이 점유 중인 락을 기다리는 상태입니다. innodb_lock_wait_timeout (기본값: 50초) 이내에 락을 획득하지 못하면 대기 중인 트랜잭션은 자동으로 롤백됩니다.
현재 락 대기 상황은 다음 쿼리로 확인할 수 있습니다:
-- 어떤 트랜잭션이 어떤 락을 기다리고 있는지
SELECT * FROM performance_schema.data_lock_waits\G
-- 현재 보유 중인 락과 대기 중인 락 전체 목록
SELECT * FROM performance_schema.data_locks\G
데드락 (Deadlock)
두 트랜잭션이 서로가 점유한 락을 기다리는 순환 대기 상태입니다.
| 순서 | 트랜잭션 A | 트랜잭션 B |
| 1 | id=1 락 획득 | id=2 락 획득 |
| 2 | id=2 락 요청 → 대기 | id=1 락 요청 → 대기 |
A는 B가 놓아줄 id=2를 기다리고, B는 A가 놓아줄 id=1을 기다립니다. 어느 쪽도 먼저 양보하지 않으면 영원히 기다리게 됩니다.
InnoDB는 데드락을 자동으로 감지하고, 변경한 데이터 양이 더 적은 트랜잭션을 롤백해서 교착 상태를 해소합니다. 롤백된 트랜잭션은 애플리케이션에서 재시도하도록 설계하는 것이 일반적입니다.

데드락을 예방하려면 다음 원칙을 따릅니다:
- 트랜잭션을 짧게 유지한다 — 락을 보유하는 시간을 최소화합니다.
- 여러 행에 접근할 때 항상 같은 순서로 락을 획득한다 — 순환 대기 구조 자체를 만들지 않습니다.
- 인덱스를 적절히 설계해 락 범위를 최소화한다 — 불필요하게 넓은 넥스트 키 락이 걸리지 않도록 합니다.
정리
| 락 종류 | 범위 | 락 타입 | 주요 목적 |
| 글로벌 락 | 서버 전체 | — | 전체 백업 시 쓰기 차단 |
| 테이블 락 | 테이블 전체 | — | MyISAM 동시성 제어 |
| 메타데이터 락 | 테이블 스키마 | — | DDL과 DML 동시 접근 방지 |
| 레코드 락 | 인덱스 레코드 1건 | S / X | 특정 행 수정 충돌 방지 |
| 갭 락 | 레코드 사이 구간 | S / X | 구간 내 INSERT 차단 |
| 넥스트 키 락 | 레코드 + 앞 갭 | S / X | 팬텀 리드 방지, REPEATABLE READ 구현 |
| 자동 증가 락 | 테이블 (즉시 해제) | X | AUTO_INCREMENT 채번 직렬화 |
다음 포스트에서는 트랜잭션 격리 수준 4가지와 MVCC를 살펴보고, 넥스트 키 락이 REPEATABLE READ를 어떻게 구현하는지 연결해서 설명합니다.
'Data > MySQL' 카테고리의 다른 글
| [MySQL] B-Tree 인덱스 완전 해부 — 구조부터 가용성까지 (1) | 2026.06.07 |
|---|---|
| [MySQL] 트랜잭션과 잠금 2편 — 격리 수준과 MVCC (0) | 2026.06.07 |
| [MySQL] 아키텍처 2편 — InnoDB 스토리지 엔진 (0) | 2026.06.07 |
| [MySQL] 아키텍처 1편 — 엔진 아키텍처 (0) | 2026.06.07 |