트리거 동작 개요
트리거 동작 개요 (Overview of Trigger Behavior)
트리거는 "특정 종류의 작업이 일어날 때 데이터베이스가 특정 함수를 자동으로 실행하게 하는 명세"예요. 테이블(파티션 포함)·뷰·외부 테이블에 붙일 수 있죠. 이 페이지에서는 트리거가 언제, 몇 번, 어떤 순서로 발화되는지 그 동작 규칙을 정리해 볼게요. "트리거 만들기"가 아니라 "트리거가 어떻게 움직이는지"에 초점을 둔 내용이에요.
트리거는 어디에, 언제 발화될까요?
테이블과 외부 테이블에는 INSERT, UPDATE, DELETE 작업의 이전(before)이나 이후(after) 중 하나로 트리거를 정의할 수 있어요. 발화 단위도 정할 수 있는데, 변경된 행마다 한 번(per-row) 또는 SQL 문장마다 한 번(per-statement)이에요. UPDATE 트리거는 UPDATE 문장의 SET 절에 특정 컬럼이 언급될 때만 발화하도록 만들 수도 있고요. TRUNCATE 문장에도 트리거를 걸 수 있어요.
뷰에는 INSERT, UPDATE, DELETE 작업 대신(instead of) 실행되는 트리거를 정의할 수 있어요. 이런 INSTEAD OF 트리거는 뷰에서 수정이 필요한 각 행마다 한 번 발화되며, 트리거 함수가 뷰의 기반이 되는 베이스 테이블을 직접 수정하고, 필요하면 수정된 행을 뷰에 보일 모습 그대로 반환하는 책임을 져요.
트리거를 만들기 전에 반드시 트리거 함수부터 정의해야 해요. 이 함수는 인자를 받지 않고 trigger 타입을 반환하도록 선언해야 합니다. (트리거 함수의 입력은 특별히 전달되는 TriggerData 구조체를 통해 받지, 일반 함수 인자 형태가 아니에요.)
함수가 준비되면 CREATE TRIGGER로 트리거를 만들고, 같은 트리거 함수를 여러 트리거에서 재사용할 수도 있어요.
행 단위 트리거 vs 문장 단위 트리거
PostgreSQL은 두 종류의 트리거를 제공해요.
- 행 단위(per-row) 트리거: 트리거를 발화시킨 문장이 영향을 주는 각 행마다 한 번 함수가 호출돼요.
- 문장 단위(per-statement) 트리거: 적절한 문장이 실행될 때 딱 한 번만 호출돼요. 영향받은 행 수와 무관하죠. 특히 0개 행에 영향을 주는 문장이라도 적용 가능한 문장 단위 트리거는 실행돼요.
이 둘을 각각 로우 레벨(row-level), 스테이트먼트 레벨(statement-level) 트리거라고도 불러요. TRUNCATE 트리거는 문장 단위로만 정의할 수 있고, 행 단위로는 정의할 수 없어요.
BEFORE · AFTER · INSTEAD OF 트리거
트리거는 발화 시점에 따라 세 가지로 분류돼요.
BEFORE트리거: 작업이 일어나기 전에.AFTER트리거: 작업이 끝난 후에.INSTEAD OF트리거: 작업 대신에.
문장 단위 BEFORE 트리거는 문장이 아무것도 시작하기 전에, 문장 단위 AFTER 트리거는 문장이 맨 끝에 발화돼요. 테이블·뷰·외부 테이블 모두에 정의할 수 있어요.
행 단위 BEFORE 트리거는 특정 행이 처리되기 직전에, 행 단위 AFTER 트리거는 문장 끝(문장 단위 AFTER보다는 먼저)에 발화돼요. 이 둘은 테이블과 외부 테이블에만 정의할 수 있고 뷰에는 안 돼요. INSTEAD OF 트리거는 오직 뷰에, 그리고 행 단위로만 정의할 수 있으며, 뷰에서 수정이 필요한 각 행이 식별되는 즉시 발화돼요.
트리거 함수의 반환값
문장 단위 트리거가 호출하는 함수는 항상 NULL을 반환해야 해요. 행 단위 트리거는 원하면 테이블 행(HeapTuple)을 실행기에 반환할 수 있어요.
작업 전에 발화되는 행 단위 트리거는 두 가지 선택지가 있어요.
NULL을 반환해 현재 행의 작업을 건너뛴다. 실행기가 해당 행에 대한 삽입·수정·삭제를 수행하지 않게 돼요.- 행 단위
INSERT·UPDATE트리거에서만, 반환된 행이 실제로 삽입되거나 수정될 행이 돼요. 이렇게 하면 트리거 함수가 삽입·수정되는 행을 수정할 수 있어요.
둘 다 의도하지 않은 행 단위 BEFORE 트리거는, 받은 그대로의 행을 반환해야 해요(즉 INSERT·UPDATE는 NEW 행, DELETE는 OLD 행).
행 단위 INSTEAD OF 트리거는 NULL을 반환해 뷰의 베이스 테이블에서 데이터를 수정하지 않았음을 나타내거나, 전달받은 뷰 행을 반환해 수정을 수행했음을 알려요. NULL이 아닌 반환값은 "트리거가 뷰에서 필요한 데이터 수정을 수행했다"는 신호로, 명령이 영향받은 행 수 카운트를 증가시켜요. INSERT·UPDATE에서는 반환 전에 NEW 행을 수정할 수 있고, 이는 INSERT RETURNING·UPDATE RETURNING이 반환하는 데이터를 바꿔요.
작업 후에 발화되는 행 단위 트리거의 반환값은 무시되므로 NULL을 반환해도 돼요.
WHEN 조건과 발화 순서
트리거 정의에는 불리언 WHEN 조건을 지정해 트리거 발화 여부를 판정할 수 있어요. 행 단위 트리거에서는 WHEN 조건이 행의 컬럼 old/new 값을 검사할 수 있어요. BEFORE 트리거에서는 함수가 실행되기 직전에 조건을 평가하므로 WHEN을 쓰는 것과 함수 시작 부분에서 직접 검사하는 것이 실질적으로 다르지 않아요. 하지만 AFTER 트리거에서는 행 업데이트 직후에 조건을 평가하고, 그 결과가 참일 때만 문장 끝에 트리거를 발화시키도록 이벤트를 큐에 넣어요. 그래서 WHEN 조건이 참이 아닌 AFTER 트리거는 이벤트도 큐에 넣지 않고 문장 끝에 행을 다시 가져오지도 않아요. 많은 행을 수정하는 문장에서 트리거가 소수 행에만 발화돼야 한다면 큰 속도 향상을 얻을 수 있어요. INSTEAD OF 트리거는 WHEN 조건을 지원하지 않아요.
같은 이벤트에 트리거가 여러 개 정의되어 있으면 트리거 이름의 알파벳 순서로 발화돼요. BEFORE·INSTEAD OF 트리거의 경우, 각 트리거가 반환한 (수정됐을 수 있는) 행이 다음 트리거의 입력이 돼요. 어떤 BEFORE·INSTEAD OF 트리거가 NULL을 반환하면 그 행에 대한 작업은 포기되고 이후 트리거도 발화되지 않아요.
넘어가기 쉬운 동작 규칙들
몇 가지 꼭 알아 둘 규칙을 모아 볼게요.
- Cascading 트리거: 트리거 함수가 또 SQL을 실행하면 그 명령이 다시 트리거를 발화시킬 수 있어요. 이를 캐스케이딩 트리거라고 불러요. 깊이 제한은 없지만, 예를 들어
INSERT트리거가 같은 테이블에 또 행을 삽입해 같은 트리거가 재발화되는 무한 재귀가 가능해요. 이런 무한 재귀를 피하는 것은 트리거 작성자의 책임이에요. - 외래 키 참조 동작: 외래 키 제약의 참조 동작(cascading update/delete)은 참조하는 테이블에 대해 일반 SQL
UPDATE·DELETE로 수행돼요. 따라서 그 테이블의 기존 트리거도 발화되는데, 만약 트리거가 이 명령의 효과를 수정하거나 막으면 참조 무결성이 깨질 수 있으니 주의해야 해요. - 트리거 인자: 트리거 정의 시 인자를 지정할 수 있어요. 서로 비슷한 요구의 여러 트리거가 같은 함수를 호출하게 하려는 목적이에요. 예를 들어 두 컬럼 이름을 인자로 받아 한쪽엔 현재 사용자, 다른쪽엔 현재 타임스탬프를 넣는 범용 함수를 만들어, 어떤 테이블의
INSERT이벤트에도 재사용할 수 있어요. - 전이 테이블(transition tables): 기본적으로 문장 단위 트리거는 문장이 수정한 개별 행을 볼 방법이 없어요. 하지만
AFTER STATEMENT트리거는 전이 테이블을 요청해 영향받은 행의 집합을 이용할 수 있어요.AFTER ROW트리거도 전이 테이블을 요청해서, 현재 발화된 개별 행뿐 아니라 테이블 전체의 변화를 볼 수 있어요.