icon

안동민 개발노트

11장 : 동시성 제어

락과 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별 데드락 처리

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)을 사용합니다.

InnoDB 잠금 유형
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)
Oracle 명시적 락
-- 행 수준 락 (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;
Oracle 테이블 수준 잠금
-- 테이블 전체 공유 잠금
LOCK TABLE orders IN SHARE MODE;

-- 테이블 전체 배타 잠금 (DDL 보호 등)
LOCK TABLE orders IN EXCLUSIVE MODE;

-- NOWAIT으로 즉시 실패
LOCK TABLE orders IN EXCLUSIVE MODE NOWAIT;

Oracle에서 SELECT는 락을 걸지 않습니다 (MVCC 덕분). 읽기는 절대 쓰기를 차단하지 않고, 쓰기도 읽기를 차단하지 않습니다.


잠금 모니터링

Oracle 잠금 모니터링
-- 현재 잠금 대기 확인
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 잠금 모니터링
-- 현재 잠금 대기 확인 (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 섹션 확인
PostgreSQL 잠금 모니터링
-- 잠금 대기 현황
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';

잠금 타임아웃 설정

DBMS별 잠금 타임아웃
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 패턴 (큐 처리)

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이 잠근 행을 건너뛰고 다음 행 처리
-- → 대기 없이 병렬 처리 가능!
SKIP LOCKED 지원 현황
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 2PLX-Lock을 COMMIT까지 유지, 연쇄 복귀 방지
데드락순환 대기, DBMS가 탐지 후 희생자 롤백
Gap LockInnoDB의 범위 잠금, Phantom Read 방지
Next-Key LockRecord + 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를 다루겠습니다.