한 호텔에서 관리하는 데이터베이스에, 호텔 룸의 예약 상황이 BOOKINGS라는 이름의 테이블에 저장되어 있습니다. 이제 이 테이블의 변경 사항을 감사 용도의 별도 테이블에 저장하고 테이블을 트랜잭션 기반으로 관리하고자 합니다. 이런 목적이라면 트리거가 가장 적격입니다.
DBA는 간단한 "after-update row" 트리거를 이용해서 기존의 값과 새로운 값, 그리고 변경 작업을 실행한 사용자 정보를 BOOKINGS_HIST 테이블에 저장합니다. 지금까지는 작업에 별다른 문제가 없습니다.
하지만 한 가지 작은 문제가 발생했습니다. "after-update row" 트리거는 모든 로우에 대해 동작합니다. 하지만 단체 예약과 같은 경우에는 한 번의 트랜잭션으로 수백 개의 로우가 업데이트되기도 합니다. 이 경우 개별 로우에 대해 "after-update row" 트리거가 동작하고 트리거가 한 번 실행될 때마다 bookings_hist 테이블에 레코드가 삽입되어야 합니다. 따라서 성능이 저하될 수 밖에 없습니다.
좀 더 나은 대안으로 INSERT 작업을 배치 처리해서 한꺼번에 bookings_hist 테이블에 입력하는 방법이 있습니다. 복잡한 형태의 트리거를 한꺼번에 실행하면 이러한 작업이 가능합니다. 로우 트리거에서 업데이트될 값들을 컬렉션에 저장한 다음, "after-update-statement" 트리거에서 컬렉션의 데이터를 로드해서 bookings_history 테이블에 한 번에 저장하는 방법입니다. 실제 INSERT 작업은 단 한 차례만 실행되므로 개별 로우를 INSERT 하는 것보다는 성능이 좋습니다.
하지만 두 개의 트리거를 별도로 작성해야 한다는 문제가 남습니다. 하나의 트리거에서 다른 트리거로 컬렉션 변수를 전달하기 위해서는 VARRAY 또는 PL/SQL TABLE과 같은 컬렉션 변수를 포함하는 패키지를 생성하고, 이 데이터를 "after-update row" 트리거로 입력한 뒤 다른 "after-statement" 트리거에서 읽어 와야 합니다. 쉬운 작업이 아닙니다. 모든 트리거 작업을 하나의 코드에 저장할 수 있다면 훨씬 간단해지지 않을까요?
Oracle Database 11g의 컴파운드 트리거(compound trigger)를 이용하면 가능합니다. 컴파운드 트리거는 4가지 서로 다른 트리거를 하나의 트리거 안에 정의하고 있습니다. 예를 들어 UPDATE 컴파운드 트리거는 "before statement", "before row", "after statement", "after row" 트리거를 하나의 컴파운드 트리거 안에 포함합니다. 코드는 하나만 존재하며 따라서 다른 PL/SQL 코드들과 마찬가지 방법으로 변수를 전달하는 것이 가능합니다.
한 가지 예를 들어 설명해 보겠습니다. 설명을 위해 코드에 라인 넘버를 추가했습니다.
1 create or replace trigger tr_bookings_track 2 for update of booking_dt 3 on bookings 4 compound trigger 5 type ty_bookings_hist is table of bookings_hist%rowtype 6 index by pls_integer; 7 coll_bookings_hist ty_bookings_hist; 8 ctr pls_integer := 0; 9 before statement is 10 begin 11 dbms_output.put_line('In before statement'); 12 end before statement; 13 before each row is 14 begin 15 dbms_output.put_line('In before each row'); 16 end before each row; 17 after each row is 18 begin 19 ctr := ctr + 1; 20 dbms_output.put_line('In after each row. booking_id='||:new.booking_id); 21 coll_bookings_hist(ctr).booking_id := :new.booking_id; 22 coll_bookings_hist(ctr).mod_dt := sysdate; 23 coll_bookings_hist(ctr).mod_user := user; 24 coll_bookings_hist(ctr).old_booking_dt := :old.booking_dt; 25 coll_bookings_hist(ctr).new_booking_dt := :new.booking_dt; 26 end after each row; 27 after statement is 28 begin 29 dbms_output.put_line('In after statement'); 30 forall counter in 1..coll_bookings_hist.count() 31 insert into bookings_hist 32 values coll_bookings_hist(counter); 33 end after statement; 34 end tr_bookings_track;
트리거의 동작 방식을 이해하기 위해 4개의 로우를 업데이트해 보기로 합시다.
br> update bookings set booking_dt = sysdate where booking_id between 100 and 103;
실행 결과가 아래와 같습니다.
In before statement In before each row In after each row. booking_id=100 In before each row In after each row. booking_id=101 In before each row In after each row. booking_id=102 In before each row In after each row. booking_id=103 In after statement
컴파운드 트리거가 동작하는 방식에 주목하시기 바랍니다. 컴파운드 트리거는 4개의 섹션으로 구분됩니다.
Before Statement ... 구문 실행 이전에 한 차례 실행 ... ... Before Row ... 작업 이전에 로우 당 한 차례씩 실행 ... After Row ... 작업 이후 로우 당 한 차례씩 실행 ... After Statement ... 구문 당 한 차례씩 실행 ...
위에서 볼 수 있듯, 코드는 하나이지만 각 섹션은 서로 다른 시점에 실행됩니다.
앞의 예제에서 dbms_output 구문을 여러 시점에서 실행하도록 함으로써 각각의 섹션이 어떤 시점에 실행되는지 확인할 수 있도록 했습니다. 그리고 bookings_ids 값이 100, 101, 102, 103인 4개의 로우를 업데이트했습니다. 이 과정에서 "before-statement", "after-statement" 트리거가 한 차례씩, 그리고 로우 트리거(before, after)들은 로우별로 한 차례씩 실행되었습니다. (앞의 예제에서는 "before-statement", "before-row" 트리거가 필요하지 않지만, 기능에 대한 이해를 돕기 위해 추가되었습니다.)
이제 bookings_hist 테이블을 살펴보면 4개의 레코드가 추가되었음을 확인할 수 있습니다. 4개의 레코드는 구문 실행의 마지막 단계에서 한꺼번에 업데이트되었으며, 로우 단위로 업데이트되지 않았습니다.
![](http://www.dbguide.net/images/oracle11g_guide/071127_oraclePLSQL_p1.jpg)
컴파운드 트리거는 트리거가 동작할 때 PL/SQL 코드 내에서 변수, 패키지와 같은 "stateful" 오브젝트가 사용되고 트리거 종료 시점에서 그 정보가 삭제되는 경우에 매우 유용합니다. 위의 예제에서, 필자는 컬렉션을 초기화하거나 컬렉션의 컨텐트를 삭제하는 작업을 별도로 수행하지 않았습니다. 이러한 작업은 데이터베이스에 의해 자동으로 수행됩니다.
|