S락·X락·갭 락·락 승격 — InnoDB 락 정리 노트

@yunhobb· April 21, 2025 · 8 min read

운영 DB에서 데드락(deadlock) 로그를 읽다가, lock_mode X locks rec but not gap, lock mode S 같은 표현이 어떤 락을 가리키는지는 알아도 어떤 문장이 왜 그 락을 잡았는지는 바로 설명하기 어려웠습니다. 그래서 데드락 추적에 앞서 락부터 정리했습니다.

이 글은 그 정리 노트입니다. InnoDB가 행을 어떻게 잠그는지 따라가며, 다음 편에서 다룰 데드락의 배경을 먼저 정리합니다. (개념 설명은 MySQL 공식 문서의 InnoDB Locking을 기준으로 했고, px201226님의 "MySQL 트랜잭션 락" 글을 참고해 구성을 잡았습니다. 갭 락과 삽입 의도 락 부분은 당근 기술블로그의 "MySQL Gap Lock 다시보기"·"두 번째 이야기"에서 실사례를 빌렸습니다. MariaDB의 InnoDB도 같은 락 모델을 따릅니다.)


1. 두 개의 기본 락: 공유락(S)과 배타락(X)

InnoDB의 행 락은 두 종류에서 출발합니다.

  • 공유락(Shared Lock, S): 읽기를 위한 락. 여러 트랜잭션이 같은 행에 동시에 S락을 쥘 수 있습니다. 단, 그 행을 바꾸려는 배타락은 막습니다. SELECT ... FOR SHARE(과거 LOCK IN SHARE MODE)로 명시적으로 걸 수 있습니다.
  • 배타락(Exclusive Lock, X): 쓰기를 위한 락. 한 트랜잭션만 쥘 수 있고, 다른 어떤 락(S든 X든)과도 공존하지 못합니다. SELECT ... FOR UPDATEUPDATE/DELETE가 이 락을 겁니다.

정리하면 S락끼리는 공존하고, X락은 단독으로만 잡힙니다. 이 규칙이 뒤에 나올 데드락의 절반을 설명합니다.

보유 ↓ \ 요청 → S X
S ✓ 공존 ✗ 대기
X ✗ 대기 ✗ 대기

여기서 한 가지를 미리 짚어 둡니다. 이미 S락을 쥔 행을 같은 트랜잭션이 X락으로 올리는 것(락 승격, lock upgrade)은, 그 사이 다른 트랜잭션도 같은 행에 S락을 쥐고 있으면 성공하지 못합니다. S락은 공존하지만, X로의 승격은 다른 락이 모두 풀려 있어야 하기 때문입니다.


2. 행을 잠그기 전에 손드는 락: 의도락(Intention Lock)

InnoDB는 테이블 락과 행 락을 함께 씁니다(다중 세분성 잠금, multiple granularity locking). 그런데 누군가 테이블 전체에 락을 걸려 할 때, "이 테이블 안 어딘가의 행에 락이 걸려 있나?"를 행을 일일이 훑어 확인하면 느립니다. 그래서 행 락을 걸기 전에, 테이블 레벨에 "나 이 테이블의 어떤 행을 잠글 거야"라는 깃발을 먼저 꽂습니다. 이게 의도락(Intention Lock)입니다.

  • IS(Intention Shared): 행에 S락을 걸 의도. (SELECT ... FOR SHARE)
  • IX(Intention Exclusive): 행에 X락을 걸 의도. (SELECT ... FOR UPDATE, UPDATE, INSERT 등)

의도락끼리는 대부분 호환됩니다. 둘 다 "행 단위로 락을 걸 예정"이라는 표시일 뿐이고, 실제 충돌은 행 레벨에서 가려지기 때문입니다. 공식 문서의 호환성 매트릭스는 다음과 같습니다.

X IX S IS
X
IX
S
IS

표에서 보이듯, 테이블 레벨 X락만 모든 것을 막습니다. 평소 우리가 만나는 데드락은 의도락이 아니라 행 레벨의 S/X에서 벌어지니, 의도락은 "이런 계층이 있다" 정도로만 알아두면 충분합니다.


3. InnoDB가 행을 잠그는 세 가지 방식: 레코드 / 갭 / 넥스트키 락

InnoDB의 행 락은 사실 "행"이 아니라 인덱스 레코드를 잠급니다. 잠그는 범위에 따라 셋으로 나뉩니다.

잠그는 대상 performance_schema 표기
레코드 락(Record Lock) 인덱스 레코드 하나 X,REC_NOT_GAP / S,REC_NOT_GAP
갭 락(Gap Lock) 레코드와 레코드 사이의 빈 구간 X,GAP / S,GAP
넥스트키 락(Next-Key Lock) 레코드 + 그 앞 갭 (기본 표기)

