ORA-04088 error during execution of trigger — 트리거 실행 오류 원인·해결·예방 완전 정리
PL/SQL 트리거가 떠 있는 테이블에 평범한 INSERT·UPDATE·DELETE를 던졌는데 갑자기 ORA-04088: error during execution of trigger 'OWNER.TRIGGER_NAME'이 떨어지는 상황은 운영 현장에서 흔하게 마주친다. ORA-04088은 트리거 내부에서 발생한 다른 에러를 감싸 보여 주는 wrapper일 뿐, 실제 원인은 함께 출력된 스택 위쪽의 ORA-xxxx에 적혀 있다.
본 글은 ORA-04088의 정확한 의미, 7가지 발생 원인, 에러 스택 해석 방법, mutating table 회피와 Compound Trigger 패턴, 그리고 운영 환경에서 트리거 장애를 미리 막는 예방 전략까지 정리한다.

이 글의 구성
01 에러 메시지와 ORA-04088의 의미
| 항목 | 내용 |
|---|---|
| 에러 코드 | ORA-04088 |
| 메시지 형식 | error during execution of trigger '<schema>.<trigger_name>' |
| 역할 | 트리거 내부 예외를 감싸는 wrapper 에러 |
| 실제 원인 | 스택 위쪽에 함께 표시되는 ORA-xxxx |
| 대상 트리거 | DML · INSTEAD OF · DDL · System(LOGON 등) · Compound |
| 동반 에러 | ORA-06512 · ORA-04091 · ORA-20xxx · ORA-01403 등 |
전형적인 에러 스택 출력
ORA-20101: Salary not in range ← 실제 원인 (스택 최상단)
ORA-06512: at "SCOTT.TSAL", line 5 ← 발생 위치
ORA-04088: error during execution ← 래퍼 (스택 최하단)
of trigger 'SCOTT.TSAL'
진단의 첫 단계는 에러 스택을 거꾸로 읽는 것이다. ORA-04088은 결과만 알려 줄 뿐 원인을 담지 않는다. 가장 위쪽에 보이는 ORA-xxxx 코드가 실제 진단 시작점이고, ORA-06512는 그 예외가 발생한 라인 번호를 알려 준다.
핵심 관찰 — ORA-04088 자체를 검색하지 말 것
ORA-04088만 검색해서는 해결책이 나오지 않는다. 스택 위쪽의 진짜 에러 코드(ORA-04091, ORA-01403, ORA-20xxx 등)를 먼저 식별하고, 그 코드의 원인에 맞춰 트리거 코드를 분석해야 한다. ORA-04088은 "트리거 안에서 무언가 터졌다"는 신호일 뿐이다.
02 발생 원인 7가지
원인 1 — 트리거 내부 unhandled exception
트리거 PL/SQL 안에서 SELECT INTO가 한 건도 못 찾아 NO_DATA_FOUND(ORA-01403)가 떠도 그대로 trigger 밖으로 전파되면 ORA-04088로 감싸져 나온다. TOO_MANY_ROWS(ORA-01422), VALUE_ERROR(ORA-06502), NULL 위반(ORA-01400) 등이 자주 등장한다.
원인 2 — Mutating Table(ORA-04091)
행 단위(row-level) 트리거가 자신을 발화시킨 같은 테이블을 SELECT 또는 DML로 다시 만지면 발생한다. 트리거가 변경 중인 테이블에 다른 행 조회를 시도하는 패턴이 대표적이다. 11g 이후 도입된 Compound Trigger로 가장 깔끔하게 해결된다.
원인 3 — 트리거 내부 SQL의 제약 위반
트리거가 다른 테이블에 INSERT/UPDATE를 시도하다 UNIQUE 제약(ORA-00001), 외래키 위반(ORA-02291), NOT NULL 위반(ORA-01400) 같은 데이터 무결성 에러를 만나는 경우다.
원인 4 — 권한 부족
트리거 소유자가 호출하는 다른 스키마 객체에 SELECT·EXECUTE 권한이 없을 때 발생한다. 트리거는 기본 definer-rights로 동작하므로 role 권한이 적용되지 않고 직접 GRANT가 필수다.
원인 5 — RAISE_APPLICATION_ERROR 의도적 발생
비즈니스 룰 위반을 알리기 위해 트리거 내부에서 `RAISE_APPLICATION_ERROR(-20000~-20999, '...')`를 직접 호출하는 경우다. 이때는 정상 동작이며, 애플리케이션은 SQLCODE -20xxx로 catch해 사용자에게 의미 있는 메시지를 보여 주면 된다.
원인 6 — 트리거 간 재귀·연쇄 발화
트리거 A가 다른 테이블을 갱신하고, 그 테이블 트리거 B가 다시 첫 테이블을 갱신하는 식의 cascade가 무한 반복되면 PL/SQL 스택이 폭주한다. 트리거 진입 가드 변수(패키지 상태 플래그)로 끊어야 한다.
원인 7 — Autonomous Transaction 관련 ORA-06519
`PRAGMA AUTONOMOUS_TRANSACTION` 선언 트리거에서 COMMIT 또는 ROLLBACK 없이 종료되면 ORA-06519가 발생한다. 분산 트랜잭션 안에서 자율 트랜잭션을 호출하는 경우 ORA-00164가 떨어진다.
03 에러 스택 해석과 진단 도구
트리거 정의 확인
SELECT trigger_name, trigger_type, triggering_event,
table_owner, table_name, status
FROM user_triggers
WHERE trigger_name = 'TSAL';
-- 트리거 본문 전체
SELECT trigger_body
FROM user_triggers
WHERE trigger_name = 'TSAL';
예외 백트레이스 로깅(권장 표준 패턴)
CREATE OR REPLACE TRIGGER trg_audit
BEFORE INSERT ON emp
FOR EACH ROW
BEGIN
-- 비즈니스 로직
audit_pkg.log_insert(:NEW.empno, :NEW.ename);
EXCEPTION
WHEN OTHERS THEN
error_pkg.log(
p_trigger => 'TRG_AUDIT',
p_sqlcode => SQLCODE,
p_sqlerrm => SQLERRM,
p_backtrc => DBMS_UTILITY.FORMAT_ERROR_BACKTRACE,
p_stack => DBMS_UTILITY.FORMAT_CALL_STACK
);
RAISE; -- 반드시 다시 던져 트랜잭션 결정권을 상위에 위임
END;
/
WHEN OTHERS THEN NULL로 예외를 묵살하는 것은 가장 흔하면서도 가장 위험한 안티패턴이다. 데이터 일관성 깨짐을 추적할 수 없게 만든다. 로깅 후 반드시 RAISE로 다시 던져야 한다.
트리거 비활성화 후 재현
ALTER TRIGGER scott.tsal DISABLE;
-- 원래 DML 재시도 → ORA-04088 사라지면 트리거 내부 원인 확정
ALTER TRIGGER scott.tsal ENABLE;
04 Mutating Table와 Compound Trigger
Mutating Table 에러의 정체
ORA-04091은 "row-level 트리거가 자신이 실행되는 테이블을 다시 만지려 한다"는 신호다. Oracle은 일관성 보장을 위해 그 시점의 테이블 상태를 row-level 트리거가 조회·갱신하지 못하도록 차단한다. 가장 빈번한 실수가 BEFORE INSERT ROW 트리거에서 같은 테이블의 다른 행을 SELECT INTO 하는 패턴이다.
Compound Trigger 패턴(11g+ 권장)
Compound Trigger는 BEFORE/AFTER STATEMENT 섹션과 ROW 섹션을 하나의 트리거 안에 묶어, ROW 섹션에서 수집한 정보를 STATEMENT 섹션에서 처리할 수 있게 해 준다. mutating table을 정공법으로 해소하는 표준 패턴이다.
CREATE OR REPLACE TRIGGER trg_emp_compound
FOR UPDATE OF sal ON emp
COMPOUND TRIGGER
TYPE empno_t IS TABLE OF emp.empno%TYPE;
g_empnos empno_t := empno_t();
BEFORE EACH ROW IS
BEGIN
g_empnos.extend;
g_empnos(g_empnos.last) := :NEW.empno;
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
-- STATEMENT 시점에는 mutating 제약이 풀려 있음
salary_audit_pkg.bulk_log(g_empnos);
END AFTER STATEMENT;
END trg_emp_compound;
/
Compound Trigger 이전에는 자율 트랜잭션이나 패키지 컬렉션·3개 트리거(BEFORE STATEMENT + ROW + AFTER STATEMENT) 분할 같은 우회 패턴이 많이 쓰였지만, 11g 이후에는 Compound Trigger 하나로 정리하는 것이 표준이다.
05 예방 전략과 트리거 코드 룰
| 영역 | 권장 룰 |
|---|---|
| 사용 원칙 | 트리거 최소 사용 — 가능하면 애플리케이션 로직·CHECK 제약·virtual column으로 대체 |
| 예외 처리 | 모든 트리거에 명시적 EXCEPTION 핸들러 + 로깅 후 RAISE |
| Mutating 회피 | Compound Trigger 사용, ROW 섹션은 데이터 수집만 |
| 권한 | definer-rights 기본, 참조 객체에 직접 GRANT 부여 |
| 재귀 방지 | 패키지 상태 플래그로 진입 가드 |
| 테스트 | utPLSQL 등 단위 테스트로 트리거 분기 전수 커버 |
| 코드 리뷰 | WHEN OTHERS THEN NULL 단독 사용 금지를 룰로 명문화 |
트리거를 "은밀한 로직"으로 두지 말 것
트리거는 호출자가 명시적으로 부르지 않기 때문에 디버깅이 까다롭다. 핵심 비즈니스 룰을 트리거에만 의존해 구현하면 운영 장애 시 원인 추적이 길어진다. 가능한 한 감사 로깅·집계 갱신·DDL 보호처럼 부수적 역할에 한정하고, 핵심 룰은 애플리케이션 또는 API 계층에서 검증하도록 설계한다.
06 인접 트리거 에러와의 차이
| 에러 코드 | 의미 | 관계 |
|---|---|---|
| ORA-04088 | 트리거 실행 중 예외 발생(wrapper) | 스택 최하단 |
| ORA-04091 | 테이블 mutating, 트리거가 볼 수 없음 | ORA-04088의 흔한 원인 1 |
| ORA-04098 | 트리거가 INVALID, 재컴파일 실패 | 컴파일 시점 에러 |
| ORA-04098 | trigger is invalid and failed re-validation | ORA-04063과 유사 계열 |
| ORA-06512 | PL/SQL 예외 발생 위치 표시 | 위치 정보만 제공 |
| ORA-06519 | active autonomous transaction 미커밋/롤백 | PRAGMA AUTONOMOUS_TRANSACTION 트리거에서 발생 |
같은 트리거 계열이라도 ORA-04088은 런타임 예외, ORA-04098은 컴파일 무효 상태라는 점에서 완전히 다른 에러다. 둘 중 어느 쪽인지에 따라 진단 시작점이 USER_ERRORS인지 USER_TRIGGERS인지 갈린다.
07 자주 묻는 질문
Q1. ORA-04088만 보이고 다른 에러가 없으면 어떻게 하나요?
애플리케이션이 SQLException을 잘라서 보여 주는 경우가 많다. 동일 DML을 SQL*Plus나 SQL Developer에서 직접 실행하면 ORA-04088 위에 실제 원인 코드가 함께 출력된다. JDBC라면 SQLException의 getCause()와 ORA 코드 전체 메시지를 로깅해 둔다.
Q2. Mutating Table 에러는 Compound Trigger 외 방법이 없나요?
대안은 있다. 자율 트랜잭션(PRAGMA AUTONOMOUS_TRANSACTION) 사용, BEFORE STATEMENT/ROW/AFTER STATEMENT 3분할 트리거 + 패키지 변수 활용 등이다. 다만 자율 트랜잭션은 일관성 측면에서 부작용이 있고, 3분할은 복잡도가 높다. 11g+ 환경이라면 Compound Trigger가 가장 단순하고 정석이다.
Q3. RAISE_APPLICATION_ERROR 사용은 권장되나요?
의도적 비즈니스 룰 위반 통보에는 권장된다. -20000~-20999 범위 안에서 코드를 일관되게 부여하고, 메시지를 사용자 친화적으로 작성한다. 애플리케이션은 SQLCODE -20xxx를 catch해 사용자 메시지로 매핑한다.
Q4. `WHEN OTHERS THEN NULL`은 절대 쓰면 안 되나요?
예외를 묵살하는 것은 데이터 무결성 측면에서 위험하다. 정말 무시해야 할 사유가 있다면(예: 멱등성이 보장되는 캐시 갱신 실패) 그 사유를 코드 주석에 남기는 것을 권한다. 대부분의 경우 로깅 후 RAISE가 정답이다.
Q5. 트리거 단위 테스트는 어떻게 하나요?
utPLSQL이 가장 일반적이다. 정상 입력·경계값·예외 입력 케이스를 모두 시나리오로 만들고, 트리거가 부수 효과로 갱신하는 로그 테이블·집계 테이블의 상태를 assert한다. CI에서 빌드 단계에 포함시키면 트리거 변경 시 회귀를 빠르게 잡을 수 있다.
마무리
ORA-04088은 진단이 어려운 에러처럼 보이지만, 본질은 다른 에러를 감싸 들고 나오는 표지판일 뿐이다. 알람이 떴을 때 가장 먼저 할 일은 에러 메시지 전체를 끝까지 받아 와서 스택 최상단의 ORA-xxxx를 찾는 것이고, 그 다음이 트리거 본문 분석이다.
장기적으로는 (1) 모든 트리거에 표준 예외 처리 패턴 적용, (2) mutating table을 Compound Trigger로 정리, (3) WHEN OTHERS THEN NULL 금지 룰 도입, (4) 단위 테스트 자동화, 네 가지가 운영 안정성을 결정한다. 트리거를 "은밀한 자동화"가 아니라 "테스트 가능한 소형 컴포넌트"로 다루는 관점 전환이 ORA-04088 재발을 막는 핵심이다.
ORA-04088 트러블슈팅 체크리스트
본 글은 Oracle Database 트리거 실행 오류 ORA-04088의 일반적 원인과 해결 방법을 정리한 자료다. 운영 환경 적용 전 테스트 환경에서 충분히 검증한다.
#ORA04088 #errorDuringTrigger #오라클에러 #PLSQL트리거 #MutatingTable #ORA04091 #CompoundTrigger #RAISE_APPLICATION_ERROR #DBMS_UTILITY #FORMAT_ERROR_BACKTRACE #DefinerRights #AutonomousTransaction #ORA06519 #utPLSQL #ORA06512
댓글