2026년 05월 22일 | Oracle DBA 가이드
?? 이 글에서 다루는 내용
ORA-00060 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.
ORA-00060란?
ORA-00060은 Oracle 데이터베이스에서 두 개 이상의 세션이 서로 상대방이 보유한 리소스(주로 테이블 행 잠금)를 기다리는 교착 상태(Deadlock)가 발생했을 때 나타나는 에러입니다. 예를 들어 세션 A가 행1을 잠그고 행2를 기다리는 동안, 세션 B는 행2를 잠근 채 행1을 기다리는 상황이 전형적인 사례입니다. Oracle은 이 상태를 자동으로 감지하여 두 세션 중 하나를 희생자(Victim)로 선택하고 해당 트랜잭션을 롤백하면서 이 에러를 발생시킵니다. 중요한 점은 Oracle이 전체 트랜잭션이 아닌 마지막 DML 문장 하나만 롤백한다는 사실이며, 따라서 애플리케이션 레벨에서 반드시 에러를 처리하고 적절히 대응해야 합니다.
주요 발생 원인
1. 서로 다른 순서로 동일한 리소스에 접근하는 트랜잭션
가장 빈번한 원인으로, 여러 트랜잭션이 동일한 테이블의 행들을 서로 다른 순서로 잠금(Lock)을 획득할 때 발생합니다. 예를 들어 주문 처리 로직에서 하나의 프로세스는 ORDER → ORDER_ITEM 순서로, 다른 프로세스는 ORDER_ITEM → ORDER 순서로 업데이트를 시도하면 교착 상태가 즉시 발생합니다. 이는 애플리케이션 설계 단계에서 잠금 획득 순서를 통일하지 않은 경우에 빈번히 나타납니다.
2. 인덱스 부재로 인한 불필요한 전체 테이블 잠금
외래 키(Foreign Key) 컬럼에 인덱스가 없을 경우, 부모 테이블의 행을 업데이트하거나 삭제할 때 Oracle은 자식 테이블 전체에 테이블 레벨 잠금(Table-Level Lock)을 걸게 됩니다. 이 상황에서 다른 세션이 자식 테이블에 DML을 시도하면 잠금 충돌이 발생하고 교착 상태로 이어집니다. 특히 대용량 OLTP 환경에서 이 원인은 성능 저하와 함께 데드락을 폭발적으로 증가시키는 주범입니다.
3. 비트맵 인덱스(Bitmap Index)가 있는 테이블에 대한 동시 DML
비트맵 인덱스는 하나의 인덱스 엔트리가 여러 행의 정보를 담기 때문에, 한 행을 업데이트하면 동일한 비트맵 세그먼트를 공유하는 다른 행들도 잠금 범위에 포함됩니다. 여러 세션이 동시에 비트맵 인덱스가 설정된 컬럼의 값을 업데이트하면 잠금 범위가 겹치면서 교착 상태가 매우 쉽게 발생합니다. 이 원인은 DW(Data Warehouse) 환경에서 데이터 로딩 시 종종 나타납니다.
해결 방법
원인 1 해결: 트랜잭션 잠금 순서 통일
데드락이 발생한 세션을 먼저 V$LOCKED_OBJECT와 트레이스 파일로 확인합니다.
-- 현재 잠금 상황 및 블로킹 세션 확인
SELECT
l.session_id,
s.serial#,
s.username,
s.program,
l.oracle_username,
l.os_user_name,
o.object_name,
o.object_type,
DECODE(l.locked_mode,
0, 'None',
1, 'Null',
2, 'Row-S (SS)',
3, 'Row-X (SX)',
4, 'Share',
5, 'S/Row-X (SSX)',
6, 'Exclusive', 'Unknown') AS lock_mode
FROM
v$locked_object l
JOIN dba_objects o ON l.object_id = o.object_id
JOIN v$session s ON l.session_id = s.sid
ORDER BY
l.session_id;
-- 데드락 트레이스 파일 위치 확인
SELECT value FROM v$diag_info WHERE name = 'Default Trace File';
-- 혹은 알림 로그에서 데드락 발생 시각 확인
SELECT originating_timestamp, message_text
FROM v$diag_alert_ext
WHERE message_text LIKE '%deadlock%'
ORDER BY originating_timestamp DESC
FETCH FIRST 20 ROWS ONLY;
잠금 순서를 통일하는 실무 패턴 예시입니다. 반드시 작은 PRIMARY KEY 순서로 정렬하여 업데이트합니다.
-- 나쁜 예: 순서가 보장되지 않아 데드락 발생 가능
-- 세션 A
UPDATE orders SET status = 'PROCESSING' WHERE order_id = 1001;
UPDATE order_items SET qty = qty - 1 WHERE item_id = 5001;
-- 세션 B (역순 접근 → 데드락 위험)
UPDATE order_items SET qty = qty - 1 WHERE item_id = 5001;
UPDATE orders SET status = 'PROCESSING' WHERE order_id = 1001;
-- 좋은 예: 항상 동일한 순서(orders → order_items)로 접근
-- 세션 A
BEGIN
-- 1단계: 항상 부모 테이블 먼저
UPDATE orders
SET status = 'PROCESSING'
WHERE order_id = 1001;
-- 2단계: 자식 테이블은 항상 나중에, PK 오름차순 정렬
UPDATE order_items
SET qty = qty - 1
WHERE order_id = 1001
ORDER BY item_id; -- 순서 보장
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
/
원인 2 해결: 외래 키 인덱스 생성
-- 외래 키가 있으나 인덱스가 없는 컬럼 찾기 (실무 필수 쿼리)
SELECT
c.table_name,
c.constraint_name,
c.column_name,
c.position
FROM
user_cons_columns c
JOIN user_constraints uc
ON c.constraint_name = uc.constraint_name
AND uc.constraint_type = 'R' -- R = Foreign Key
WHERE
NOT EXISTS (
SELECT 1
FROM user_ind_columns ic
WHERE ic.table_name = c.table_name
AND ic.column_name = c.column_name
)
ORDER BY
c.table_name, c.position;
-- 발견된 FK 컬럼에 인덱스 생성
-- 예시: ORDER_ITEMS 테이블의 ORDER_ID(FK) 컬럼
CREATE INDEX idx_order_items_order_id
ON order_items (order_id)
TABLESPACE users
PARALLEL 4
NOLOGGING;
-- 인덱스 생성 후 통계 수집
BEGIN
DBMS_STATS.GATHER_TABLE_STATS(
ownname => USER,
tabname => 'ORDER_ITEMS',
estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE,
cascade => TRUE
);
END;
/
원인 3 해결: 비트맵 인덱스 환경에서의 동시성 제어
-- OLTP 환경이라면 비트맵 인덱스를 B-Tree 인덱스로 교체
-- 기존 비트맵 인덱스 확인
SELECT index_name, index_type, table_name
FROM user_indexes
WHERE index_type = 'BITMAP';
-- 비트맵 인덱스 제거 후 B-Tree 인덱스로 대체
DROP INDEX bitmap_idx_status;
CREATE INDEX btree_idx_status
ON orders (status)
TABLESPACE users;
-- 대용량 배치 로딩 시 비트맵 인덱스가 필요한 경우:
-- 로딩 전 인덱스 비활성화 → 로딩 → 인덱스 재생성 패턴 사용
ALTER INDEX bitmap_idx_status UNUSABLE;
-- 데이터 로딩 수행
INSERT /*+ APPEND */ INTO orders SELECT * FROM orders_stg;
COMMIT;
-- 인덱스 재생성
ALTER INDEX bitmap_idx_status REBUILD PARALLEL 8 NOLOGGING;
ALTER INDEX bitmap_idx_status NOPARALLEL;
ALTER INDEX bitmap_idx_status LOGGING;
데드락 발생 시 애플리케이션 재시도 로직 (PL/SQL)
-- 실무에서 바로 활용 가능한 데드락 재시도 프로시저
CREATE OR REPLACE PROCEDURE process_order_with_retry(
p_order_id IN orders.order_id%TYPE,
p_max_retry IN INTEGER DEFAULT 3
)
AS
v_retry_count INTEGER := 0;
deadlock_detected EXCEPTION;
PRAGMA EXCEPTION_INIT(deadlock_detected, -60); -- ORA-00060
BEGIN
LOOP
BEGIN
-- 실제 비즈니스 로직
UPDATE orders
SET status = 'PROCESSING',
updated_dt = SYSDATE
WHERE order_id = p_order_id;
UPDATE order_items
SET status = 'PROCESSING'
WHERE order_id = p_order_id;
COMMIT;
EXIT; -- 성공 시 루프 탈출
EXCEPTION
WHEN deadlock_detected THEN
ROLLBACK;
v_retry_count := v_retry_count + 1;
IF v_retry_count >= p_max_retry THEN
-- 최대 재시도 횟수 초과 시 로그 기록 후 예외 전파
INSERT INTO error_log (
error_code, error_msg,
session_id, occurred_dt
) VALUES (
-60, 'Deadlock exceeded max retry: ' || p_order_id,
SYS_CONTEXT('USERENV','SID'), SYSTIMESTAMP
);
COMMIT;
RAISE;
END IF;
-- 잠시 대기 후 재시도 (지수 백오프 적용)
DBMS_LOCK.SLEEP(v_retry_count * 0.5);
END;
END LOOP;
END process_order_with_retry;
/
예방 방법
1. 일관된 오브젝트 접근 순서 표준화 및 코드 리뷰 의무화
모든 개발자가 동일한 테이블 접근 순서를 따르도록 개발 표준 문서에 명시해야 합니다. 구체적으로는 “항상 부모 테이블 → 자식 테이블 순서로 DML을 수행하고, 동일 테이블 내 다중 행 업데이트 시에는 반드시 PK 오름차순으로 정렬한다”는 규칙을 정립합니다. 코드 리뷰 단계에서 이 순서를 위반하는 트랜잭션을 사전에 차단하고, CI/CD 파이프라인에 정적 분석 도구를 통합하여 자동 검사하는 것이 이상적입니다. 또한 신규 기능 개발 시 데이터 모델 리뷰 단계에서 DBA가 잠금 경합 가능성을 검토하는 게이팅 프로세스를 도입하면 운영 환경에서의 데드락 발생을 크게 줄일 수 있습니다.
2. 주기적인 FK 인덱스 점검 및 AWR/트레이스 모니터링 자동화
데이터베이스 운영 중 새로운 테이블과 FK 제약조건이 추가될 때마다 인덱스 누락 여부를 자동으로 점검하는 스크립트를 스케줄러(DBMS_SCHEDULER)에 등록해 두어야 합니다. AWR(Automatic Workload Repository) 리포트에서 enq: TX - row lock contention 대기 이벤트가 급증하는 경우 데드락 전조일 수 있으므로, 임계값 초과 시 알림을 발송하는 모니터링 체계를 구축하는 것이 중요합니다. Oracle Alert Log와 트레이스 파일을 자동으로 파싱하여 ORA-00060 발생 빈도를 추적하고, 발생 추세가 증가할 때 즉시 분석에 착수할 수 있는 운영
주요 ORA 에러를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.
? 본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.