오라클 데이터베이스를 운영하다 보면 가장 빈번하게 마주치는 대기 이벤트(Wait Event)가 바로 enq: TX - ...로 시작하는 트랜잭션 락(Transaction Enqueue) 경합입니다. 흔히 TX Lock이라고 하면 "두 세션이 같은 로우(Row)를 동시에 수정하려다 발생하는 락"으로만 단순하게 생각하기 쉽습니다.
하지만 오라클 아키텍처 내부를 들여다보면, 서로 전혀 다른 로우를 수정하거나 심지어 단순 INSERT만 수행하는 상황에서도 TX Lock 경합이 발생할 수 있습니다. 오라클 10g부터는 이러한 TX Lock의 발생 원인을 직관적으로 구분할 수 있도록 대기 이벤트를 4가지 세부 유형으로 세분화했습니다.
이번 포스팅에서는 데이터 아키텍트(DA)와 DBA가 반드시 숙지해야 할 TX Lock의 내부 동작 메커니즘과 4가지 주요 대기 이벤트(Row Lock Contention, ITL Shortage, Index Contention, Bitmap Index)의 발생 원리 및 실무 해결 방안을 완벽하게 정리합니다.
1. 개요: TX Lock(Transaction Enqueue)이란 무엇인가?
Enqueue(인큐)는 오라클이 공유 리소스에 대한 동시 접근을 직렬화하기 위해 사용하는 정교한 잠금(Lock) 메커니즘입니다. 래치(Latch)와 달리 FIFO(선입선출) 방식의 대기열(Queue)을 관리하므로, 먼저 요청한 세션이 순서대로 리소스를 획득할 수 있습니다.
그중에서도 TX Lock(Transaction Lock)은 트랜잭션의 원자성(Atomicity)과 일관성(Consistency)을 보장하기 위한 핵심 장치입니다.
- 어떤 세션이 첫 번째 DML(INSERT, UPDATE, DELETE, MERGE, SELECT ... FOR UPDATE)을 수행하여 트랜잭션을 시작하면, 오라클은 해당 트랜잭션에 고유한 XID(Transaction ID: USN + Slot + Sq#)를 부여하고 동시에 TX Lock을 배타적 모드(Exclusive Mode, Mode 6)로 획득합니다.
- 이 TX Lock은 트랜잭션이 COMMIT 또는 ROLLBACK을 수행하여 완전히 종료될 때까지 유지됩니다.
다른 세션이 현재 진행 중인 트랜잭션과 충돌하는 작업을 시도할 때, 요청하는 작업의 성격에 따라 Mode 6(Exclusive) 또는 Mode 4(Share)로 TX Lock을 요청하며 대기열에 들어가게 됩니다. 이때 왜 대기하느냐에 따라 아래 4가지 대기 이벤트로 나뉘게 됩니다.
2. 내부 메커니즘: TX Lock은 오라클 내부에서 어떻게 동작하는가?
오라클은 타 DBMS와 달리 메모리 상에 별도의 거대한 'Lock List'를 두고 로우 레벨 락을 관리하지 않습니다. 대신 데이터 블록 자체에 락 정보를 기록하는 독특한 아키텍처를 가집니다.
2.1. 로우 레벨 락과 ITL(Interested Transaction List)의 연결 고리
- Row Header의 Lock Byte: 테이블의 각 로우 헤더에는 현재 이 로우를 변경 중인 트랜잭션이 블록 헤더의 몇 번 ITL 슬롯을 사용하고 있는지를 가리키는 1바이트 크기의 포인터(Lock Byte)가 존재합니다.
- Block Header의 ITL: 데이터 블록 헤더의 ITL 슬롯에는 해당 블록을 변경 중인 트랜잭션의 XID(Undo 세그먼트 번호 + 트랜잭션 테이블 슬롯 번호 + 시퀀스 번호)와 UBA(Undo Block Address), 그리고 커밋 상태 플래그가 기록됩니다.
- Undo Segment Header의 Transaction Table: ITL에 적힌 XID를 따라가면 UNDO 세그먼트 헤더의 트랜잭션 테이블 슬롯이 나오며, 이곳에 실제 트랜잭션의 최종 상태(Active 또는 Committed)가 기록되어 있습니다.
2.2. TX Lock 대기 발생 과정
세션 B가 특정 블록의 로우를 수정하려고 도달했을 때의 내부 처리 과정입니다.
- 수정하려는 로우의 Lock Byte를 확인합니다. 만약 0이 아니라면 누군가 변경 중이라는 뜻이므로, 가리키는 블록 헤더의 ITL 슬롯을 확인합니다.
- ITL 슬롯에 기록된 XID를 읽고, 해당 UNDO 세그먼트 헤더의 트랜잭션 테이블을 찾아가 트랜잭션(세션 A)이 아직 Active(진행 중) 상태인지 확인합니다.
- 만약 세션 A가 아직 커밋하지 않은 Active 상태라면, 세션 B는 세션 A가 쥐고 있는 XID를 리소스 식별자로 삼아 TX Enqueue 대기열에 줄을 섭니다.
- 이때 무엇 때문에 막혔는지에 따라 요청하는 Lock Mode(Mode 6 vs Mode 4)와 대기 이벤트 명이 결정됩니다.
3. 4가지 주요 TX 대기 이벤트 상세 원인 분석
실무에서 발생하는 4가지 TX Lock 대기 이벤트의 정확한 발생 원리와 요청 모드(Mode)의 차이를 살펴보겠습니다.
3.1. enq: TX - row lock contention (Mode 6 또는 Mode 4)
가장 흔하고 직관적인 락 경합입니다. 앞서 세션 A가 특정 로우를 수정(UPDATE/DELETE)하고 아직 커밋하지 않은 상태에서, 세션 B가 정확히 동일한 로우를 수정하려고 할 때 발생합니다. 이때 세션 B는 Mode 6(Exclusive)으로 TX Lock을 요청하며 대기합니다.
💡 주의: PK/Unique Index 중복 INSERT 시의 Row Lock Contention (Mode 4) 물리적으로 존재하는 같은 로우를 수정할 때만 이 이벤트가 발생하는 것은 아닙니다. 테이블에 PK나 Unique 인덱스가 걸려 있을 때, 세션 A가 'A001'이라는 키 값을 INSERT(미커밋)한 상태에서 세션 B가 똑같은 'A001' 값을 INSERT하면, 세션 B는 *Mode 4(Share)*로 enq: TX - row lock contention 대기에 빠집니다. 세션 A가 커밋하면 세션 B는 ORA-00001(Unique constraint violated) 에러를 맞고, 롤백하면 세션 B의 INSERT가 정상 수행됩니다.
3.2. enq: TX - allocate ITL entry (Mode 4,Share)
서로 전혀 다른 로우를 수정하거나 INSERT하는데도 락 대기가 걸리는 대표적인 케이스입니다.
- 데이터 블록의 데이터를 변경하려면 반드시 블록 헤더의 ITL(Interested Transaction List) 슬롯을 하나씩 할당받아야 합니다.
- 기본 할당된 ITL 슬롯(INITRANS, 기본값 2)이 모두 사용 중이면, 오라클은 블록 내의 여유 공간(PCTFREE 영역)을 사용해 최대 MAXTRANS(10g부터 255 고정)까지 ITL 슬롯을 동적으로 늘립니다(슬롯당 24바이트 소요).
- 하지만 블록 내에 여유 공간이 전혀 없어서 더 이상 새로운 ITL 슬롯을 생성할 수 없을 때, 해당 블록의 다른 로우를 변경하려는 새로운 트랜잭션은 기존에 ITL 슬롯을 점유하고 있는 트랜잭션 중 하나가 끝나기를 기다리며 Mode 4(Share)로 대기하게 됩니다.
3.3. enq: TX - index contention (Mode 4, Share)
대규모 동시 트랜잭션이 몰리는 OLTP 환경에서 인덱스 구조가 변경될 때 발생하는 내부 경합입니다.
- 세션들이 테이블에 데이터를 INSERT하면 인덱스 리프 블록(Leaf Block)에도 정렬된 순서대로 키 값을 넣어야 합니다.
- 만약 특정 인덱스 리프 블록이 가득 차서 더 이상 공간이 없으면, 오라클은 새로운 블록을 할당받아 기존 데이터를 나누어 담는 Index Block Split(인덱스 블록 분할, 50-50 또는 90-10 Split) 작업을 수행합니다.
- 세션 A가 인덱스 블록을 분할하는 도중(자율 트랜잭션 형태로 수행됨), 세션 B가 분할 중인 바로 그 인덱스 블록에 새로운 키 값을 INSERT하려고 진입하면, 블록 분할이 완료될 때까지 Mode 4(Share)로 enq: TX - index contention 대기를 겪게 됩니다. 특히 시퀀스 기반의 우편향(Right-Hand) 인덱스에서 극심하게 발생합니다.
3.4. enq: TX - bitmap index contention (Mode 4, Share) (특수 케이스)
이름 그대로는 별도 이벤트가 아닌, enq: TX - row lock contention (Mode 4)로 나타나지만 원인이 비트맵 인덱스(Bitmap Index)에 있는 치명적인 경합입니다.
- B-Tree 인덱스는 인덱스 엔트리 하나가 하나의 ROWID만 가리키지만, Bitmap Index는 하나의 인덱스 엔트리가 특정 키 값(예: 성별 = 'M')을 가진 수천~수만 개의 ROWID 범위(Start ROWID ~ End ROWID)를 압축된 비트맵(0과 1) 형태로 묶어서 관리합니다.
- 따라서 세션 A가 단 1건의 로우를 수정하거나 INSERT하더라도, 해당 로우가 포함된 비트맵 인덱스 청크(Chunk) 전체에 락이 걸립니다.
- 이때 세션 B가 전혀 다른 로우를 수정하더라도, 그 로우가 같은 비트맵 범위 내에 속해 있다면 Mode 4(Share)로 TX Lock 대기에 빠집니다. OLTP 환경에서 비트맵 인덱스를 절대 사용하면 안 되는 결정적인 이유입니다.
4. 트러블슈팅 및 해결 방안
실무에서 TX Lock 대기가 발생했을 때, 어떤 유형(Mode 6 vs Mode 4)인지 즉시 판별하고 홀더(Blocker)와 대상 오브젝트를 찾아 해결하는 단계별 가이드입니다.
4.1. 1단계: 실시간 TX Lock 대기 세션 및 Lock Mode(6 vs 4) 진단 쿼리
가장 먼저 현재 대기 중인 세션이 요청하는 락 모드(REQUEST)가 6(Exclusive)인지 4(Share)인지 확인하여 원인을 좁혀야 합니다.
-- 실시간 TX Lock Blocker(홀더)와 Waiter(대기자) 및 요청 모드 확인 쿼리
SELECT DECODE(l.REQUEST, 0, '🔴 Holder: ', '⚪ Waiter: ') || l.SID AS SESS_ROLE,
s.SERIAL#,
s.USERNAME,
s.PROGRAM,
s.SQL_ID,
s.EVENT,
l.ID1 AS USN_SLOT,
l.ID2 AS SEQ,
l.LMODE AS HELD_MODE, -- 6: Exclusive
l.REQUEST AS REQ_MODE, -- 6: 일반 Row Lock, 4: PK중복/ITL/Index/Bitmap 등
l.CTIME AS WAIT_TIME_SEC,
s.ROW_WAIT_OBJ# AS TARGET_OBJ_ID,
s.ROW_WAIT_FILE# AS FILE_ID,
s.ROW_WAIT_BLOCK# AS BLOCK_ID,
s.ROW_WAIT_ROW# AS ROW_ID
FROM V$LOCK l,
V$SESSION s
WHERE l.SID = s.SID
AND l.TYPE = 'TX'
AND (l.ID1, l.ID2) IN (SELECT ID1, ID2 FROM V$LOCK WHERE TYPE = 'TX' AND REQUEST > 0)
ORDER BY l.ID1, l.REQUEST;
위 쿼리에서 추출된 TARGET_OBJ_ID(ROW_WAIT_OBJ#)를 아래 쿼리에 넣으면 정확히 어떤 테이블이나 인덱스에서 경합이 났는지, 심지어 어떤 로우(ROWID)가 문제인지까지 추적할 수 있습니다. (단, Mode 4 대기의 경우 ROW_WAIT_OBJ#가 -1로 나오는 경우가 많으므로 이때는 ASH의 CURRENT_OBJ#를 함께 확인해야 합니다.)
-- 경합 대상 오브젝트 및 실제 충돌 ROWID 추출 (Mode 6 Row Lock 시 유효)
SELECT o.OWNER,
o.OBJECT_NAME,
o.OBJECT_TYPE,
DBMS_ROWID.ROWID_CREATE(1, o.DATA_OBJECT_ID, &FILE_ID, &BLOCK_ID, &ROW_ID) AS TARGET_ROWID
FROM DBA_OBJECTS o
WHERE o.OBJECT_ID = &TARGET_OBJ_ID;
4.2. 2단계: 4가지 유형별 맞춤 해결 전략
① enq: TX - row lock contention 해결 방안
- Mode 6 (일반 로우 충돌): 전형적인 애플리케이션 설계 및 트랜잭션 관리 문제입니다.
- 트랜잭션 내에서 외부 API 호출(결제, 메일 발송 등)이나 무거운 조회 작업을 수행하느라 COMMIT이 지연되는 구간을 찾아 트랜잭션 단위를 짧게 쪼개야 합니다.
- 채번 테이블(UPDATE TB_SEQ SET SEQ = SEQ + 1)을 사용하고 있다면 즉시 오라클 시퀀스(Sequence)로 전환해야 합니다.
- 데드락(ORA-00060)이나 장기 락을 유발하는 미커밋 세션은 현업 확인 후 ALTER SYSTEM KILL SESSION 'SID,SERIAL#' IMMEDIATE;로 강제 종료합니다.
- Mode 4 (PK 중복 또는 FK 인덱스 누락):
- 외래키(Foreign Key) 인덱스 생성 점검: 부모 테이블의 PK를 수정하거나 삭제할 때 자식 테이블의 FK 컬럼에 인덱스가 없으면, 자식 테이블 전체에 Table Lock이나 불필요한 경합이 발생합니다. 모든 FK 컬럼에는 반드시 인덱스를 생성해야 합니다.
② enq: TX - allocate ITL entry 해결 방안
특정 블록에 동시 DML이 집중되는데 블록 여유 공간이 부족해 ITL 슬롯을 못 늘리는 상황이므로, 초기 보장 ITL 슬롯 개수(INITRANS)와 블록 여유 공간 비율(PCTFREE)을 상향 조정해야 합니다.
-- ITL 경합이 발생한 세그먼트 확인 (V$SEGSTAT 활용)
SELECT OWNER, OBJECT_NAME, OBJECT_TYPE, VALUE AS ITL_WAITS
FROM V$SEGSTAT
WHERE STATISTIC_NAME = 'ITL waits'
AND VALUE > 0
ORDER BY VALUE DESC;
-- 해결: 테이블 및 인덱스의 INITRANS와 PCTFREE 상향 변경
ALTER TABLE TB_ORDER_DTL INITRANS 10 PCTFREE 20;
ALTER INDEX IDX_ORDER_DTL_01 INITRANS 20;
💡 DBA 실무 주의사항: 단순히 ALTER TABLE ... INITRANS 10; 명령만 수행하면, 이후에 새로 할당되는 블록부터만 변경된 설정이 적용됩니다. 이미 데이터가 들어있는 기존 블록들의 ITL 슬롯까지 즉시 늘려주려면 반드시 테이블 재구성(ALTER TABLE ... MOVE)과 인덱스 리빌드(ALTER INDEX ... REBUILD) 작업을 병행해야 효과가 나타납니다.
③ enq: TX - index contention 해결 방안
인덱스 블록 분할(Block Split)이 집중되는 것을 막고, 분할 작업 자체가 빠르게 끝나도록 유도해야 합니다.
- Global Hash Partitioned Index 적용 (가장 확실한 해결책): 순차 증가하는 시퀀스 기반 인덱스의 가장 오른쪽 리프 블록(Rightmost Leaf Block)에 집중되는 90-10 Split을 막기 위해, 인덱스를 해시 파티션(예: 16개 또는 32개)으로 쪼개어 INSERT 부하를 물리적으로 분산시킵니다.
- 시퀀스 CACHE 사이즈 대폭 상향: 인덱스 키 값으로 사용되는 시퀀스의 CACHE 크기가 기본값(20)으로 되어 있다면, 동시 INSERT 시 enq: SQ - contention과 함께 인덱스 경합이 심화됩니다. 고부하 환경에서는 시퀀스 캐시를 최소 1,000 ~ 10,000 이상으로 넉넉히 부여해야 합니다.
- 불필요한 인덱스 삭제 및 리빌드: 거의 사용되지 않으면서 DML 시 Split 부하만 주는 과도한 인덱스를 정리합니다. 또한 대량 DELETE 이후 빈 공간(Deleted Leaf Row)이 방치된 왜곡된 인덱스는 ALTER INDEX ... REBUILD ONLINE 또는 COALESCE를 통해 정리해 줍니다.
④ enq: TX - bitmap index contention 해결 방안
- OLTP 환경에서 Bitmap Index 즉시 제거: 동시 DML이 발생하는 온라인 트랜잭션 테이블에 생성된 비트맵 인덱스는 아키텍처상의 설계 오류입니다. 즉시 일반 B-Tree Index로 전환해야 합니다.
- DW/배치 환경에서의 대량 적재 수칙: 데이터 웨어하우스(DW) 환경에서 불가피하게 비트맵 인덱스를 사용한다면, 대량 데이터를 적재(ETL)할 때는 반드시 비트맵 인덱스를 UNUSABLE 상태로 변경(또는 Drop)한 뒤, 적재가 완료된 후 일괄 REBUILD하는 방식으로 파이프라인을 구성해야 합니다.
5. 요약
- TX Lock(Transaction Enqueue)은 트랜잭션의 무결성을 지키기 위해 첫 DML 시 획득하여 COMMIT/ROLLBACK 시까지 유지하는 락이며, 대기 원인에 따라 요청 모드(Mode 6 vs Mode 4)와 세부 대기 이벤트가 달라진다.
- enq: TX - row lock contention은 동일한 로우를 동시에 수정하려 할 때(Mode 6), 또는 PK/Unique 키 값 중복 INSERT 시(Mode 4) 발생하며, 트랜잭션 길이를 단축하고 채번 테이블을 시퀀스로 교체하여 해결한다.
- enq: TX - allocate ITL entry (Mode 4)는 블록 내 여유 공간 부족으로 트랜잭션 슬롯(ITL)을 할당받지 못해 발생하며, INITRANS와 PCTFREE를 높인 후 세그먼트를 MOVE/REBUILD하여 해결한다.
- enq: TX - index contention (Mode 4)은 동시 INSERT 중 인덱스 리프 블록 분할(Block Split) 과정에서 발생하며, 우편향 인덱스를 Global Hash Partitioned Index로 분산 설계하여 해결한다.
- Bitmap Index 경합 (Mode 4)은 하나의 비트맵 엔트리가 수천 개의 로우를 묶어 관리하는 구조적 특성 때문에 발생하므로, 동시 DML이 발생하는 OLTP 환경에서는 절대 사용해서는 안 된다.
