트리거는 CPU와 메모리를 많이 필요로 하는 아주 비싼 자원이다. 강력하지만 위험한 도구다. 잘 쓰면 업무를 자동화하고 데이터 일관성을 유지하는 약이 되지만, 잘못 쓰면 성능을 저하시키고 유지보수를 어렵게 만드는 독이 된다.
실무에서는 트리거를 최소한으로만 사용하고, 꼭 필요한 경우에만 신중하게 적용해야 한다. 특히 대용량 데이터를 처리하는 시스템에서는 트리거보다 배치 작업이나 프로시저를 사용하는 것이 안전하다.
트리거를 만들 때는 항상 "이게 정말 트리거로 해야 하나?" 를 먼저 고민해야 한다. 애플리케이션 레벨에서 처리할 수 있다면 그쪽이 더 나은 선택일 수 있다.
트리거(trigger)의 사전적인 의미는 방아쇠나 (방아쇠를) 쏘다, 발사하다, 행위를 취하면 동작을 발생시킨다, (사건을) 유발시키다라는 의미가 있다.
PL/SQL에서의 트리거 역시 방아쇠가 당겨지면 자동으로 총알이 발사되듯이 어떠한 이벤트가 발생하면 그에 따라 다른 작업이 자동으로 처리되는 것을 의미한다.
트리거란 특정 테이블의 데이터에 (INSERT, UPDATE, DELETE) 이벤트가 발생하면 자동으로 수행되는 저장 프로시저라고 할 수 있다.
앞서 배운 저장 프로시저는 필요할 때마다 사용자가 직접 EXECUTE 명령어로 호출해야 했다.
하지만 트리거는 이와 달리 테이블의 데이터가 INSERT, UPDATE, DELETE 문에 의해 변경되어질 때 자동으로 수행되므로 이 기능을 이용하여 여러 가지 작업을 할 수 있다.
자동 수행 되므로 사용자가 직접 실행시킬 수 없다.
상품이 입고(INSERT)되면 재고도 INSERT 되야한다. 이런 경우에
입고 테이블에 데이터를 INSERT하면 트리거가 자동으로 재고 테이블도 업데이트하는 방식으로 사용한다.
하지만 이렇게 여러 테이블이 연쇄적으로 변경되면 트랜잭션이 길어지고 락(lock)이 발생할 위험이 있다.
락이 걸리면 롤백이 되는 동안 테이블에 접근을 못하게 되므로 성능이 저하된다.
BEFORE 트리거: 테이블에서 DML 실행되기 전에 트리거가 동작한다. 데이터 검증, 값 변경, 제약 조건 체크 등에 사용한다.
AFTER 트리거: 테이블에서 DML 실행 후에 트리거가 동작한다. 로그 기록, 다른 테이블 업데이트, 알림 발송 등에 사용한다.
CREATE [OR REPLACE] TRIGGER trigger_name
{BEFORE | AFTER} triggering_event [OF column1, ...] ON table_name
[FOR EACH ROW [WHEN trigger_condition]]
trigger_body;
trigger_name: TRIGGER의 식별자
BEFORE | AFTER: DML 문장이 실행되기 전에 TRIGGER를 실행할 것인지 실행된 후에 TRIGGER를 실행할 것인지를 정의
triggering_event: TRIGGER를 실행하는 DML(INSERT, UPDATE, DELETE)문을 기술한다
OF column: TRIGGER가 실행되는 테이블에서 COLUMN 명을 기술한다
table_name: TRIGGER가 실행되는 테이블 이름
FOR EACH ROW: 이 옵션을 사용하면 행 레벨 트리거가 되어 triggering 문장에 의해 영향받은 행에 대해 각각 한번씩 실행하고, 사용하지 않으면 문장 레벨 트리거가 되어 DML 문장 당 한번만 실행된다
행 레벨 TRIGGER에서만 사용할 수 있는 예약어로 트리거 내에서 현재 처리되고 있는 행을 액세스할 수 있다. 즉 두 개의 의사 레코드를 통하여 이 작업을 수행할 수 있다.
:OLD는 INSERT 문에 의해 정의되지 않고, :NEW는 DELETE에 대해 정의되지 않는다. 그러나 UPDATE는 :OLD와 :NEW를 모두 정의한다.
| 트리거 이벤트 | :OLD | :NEW |
|---|---|---|
| INSERT | 모든 필드는 NULL | 삽입된 새로운 값 |
| UPDATE | 갱신 전의 원래 값 | 갱신된 새로운 값 |
| DELETE | 삭제 전의 원래 값 | 모든 필드는 NULL |
참고: 오라클과 MySQL은 OLD, NEW라는 이름을 사용하지만, MS-SQL에서는 DELETED, INSERTED라는 이름을 사용한다.
트리거 삭제
DROP TRIGGER trigger_name;
트리거 비활성화/활성화
ALTER TRIGGER trigger_name {DISABLE | ENABLE};
트리거 정보 조회
트리거가 생성될 때 소스 코드는 데이터 사전 VIEW인 user_triggers에 저장된다.
SELECT * FROM user_triggers WHERE table_name='테이블명';
CREATE TABLE tri_emp
AS
SELECT empno, ename FROM emp WHERE 1=2;
CREATE OR REPLACE TRIGGER tri_01
AFTER INSERT ON tri_emp
BEGIN
DBMS_OUTPUT.PUT_LINE('신입사원 입사');
END;
INSERT INTO tri_emp(empno, ename) VALUES(100, '홍길동');
-- 출력: 신입사원 입사
CREATE OR REPLACE TRIGGER tri_02
AFTER UPDATE ON tri_emp
BEGIN
DBMS_OUTPUT.PUT_LINE('신입사원 수정');
END;
UPDATE tri_emp
SET ename='아하'
WHERE empno=100;
-- 출력: 신입사원 수정
CREATE OR REPLACE TRIGGER tri_03
AFTER DELETE ON tri_emp
BEGIN
DBMS_OUTPUT.PUT_LINE('신입사원 삭제');
END;
하나의 테이블에 여러 개의 트리거(INSERT, UPDATE, DELETE)를 각각 만들 수 있다.
테이블에 INSERT, UPDATE, DELETE를 할 때 사용자(user), 구분(I, U, D), 시간(sysdate)을 기록하는 테이블(emp_audit)에 내용을 저장하는 예제다.
-- 시퀀스 생성
CREATE SEQUENCE emp_audit_tr
INCREMENT BY 1
START WITH 1
MAXVALUE 999999
MINVALUE 1
NOCYCLE
NOCACHE;
-- 추적 테이블 생성
CREATE TABLE emp_audit(
e_id NUMBER(6) CONSTRAINT emp_audit_pk PRIMARY KEY,
e_name VARCHAR2(30),
e_gubun VARCHAR2(10),
e_date DATE
);
-- 테스트용 테이블 생성
CREATE TABLE emp2
AS
SELECT * FROM emp;
CREATE OR REPLACE TRIGGER emp_audit_tr
AFTER INSERT OR UPDATE OR DELETE ON emp2
BEGIN
IF INSERTING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'inserting', SYSDATE);
ELSIF UPDATING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'updating', SYSDATE);
ELSIF DELETING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'deleting', SYSDATE);
END IF;
END;
-- 3개 행을 업데이트해도 로그는 1건만 기록된다
UPDATE emp2
SET deptno = 20
WHERE deptno = 10;
SELECT * FROM emp_audit;
-- 결과: 1건의 'updating' 로그
FOR EACH ROW를 선언하지 않았을 때는 명령어 한 번에 대하여 한 건으로 기록된다. 실제로는 3번 업데이트되지만 단건처럼 나온다.
CREATE OR REPLACE TRIGGER emp_audit_tr
AFTER INSERT OR UPDATE OR DELETE ON emp2
FOR EACH ROW
BEGIN
IF INSERTING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'inserting', SYSDATE);
ELSIF UPDATING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'updating', SYSDATE);
ELSIF DELETING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'deleting', SYSDATE);
END IF;
END;
-- 3개 행을 업데이트하면 로그도 3건 기록된다
UPDATE emp2
SET deptno = 20
WHERE deptno = 10;
SELECT * FROM emp_audit;
-- 결과: 3건의 'updating' 로그
FOR EACH ROW를 선언했을 때는 명령어 한 번에 변경된 행만큼 기록된다.
INSERT, UPDATE, DELETE로 변경되는 내용에 대하여 전/후 데이터를 기록하는 고급 감사 추적 예제다.
CREATE TABLE emp_audit (
id NUMBER(6) CONSTRAINT emp_audit_pk PRIMARY KEY,
name VARCHAR2(30),
gubun VARCHAR2(10),
wdate DATE,
etc1 VARCHAR2(20), -- old 값
etc2 VARCHAR2(20) -- new 값
);
CREATE OR REPLACE TRIGGER emp_audit_tr
AFTER INSERT OR UPDATE OR DELETE ON emp2
FOR EACH ROW
BEGIN
IF INSERTING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'inserting', SYSDATE,
:OLD.deptno, :NEW.deptno);
ELSIF UPDATING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'updating', SYSDATE,
:OLD.deptno, :NEW.deptno);
ELSIF DELETING THEN
INSERT INTO emp_audit
VALUES(emp_audit_tr.NEXTVAL, USER, 'deleting', SYSDATE,
:OLD.deptno, :NEW.deptno);
END IF;
END;
:OLD는 기존 데이터가 가상 테이블에 보관되어 있고, :NEW는 새로운 데이터를 의미한다.
-- INSERT 테스트
INSERT INTO emp2(empno, ename, deptno) VALUES (9999, '홍길동', 100);
SELECT * FROM emp_audit;
-- etc1(old): NULL, etc2(new): 100
-- UPDATE 테스트
UPDATE emp2 SET deptno=200 WHERE empno=9999;
SELECT * FROM emp_audit;
-- etc1(old): 100, etc2(new): 200
-- DELETE 테스트
DELETE FROM emp2 WHERE empno=9999;
SELECT * FROM emp_audit;
-- etc1(old): 200, etc2(new): NULL
관리자가 자주 사용하는 예제로, 특정 시간대나 특정 조건에서 데이터 입력을 제한할 수 있다.
점심시간이나 출장 갔을 때 작업이 안 되게 막고 싶을 때 BEFORE 트리거로 미리 검사한다.
CREATE TABLE tri_order
(
no NUMBER,
ord_code VARCHAR2(10),
ord_date DATE
);
CREATE OR REPLACE TRIGGER trigger_order
BEFORE INSERT ON tri_order
BEGIN
IF(TO_CHAR(SYSDATE, 'HH24:MI') NOT BETWEEN '11:00' AND '16:00') THEN
RAISE_APPLICATION_ERROR(-20002, '허용시간 오류 쉬세요');
END IF;
END;
BEFORE 트리거의 동작 시점이 실제 tri_order 테이블에 INSERT 되기 전에 트리거가 먼저 동작하고, 그 이후 INSERT 작업이 진행된다.
-- 11시~16시 사이에만 성공
INSERT INTO tri_order VALUES(1, 'notebook', SYSDATE);
DBA가 "이번 달은 데스크탑 받지 마!" 라고 했을 때, 들어오는 데이터를 미리 검사하고 desktop이면 오류를 발생시킨다.
CREATE OR REPLACE TRIGGER tri_order2
BEFORE INSERT ON tri_order
FOR EACH ROW
BEGIN
IF(:NEW.ord_code) NOT IN('desktop') THEN
RAISE_APPLICATION_ERROR(-20002, '제품코드 오류');
END IF;
END;
-- 오류 발생 (notebook은 안 됨)
INSERT INTO tri_order VALUES(200, 'notebook', SYSDATE);
-- 성공 (desktop만 허용)
INSERT INTO tri_order VALUES(200, 'desktop', SYSDATE);
입고 테이블에 데이터가 들어오면 자동으로 재고 테이블에도 반영되는 실무에서 가장 많이 사용하는 패턴이다.
CREATE TABLE t_01 -- 입고
(
no NUMBER,
pname VARCHAR2(20)
);
CREATE TABLE t_02 -- 재고
(
no NUMBER,
pname VARCHAR2(20)
);
CREATE OR REPLACE TRIGGER insert_t_01
AFTER INSERT ON t_01
FOR EACH ROW
BEGIN
INSERT INTO t_02(no, pname)
VALUES(:NEW.no, :NEW.pname);
END;
-- 입고
INSERT INTO t_01 VALUES(1, 'notebook');
SELECT * FROM t_01; -- 1건
SELECT * FROM t_02; -- 1건 (자동 생성)
입고 데이터가 들어오면 가상 테이블 NEW를 이용해서 같은 데이터를 재고에 자동으로 입력한다.
주의: 만약 연결이 길었는데 성공-성공-성공-실패 시 전부 롤백된다. 트랜잭션을 물고 있어서 다른 사람 작업이 안 된다.
CREATE OR REPLACE TRIGGER update_t_01
AFTER UPDATE ON t_01
FOR EACH ROW
BEGIN
UPDATE t_02
SET pname = :NEW.pname
WHERE no = :OLD.no;
END;
UPDATE t_01
SET pname = 'notebook2'
WHERE no = 1;
SELECT * FROM t_01; -- notebook2
SELECT * FROM t_02; -- notebook2 (자동 변경)
CREATE OR REPLACE TRIGGER delete_tri_01
AFTER DELETE ON t_01
FOR EACH ROW
BEGIN
DELETE FROM t_02
WHERE no = :OLD.no;
END;
DELETE FROM t_01 WHERE no=1;
SELECT * FROM t_01; -- 0건
SELECT * FROM t_02; -- 0건 (자동 삭제)
전화번호가 바뀌었을 때만 로그 테이블에 기록한다.
CREATE OR REPLACE TRIGGER trg_audit_phone_change
AFTER UPDATE OF phone ON customer --특정 컬럼의 데이터 변경
FOR EACH ROW
WHEN (OLD.phone IS NOT NULL AND NEW.phone IS NOT NULL AND OLD.phone <> NEW.phone)
BEGIN
INSERT INTO audit_log (cust_id, old_phone, new_phone)
VALUES (:OLD.cust_id, :OLD.phone, :NEW.phone);
END;
사원이 삭제되기 전에 삭제되기 전의 데이터를 emp_backup 백업 테이블에 저장한다.
CREATE TABLE emp (
empno NUMBER PRIMARY KEY,
ename VARCHAR2(50),
job VARCHAR2(50),
sal NUMBER
);
CREATE TABLE emp_backup AS SELECT * FROM emp WHERE 1=0; -- 구조만 복사
CREATE OR REPLACE TRIGGER trg_backup_before_delete
BEFORE DELETE ON emp
FOR EACH ROW
BEGIN
INSERT INTO emp_backup (empno, ename, job, sal)
VALUES (:OLD.empno, :OLD.ename, :OLD.job, :OLD.sal);
END;
| 사용 목적 | 예시 트리거 내용 |
|---|---|
| 변경 이력 기록 | 고객 정보 변경 시 로그 테이블에 INSERT |
| 데이터 보존/백업 | DELETE 전에 백업 테이블에 저장 |
| 자동 계산 | 총금액·세금 등 파생 값 자동 계산 |
| 제약 조건 대체 | 특정 UPDATE 차단 (예: 관리자 계정 비활성화 금지) |
| 동기화 작업 | 마스터 테이블 변경 시 서브 테이블 자동 반영 |
트리거는 CPU와 메모리를 많이 소비하는 비싼 자원이다. 특히 행 레벨 트리거에서 수만 건의 데이터를 한 번에 처리하면 성능이 급격히 저하될 수 있다. 가능하면 배치 작업이나 프로시저로 처리하는 것을 고려해야 한다.
트리거가 여러 테이블을 연쇄적으로 수정하면 트랜잭션이 길어지고 락(lock)이 오래 유지된다. 이는 동시 사용자가 많은 시스템에서 심각한 병목 현상을 유발한다. 만약 연결이 길었는데 성공-성공-성공-실패 시 전부 롤백되므로, 다른 사람의 작업도 대기하게 된다.
A 테이블의 트리거가 B 테이블을 수정하고, B 테이블의 트리거가 다시 A 테이블을 수정하면 무한 루프에 빠질 수 있다. 트리거 설계 시 반드시 순환 참조를 확인해야 한다.
트리거는 자동으로 실행되므로 문제가 발생했을 때 원인을 찾기 어렵다. 어떤 트리거가 언제 실행되었는지 추적하기 힘들고, 여러 트리거가 연쇄적으로 실행되면 더욱 복잡해진다.
트리거를 사용할 때는 다음 원칙을 지켜야 한다:
| 구분 | 트리거 | 프로시저 | 제약조건 |
|---|---|---|---|
| 실행 방식 | 자동 실행 | 명시적 호출 | 자동 검증 |
| 용도 | 연쇄 작업 | 비즈니스 로직 | 데이터 무결성 |
| 성능 | 낮음 | 중간 | 높음 |
| 디버깅 | 어려움 | 보통 | 쉬움 |
단순한 데이터 검증은 제약조건으로, 복잡한 비즈니스 로직은 프로시저로, 자동화된 연쇄 작업만 트리거로 처리하는 것이 좋다.