갭 락은 값을 잠그는 게 아니라 그 구간에 새 행이 끼어드는 것(INSERT)을 막는 락입니다. 같은 조건으로 두 번 읽었을 때 없던 행이 생기는 팬텀 리드(phantom read)를 막기 위한 장치입니다. 넥스트키 락은 레코드 락과 갭 락을 합친 것으로, REPEATABLE READ에서 범위 조건을 다룰 때 기본으로 쓰입니다.

갭 락이 언제 붙는지는 인덱스 종류를 탑니다.

  • 기본키·유니크 인덱스 + 동등 조건(=)으로 정확히 1건: 레코드 락만. 갭은 안 잠급니다.
  • 존재하지 않는 값 조회, 범위 조건, 또는 보조 인덱스(secondary index): 레코드 락 + 갭 락이 함께 붙습니다. 특히 보조 인덱스 동등 조회는 거의 항상 갭까지 잠그기 때문에, 한 건만 건드린 것처럼 보여도 인접 구간의 INSERT가 막힐 수 있습니다.

여기에 갭과 짝을 이루는 락이 하나 더 있습니다. 삽입 의도 락(INSERT Intention Lock)입니다. INSERT가 어떤 갭에 행을 넣기 직전 그 갭에 거는 특수한 갭 락이며, 다음 성질이 중요합니다.

  • 갭 락끼리는 서로 호환됩니다. 여러 트랜잭션이 같은 갭에 갭 락을 함께 쥘 수 있습니다.
  • 하지만 갭 락과 삽입 의도 락은 충돌합니다. 누군가 갭을 잠가두면, 그 갭에 INSERT 하려는 트랜잭션은 대기합니다.

이 "갭 락은 공존하지만 삽입 의도 락과는 비호환"이라는 비대칭이 6절의 또 다른 데드락 패턴을 만듭니다.

참고: 2편의 데드락 로그에는 locks rec but not gap이 등장합니다. 갭은 잠그지 않고 레코드 하나만 잠근, 위 표의 "레코드 락"입니다. 즉 2편의 교착은 갭 락이 아니라 레코드 락 + S→X 승격이 원인이며, 지금 설명한 갭 락은 그것과 별개의 데드락 갈래입니다.


4. 격리 수준이 락을 바꾼다 — 기본은 REPEATABLE READ

같은 SQL이라도 트랜잭션 격리 수준(isolation level)에 따라 잡는 락이 달라집니다. MySQL·MariaDB의 기본값은 REPEATABLE READ(RR)입니다.

  • 일반 SELECT는 락을 안 건다. RR에서 평범한 조회는 MVCC(다중 버전 동시성 제어)로 스냅샷을 읽기 때문에, 락 없이도 일관된 결과를 봅니다.
  • 하지만 쓰기는 다르다. UPDATE, DELETE, 그리고 다른 테이블을 읽어 값을 넣는 INSERT ... SELECT는 MVCC로 처리되지 않고 실제 락을 잡습니다. RR이라도 MVCC는 읽기 동시성을 위한 것이며, 쓰기에는 적용되지 않습니다.

READ COMMITTED(RC)로 낮추면 갭 락을 거의 쓰지 않아 잠금 범위가 좁아지지만, 일관성 보장도 함께 약해집니다. 이 글과 2편의 내용은 모두 기본값 RR을 전제로 합니다.


5. 무슨 문장이 어떤 락을 잡나

이번 노트의 핵심 표입니다. 기본 격리 수준(RR) 기준으로, 자주 쓰는 문장이 거는 락을 정리했습니다.

문장 거는 락
일반 SELECT 없음 (MVCC 스냅샷 읽기)
SELECT ... FOR SHARE 읽은 행에 S락 (+ IS)
SELECT ... FOR UPDATE 읽은 행에 X락 (+ IX)
UPDATE ... WHERE 조건 범위에 넥스트키 락, 실제 수정 행에 X락
단순 INSERT 삽입 행에 X락
INSERT ... SELECT 읽어온 원본 행에 S락 + 삽입 행에 X락
INSERT ... ON DUPLICATE KEY UPDATE 중복키 감지 시 먼저 S락 → 갱신하며 X락

마지막 두 줄이 2편 데드락의 두 축입니다.

  • INSERT ... SELECT(또는 INSERT의 VALUES 안 서브쿼리): 값을 읽어온 원본 행에 S락이 걸립니다. 읽기만 한 것처럼 보여도, 그 값으로 다른 곳에 쓰는 이상 일관성을 지키기 위해 InnoDB가 S락을 잡습니다.
  • ON DUPLICATE KEY UPDATE: 중복키를 만나면 그 인덱스 레코드에 먼저 S락을 잡아 확인하고, 갱신을 진행하며 X락으로 올립니다. 여기에 락 승격(S→X)이 들어 있습니다.

6. 데드락이 만들어지는 과정

