DB Study

8장. 트랜잭션과 락

Oracle의 행 락은 별도의 락 매니저 메모리가 아니라 데이터 블록 안(ITL)에 삽니다. 그래서 락이 아무리 많아져도 에스컬레이션이 없습니다 — 이 설계 하나가 Oracle 동시성의 성격을 결정합니다.

중급 ⏱ 약 30분 🎬 애니메이션 3개 선수 지식: 5장 블록 구조, 6장 읽기 일관성
🧭
이 장에서 배우는 것

① Oracle 락의 특징 — 행 락은 블록 속 ITL에 기록, 락 에스컬레이션 없음 ② TX/TM 락과 SELECT FOR UPDATE ③ deadlock(ORA-00060)의 형성과 자동 감지 ④ v$session으로 락 대기 진단, 그리고 격리 수준(READ COMMITTED / SERIALIZABLE)

8.1 Oracle 락의 철학 — 락은 블록 안에 산다

많은 DBMS는 "어느 세션이 어느 행을 잠갔나"를 메모리의 락 테이블에서 중앙 관리합니다. 이 방식의 약점은 락이 많아지면 락 테이블이 메모리를 잠식한다는 것 — 그래서 일부 DBMS는 행 락 수천 개를 페이지 락·테이블 락 하나로 뭉치는 락 에스컬레이션(lock escalation)을 합니다. 동시성이 갑자기 나빠지는 원인이죠.

Oracle의 설계는 다릅니다. 행 락은 그 행이 사는 데이터 블록 안에 기록됩니다. 5장에서 본 블록 헤더의 ITL(Interested Transaction List)에 트랜잭션이 이름을 올리고, 각 행은 자신을 잠근 트랜잭션의 ITL 슬롯 번호를 가리킬 뿐입니다. 그 결과:

💡
그리고 SELECT는 락을 잡지 않는다

6장의 읽기 일관성 덕분에 일반 SELECT는 어떤 락도 잡지 않고, 어떤 락도 기다리지 않습니다. 읽기는 쓰기를 막지 않고 쓰기는 읽기를 막지 않습니다. Oracle에서 락 경합은 오직 쓰기 vs 쓰기, 그것도 같은 행을 두고서만 일어납니다.

8.2 TX 락과 TM 락 — DML 한 번에 락 두 종류

UPDATE 한 문장을 실행하면 Oracle은 두 종류의 락을 잡습니다.

이름역할
TX트랜잭션 락 (row lock)트랜잭션당 1개. 그 트랜잭션이 고친 모든 행의 "주인 표식". 다른 세션이 같은 행을 고치려 하면 주인의 TX 락이 풀리기를 대기합니다(enqueue).
TM테이블 락 (DML lock)DML이 건드린 테이블마다 1개(보통 Row Exclusive 모드). 행을 고치는 도중에 누가 테이블을 DROP하거나 구조를 바꾸는 것을 막습니다.

"행 락 대기"의 실체는 이렇습니다: 세션 B가 세션 A가 잠근 행을 고치려 하면, B는 그 행이 아니라 A의 TX 락에 줄을 섭니다. A가 COMMIT/ROLLBACK으로 TX 락을 풀면 B가 깨어납니다. 대기 이벤트 이름도 enq: TX - row lock contention입니다.

8.3 같은 행 vs 다른 행 — 대기는 언제 생기나

세션 A UPDATE ... WHERE id=1 세션 B UPDATE ... WHERE id=1 ACCOUNTS 테이블 id=1 bal=100 🔒 TX-A id=2 bal=500 (락 없음) ⛔ 대기! enq: TX - row lock contention id=2 bal=500 🔒 TX-C (즉시 성공) 세션 C WHERE id=2 ✅ A: COMMIT → TX 락 해제 🔓 id=1 bal=200 🔒 TX-B 획득! ▶ B 진행 재개 행 락에는 타임아웃이 없다 — A가 커밋을 미루면 B는 하염없이 기다린다

8.4 ITL 슬롯 — 행 락의 실제 저장소

