2단계 잠금(Two-Phase Locking). 락으로 직렬 가능성(여러 트랜잭션을 한 줄로 세운 것과 같은 결과)을 보장하는 고전적 규칙이다.
두 단계
확장 단계(growing) 락을 얻기만 한다
↓ (첫 해제가 일어나는 순간 전환)
수축 단계(shrinking) 락을 풀기만 한다 — 새 락을 못 얻는다
이 규칙만으로 직렬 가능성이 보장된다. 실무 DB는 여기서 더 나아간 Strict 2PL을 쓴다 — 모든 쓰기 락을 커밋 시점까지 쥔다. 중간에 풀면 남이 미완성 데이터를 읽어(dirty read) 연쇄 롤백이 생기기 때문이다.
락의 두 종류와 양립표
- 공유 락(S, shared) — 읽기용 — 여럿이 동시에 잡을 수 있다
- 배타 락(X, exclusive) — 쓰기용 — 혼자만 잡는다
| 요청 ↓ \ 보유 → | S(공유) | X(배타) |
|---|---|---|
| S(공유) | ✅ 가능 | ❌ 대기 |
| X(배타) | ❌ 대기 | ❌ 대기 |
공유끼리만 같이 잡힌다 — 이 한 줄로 표 전체를 외운다.
여기에 행 락·테이블 락과, "이 테이블 안 어딘가에 행 락을 걸 것" 을 미리 표시해 충돌 판단을 빠르게 하는 의도 락(IS/IX) 이 있다.
치명적 한계 — 그래서 MVCC가 나왔다
위 표의 ❌들이 말하는 바는 이것이다.
한 사람이 어떤 행을 읽는 동안 → 아무도 그 행을 못 바꾼다
- 누가 바꾸는 동안 — → 아무도 못 읽는다
보고서를 만드느라 큰 테이블을 읽는 트랜잭션 하나가 그 테이블 전체의 쓰기를 막아 버린다. 동시성이 극도로 떨어진다. 이 벽을 깬 것이 MVCC이고, 현대 DB는 읽기는 MVCC · 쓰기는 락으로 둘을 섞는다.
면접 함정
- ❌ "2PL은 데드락을 막아 준다" → 아니다. 직렬 가능성만 보장하고 데드락은 그대로 생긴다. 탐지·롤백으로 다룬다.
- ❌ "요즘 DB는 2PL을 안 쓴다" → 쓰기 경로에는 여전히 락이 쓰인다. 읽기 경로만 MVCC로 대체됐다.
지금 누가 무엇을 잠그고 있나
-- PostgreSQL: 대기 중인 락과 그 원인을 한 번에
SELECT blocked.pid AS blocked_pid, blocked.query AS blocked_query,
blocking.pid AS blocking_pid, blocking.query AS blocking_query
FROM pg_stat_activity blocked
JOIN pg_stat_activity blocking
ON blocking.pid = ANY(pg_blocking_pids(blocked.pid))
WHERE cardinality(pg_blocking_pids(blocked.pid)) > 0;
-- MySQL 8.0+
SELECT * FROM performance_schema.data_lock_waits;
SELECT * FROM performance_schema.data_locks;
pg_blocking_pids()는 "누가 나를 막고 있나" 를 한 번에 알려 주는 함수다. 락 대기 장애의 첫 진단 도구다.
명시적으로 락을 잡는 문장들
SELECT ... FOR UPDATE; -- 배타 락(X). 읽고 곧 바꿀 행
SELECT ... FOR SHARE; -- 공유 락(S). 읽는 동안 남이 못 바꾸게만
LOCK TABLE t IN EXCLUSIVE MODE; -- 테이블 락 (거의 쓸 일이 없다)
락 대기를 무한정 두지 않는다
-- PostgreSQL: 락을 5초 이상 기다리면 실패시킨다
SET lock_timeout = '5s';
-- MySQL
SET SESSION innodb_lock_wait_timeout = 5; -- 기본 50초
타임아웃을 짧게 잡는 것이 장애 확산을 막는 핵심이다. 기본값(50초)이면 락 하나가 막혔을 때 커넥션 풀이 먼저 마르고 서비스 전체가 멈춘다.