RDBMS에서 가장 중요한 원칙은 여럿 있겠지만,
트랜잭션의 ACID 원칙은 놓칠 수 없는 핵심 중 핵심이다.
개발을 하다 보면 데이터베이스, 특히 Oracle의 트랜잭션 처리 방식 때문에 얘기치 못한 난관에 부딪히곤 한다.
오늘은 회사에서 프로젝트를 진행하며 트리거(Trigger)
와 프로시저(Procedure)
를 이용한 데이터 처리 과정에서 겪었던 "뮤테이팅 테이블(Mutating Table)" 에러와
"독립 트랜잭션(Autonomous Transaction)" 의 데이터 가시성 문제를 해결한 방식을 회고하겠다.
만약 트리거를 연쇄적으로 사용하다가 원인 모를 에러를 마주했거나,
트랜잭션의 원자성(Atomicity)
을 몸소 체험하고 있다면 좋은 참고가 될 것이다.
먼저 시스템의 데이터 흐름은 다음과 같았다.
TABLE_A
에INSERT
→TRIGGER_AtoB
발동 →TABLE_B
에INSERT
→TRIGGER_BtoC
발동 →PROCEDURE_MAIN
호출 →TABLE_C
에 데이터 적재
여기서 핵심 문제는 바로 TRIGGER_BtoC
에서 발생했다.
트랜잭션 처리 방식을 어떻게 가져가느냐에 따라 서로 다른 두 가지 문제에 직면했다.
TRIGGER_BtoC
를 독립 트랜잭션으로 처리한 경우독립 트랜잭션(PRAGMA AUTONOMOUS_TRANSACTION
)을 사용하면 부모 트랜잭션과 별개로 동작하여 COMMIT
과 ROLLBACK
을 독자적으로 수행할 수 있다.
Mutating Table ERROR
를 피하기 위해 이 방법을 시도했지만, 새로운 문제가 발생했다.
PROCEDURE_MAIN
이 새로운 트랜잭션으로 실행되다 보니, 아직 COMMIT
되지 않은 TABLE_A
의 새로운 데이터를 읽지 못했다.TABLE_C
에 타겟 데이터는 들어가지 않았다. 😭TRIGGER_BtoC
를 일반 트랜잭션으로 처리한 경우그렇다면 독립 트랜잭션을 사용하지 않고, 전체를 하나의 트랜잭션으로 묶으면 어떨까?
이번엔 Oracle의 강력한 제약사항과 마주했다.
TABLE_A
에 INSERT
된 후, 연쇄적으로 동작하는 트리거가 아직 COMMIT
되지 않은 TABLE_A
의 데이터를 다시 조회하려고 시도했다.ORA-04091: table is mutating, trigger/function may not see it
에러가 발생하며 프로시저 실행 자체가 실패했다. 🚫상황 | 독립 트랜잭션 사용 | 독립 트랜잭션 미사용 |
---|---|---|
문제 | COMMIT 이전 데이터 읽기 불가 | ORA-04091 뮤테이팅 테이블 에러 |
결과 | 데이터 동기화 실패 | 프로시저 실행 실패 |
원인 | 독립 트랜잭션의 격리 수준 | Oracle의 데이터 일관성 보장을 위한 제약 |
🔒 뮤테이팅 테이블(Mutating Table)이란?
DML (
INSERT
,UPDATE
,DELETE
) 문에 의해 변경되고 있는 테이블을 해당 DML 문을 발생시킨 트리거 안에서 다시SELECT
하려고 할 때 발생하는 문제다.
Oracle은 데이터의 읽기 일관성(Read Consistency)을 보장하기 위해 이를 엄격히 금지한다.
🔒 독립 트랜잭션(PRAGMA AUTONOMOUS_TRANSACTION)이란?
PRAGMA AUTONOMOUS_TRANSACTION
는 새로운 트랜잭션 컨텍스트를 여는 기능으로,
호출한 PL/SQL 서브프로그램(OR 트리거) 안에서 독립적인COMMIT/ROLLBACK
을 수행하고,
종료 후 바로 본 트랜잭션으로 복귀한다.
- 메인 트랜잭션을 일시정지(
suspend
)했다가 돌아오는 구조- 동일 세션, 프로세스 안에서 트랜잭션 컨텍스트만 분리해 실행되는 동기식 구조이다.
이 복잡한 트랜잭션 문제를 해결하기 위해 두 가지 방안을 고려했다.
첫 번째는 트랜잭션 자체를 분리하는 것이다.
기존 시스템의 설계 상, 매 정시 25분마다 호출되는 PROCEDURE_MAIN
을 정시 57분 호출로 변경하고,
트리거에서 즉시 호출해 변경 사항을 바로 반영하는 과정을 제거하는 것이다.
왜냐하면, 매 정시에 실행되는 가장 핵심 프로시저인
PROCEDURE_CORE가 최신 데이터를 기준으로 동작해야 하기 때문이다.
이는 Oracle Job Scheduler
를 이용해 특정 시간마다 주기적으로 실행하는 방식이다.
PROCEDURE_MAIN
을 실행하도록 스케줄링.TABLE_C
로 반영되지 않는다는 것.두 번째는 데이터베이스의 책임을 애플리케이션으로 옮기는 것이다.
트리거에서는 프로시저 호출 로직을 제거하고, 애플리케이션이 TABLE_A
에 데이터를 INSERT
한 후 명시적으로 PROCEDURE_MAIN
을 호출하도록 변경한다.
두 방법 모두 장단점이 있지만, 현대적인 아키텍처 관점에서는 애플리케이션 레벨 처리가 더 효과적이다.
(다만, 현재 회사의 시스템이 현대적이지 않다는게 문제...)
DB는 데이터 저장 및 무결성 유지에 집중하고, 비즈니스 로직 처리는 애플리케이션이 담당하는 것이 책임 분리 원칙(Separation of Concerns)에 부합하기 때문이다.
물론, 비즈니스 요구사항에 따라 하이브리드 접근법도 훌륭한 대안이 될 수 있다.
이번 트러블슈팅을 통해 트랜잭션과 데이터베이스 아키텍처에 대해 많은 것을 배울 수 있었다.
데이터 가시성
이라는 큰 제약이 있음을 깨달았다.단순한 데이터 처리 문제로 시작했지만, 결국 트랜잭션의 깊은 이해와 아키텍처 재설계까지 고민하게 된 값진 경험이었다.
복잡한 데이터베이스 로직과 오래된 시스템 구조로 어려움을 겪는 누군가에게 작은 도움이 되기를 바란다.
사실, 프론트엔드, 백엔드, DB 단을 별도로 구성하기만 해도 고민을 아에 안할 문제이긴 하다...
데이터 저장과 프로세스 로직을 구분하기만 해도 해결되는 부분이기 때문.
다만, 모든 개발 환경이 동일하지 않고 비즈니스 환경에 맞는 기술적 발전이 필요하니까... 함께 발전해보겠다.
(해당 글은 Claude Sonnet 4, Gemini 2.5 pro를 통해 작성되고, 게시되었습니다.)