데드락은 둘 이상의 트랜잭션이 서로가 쥔 락을 맞물려 기다리는 순환 대기(circular wait)입니다. 실무에서 마주치는 형태는 크게 셋입니다.

패턴 A — 락 순서 역전. 가장 고전적인 형태입니다.

trx-1:  id=1 X락 획득 → id=2 X락 대기
trx-2:  id=2 X락 획득 → id=1 X락 대기
        └──────── 서로 물림 → 교착 ────────┘

두 트랜잭션이 같은 자원들을 반대 순서로 잠그면 발생합니다. 해결책은 모든 트랜잭션이 같은 순서로 잠그도록 맞추는 것입니다.

패턴 B — S→X 승격 경쟁. 1절에서 짚어 둔 락 승격이 여기서 문제가 됩니다.

trx-1, trx-2:  같은 행에 S락을 함께 획득   (S끼리는 공존 OK)
trx-1:  그 행을 X로 승격하려 함 → trx-2의 S락 때문에 대기
trx-2:  그 행을 X로 승격하려 함 → trx-1의 S락 때문에 대기
        └──────── 둘 다 못 올라감 → 교착 ────────┘

INSERT ... ON DUPLICATE KEY UPDATE처럼 S락을 먼저 잡고 X로 올리는 문장을 두 세션이 같은 키로 동시에 실행하면 이 패턴에 빠집니다.

패턴 C — 갭 락과 삽입 의도 락의 충돌. 3절의 비대칭("갭 락끼리는 공존, 삽입 의도 락과는 비호환")이 여기서 작용합니다.

세션1: SELECT ... FOR UPDATE (없는 id=2) → 갭 락 획득
세션2: SELECT ... FOR UPDATE (없는 id=2) → 갭 락 획득   (갭 락끼리는 공존 OK)
세션1: INSERT id=2 → 삽입 의도 락 필요, 세션2의 갭 락과 충돌 → 대기
세션2: INSERT id=2 → 삽입 의도 락 필요, 세션1의 갭 락과 충돌 → 대기
        └──────── 서로 물림 → 교착 ────────┘

존재하지 않는 값 조회·범위 조건·보조 인덱스가 갭 락을 만들고, 거기에 두 세션이 같은 갭으로 INSERT를 시도하면 교착이 됩니다. 큐(queue)처럼 쓰는 테이블이나 빈 테이블(갭이 테이블 전체로 넓어집니다)에서 특히 자주 나타납니다. 해결책은 격리 수준을 READ COMMITTED로 낮춰 갭 락을 없애거나, 범위 조회(BETWEEN)를 기본키 동등 조회(IN (...))로 바꾸거나, 선택도 높은 인덱스를 더해 잠금 범위를 좁히는 것입니다. (당근 기술블로그의 두 글이 이 패턴과, 페이지 끝의 supremum 갭까지 다룹니다 — 위 참고 링크.)

참고: 패턴 B와 C는 둘 다 공존하던 약한 락(S락 / 갭 락)을 더 강한 락(X락 / 삽입 의도 락)으로 올리는 시점에서 막힌다는 공통점이 있습니다. 여럿이 함께 쥐던 락을 동시에 독점하려는 순간이 교착의 전형적인 구도입니다.

교착이 감지되면 InnoDB는 가장 가벼운 트랜잭션 하나를 골라 롤백합니다. "가벼움"은 information_schema.INNODB_TRXtrx_weight(변경·잠근 행 수 등을 반영한 값)로 판단합니다. 살아남은 쪽은 정상 진행하므로, 애플리케이션은 데드락(에러 코드 1213)을 만나면 재시도하도록 작성하는 것이 일반적입니다.


7. 마무리 — 2편으로

여기까지가 데드락 로그를 읽기 위한 최소한의 어휘입니다. 정리하면,

  • S락은 공존하고 X락은 독점한다. 그리고 S→X 승격은 다른 S락이 남아 있으면 막힌다.
  • 행 락은 인덱스 레코드를 잠그며, 레코드 / 갭 / 넥스트키로 범위가 나뉜다.
  • 기본 격리 수준 RR에서 일반 SELECT는 락이 없지만, 쓰기와 INSERT ... SELECT는 락을 잡는다.
  • INSERT ... SELECT는 원본 행에 S락, ON DUPLICATE KEY UPDATE는 중복키에 S락 → X락 승격.
  • 데드락은 락 순서 역전(패턴 A), S→X 승격 경쟁(패턴 B), 갭 락↔삽입 의도 락 충돌(패턴 C)에서 발생한다.

이 요소들이 실제로 어떻게 맞물리는지를, 다음 편에서 운영 DB에 기록된 데드락 로그로 추적합니다. 평범해 보이는 upsert 쿼리 하나가 위 패턴 B를 그대로 재현한 사례입니다.2편: INSERT 한 번에 데드락이?에서 이어가겠습니다.

@yunhobb
녹차 주도 개발