안녕하세요, 개발자 SEOBI 입니다. 전 회사에서 프로젝트 전반을 책임졌던 서버 리딩 경험은 제 커리어의 가장 큰 설렘이었습니다.
사용자와 거래 데이터를 지켜내는 그 긴장감과 성취감을 다시 느끼고 싶어, 공부한 것들을 블로그에 아낌없이 공유하려 합니다.
1. 잠금(lock) 이란?
잠금은 비유하자면 트랜잭션이 데이터(행·범위·테이블)를 자신만 안전하게 쓰거나 읽기 위해 ‘예약표’를 꽂아 두는 것입니다. 다른 트랜잭션이 같은 구역에 들어오면, 꽂혀 있는 예약표(잠금)와 호환되는지를 보고 — 호환되면 통과, 안 되면 대기(LOCK WAIT) 시킵니다. LOCK WAIT 된 트랜잭션은 원래 트랜잭션의 COMMIT 이 끝나야 다시 쿼리가 실행됩니다.
'왜 잠근다고 표현할까?'
- 공유(Shared, S) 잠금 - "읽기만 할 테니 너도 읽어"
- SELECT ... FOR SHARE (또는 LOCK IN SHARE MODE) 처럼 다 같이 조회할 때 건다.
- 독점(Exclusive, X) 잠금 - "나혼자 쓰고 읽겠다."
- UPDATE·DELETE·INSERT, SELECT … FOR UPDATE 등을 실행할 때 InnoDB가 자동으로 건다.
잠금을 잡는다(획득한다) = 트랜잭션이 그 구역에 대해 S·X 같은 잠금 모드를 성공적으로 등록했다는 뜻입니다.
MySQL 에서 FOR SHARE 는 S잠금, FOR UPDATE 는 X 잠금 입니다.
--1. 트랜잭션 A
-- id = 42 레코드에 X 잠금 획득
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 42;
-- COMMIT; 을 하지 않을때..
--2. 트랜잭션 B
-- 같은 레코드에 S 잠금 시도
-- X 잠금과 호환 안 됨 ⇒ LOCK WAIT
START TRANSACTION;
SELECT * FROM accounts WHERE id = 42 FOR SHARE;
위 예제에서 같은 행에 대하여 “B가 잡으려는 잠금”이 S 잠금이고, 이미 “A가 잡아 둔 잠금”이 X 잠금입니다. 둘이 호환되지 않으니 B는 기다립니다.
-- 트랜 잭션 A
-- id = 42 에 X 잠금 획득
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 42;
-- COMMIT; 을 하지 않을때..
-- 트랜 잭션 B (거의 동시에 실행)
-- 같은 행에 X 잠금 시도 → 세션 A 가 끝날 때까지 LOCK WAIT
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE id = 42;
위 예제는 같은 행을 둘 다 Update 하는 경우 입니다. 두 X 잠금은 절대 함께 존재 할 수 없으므로, 나중에 온 B가 기다립니다.
-- 트랜 잭션 A
-- id = 42 에 S 잠금 획득
START TRANSACTION;
SELECT * FROM accounts WHERE id = 42 FOR SHARE;
-- COMMIT; 을 하지 않을때..
-- 트랜 잭션 B (거의 동시에 실행)
-- 같은 행에 S 잠금 시도 S 잠금 끼리는 읽을 수 있어서 LOCK WAIT 발생 X
START TRANSACTION;
SELECT * FROM accounts WHERE id = 42 FOR SHARE;
위 예제는 같은 행을 둘 다 SELETE 하는 경우 입니다. 두 S 잠금은 서로 읽을 수 있음으로 잠금 대기는 발생하지 않습니다.
'서로 다른 행을 잡으면 데드락 발생 가능성이 있다.'
데드락은 잠긴 대상을 서로 교차로 기다리는 순환 대기가 있을 때 발생합니다. 행이 다르더라도 트랜잭션 A가 행 1을 잡은 뒤 행2를 기다리고 반대로 트랜잭션 B가 행2를 잡은 뒤 행1을 기다리면 두 트랜잭션은 서로 풀어 주지 않는 '막힌 고리'를 만들게 됩니다.
t0 세션 A START TRANSACTION;
t1 세션 A UPDATE accounts SET … WHERE id = 1; -- id1 에 X 잠금 획득
t2 세션 B START TRANSACTION;
t3 세션 B UPDATE accounts SET … WHERE id = 2; -- id2 에 X 잠금 획득
t4 세션 A UPDATE accounts SET … WHERE id = 2; -- id2 잠금 요청 → LOCK WAIT
t5 세션 B UPDATE accounts SET … WHERE id = 1; -- id1 잠금 요청 → LOCK WAIT
▶ A는 id2, B는 id1 잠금을 서로 기다리는 순환 대기 → InnoDB가 데드락 감지
ERROR 1213 (40001) 중 한 트랜잭션이 롤백됨
그래서 데드락을 예방 하기 위해
- 트랜잭션 AB 둘다 항상 같은 순서(id 오름차순 등)로 행을 업데이트
- 트랜잭션을 짦게 유지하고 즉시 COMMIT
- ERROR 1213 발생 시 재시도 로직 적용
같은 방지가 필요합니다.
2. Record Lock 과 Next-key Lock 란?
구분 | 무엇을 잠그나? | 왜 필요하나 | 충돌 (대기 상황) |
Record Lock | 인덱스 행 (Record) 하나 | 같은 행(프라미머리 키, 보조 인덱스)등 수정 충돌 방지 | 트랜잭션 A가 UPDATE … WHERE id = 10 → X(배타) 잠금. 트랜잭션 B가 같은 id = 10 업데이트 시도 → 대기 |
Next - Key Lock | Record Lock + Gap Lock | 해레코드 읽기 + 범위 내 INSERT 모두 제어 | 트랜잭션 A가 SELECT … WHERE score BETWEEN 80 AND 90 FOR UPDATE → 80 ≤ score < 다음 레코드까지 범위 잠금. 트랜잭션 B가 score 85 INSERT 시도 → 대기 |
Record Lock 은 같은 행을 동시에 수정, 삭제를 못하도록 합니다. Next - key Lock 은 아래에서 상세 설명하겠습니다.
'Next‑key Lock = Record Lock + Gap Lock'
우선 Gap Lock 은 두 행 사이 '빈 구간' 입니다. (레코드 앞,뒤 공간)
예시) 트랜잭션 A가 WHERE pk BETWEEN 5 AND 9 FOR UPDATE 를 실행
→ 인덱스상 (5, 9) 구간 전체가 Gap Lock 으로 잠김
반면 Next-key Lock은 Record Lock 과 Gap Lock 합쳐졌다고 보시면 됩니다. 즉,
따라서 범위 안에 이미 존재하는 행과 범위 안에 아직 없는 공간(갭) 을 둘 다 잠급니다.
UPDATE / DELETE (기존 행) | 불가 | 그 행 자체가 Record Lock(X) 에 걸려 있음 |
INSERT (새 행) | 불가 | 행과 행 사이 공간이 Gap Lock 으로 잠겨 있음 |
UPDATE (범위 밖 행) | 가능 | Next‑key Lock 영역과 겹치지 않음 |
즉 Insert뿐 아니라, 범위 안 기존 레코드를 건드리는 Update·Delete도 모두 대기합니다.
Next‑key Lock이 “레코드 수정+새 행 삽입” 두 가지를 동시에 막는 이유는 팬텀 리드를 완전히 차단하기 위해서입니다.
-- 준비: 한 번만 (어느 세션이든)
DROP TABLE IF EXISTS orders;
CREATE TABLE orders (id INT PRIMARY KEY, price INT) ENGINE=InnoDB;
INSERT INTO orders VALUES (1,80),(5,120),(9,220);
───────────── SESSION A ─────────────
START TRANSACTION;
-- Next‑key Lock (price 100~200)
SELECT * FROM orders
WHERE price BETWEEN 100 AND 200
FOR UPDATE;
───────────── SESSION B ─────────────
START TRANSACTION;
-- WAIT
UPDATE orders SET price = 130 WHERE id = 5;
-- WAIT
INSERT INTO orders VALUES (6,150);
-- OK
INSERT INTO orders VALUES (10,250);
-- ───────────── SESSION A ─────────────
-- 잠금 해제
COMMIT;
위 예제에 대한 결과는 아래와 같습니다.
- SELECT … WHERE price BETWEEN 100 AND 200 FOR UPDATE
범위 조건 → 레코드 id 5 + 둘러싼 갭(100 ~ 200) 가 Next‑key Lock(X) 로 잠김. - SESSION B – UPDATE id = 5
레코드 5는 이미 X 잠금이라 LOCK WAIT - SESSION B – INSERT 150
값 150은 갭 (100 ~ 200) 안 → 갭 락에 막혀 LOCK WAIT - SESSION B – INSERT 250
값 250은 (200 ~ ∞) 갭 → 범위 밖, 잠금 없음 → 즉시 성공. - SESSION A – COMMIT
Next‑key Lock 해제 → B의 대기 쿼리(UPDATE 5·INSERT 150) 순차 실행 완료.
3. 왜 잠금(Lock)을 알아야 하나?
실무에서 다음과 같은 문제가 발생할 위험이 있기 때문 입니다.
- 서비스 지연: 평소 주요 API 응답이 잠금 충돌로 인해 100ms -> 1.5s 로 늘어 날때, 대부분 Record/Next-Key Lock 충돌 입니다.
- 동시성 높은 UPDATE/DELETE/SELECT … FOR UPDATE 구문이 같은 인덱스 레코드(Record lock)나 범위(Next‑key lock = Record + Gap)를 건드리면 대기 시간이 선형 이상으로 늘어날 수 있다는 것이 MySQL 매뉴얼과 여러 운영 사례에서 확인됩니다 - 운영 해결 난이도:잠금 이슈는 재현이 까다로워, 일반 장애보다 2‑3배 긴 분석‑해결 시간이 필요합니다.
- 락 대기는 트랜잭션 타이밍 의존성이 크고 재현 로드가 필요해 디버깅 난이도가 높은 편입니다. 공식 지표는 없지만, 여러 사례에서 일반 성능 문제보다 수 배의 분석 시간이 든다고 보고됩니다.
- MySql 8 이후에는 Percona PMM·Datadog DB Monitoring 같은 SaaS APM이 실시간으로 잠금 대기·데드락·블로킹 트리를 보여 줍니다. 그래서 장애 원인이 잠금이라는 사실은 예전처럼 수 시간 걸리지 않고 몇 분 안에 확인하는 경우가 많습니다.
- 데이터 무결성: 잘못된 트랜잭션 격리는 중복 결제·포인트 이중 적립 같은 오류를 야기합니다.
- 낮은 격리 수준이거나 적절한 잠금이 없으면 팬텀 리드·Dirty Write가 발생합니다. MySQL InnoDB는 기본 격리 수준인 Repeatable Read에서 Next‑key Lock(Record + Gap) 을 자동으로 사용해 이러한 문제를 방지합니다. 단, 격리를 READ COMMITTED로 낮추면 갭 락이 꺼져 팬텀 리드를 막지 못합니다.
트랜잭션 격리 수준(lsolation Level) 이란?
동시에 실행되는 트랜잭션 사이에서 ‘어디까지 서로를 보지 못하게 할지’ 규칙입니다. SQL 표준은 4단계(높음→낮음)로 정의합니다.
SERIALIZABLE | 완전 직렬화 | — | 모든 SELECT도 공유 잠금 |
REPEATABLE READ (MySQL 기본) |
같은 쿼리 두 번 ‑ 결과 동일 | 팬텀 리드 차단 O | Next‑key Lock(Record+Gap) MySQL Developer Zone |
READ COMMITTED | “커밋된 것만 본다” | 팬텀 리드 O, 전/후 값 차이 O | Record Lock만, 갭 락 대부분 생략 MySQL Developer Zone |
READ UNCOMMITTED | 언커밋 데이터까지 노출 | Dirty Read | 최소 잠금 |
팬텀 리드란?
(next-key locking과 갭락이 비활성화 일경우)
-- 트랜잭션 A
SELECT * FROM orders WHERE amount BETWEEN 100 AND 200;
-- 트랜잭션 B (다른 트랜잭션)
INSERT INTO orders VALUES (999, 150);
-- 트랜잭션 A - 팬텀 리드 발생
SELECT * FROM orders WHERE amount BETWEEN 100 AND 200;
A가 같은 쿼리를 실행했는데, 중간에 B가 끼워 넣은 150원 주문이 팬텀(새 행)처럼 나타나 개수가 달라집니다.
팬텀 리드 결과.
1 | 120 |
2 | 140 |
999 | 150 |
3 | 180 |
이상 입니다 ~
'서버 Server' 카테고리의 다른 글
서버개발 (2) 실제 운영 경험담: Spring Boot 로그인 PessimisticLockException (2) | 2025.04.23 |
---|