Oracle ORA-00036 오류 원인과 해결 방법 완벽 가이드

ORA-00036
2026년 10월 22일 | DBMS Error 가이드

이 글에서 다루는 내용

ORA-00036 에러의 원인 분석, 해결 SQL, 예방 방법을 실무 관점에서 정리합니다.

ORA-00036 maximum number of recursive SQL levels exceeded 는?

ORA-00036 에러는 Oracle 데이터베이스 내부에서 재귀적 SQL 호출이 허용된 최대 깊이를 초과했을 때 발생하는 에러입니다. Oracle은 내부적으로 트리거, 저장 프로시저, 함수 등이 서로를 호출할 때 재귀 깊이를 제한하며, 기본값은 약 2000 레벨로 설정되어 있습니다. 이 에러는 주로 트리거가 자기 자신을 간접적으로 호출하거나, 상호 참조하는 트리거/프로시저 체인이 무한 루프에 빠졌을 때 발생하며, 방치하면 세션 장애 및 시스템 성능 저하로 이어질 수 있습니다.


주요 발생 원인

1. 트리거의 무한 재귀 호출 (가장 빈번한 원인)

트리거 내부에서 해당 트리거를 발생시키는 DML(INSERT, UPDATE, DELETE)을 다시 실행하는 구조가 만들어질 때 발생합니다. 예를 들어, AFTER UPDATE 트리거가 동일 테이블에 UPDATE를 수행하면 트리거가 끝없이 자기 자신을 호출하게 됩니다. 이는 실무에서 가장 자주 마주치는 패턴이며, 트리거 설계 시 가장 주의해야 할 부분입니다.

2. 상호 참조 저장 프로시저 / 함수의 순환 호출

프로시저 A가 프로시저 B를 호출하고, 프로시저 B가 다시 프로시저 A를 호출하는 순환 참조 구조가 만들어질 경우 스택이 점점 쌓여 최대 레벨을 초과하게 됩니다. 이런 경우 코드 리뷰 없이는 발견하기 어렵고, 특히 복잡한 비즈니스 로직이 얽혀 있는 레거시 시스템에서 자주 발생합니다.

3. 뷰(View)의 순환 참조 또는 과도한 중첩

뷰가 다른 뷰를 참조하고, 그 뷰가 또 다른 뷰를 참조하는 방식으로 지나치게 깊게 중첩되거나, 잘못된 설계로 순환 참조가 생긴 경우에도 이 에러가 발생할 수 있습니다. Oracle의 뷰 최대 중첩 깊이는 제한적이며, 특히 DML 트리거와 결합된 뷰 체계에서는 더욱 쉽게 한계에 도달합니다.


해결 방법

원인 1 해결: 트리거 내 재귀 방지 처리

트리거가 자기 자신을 호출하지 않도록 조건 분기 또는 플래그 변수를 사용하는 것이 핵심입니다.

-- 문제가 되는 트리거 패턴 (무한 재귀 발생)
CREATE OR REPLACE TRIGGER trg_emp_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
  -- 동일 테이블에 다시 UPDATE → 무한 재귀 발생
  UPDATE employees
  SET last_modified = SYSDATE
  WHERE employee_id = :NEW.employee_id;
END;
/

-- 해결책 1: 조건 분기로 불필요한 재귀 차단
CREATE OR REPLACE TRIGGER trg_emp_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
  -- last_modified 컬럼이 변경된 경우에는 추가 UPDATE를 수행하지 않음
  IF :OLD.last_modified = :NEW.last_modified THEN
    UPDATE employees
    SET last_modified = SYSDATE
    WHERE employee_id = :NEW.employee_id;
  END IF;
END;
/

-- 해결책 2: 패키지 레벨 플래그 변수 활용
CREATE OR REPLACE PACKAGE trg_flag_pkg AS
  g_trigger_running BOOLEAN := FALSE;
END trg_flag_pkg;
/

CREATE OR REPLACE TRIGGER trg_emp_update_safe
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
  IF NOT trg_flag_pkg.g_trigger_running THEN
    trg_flag_pkg.g_trigger_running := TRUE;
    UPDATE employees
    SET last_modified = SYSDATE
    WHERE employee_id = :NEW.employee_id;
    trg_flag_pkg.g_trigger_running := FALSE;
  END IF;
END;
/

원인 2 해결: 순환 참조 프로시저 구조 개선

순환 참조를 끊기 위해 공통 로직을 별도 프로시저로 분리하거나, 호출 깊이를 추적하는 파라미터를 추가합니다.

-- 문제 패턴: 순환 호출 구조
CREATE OR REPLACE PROCEDURE proc_a AS
BEGIN
  proc_b(); -- proc_b가 다시 proc_a를 호출
END;
/

CREATE OR REPLACE PROCEDURE proc_b AS
BEGIN
  proc_a(); -- 무한 순환 발생
END;
/

-- 해결책: 공통 로직을 별도 프로시저로 분리
CREATE OR REPLACE PROCEDURE proc_common AS
BEGIN
  -- 공통 비즈니스 로직 처리
  DBMS_OUTPUT.PUT_LINE('공통 로직 실행');
END;
/

CREATE OR REPLACE PROCEDURE proc_a_fixed AS
BEGIN
  proc_common(); -- 공통 프로시저 호출로 순환 구조 제거
