8장. 트랜잭션과 락
Oracle의 행 락은 별도의 락 매니저 메모리가 아니라 데이터 블록 안(ITL)에 삽니다. 그래서 락이 아무리 많아져도 에스컬레이션이 없습니다 — 이 설계 하나가 Oracle 동시성의 성격을 결정합니다.
① 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 슬롯 번호를 가리킬 뿐입니다. 그 결과:
- 락 개수에 비례하는 중앙 메모리가 없습니다. 1행을 잠그든 1억 행을 잠그든 별도 락 메모리 비용은 사실상 같습니다.
- 그래서 락 에스컬레이션이 존재하지 않습니다. 행 락이 테이블 락으로 승격되는 일은 Oracle에 없습니다.
- 락 정보가 블록과 함께 디스크에 쓰이고 캐시됩니다 — 커밋 후 뒷정리(block cleanout)도 블록 단위로 이뤄집니다.
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가 id=1 행을 UPDATE합니다. 블록의 ITL에 A의 트랜잭션이 등록되고 행에 락 표식이 남습니다. A는 아직 커밋하지 않았습니다.
세션 B가 같은 행을 UPDATE하려 합니다. 행의 주인 TX-A가 살아있으므로 B는 A의 TX 락에 줄을 서서 대기합니다. 대기 이벤트: enq: TX - row lock contention.
반면 세션 C가 다른 행(id=2)을 UPDATE하면? 즉시 성공. Oracle의 락은 정확히 행 단위이고 에스컬레이션이 없으므로, 다른 행끼리는 절대 부딪히지 않습니다.
A가 COMMIT하는 순간 TX 락이 풀리고 B가 깨어나 UPDATE를 완수합니다. 주의: 행 락 대기에는 타임아웃이 없습니다. A가 커밋 없이 퇴근하면 B는 밤새 기다립니다 — 트랜잭션은 짧게!
8.4 ITL 슬롯 — 행 락의 실제 저장소
블록 헤더의 ITL은 "이 블록에서 지금 작업 중인 트랜잭션 명단"입니다. 슬롯 초기 개수는 INITRANS(테이블 기본 1)로 정해지고, 필요하면 블록의 free space를 먹으며 늘어납니다(최대 255). 이 슬롯이 모자라면 어떻게 될까요?
TX-A가 row 1을 고치면: ①ITL 슬롯1에 자기 트랜잭션 ID와 undo 주소를 등록하고 ②row 1의 락 바이트(lb)가 슬롯1을 가리킵니다. 이 두 표식이 행 락의 전부 — 중앙 락 테이블은 없습니다.
TX-B가 row 2를 고치려면 자기 슬롯이 필요합니다. 빈 슬롯이 없으면 free space를 조금 먹어 슬롯2를 확장해 등록합니다(INITRANS 초과분, 최대 255).
그런데 블록이 꽉 차서 free space가 없다면? 세션 C는 잠글 행(row 3)이 비어 있는데도 명단에 이름을 못 올려 대기합니다 — enq: TX - allocate ITL entry 대기의 정체입니다.
동시 DML이 몰리는 테이블/인덱스에서 이 대기가 보이면 INITRANS를 높이고 PCTFREE로 여유 공간을 확보한 뒤 재구성(MOVE/REBUILD)하는 것이 정석입니다.
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가 row X를, 세션 B가 row Y를 각각 UPDATE했습니다. 아직 아무도 커밋하지 않았고, 여기까지는 정상입니다.
세션 A가 이번엔 row Y를 UPDATE하려 합니다. Y의 주인은 B — A는 B의 TX 락에 줄을 서서 기다립니다.
세션 B가 row X를 UPDATE하려 합니다. X의 주인은 A... 서로가 서로를 기다리는 순환이 완성됐습니다. 이대로면 둘 다 영원히 멈춥니다.
Oracle이 대기 그래프의 순환을 자동 감지하고, 감지한 세션(A)의 현재 문장 하나만 롤백시키며 ORA-00060을 던집니다. 트랜잭션 전체가 아니라 문장만 롤백이므로, 애플리케이션은 에러를 받고 전체 ROLLBACK 후 재시도하도록 짜야 합니다. 상세 내역은 트레이스 파일에 남습니다.
deadlock의 90%는 두 트랜잭션이 같은 자원들을 서로 다른 순서로 잠그기 때문입니다. 해법도 애플리케이션에 있습니다: ① 여러 행/테이블을 갱신할 때 항상 같은 순서(예: PK 오름차순)로 ② 트랜잭션은 짧게 ③ 외래키 컬럼에 인덱스 생성(없으면 자식 테이블 DML이 부모에 더 넓은 TM 락을 유발해 deadlock 단골 원인이 됩니다).
8.7 락 대기 진단 — 범인은 v$session에 있다
"화면이 멈췄어요"라는 신고가 오면, 누가 누구를 막고 있는지부터 찾습니다. v$session의 blocking_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#' — 최후의 수단!
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 Database 19c Concepts Guide: Data Concurrency and Consistency — 락 종류 전체 목록, deadlock 감지, 격리 수준의 정식 정의 · Transactions — 트랜잭션 구조와 자율 트랜잭션까지.