1. CUBRID 트리거 문법 정정
검증 없이 추측으로 작성했던 트리거 구문을 CUBRID 공식 매뉴얼 예제와 대조해 바로잡은 기록임.
1.1 추측으로 잘못 작성했던 부분
- 행 값 참조 — 바뀐 행의 컬럼을 테이블명.컬럼명(예: test_dmy_table.mng_no) 형태로 참조할 수 있다고 가정했으나 틀린 문법이었음
- 현재시각 함수 — SYS_DATETIME으로 작성했으나 CUBRID에는 해당 이름의 함수가 없음
- 공통 원인 — 다른 DBMS(오라클의 :NEW, MySQL의 NEW.)의 감각을 그대로 옮겼고, 매뉴얼 확인을 뒤로 미룬 것이 원인이다
1.2 매뉴얼 기준 정답
CREATE TABLE update_logs(event_code INTEGER, score VARCHAR(10), dt DATETIME);
CREATE TRIGGER example
BEFORE UPDATE ON history(score)
EXECUTE INSERT INTO update_logs VALUES (obj.event_code, obj.score, SYSDATETIME);
- obj — 트리거를 발동시킨 그 한 행을 가리키는 고정 예약어. 테이블명이 아니라 항상 obj로 씀
- SYSDATETIME — 언더스코어 없는 형태가 정식 함수명이다
1.3 obj 예약어가 도입된 배경
- 모호함이 없는 구조 — 트리거는 ON 절에서 감시 대상 테이블을 하나로 확정하므로, obj가 가리킬 대상은 언제나 "지금 이 트리거를 발동시킨 딱 한 행"으로 유일하게 결정됨
- 테이블명을 쓸 수 없는 이유 — 테이블명을 그대로 상관 참조(correlation name)로 허용하면 "테이블 전체(여러 행의 집합)"라는 원래 의미와 "지금 이 한 행"이라는 의미가 같은 문자열에 겹쳐 파서가 구분할 수 없게 됨
- 해법 — 실제 테이블명이 될 수 없는 별도 예약어를 도입해 모호함을 원천 차단한 것이다. 다른 언어의 this / self와 같은 역할
상관 참조 이름이 예약어인 DBMS는 대부분 같은 이유를 공유함 — 집합을 가리키는 이름과 원소를 가리키는 이름을 문법 차원에서 분리한 것이다.
2. 수정된 트리거 DDL
감사 로그 테이블에 INSERT/UPDATE 이력을 남기는 실제 적용 구문임.
2.1 최종 구문
CREATE TRIGGER test_ins_trigger
AFTER INSERT ON NCSC_INTEG.test_dmy_table
EXECUTE INSERT INTO NCSC_INTEG.test_trigger_table (tbl_nm, pk_vl, trig_type_cd, chg_dt)
VALUES ('test_dmy_table', obj.mng_no, 'i', SYSDATETIME)
COMMENT 'trigger for my study';
CREATE TRIGGER test_upt_trigger
AFTER UPDATE ON NCSC_INTEG.test_dmy_table
EXECUTE INSERT INTO NCSC_INTEG.test_trigger_table (tbl_nm, pk_vl, trig_type_cd, chg_dt)
VALUES ('test_dmy_table', obj.mng_no, 'u', SYSDATETIME)
COMMENT 'trigger for my study';
2.2 스키마 접두사 규칙
| 위치 스키마 |
접두사 |
비고 |
| ON 절의 대상 테이블 |
허용 |
NCSC_INTEG.test_dmy_table |
| INSERT INTO 대상 테이블 |
허용 |
NCSC_INTEG.test_trigger_table |
| VALUES 안의 obj.컬럼명 |
불가 |
obj는 고정 문법이므로 스키마를 붙이면 안 됨 |
2.3 남은 검증 항목
- 함수명 일괄 교체 — 기존 설계에서 SYS_DATETIME으로 작성한 지점(chg_dt, job_proc_dt 등) 전체를 SYSDATETIME으로 교체 검토 필요
- 트리거 밖 동작 확인 — NiFi ExecuteSQL의 UPDATE 문처럼 트리거 외부의 일반 SQL에서도 SYSDATETIME이 동일하게 동작하는지 테스트 필요함
3. DDL에 내장된 DML의 동작 차이
이번 삽질에서 문법보다 더 크게 남은 것은 "같은 DML이라도 어디에 놓이느냐에 따라 이름 해석 시점이 달라진다"는 점이다.
3.1 독립 DML과 내장 DML
- 전체 구조 — CREATE TRIGGER 문 전체는 객체를 정의하는 DDL이고, 그 안의 EXECUTE INSERT INTO ... VALUES (...)는 DML이 DDL 정의 안에 내장된 형태임
- 독립 DML — 실행하는 바로 그 순간의 세션/계정을 기준으로 이름 해석과 권한 체크가 즉석에서 이루어짐(동적)
- 내장 DML — 트리거가 정의되는 CREATE TRIGGER 실행 시점의 계정 기준으로 이름 해석과 권한이 영구히 고정됨(정적). 이후 누가 트리거를 발동시키든 정의 당시 고정된 대로만 동작한다
3.2 정의자 권한
- definer's rights — 객체를 정의한 계정의 권한으로 실행되는 방식이며, 트리거뿐 아니라 뷰와 저장 프로시저도 대부분 같은 원리를 따름
- 실행자 권한과의 대비 — 호출자 기준으로 매번 해석하는 invoker's rights 방식과 반대 개념이다
| 구분 이름 |
해석 시점 |
권한 기준 |
| 독립 DML |
실행 시점 |
실행 계정 |
| 트리거 내장 DML |
정의 시점 |
정의 계정 |
3.3 실제로 겪은 증상
- 증상 — dba 계정으로 트리거를 생성하자 트리거 안의 테이블 참조가 dba 스키마 기준으로 고정됨. 실제 감시/참조 대상은 NCSC_INTEG 소유 테이블인데도 계속 dba 기준으로 해석되며 실패했음
- 효과 없던 시도 — 스키마 접두사를 SQL 문법 차원에서 아무리 바꿔도 해결되지 않음
- 해결 — 정의하는 계정 자체를 바꾸는 것(NCSC_INTEG/admin으로 재로그인 후 재생성)만이 유일한 해법이었다
에러가 SQL 문법이 아니라 "누가 정의했는가"에서 비롯될 수 있음 — 구문을 계속 고쳐도 증상이 그대로면 세션 계정을 먼저 의심해야 함.
3.4 이렇게 설계된 이유
- 반복 발동되는 객체 — 트리거는 한 번 정의되고 이후 수많은 다른 주체(예: Python 생성기, NiFi 프로세서, 운영자 콘솔)에 의해 반복 발동됨
- 발동 시점 권한의 위험 — 매번 발동시키는 주체의 권한에 따라 동작이 달라지면 결과가 예측 불가능해지고, 권한 상승 경로가 열릴 수 있음
- 일관성 확보 — 발동 시점이 아니라 정의 시점의 권한으로 고정시켜 언제나 동일하게 동작하도록 설계된 것이다
요약
| 항목 |
잘못 알던 것 |
실제 |
| 행 값 참조 |
테이블명.컬럼명 |
obj.컬럼명 (고정 예약어) |
| 현재시각 함수 |
SYS_DATETIME |
SYSDATETIME |
| 스키마 접두사 |
모든 위치에 사용 가능 |
obj.컬럼명에는 사용 불가 |
| 권한 기준 |
트리거를 발동시킨 계정 |
트리거를 정의한 계정 |
| 스키마 오류 해결법 |
SQL 구문 수정 |
정의 계정 변경 후 재생성 |
트리거는 DDL로 박제된 DML임 — 문법은 매뉴얼에서 확인하고, 동작 범위는 정의하는 계정에서 확인해야 한다.