END;
/

CREATE OR REPLACE PROCEDURE proc_b_fixed AS
BEGIN
  proc_common(); -- 동일하게 공통 프로시저 호출
END;
/

-- 재귀 깊이 제한을 두는 방어 코딩 패턴
CREATE OR REPLACE PROCEDURE proc_recursive(p_depth IN NUMBER DEFAULT 0) AS
BEGIN
  IF p_depth > 10 THEN
    RAISE_APPLICATION_ERROR(-20001, '최대 재귀 깊이 초과. 무한 루프 가능성 확인 필요.');
  END IF;
  -- 비즈니스 로직
  proc_recursive(p_depth + 1);
END;
/

원인 3 해결: 뷰 중첩 구조 분석 및 단순화

과도하게 중첩된 뷰는 Materialized View나 WITH 절을 활용하여 평탄화합니다.

-- 뷰 중첩 깊이 확인 쿼리
SELECT name, referenced_name, referenced_type, level AS nesting_level
FROM dba_dependencies
START WITH name = 'YOUR_TOP_VIEW_NAME'
           AND type = 'VIEW'
CONNECT BY PRIOR referenced_name = name
           AND referenced_type = 'VIEW'
ORDER BY level DESC;

-- 과도하게 중첩된 뷰를 Materialized View로 교체
CREATE MATERIALIZED VIEW mv_complex_data
BUILD IMMEDIATE
REFRESH FAST ON COMMIT
AS
SELECT e.employee_id, e.first_name, d.department_name, l.city
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN locations l ON d.location_id = l.location_id;

-- WITH 절을 활용한 뷰 평탄화 예시
CREATE OR REPLACE VIEW v_flat_employee_info AS
WITH dept_info AS (
  SELECT d.department_id, d.department_name, l.city
  FROM departments d
  JOIN locations l ON d.location_id = l.location_id
),
emp_summary AS (
  SELECT e.employee_id, e.first_name, e.last_name, e.department_id
  FROM employees e
)
SELECT es.employee_id, es.first_name, es.last_name,
       di.department_name, di.city
FROM emp_summary es
JOIN dept_info di ON es.department_id = di.department_id;

예방 방법

1. 트리거 및 프로시저 작성 시 재귀 호출 방지 코딩 표준 수립

모든 트리거와 저장 프로시저를 개발할 때 팀 내 코딩 표준으로 “트리거 내 동일 테이블 DML 금지” 원칙을 수립하고, 코드 리뷰 단계에서 순환 참조 여부를 필수 체크 항목으로 포함시켜야 합니다. 또한 개발 환경에서 ALTER SESSION SET EVENTS '36 TRACE NAME CONTEXT FOREVER' 와 같은 트레이스 설정을 활용하여 재귀 호출 패턴을 조기에 탐지하는 습관을 들이는 것이 중요합니다.

-- 현재 세션의 트리거 및 프로시저 의존성 사전 확인
SELECT name, type, referenced_name, referenced_type
FROM user_dependencies
WHERE referenced_type IN ('TRIGGER', 'PROCEDURE', 'FUNCTION')
ORDER BY name;

2. 정기적인 DB 오브젝트 의존성 모니터링 및 리뷰

운영 환경에서는 주기적으로 DBA_DEPENDENCIES 뷰를 쿼리하여 순환 의존성이 존재하는지 사전에 탐지하고, 특히 릴리즈 배포 전후에는 반드시 의존성 체인 분석을 수행해야 합니다. 복잡한 시스템일수록 무심코 추가된 로직이 기존 순환 구조를 악화시킬 수 있으므로, 변경 관리(Change Management) 프로세스 내에 의존성 검토 단계를 공식화하는 것을 강력히 권장합니다.

-- 순환 의존성 탐지 쿼리 (DBA 권한 필요)
SELECT d1.name AS object_name, d1.type,
       d1.referenced_name, d1.referenced_type
FROM dba_dependencies d1
WHERE EXISTS (
  SELECT 1 FROM dba_dependencies d2
  WHERE d2.name = d1.referenced_name
    AND d2.referenced_name = d1.name
    AND d2.type = d1.referenced_type
)
ORDER BY d1.name;

관련 에러

  • ORA-00604: 재귀 SQL 레벨에서 에러 발생 시 함께 출력되는 경우가 많으며, ORA-00036의 상위 에러로 같이 확인해야 합니다.
  • ORA-04020: 데드락과 연관된 에러로, 순환 호출이 락 경합으로 이어질 때 발생할 수 있습니다.
  • ORA-06512: PL/SQL 스택 트레이스 에러로, ORA-00036 발생 시 콜 스택 추적에 함께 표시되어 재귀 호출 경로를 파악하는 데 유용합니다.
  • ORA-00035: 재귀 SQL 수준 관련 에러로 ORA-00036과 유사한 맥락에서 발생합니다.

DBMS 에러 코드 시리즈

주요 DBMS error code를 정리하는 시리즈입니다.
블로그 홈에서 다른 에러도 확인하세요.

본 포스트는 AI가 생성한 기술 가이드입니다. 운영 환경 적용 전 충분한 검토를 권장합니다.

댓글 남기기