락과 2단계 잠금
격리 수준을 구현하는 가장 기본적인 메커니즘이 락(Lock)입니다. 누가 데이터를 사용하고 있으면 다른 사람은 기다리는 방식입니다. 락의 종류, 적용 범위, 데드락 처리까지 이해해야 실무에서 동시성 문제를 진단할 수 있습니다.
공유 락과 배타 락
| 락 유형 | 약어 | 용도 | 호환성 |
|---|---|---|---|
| 공유 락 (Shared Lock) | S-Lock | 읽기 (SELECT) | S-Lock끼리 호환 (동시 읽기 가능) |
| 배타 락 (Exclusive Lock) | X-Lock | 쓰기 (UPDATE, DELETE) | 다른 모든 락과 불호환 |
이미 걸린 락
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 (테이블 공유 + 일부 행 배타)잠금 범위: 행, 페이지, 테이블
2단계 잠금 프로토콜 (2PL)
2단계 잠금(Two-Phase Locking, 2PL)은 직렬 가능성을 보장하는 잠금 프로토콜입니다.
2PL을 지키면 충돌 직렬 가능성(Conflict Serializability)이 보장됩니다. 하지만 데드락이 발생할 수 있습니다.
2PL의 변형
데드락
데드락(Deadlock, 교착 상태)은 두 트랜잭션이 서로가 가진 락을 기다리며 영원히 대기하는 상태입니다.
데드락 탐지
DBMS는 데드락을 탐지(Detection)하여 해결합니다. 주기적으로 대기 그래프(Wait-For Graph)를 검사하고, 사이클이 발견되면 한 트랜잭션을 강제 ROLLBACK합니다.
DBMS별 데드락 처리
Oracle:
* 데드락 발생 즉시 탐지 (대기 중인 트랜잭션이 스스로 확인)
* 하나의 DML 문만 롤백 (전체 트랜잭션은 아님!)
* ORA-00060: deadlock detected 에러 발생
* 애플리케이션에서 명시적 ROLLBACK 또는 재시도 필요
MySQL InnoDB:
* 5초마다 또는 대기 시작 시 탐지
* 전체 트랜잭션 롤백
* ERROR 1213: Deadlock found 에러
* innodb_lock_wait_timeout (기본 50초)
PostgreSQL:
* 1초마다 탐지 (deadlock_timeout, 기본 1초)
* 전체 트랜잭션 롤백
* ERROR: deadlock detected
SQL Server:
* 5초마다 탐지
* 전체 트랜잭션 롤백
* SET DEADLOCK_PRIORITY로 희생자 우선순위 설정 가능데드락 예방과 대응
| 방법 | 설명 |
|---|---|
| 일관된 접근 순서 | 모든 트랜잭션이 테이블 A → B 순서로 접근 |
| 트랜잭션 짧게 | 락 점유 시간을 최소화 |
| 타임아웃 설정 | 대기 시간 초과 시 롤백 |
| 인덱스 최적화 | 인덱스 사용으로 락 범위 축소 |
| NOWAIT 사용 | 대기 대신 즉시 실패 처리 |
1. 접근 순서 통일:
모든 트랜잭션에서 accounts → orders → items 순서로 접근
→ 순환 대기 불가능 → 데드락 원천 차단
2. SELECT FOR UPDATE NOWAIT:
잠금 획득 실패 시 즉시 에러 → 애플리케이션에서 재시도
→ 대기 없이 빠른 실패
3. 인덱스를 통한 잠금 범위 최소화:
인덱스 없이: UPDATE WHERE status='ACTIVE' → 전체 테이블 스캔
→ 불필요한 행까지 잠금!
인덱스 있으면: 해당 행만 잠금 → 데드락 확률 감소MySQL InnoDB의 갭 락과 넥스트키 락
MySQL InnoDB는 행 수준 잠금 외에 갭 락(Gap Lock)과 넥스트키 락(Next-Key Lock)을 사용합니다.
Record Lock
* 인덱스 레코드에 대한 잠금
* WHERE id = 10 → id=10 레코드만 잠금
Gap Lock
* 인덱스 레코드 사이의 빈 공간에 대한 잠금
* 해당 범위에 새 행 INSERT를 차단
* REPEATABLE READ 이상에서 사용
Next-Key Lock
* Record Lock + Gap Lock 결합
* 레코드 자체 + 그 앞의 갭을 함께 잠금
* InnoDB의 기본 잠금 방식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는 락을 걸지 않습니다 (MVCC 덕분). 읽기는 절대 쓰기를 차단하지 않고, 쓰기도 읽기를 차단하지 않습니다.
잠금 모니터링
-- 현재 잠금 대기 확인
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 섹션 확인-- 잠금 대기 현황
SELECT blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_query
FROM pg_catalog.pg_locks blocked_locks
JOIN pg_catalog.pg_locks blocking_locks
ON blocking_locks.locktype = blocked_locks.locktype
WHERE NOT blocked_locks.granted;
-- 오래 대기중인 쿼리
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 패턴 (큐 처리)
-- 여러 워커가 동시에 작업을 처리하는 큐 패턴
-- 워커 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이 잠근 행을 건너뛰고 다음 행 처리
-- → 대기 없이 병렬 처리 가능!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을 COMMIT까지 유지, 연쇄 복귀 방지 |
| 데드락 | 순환 대기, DBMS가 탐지 후 희생자 롤백 |
| Gap Lock | InnoDB의 범위 잠금, Phantom Read 방지 |
| 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를 다루겠습니다.