블록 헤더의 ITL은 "이 블록에서 지금 작업 중인 트랜잭션 명단"입니다. 슬롯 초기 개수는 INITRANS(테이블 기본 1)로 정해지고, 필요하면 블록의 free space를 먹으며 늘어납니다(최대 255). 이 슬롯이 모자라면 어떻게 될까요?

데이터 블록 ITL (트랜잭션 명단) 슬롯1: TX-A (undo 주소) 슬롯2: TX-B (새로 확장) 빈 슬롯 (필요시 확장) row 1: lb=1 → 슬롯1(TX-A)이 잠금 row 2: lb=0 (락 없음) row 2: lb=2 → 슬롯2(TX-B)가 잠금 row 3: lb=0 (락 없음) row 3: 세션 C가 잠그려는데... ITL 슬롯 부족! free space도 없어 확장 불가 C는 빈 슬롯이 날 때까지 대기 enq: TX - allocate ITL entry 해법 동시 DML 많은 테이블은 INITRANS 상향 + PCTFREE 여유 (재구성 필요)

8.5 SELECT FOR UPDATE — 읽으면서 미리 잠그기

"읽고 → 판단하고 → 고치는" 로직에서는 읽기와 쓰기 사이에 다른 세션이 끼어들 수 있습니다. 읽는 시점에 행을 잠그고 싶다면 SELECT ... FOR UPDATE를 씁니다. 대기 방식도 고를 수 있습니다.

-- 기본: 행 락을 얻을 때까지 무한 대기
SELECT bal FROM accounts WHERE id = 1 FOR UPDATE;

-- 잠겨 있으면 즉시 에러 (ORA-00054)
SELECT bal FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;

-- 5초만 기다려보고 포기 (ORA-30006)
SELECT bal FROM accounts WHERE id = 1 FOR UPDATE WAIT 5;

-- 잠긴 행은 건너뛰고 잠글 수 있는 행만 — 작업 큐 패턴의 단골
SELECT * FROM job_queue WHERE status = 'READY'
FOR UPDATE SKIP LOCKED;

8.6 Deadlock — ORA-00060과 Oracle의 자동 심판

두 세션이 서로가 잡은 행을 서로 기다리면 아무도 영원히 진행할 수 없습니다 — deadlock입니다. Oracle은 이를 자동 감지해 한쪽의 문장을 강제로 실패시킵니다.

세션 A TX 진행 중 세션 B TX 진행 중 row X (id=1) row Y (id=2) A가 X 잠금 🔒 B가 Y 잠금 🔒 A가 Y를 원함 ⏳ B가 X를 원함 ⏳ 🔄 상호 대기 완성 = deadlock ORA-00060 → A의 문장만 롤백 (TX는 유지) Oracle이 대기 그래프의 순환을 감지 — DBA 개입 불필요
⚠️
deadlock은 Oracle 탓이 아니라 코드 탓

deadlock의 90%는 두 트랜잭션이 같은 자원들을 서로 다른 순서로 잠그기 때문입니다. 해법도 애플리케이션에 있습니다: ① 여러 행/테이블을 갱신할 때 항상 같은 순서(예: PK 오름차순)로 ② 트랜잭션은 짧게 ③ 외래키 컬럼에 인덱스 생성(없으면 자식 테이블 DML이 부모에 더 넓은 TM 락을 유발해 deadlock 단골 원인이 됩니다).

8.7 락 대기 진단 — 범인은 v$session에 있다

"화면이 멈췄어요"라는 신고가 오면, 누가 누구를 막고 있는지부터 찾습니다. v$sessionblocking_session 컬럼이 정답을 바로 알려줍니다.

-- 지금 락에 막혀 있는 세션과, 그를 막고 있는 범인
SELECT sid, serial#, username, event,
       blocking_session,                    -- 나를 막는 세션의 SID
       final_blocking_session,              -- 대기 사슬 맨 끝의 최종 범인
       seconds_in_wait
