제약·트리거·뷰 — DB가 직접 지키는 규칙과 가상 테이블
왜 규칙을 DB에 두는가 — 마지막 방어선
"음수 재고는 없다", "없는 회원의 주문은 없다" 같은 규칙은 애플리케이션 코드에서 검사하면 되지 않나? 싶다. 문제는 — 애플리케이션은 깜빡하고, 여러 앱·배치·관리자 콘솔이 같은 DB를 건드리며, 동시 요청이 검사를 비집고 들어온다. 앱에서 "재고 > 0인지 확인 후 차감"해도, 두 요청이 동시에 확인을 통과하면 둘 다 차감해 음수가 된다([04 Lost Update]). 그래서 진짜 중요한 규칙은 DB 자신이 못 박아야 한다 — **제약(constraint)**으로 값의 규칙을, **트리거(trigger)**로 변경에 반응하는 동작을, **뷰(view)**로 복잡한 쿼리·보안을 캡슐화한다. 이들은 애플리케이션이 무엇을 하든 DB가 지키는 마지막 방어선이다. (출처: PostgreSQL — Constraints/Triggers/Views / MySQL 8.4.)
제약 — 값의 규칙을 못 박는다
CREATE TABLE orders (
id bigint PRIMARY KEY, -- 개체 무결성(유일+NOT NULL)
member_id bigint NOT NULL REFERENCES member(id) ON DELETE RESTRICT, -- 참조 무결성(FK)
status text NOT NULL DEFAULT 'pending', -- 필수값 + 기본값
amount numeric(12,2) NOT NULL CHECK (amount >= 0), -- 도메인 무결성(CHECK)
coupon text UNIQUE, -- 중복 금지
CONSTRAINT valid_status CHECK (status IN ('pending','paid','shipped')) -- 명명 제약
);
각 제약이 무엇을 보장하는지 정확히 알아야 한다.
- PRIMARY KEY — 유일 + NOT NULL. "식별할 수 없는 행"을 금지한다([01 개체 무결성]).
- FOREIGN KEY — 참조 무결성.
member_id는 반드시 존재하는 member를 가리키거나 NULL이어야 한다 — 없는 회원의 주문을 원천 차단한다. 그리고ON DELETE로 부모가 사라질 때를 정한다(아래). - UNIQUE — 중복 금지(여러 칼럼 복합도 가능). 주의 — NULL은 보통 서로 다르다고 보아 여럿 허용된다(쿠폰을 안 쓴 주문이 여럿 NULL이어도 OK).
- CHECK — 임의 조건(범위·열거·칼럼 간 관계). PostgreSQL은 오래전부터 강력하고, MySQL도 8.0.16+부터 실제로 강제한다(그 전엔 무시됐다).
- NOT NULL / DEFAULT — 필수값 / 미지정 시 기본값.
ON DELETE의 선택이 연쇄 사고를 가른다. RESTRICT(자식이 있으면 부모 삭제 거부, 기본), CASCADE(부모 삭제 시 자식도 함께 삭제), SET NULL(자식의 FK를 NULL로). CASCADE는 편하지만 — 회원 하나 지웠는데 주문·결제·로그가 줄줄이 사라질 수 있어 신중해야 한다. *명명 제약(CONSTRAINT name)*은 에러 메시지를 알아보기 쉽게 하고 나중에 ALTER로 떼기도 쉽다. PostgreSQL의 *지연 제약(DEFERRABLE)*은 검사를 트랜잭션 끝으로 미뤄 순환 FK 삽입 같은 경우를 푼다.
뷰 — 복잡한 쿼리를 가상 테이블로
**뷰(view)**는 저장된 SELECT — 물리 데이터를 복제하는 게 아니라, 쿼리에 이름을 붙인 가상 테이블이다. 쓰임이 둘이다. 하나는 복잡한 조인·집계를 감춰 사용 측을 단순하게(매번 긴 쿼리를 안 써도 됨), 다른 하나는 보안 — 특정 칼럼·행만 노출하는 것이다.
-- 보안: 민감 칼럼(email·주민번호)을 빼고 활성 회원만 노출
CREATE VIEW active_member AS
SELECT id, name FROM member WHERE status = 'active';
SELECT * FROM active_member; -- 테이블처럼 쓰되, 노출된 것만 보임
특정 역할에게 원본 테이블 대신 뷰에만 권한을 주면 — 그 역할은 email을 아예 볼 수 없다([17 보안]의 컬럼 통제). 두 가지 더 — 단순 뷰(단일 테이블·집계 없음)는 INSERT/UPDATE도 가능하고(WITH CHECK OPTION으로 뷰 조건을 어기는 변경을 막는다), PostgreSQL의 **구체화 뷰(materialized view)**는 결과를 실제로 저장해 무거운 집계를 캐싱한다(REFRESH로 갱신 — 최신성↔성능 트레이드오프). MySQL엔 구체화 뷰가 없어 요약 테이블 + 트리거/배치로 흉내 낸다.
트리거 — 변경에 반응하는 자동 동작
트리거는 INSERT/UPDATE/DELETE가 일어날 때 DB가 자동으로 실행하는 로직이다 — 감사 로그·파생 값 계산·복잡한 무결성에 쓴다.
-- PostgreSQL: 트리거 '함수'를 만들고 트리거로 연결 (수정 시각 자동 기록)
CREATE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END; -- BEFORE라 NEW를 바꿔 저장
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_updated BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
-- MySQL: 트리거 본문을 직접 작성 (상태 변경을 감사 테이블에 기록)
CREATE TRIGGER trg_audit AFTER UPDATE ON orders FOR EACH ROW
INSERT INTO order_audit(order_id, old_status, new_status, changed_at)
VALUES (OLD.id, OLD.status, NEW.status, NOW());
세 가지 축을 안다. 타이밍 — BEFORE(변경 전, NEW를 수정·검증)·AFTER(변경 후, 로그·파생)·PostgreSQL의 INSTEAD OF(뷰에 대한 변경을 가로챔). 레벨 — FOR EACH ROW(영향받는 행마다)·PostgreSQL의 FOR EACH STATEMENT(문장당 1회). NEW/OLD — 새/옛 행 값을 참조한다.
트리거의 양날 — 강력하지만 **"숨은 로직"**이다. 애플리케이션 코드에는 안 보이는데 DB가 몰래 동작하니, 왜 이 값이 바뀌었는지 추적이 어렵고, 트리거가 또 다른 트리거를 부르는 연쇄·성능 문제가 생긴다. 그래서
updated_at자동 기록·감사 로그처럼 명확하고 단순한 용도에만 쓰고, *복잡한 비즈니스 로직은 애플리케이션(또는 명시적 코드)*에 두는 게 정석이다.
저장 프로시저·함수
DB 안에 로직을 함수로 둘 수도 있다 — PostgreSQL의 PL/pgSQL, MySQL의 stored procedure/function. DB 가까이서 대량 데이터를 처리할 땐 네트워크 왕복을 줄여 유리하지만 — 버전 관리(Git)·테스트·이식성이 약하고 디버깅이 어려워 과용은 피한다. 단순한 파생 값은 트리거 대신 *생성 칼럼(GENERATED ALWAYS AS)*으로 더 깔끔하게 처리한다([06·08]).
정리 — 규칙은 DB에, 로직은 신중히
- 제약 — 값의 규칙(PK·FK·UNIQUE·CHECK·NOT NULL)을 DB가 강제하는 애플리케이션 버그·동시성의 마지막 방어선.
ON DELETE로 부모 삭제 시 동작(RESTRICT/CASCADE/SET NULL)을 정한다. - 뷰 — 복잡 쿼리·보안(컬럼/행 제한)을 가상 테이블로 캡슐화. 구체화 뷰는 집계 캐싱(최신성↔성능).
- 트리거 — *변경에 자동 반응(감사·파생·updated_at)*하되, "숨은 로직"이라 단순·명확한 용도에만 쓰고 복잡 로직은 앱에.
한 줄 요약 — 제약: PK(유일+NOT NULL)·FK(참조 무결성·ON DELETE RESTRICT/CASCADE/SET NULL)·UNIQUE(NULL은 여럿)·CHECK(MySQL 8.0.16+ 강제)·NOT NULL/DEFAULT·명명/DEFERRABLE — 동시성에도 뚫리지 않는 마지막 방어선. 뷰=저장된 SELECT(보안·캡슐화, 단순 뷰는 갱신, materialized view는 집계 캐싱). 트리거=DML에 자동 반응(BEFORE 검증/AFTER 로그·NEW/OLD)·숨은 로직이라 감사/updated_at만. 복잡 로직·대량 처리는 신중히(앱 또는 생성 칼럼).
(출처: PostgreSQL — Constraints / Views / Triggers / PL/pgSQL / MySQL 8.4 — CHECK / CREATE VIEW / CREATE TRIGGER.)