락과 2단계 잠금
격리 수준과 쓰기 충돌 제어를 구현하는 핵심 메커니즘 중 하나가 락(Lock)입니다. 누가 데이터를 수정하거나 보호 중이면 다른 트랜잭션은 기다리거나 실패합니다. 락의 종류, 적용 범위, 데드락 처리, DBMS별 MVCC와의 조합까지 이해해야 실무에서 동시성 문제를 진단할 수 있습니다.
공유 락과 배타 락
| 락 유형 | 약어 | 대표 용도 | 호환성 |
|---|---|---|---|
| 공유 락 (Shared Lock) | S-Lock | 잠금 읽기, 참조 무결성 확인 | S-Lock끼리 호환 (동시 읽기 가능) |
| 배타 락 (Exclusive Lock) | X-Lock | UPDATE, DELETE, SELECT FOR UPDATE | 다른 모든 락과 불호환 |
MVCC 기반 DBMS에서는 일반 SELECT가 항상 S-Lock을 잡는다고 보면 안 됩니다. Oracle, PostgreSQL, InnoDB의 일반 읽기는 보통 스냅샷을 읽고 쓰기를 직접 막지 않으며, SELECT ... FOR UPDATE, locking read, 제약 조건 확인, DDL, 메타데이터 작업처럼 보호가 필요한 경우에 잠금이 관여합니다.
이미 걸린 락
S-Lock X-Lock
요청하는 락
S-Lock ✓ 허용 ✗ 대기
X-Lock ✗ 대기 ✗ 대기
의미:
S + S = 허용 → 여러 트랜잭션이 동시에 잠금 읽기 가능
S + X = 대기 → 보호 중인 데이터를 쓰려면 대기
X + S = 대기 → 쓰는 중인 데이터를 잠금 읽기하려면 대기
X + X = 대기 → 누군가 쓰고 있으면 쓰기 대기의도 잠금 (Intent Lock)
행 수준 잠금이 설정될 때, 상위 수준(테이블)에 의도 잠금이 함께 설정됩니다. 의도 잠금은 “이 테이블 안의 어떤 행을 잠글 예정이거나 이미 잠갔다”는 표시라서, 테이블 전체 잠금과 행 잠금이 충돌하는지 빠르게 판단하게 해 줍니다.
요청 \ 보유 IS IX S SIX X
IS ✓ ✓ ✓ ✓ ✗
IX ✓ ✓ ✗ ✗ ✗
S ✓ ✗ ✓ ✗ ✗
SIX ✓ ✗ ✗ ✗ ✗
X ✗ ✗ ✗ ✗ ✗
S = Shared (테이블 전체 공유 잠금)
X = Exclusive (테이블 전체 배타 잠금)
IS = Intent Shared
IX = Intent Exclusive
SIX = S + IX (테이블 공유 + 일부 행 배타)잠금 범위: 행, 페이지, 테이블
잠금 범위와 승격 방식은 DBMS마다 다릅니다. SQL Server는 행/페이지/테이블 잠금과 lock escalation을 적극적으로 다루고, InnoDB는 주로 인덱스 레코드와 범위를 잠그며 전통적인 의미의 테이블 단위 lock escalation은 하지 않습니다. PostgreSQL과 Oracle도 일반 행 잠금, 테이블 수준 메타 잠금, 명시적 잠금이 서로 다른 규칙으로 동작합니다.
2단계 잠금 프로토콜 (2PL)
2단계 잠금(Two-Phase Locking, 2PL)은 락을 얻는 단계와 락을 푸는 단계를 분리하여 충돌 직렬 가능성을 보장하는 잠금 프로토콜입니다.
2PL을 지키면 충돌 직렬 가능성(Conflict Serializability)이 보장됩니다. 하지만 모든 DBMS가 모든 격리 수준을 순수 2PL로 구현하는 것은 아니며, Strict 2PL, MVCC, SSI, gap lock 같은 기법을 섞습니다. 또한 락 대기 때문에 데드락이 발생할 수 있습니다.
2PL의 변형
Strict 2PL은 보통 X-Lock을 커밋/롤백까지 유지해 다른 트랜잭션이 미확정 쓰기를 읽거나 덮어쓰지 못하게 합니다. 모든 S-Lock과 X-Lock을 끝까지 유지하는 더 강한 형태는 Rigorous 2PL이라고 부르며, 실제 DBMS 구현은 격리 수준과 읽기 방식에 따라 이보다 완화된 전략을 함께 사용합니다.
데드락
데드락(Deadlock, 교착 상태)은 두 개 이상의 트랜잭션이 서로가 가진 락을 기다리며 더 이상 진행할 수 없는 상태입니다. DBMS는 이를 탐지해 한쪽을 실패시키거나, 타임아웃으로 대기를 끊습니다.
데드락 탐지
DBMS는 데드락을 탐지(Detection)하여 해결합니다. 대기 그래프(Wait-For Graph)에서 사이클이 발견되면 희생자 트랜잭션 또는 문장을 실패시켜 순환 대기를 끊습니다. 이때 애플리케이션은 실패한 작업을 롤백하고 재시도할 수 있어야 합니다.
데드락은 "비정상 장애"라기보다 강한 동시성 제어에서 발생할 수 있는 정상적인 실패 경로입니다. 따라서 결제, 주문, 재고 차감처럼 중요한 경로는 트랜잭션 전체를 다시 실행해도 안전하도록 멱등성 키, 중복 방지 제약, 재시도 횟수 제한을 함께 둡니다.
DBMS별 데드락 처리
Oracle:
* 데드락 발생 즉시 탐지 (대기 중인 트랜잭션이 스스로 확인)
* 데드락을 일으킨 SQL 문만 롤백될 수 있음
* ORA-00060: deadlock detected 에러 발생
* 애플리케이션에서 명시적 ROLLBACK 또는 재시도 필요
MySQL InnoDB:
* innodb_deadlock_detect가 켜져 있으면 대기 그래프를 탐지
* 보통 희생자 트랜잭션을 롤백
* ERROR 1213: Deadlock found 에러
* innodb_lock_wait_timeout (기본 50초)
PostgreSQL:
* deadlock_timeout 이후 데드락 검사를 수행
* 트랜잭션은 오류 상태가 되어 롤백/재시도 필요
* ERROR: deadlock detected
SQL Server:
* lock monitor가 데드락을 탐지하고 주기를 조정
* 희생자 트랜잭션 롤백
* SET DEADLOCK_PRIORITY로 희생자 우선순위 설정 가능데드락 예방과 대응
| 방법 | 설명 |
|---|---|
| 일관된 접근 순서 | 모든 트랜잭션이 테이블 A → B 순서로 접근 |
| 트랜잭션 짧게 | 락 점유 시간을 최소화 |
| 타임아웃 설정 | 대기 시간 초과 시 롤백 |
| 인덱스 최적화 | 인덱스 사용으로 락 범위 축소 |
| NOWAIT 사용 | 대기 대신 즉시 실패 처리 |
1. 접근 순서 통일:
모든 트랜잭션에서 accounts → orders → items 순서로 접근
→ 순환 대기 가능성을 크게 줄임
2. SELECT FOR UPDATE NOWAIT:
잠금 획득 실패 시 즉시 에러 → 애플리케이션에서 재시도
→ 대기 없이 빠른 실패
3. 인덱스를 통한 잠금 범위 최소화:
InnoDB 등 인덱스 레코드/범위 잠금 기반 DBMS에서
인덱스 없이 넓게 스캔하면 잠금 후보와 대기 범위가 커질 수 있음
인덱스가 맞으면 스캔 범위가 좁아져 경합과 데드락 확률 감소MySQL InnoDB의 갭 락과 넥스트키 락
MySQL InnoDB는 행 수준 잠금 외에 갭 락(Gap Lock)과 넥스트키 락(Next-Key Lock)을 사용합니다.
Record Lock
* 인덱스 레코드에 대한 잠금
* WHERE id = 10 → id=10 레코드만 잠금
Gap Lock
* 인덱스 레코드 사이의 빈 공간에 대한 잠금
* 해당 범위에 새 행 INSERT를 차단
* 주로 REPEATABLE READ의 범위 잠금에서 중요
* READ COMMITTED에서는 중복/외래키 검사 등으로 제한적으로 사용
* 행 자체의 값을 보호하기보다 "그 구간에 새 인덱스 레코드가 들어오는 것"을 제어
Next-Key Lock
* Record Lock + Gap Lock 결합
* 레코드 자체 + 그 앞의 갭을 함께 잠금
* 고유 인덱스 동등 검색은 gap 없이 record만 잠글 수 있음
* 비고유/범위 검색은 스캔한 인덱스 범위가 잠금 대상Gap/Next-Key Lock은 InnoDB의 인덱스 기반 잠금 설명입니다. 조건에 맞는 논리 행만 잠그는 것이 아니라 실제로 선택한 실행 계획과 스캔한 인덱스 범위가 중요하므로, 같은 SQL도 인덱스 유무와 조건 형태에 따라 잠금 폭이 달라질 수 있습니다.
Oracle의 락 체계
Oracle은 행 수준 락(Row-Level Lock)을 기본으로 사용합니다.
| 락 유형 | 설명 |
|---|---|
| TX 락 (Row Lock) | 행 수준의 배타 락, DML 시 자동 획득 |
| TM 락 (Table Lock) | 테이블 수준의 의도 락 (RS, RX, S, SRX, X) |
-- 행 수준 락 (SELECT FOR UPDATE)
SELECT * FROM products WHERE id = 101 FOR UPDATE;
-- → 해당 행에 X-Lock, 다른 트랜잭션은 수정 대기
-- NOWAIT 옵션 (락 획득 실패 시 즉시 에러)
SELECT * FROM products WHERE id = 101 FOR UPDATE NOWAIT;
-- WAIT 옵션 (최대 5초 대기)
SELECT * FROM products WHERE id = 101 FOR UPDATE WAIT 5;
-- SKIP LOCKED (잠긴 행 건너뛰기 — 큐 패턴)
SELECT * FROM jobs WHERE status = 'PENDING'
FOR UPDATE SKIP LOCKED FETCH FIRST 1 ROW ONLY;-- 테이블 전체 공유 잠금
LOCK TABLE orders IN SHARE MODE;
-- 테이블 전체 배타 잠금 (DDL 보호 등)
LOCK TABLE orders IN EXCLUSIVE MODE;
-- NOWAIT으로 즉시 실패
LOCK TABLE orders IN EXCLUSIVE MODE NOWAIT;Oracle에서 일반 SELECT는 DML 행 락을 걸지 않습니다. MVCC 기반 읽기 일관성 덕분에 일반 읽기와 쓰기는 보통 서로 차단하지 않습니다. 단, SELECT FOR UPDATE, DDL, 명시적 테이블 락, 제약 조건 검사처럼 잠금이 필요한 작업은 예외입니다.
잠금 모니터링
-- 현재 잠금 대기 확인
SELECT l1.sid blocking_sid, l2.sid waiting_sid,
l1.type lock_type
FROM v$lock l1, v$lock l2
WHERE l1.id1 = l2.id1 AND l1.block = 1 AND l2.request > 0;
-- 블로킹 세션 상세 정보
SELECT blocking_session, sid, serial#,
wait_class, seconds_in_wait
FROM v$session WHERE blocking_session IS NOT NULL;
-- 잠금 트리 (누가 누구를 블로킹하는지)
SELECT LPAD(' ', (LEVEL-1)*2) || sid sid_tree,
blocking_session, event, seconds_in_wait
FROM v$session
START WITH blocking_session IS NULL
CONNECT BY PRIOR sid = blocking_session;-- 현재 잠금 대기 확인 (MySQL 8.0+)
SELECT * FROM performance_schema.data_lock_waits;
-- 잠금 상세 정보
SELECT * FROM performance_schema.data_locks;
-- InnoDB 상태 (데드락 정보 포함)
SHOW ENGINE INNODB STATUS;
-- 최근 데드락 로그 확인
-- SHOW ENGINE INNODB STATUS 출력에서
-- LATEST DETECTED DEADLOCK 섹션 확인-- 잠금 대기 현황: 현재 세션을 막는 PID를 배열로 확인
SELECT pid AS blocked_pid,
pg_blocking_pids(pid) AS blocking_pids,
wait_event_type,
wait_event,
query
FROM pg_stat_activity
WHERE cardinality(pg_blocking_pids(pid)) > 0;
-- 오래 대기중인 쿼리
SELECT pid, age(clock_timestamp(), query_start), query
FROM pg_stat_activity
WHERE state != 'idle' AND query_start < NOW() - INTERVAL '5 minutes';잠금 타임아웃 설정
Oracle:
* DDL_LOCK_TIMEOUT (DDL 대기 시간)
ALTER SESSION SET DDL_LOCK_TIMEOUT = 10; -- 10초
* DML은 FOR UPDATE WAIT n 으로 개별 설정
* 기본적으로 DML은 무한 대기
MySQL:
* innodb_lock_wait_timeout = 50 (기본 50초)
SET innodb_lock_wait_timeout = 10; -- 세션 단위
* lock_wait_timeout = 31536000 (메타데이터 잠금 대기)
PostgreSQL:
* lock_timeout = 0 (기본 무한 대기)
SET lock_timeout = '10s';
* statement_timeout (쿼리 전체 타임아웃)
SET statement_timeout = '30s';
SQL Server:
* SET LOCK_TIMEOUT 10000; (밀리초 단위, 10초)
* -1 = 무한 대기 (기본)잠금과 인덱스의 관계
SKIP LOCKED 패턴 (큐 처리)
-- PostgreSQL / Oracle 계열 예시
-- 여러 워커가 동시에 작업을 처리하는 큐 패턴
-- 워커 1:
SELECT id, payload FROM job_queue
WHERE status = 'PENDING'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
FETCH FIRST 1 ROW ONLY;
-- → 잠긴 행(다른 워커가 처리 중)은 건너뛰고
-- → 잠금 가능한 첫 번째 행을 가져옴
UPDATE job_queue SET status = 'PROCESSING' WHERE id = ?;
COMMIT;
-- 워커 2:
-- 같은 쿼리 실행 → 워커1이 잠근 행을 건너뛰고 다음 행 처리
-- → 대기 없이 병렬 처리 가능!
-- MySQL 8.0+에서는 마지막 줄을 LIMIT 1로 쓰는 편이 자연스럽습니다.
-- SQL Server는 WITH (READPAST, UPDLOCK) 같은 힌트로 유사 패턴을 구성합니다.Oracle: 12c+ (FOR UPDATE SKIP LOCKED)
MySQL: 8.0+ (FOR UPDATE SKIP LOCKED)
PostgreSQL: 9.5+ (FOR UPDATE SKIP LOCKED)
SQL Server: WITH (READPAST) 힌트로 유사 기능정리
| 개념 | 핵심 내용 |
|---|---|
| 공유 락 (S) | 잠금 읽기, 동시 잠금 읽기 허용 |
| 배타 락 (X) | 쓰기 잠금, 모든 다른 잠금 차단 |
| 의도 잠금 | 테이블 수준에서 행 잠금 존재 표시 |
| 2PL | 확장 → 축소 단계로 직렬 가능성 보장 |
| Strict 2PL | 커밋/롤백까지 X-Lock 유지, 연쇄 롤백 위험 축소 |
| 데드락 | 순환 대기, DBMS가 탐지 후 희생자 롤백 |
| Gap Lock | InnoDB의 인덱스 갭 보호, INSERT 진입 제어 |
| Next-Key Lock | Record + Gap Lock 결합, 조건에 따라 적용 |
| SKIP LOCKED | 잠긴 행 건너뛰기, 큐 처리 패턴 |
| 잠금 타임아웃 | DBMS별 설정, 무한 대기 방지 |
| 잠금과 인덱스 | 스캔 범위가 넓으면 잠금 경합도 커질 수 있음 |
락은 데이터 일관성을 지키는 핵심 메커니즘이지만, 대기와 데드락을 만들 수 있습니다. 현대 DBMS는 MVCC와 락을 결합하여 일반 읽기-쓰기 차단을 줄이면서도 쓰기-쓰기 충돌은 락과 충돌 감지로 제어합니다. 락의 범위를 줄이고, 트랜잭션을 짧게 유지하며, 접근 순서를 통일하는 것이 동시성 최적화의 핵심입니다.
1. 트랜잭션은 가능한 짧게 유지
→ 잠금 보유 시간 최소화 → 동시성 향상
2. WHERE 절 컬럼에 인덱스 확인
→ 넓은 스캔과 잠금 경합 가능성 축소
3. 잠금 순서 통일
→ 모든 트랜잭션에서 동일한 순서로 테이블/행 접근
→ 순환 대기 가능성 축소
4. 적절한 격리 수준 선택
→ 불필요하게 높은 격리 수준 = 불필요한 잠금
5. FOR UPDATE NOWAIT 또는 WAIT n 사용
→ 무한 대기 방지, 빠른 실패 후 재시도
6. 대량 DML은 배치 처리
→ 한 번에 100만 행 UPDATE → 잠금 폭발
→ 1000행씩 나눠서 COMMIT → 안전
7. 잠금 모니터링 쿼리 준비
→ 운영 환경에서 잠금 경합 즉시 감지
8. 데드락 로그 분석 습관화
→ 반복되는 데드락 패턴을 찾아 코드 수정다음 절에서는 일반 읽기의 락 의존을 줄여 동시성을 높이는 MVCC를 다루겠습니다.