FROM   v$session
WHERE  blocking_session IS NOT NULL;

 SID  USERNAME  EVENT                              BLOCKING_SESSION
----  --------  ---------------------------------  ----------------
 152  APP_USER  enq: TX - row lock contention                   34   ← 34번이 범인

-- 범인(34번)이 뭘 하는 중인지 확인 후, 응답 없으면 강제 종료
SELECT sid, serial#, status, sql_id, last_call_et FROM v$session WHERE sid = 34;
ALTER SYSTEM KILL SESSION '34,56789';   -- 'sid,serial#' — 최후의 수단!
🔬
대기 사슬이 길 때는 final_blocking_session

A를 B가 막고, B를 C가 막는 연쇄 대기에서는 중간 세션이 아니라 사슬 끝의 최종 범인을 찾아야 합니다. final_blocking_session이 그 답이고, 12장에서 배울 AWR/ASH로는 과거의 락 대기 이력까지 추적할 수 있습니다.

8.8 격리 수준 — Oracle의 선택지

Oracle의 기본 격리 수준은 READ COMMITTED입니다. 문장 하나하나가 자기 시작 시점의 SCN으로 읽기 일관성을 보장받습니다(문장 수준 일관성). 더 강한 보장이 필요하면 SERIALIZABLE로 올려 트랜잭션 시작 시점의 스냅샷을 트랜잭션 끝까지 고정할 수 있습니다.

격리 수준일관성 범위특징
READ COMMITTED (기본)문장 단위각 문장이 시작 SCN 기준으로 읽음. 같은 TX 안에서 두 번 읽으면 다른 결과 가능(non-repeatable read).
SERIALIZABLE트랜잭션 단위TX 시작 시점 스냅샷 고정. 그 후 다른 TX가 커밋한 행을 고치려 하면 ORA-08177: can't serialize access — 재시도 로직 필수.
READ ONLY트랜잭션 단위SERIALIZABLE의 읽기 전용 변형. 장시간 리포트의 일관성 확보용 (DML 불가).

눈치채셨나요? READ UNCOMMITTED가 없습니다. 읽기 일관성이 공짜(락 없음)로 제공되는 Oracle에서는 dirty read를 허용할 이유가 아예 없기 때문입니다. 또 REPEATABLE READ도 별도로 제공하지 않습니다 — 필요하면 SERIALIZABLE 또는 SELECT FOR UPDATE로 해결합니다.

-- 트랜잭션 단위 스냅샷 고정
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET bal = bal - 100 WHERE id = 1;
-- 이 사이 다른 TX가 id=1을 고치고 커밋했다면 → ORA-08177 발생, 재시도 필요
COMMIT;

✍️ 이해도 체크

Oracle에 락 에스컬레이션(행 락 → 테이블 락 승격)이 없는 구조적 이유는?
✅ 에스컬레이션은 "락 메모리 절약"이 목적인데, Oracle은 락 정보를 블록 헤더의 ITL과 행의 락 바이트에 분산 저장하므로 락이 100만 개여도 중앙 메모리 비용이 없습니다. 절약할 것이 없으니 승격도 없습니다. 255는 블록당 ITL 슬롯의 최대치이지 세션당 행 락 제한이 아닙니다.
deadlock이 발생해 ORA-00060을 받은 세션에 대한 설명으로 옳은 것은?
✅ Oracle은 deadlock을 자동 감지하지만 롤백하는 것은 문장 하나뿐입니다. 그 세션의 트랜잭션(이미 잡은 다른 락 포함)은 그대로 살아 있으므로, 에러를 받은 애플리케이션이 명시적으로 ROLLBACK하고 재시도해야 상대 세션도 풀립니다. 감지가 자동이므로 DBA 개입은 불필요합니다.
📚
원문으로 더 깊이

Oracle Database 19c Concepts Guide: Data Concurrency and Consistency — 락 종류 전체 목록, deadlock 감지, 격리 수준의 정식 정의 · Transactions — 트랜잭션 구조와 자율 트랜잭션까지.