[MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE
![[MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE](https://blog.kakaocdn.net/dna/q2k9P/dJMcahLrGjy/AAAAAAAAAAAAAAAAAAAAAJSlrBUbjXbNyXJPTVKV4PrPAZcjnL6HlJ0SPNaqb0lX/img.png?credential=yqXZFxpELC7KVnFOS48ylbz2pIh7yKj8&expires=1788188399&allow_ip=&allow_referer=&signature=rh2hGA81BoDXK4pADFrCrcFKFgU%3D)
[MySQL] InnoDB 락 정리: 레코드 락, 갭 락, 넥스트키 락, SELECT FOR UPDATE
"행 하나만 UPDATE했는데 왜 다른 INSERT가 막히죠?" InnoDB 락을 모르면 미스터리지만, 알면 당연한 동작이에요. InnoDB는 행만 잠그는 게 아니라 행 사이의 틈(갭)도 잠그거든요. 이 글은 그 락의 실체를 정리해요.
레코드 락·갭 락·넥스트키 락이 각각 무엇을 잠그는지, 갭 락이 팬텀을 어떻게 막는지, 공유 락과 배타 락은 어떻게 다른지, 그리고 "인덱스 없는 UPDATE가 테이블을 통째로 잠그는" 최악의 함정까지 짚어요. 격리수준·MVCC 편과 한 쌍이에요 — MVCC가 "안 막고 읽는" 장치라면, 락은 "막아야 할 때"의 장치예요.
01. 잠그는 읽기와 안 잠그는 읽기
먼저 구분부터요. 일반 SELECT는 MVCC 일관 읽기라 락을 안 잡아요. 락이 등장하는 건 쓰기(UPDATE·DELETE·INSERT)와 잠그는 읽기예요.
SELECT * FROM orders WHERE id = 1; -- 락 없음 (스냅샷 읽기)
SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- 배타 락 (X)
SELECT * FROM orders WHERE id = 1 FOR SHARE; -- 공유 락 (S)
UPDATE orders SET status = 'PAID' WHERE id = 1; -- 배타 락 (X)
공유 락(S)끼리는 같이 잡을 수 있지만(여럿이 읽기), 배타 락(X)은 어떤 락과도 같이 못 잡아요. FOR UPDATE는 "내가 곧 수정할 테니 아무도 건드리지 마"고, JPA의 비관적 락이 바로 이걸 쓰는 거예요.
02. 락 3종, 행·틈·둘 다

- 레코드 락 — 존재하는 행(정확히는 인덱스 레코드)을 잠가요.
WHERE id = 20의 UPDATE가 잡는 그 락이에요. - 갭 락 — 행 사이의 틈을 잠가요. id 10과 20 사이의 갭을 잠그면, 그 사이 값(11~19)의 INSERT가 막혀요. 행이 아니라 "없는 자리"를 잠그는 거예요.
- 넥스트키 락 — 레코드 락 + 직전 갭 락이에요. REPEATABLE READ에서 범위 조건 잠금의 기본 단위예요.
"행 하나 잠갔는데 INSERT가 막혀요"의 정체가 갭 락이에요. 버그가 아니라, 다음에 볼 팬텀 방지를 위한 의도된 동작이에요.
03. 갭 락이 팬텀을 막는 구조

트랜잭션 A가 WHERE id BETWEEN 10 AND 30 FOR UPDATE로 범위를 잠그면, 그 범위의 행들뿐 아니라 사이사이 틈까지 넥스트키 락으로 잠겨요. 그래서 B가 id=15를 INSERT하려 하면 A가 끝날 때까지 대기해요. A가 같은 범위를 다시 읽어도 새 행이 나타날 수 없죠 — 앞 편에서 "InnoDB는 RR에서 팬텀을 대부분 막는다"고 한 그 장치예요.
대가도 분명해요. 갭 락은 "없는 자리"까지 잠그니, 동시 INSERT가 많은 테이블에서 범위 잠금이 잦으면 INSERT들이 줄줄이 대기해요. 심지어 조건에 맞는 행이 하나도 없어도 그 구간의 갭은 잠겨요 — "없는 걸 조회했는데 락 경합"이라는 황당한 상황의 정체예요. 갭 락 경합이 심하면 잠그는 범위를 좁히거나(동등 조건+유니크 인덱스는 갭 락 없이 레코드 락만), READ COMMITTED(갭 락 거의 없음)를 검토하는 게 그 "왜"가 될 수 있어요.
04. 최악의 함정, 인덱스 없는 UPDATE
여기가 이 글에서 제일 중요한 부분이에요. 락은 인덱스 레코드 위에 걸려요. 그러면 조건 컬럼에 인덱스가 없으면 어떻게 될까요?

인덱스가 없으니 풀 스캔으로 모든 행을 읽으면서 검사하는데, InnoDB는 읽은 행을 전부 잠가요. 조건에 안 맞는 행도요(RR 기준). 행 1건 고치려던 UPDATE가 사실상 테이블 전체 잠금이 되고, 그 테이블의 모든 쓰기가 줄을 서요. 트래픽 있는 서비스면 이거 하나로 장애예요.
그래서 UPDATE·DELETE의 WHERE 컬럼 인덱스는 성능 문제가 아니라 안전 문제예요. 어드민에서 가끔 도는 정리 쿼리, 배치의 조건 컬럼까지 전부 점검 대상이에요. "느린 건 참아도 잠그는 건 못 참는다"가 운영의 감각이에요.
05. INSERT의 락, 중복 키와 인텐션 락
INSERT도 락과 무관하지 않아요. 두 가지만 알아둬요.
첫째, 유니크 키 중복 검사예요. 같은 유니크 값을 두 트랜잭션이 INSERT하면, 뒤의 것이 앞의 커밋/롤백까지 대기해요(중복 여부가 그때 결정되니까). 선착순 가입 같은 데서 의외의 대기가 생기는 지점이에요.
둘째, 인텐션 락이라는 게 SHOW ENGINE INNODB STATUS나 바로 다음에 볼 data_locks 출력에 보이는데(IS·IX), 이건 "이 테이블 어딘가에 행 락을 걸 예정"이라는 표식일 뿐이에요. 행 락끼리의 경합과는 무관하니 보여도 놀랄 필요 없어요. 테이블 전체 작업(DDL 등)과의 조율용이에요.
06. 락을 눈으로 확인하기
지금 누가 뭘 잠그고 누가 기다리는지는 performance_schema로 봐요.
-- 현재 락 대기 관계 (누가 누구를 기다리나)
SELECT * FROM sys.innodb_lock_waits;
-- 잡혀 있는 락 상세
SELECT * FROM performance_schema.data_locks;
"UPDATE가 안 끝나요" 신고가 오면 이 두 개부터예요. 기다리는 쿼리와 잡고 있는 트랜잭션이 바로 보여요. 잡고 있는 쪽이 안 끝나는 장수 트랜잭션이면 그게 근본 원인이고요. 락 대기가 서로 맞물리면 데드락인데, 그건 다음 편의 주제예요.
07. 자주 만나는 문제
행 하나 잠갔는데 INSERT가 막혀요
갭 락이에요. 범위·비유니크 조건의 잠금은 틈까지 잠가요. 유니크 인덱스 동등 조건으로 잠그면 레코드 락만 걸려요.
UPDATE 하나에 서비스 전체 쓰기가 멈췄어요
인덱스 없는 WHERE의 풀 스캔 잠금을 의심해요. data_locks에서 락 범위를 확인하고 조건 컬럼에 인덱스를 추가해요.
락 타임아웃(Lock wait timeout exceeded)이 나요
누군가 오래 잡고 있는 거예요. innodb_lock_waits로 블로커를 찾아요. 대부분 커밋 안 하고 열려 있는 트랜잭션(애플리케이션 버그, 수동 세션)이에요.
정리
InnoDB 락은 레코드(행)·갭(틈)·넥스트키(둘 다) 세 단위로 나뉘고, 전부 인덱스 레코드 위에 걸려요. 갭 락이 팬텀을 막는 대신 INSERT 대기를 만들고, 인덱스 없는 UPDATE는 풀 스캔 잠금으로 테이블을 통째로 세워요. UPDATE·DELETE의 WHERE에는 인덱스 — 이 글에서 단 하나만 챙긴다면 이 문장이에요.
그런데 락 대기가 한 방향이 아니라 서로 맞물리면 어떻게 될까요. 두 트랜잭션이 서로의 락을 기다리며 영원히 멈추는 그 상황, 데드락의 진단과 해결로 이어집니다. 애플리케이션 레벨의 락 선택은 JPA 낙관적·비관적 락 편과 맞닿아 있고요.
출처: MySQL — InnoDB Locking · MySQL — Locks Set by Different SQL Statements
'백엔드 > 데이터 & DB' 카테고리의 다른 글
| [MySQL] HikariCP 커넥션 풀 설정: 풀 사이즈 공식, 타임아웃, 커넥션 누수 잡기 (0) | 2026.07.27 |
|---|---|
| [MySQL] 데드락 원인과 해결: SHOW ENGINE INNODB STATUS 읽는 법, 재시도 전략 (0) | 2026.07.24 |
| [MySQL] 트랜잭션 격리수준과 MVCC: REPEATABLE READ, 언두 로그, 팬텀 리드 (0) | 2026.07.22 |
| [MySQL] EXPLAIN 실행계획 읽는 법: type, Extra, rows 그리고 EXPLAIN ANALYZE (0) | 2026.07.21 |
| [MySQL] 복합 인덱스 설계: 컬럼 순서, 커버링 인덱스, 인덱스 못 타는 조건 (0) | 2026.07.20 |
📚 같이 보면 좋은
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 일정액의 수수료를 제공받습니다."