무결성 제약
데이터베이스에 잘못된 데이터가 들어가면, 그 위에서 동작하는 모든 애플리케이션이 잘못된 결과를 냅니다. 무결성 제약(Integrity Constraint)은 데이터의 정확성과 일관성을 보장하기 위한 규칙입니다.
무결성 제약은 스키마에 선언된 규칙을 DBMS가 강제하게 만드는 장치입니다. 애플리케이션 코드에서 검증하는 것보다 안전한 이유는, 코드에서는 검증을 빠뜨릴 수 있지만 선언되고 활성화된 DB 제약 조건은 모든 쓰기 경로에 같은 기준으로 적용되기 때문입니다. 일부 DBMS는 지연 제약이나 통제된 비활성화 기능을 제공하므로, 운영 중에는 “선언·활성화된 제약”이 기준이라고 이해하면 됩니다.
무결성 제약의 분류
이 장에서는 무결성 제약을 크게 5가지 관점으로 분류합니다. 분류 방식은 교재나 DBMS 문맥에 따라 조금씩 달라질 수 있습니다.
개체 무결성 (Entity Integrity)
기본키는 NULL이 될 수 없고, 중복될 수 없습니다. 기본키가 NULL이면 해당 튜플을 식별할 수 없고, 중복이면 어떤 튜플인지 구분할 수 없기 때문입니다.
CREATE TABLE students (
id NUMBER PRIMARY KEY, -- NULL 불가, 중복 불가
name VARCHAR2(50) NOT NULL
);
INSERT INTO students VALUES (NULL, '김철수'); -- 에러! PK에 NULL
INSERT INTO students VALUES (1, '김철수');
INSERT INTO students VALUES (1, '이영희'); -- 에러! PK 중복복합 기본키에서의 개체 무결성
복합 기본키(두 개 이상의 컬럼으로 구성된 기본키)에서는 각 개별 컬럼에 NULL이 올 수 없습니다.
CREATE TABLE enrollments (
student_id NUMBER,
course_id NUMBER,
grade CHAR(2),
PRIMARY KEY (student_id, course_id) -- 복합 PK
);
-- 복합 PK의 어느 한 컬럼이라도 NULL이면 오류
INSERT INTO enrollments VALUES (NULL, 101, 'A'); -- 에러!
INSERT INTO enrollments VALUES (1, NULL, 'A'); -- 에러!
-- 조합이 같으면 중복 오류
INSERT INTO enrollments VALUES (1, 101, 'A');
INSERT INTO enrollments VALUES (1, 101, 'B'); -- 에러! (1, 101) 중복기본키 자동 생성
대부분의 DBMS는 기본키 값을 자동으로 생성하는 기능을 제공합니다.
-- MySQL: AUTO_INCREMENT
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL
);
-- PostgreSQL: SERIAL / IDENTITY
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- 또는: id INT GENERATED ALWAYS AS IDENTITY
name VARCHAR(50) NOT NULL
);
-- Oracle: SEQUENCE + TRIGGER (또는 12c부터 IDENTITY)
CREATE SEQUENCE user_seq START WITH 1 INCREMENT BY 1;
CREATE TABLE users (
id NUMBER DEFAULT user_seq.NEXTVAL PRIMARY KEY,
name VARCHAR2(50) NOT NULL
);자동 증가 PK를 사용하면 기본키 값을 직접 관리할 때보다 중복이나 누락 위험이 줄고, PK 제약이 개체 무결성을 강제합니다.
참조 무결성 (Referential Integrity)
외래키 값은 참조 대상 테이블의 기본키 또는 UNIQUE로 보장된 키에 존재하는 값이어야 합니다. 외래키 컬럼이 NULL을 허용한다면 NULL도 가능하지만, NULL이 허용되지 않는 외래키라면 반드시 참조 가능한 값이 들어가야 합니다. 이 규칙은 테이블 간의 관계를 정확하게 유지합니다.
참조 무결성이 위반될 수 있는 4가지 연산
참조 동작 옵션
참조 중인 데이터를 삭제하거나 수정할 때, DBMS가 어떻게 처리할지를 지정할 수 있습니다. 다만 ON UPDATE, SET DEFAULT, NO ACTION의 세부 동작은 DBMS마다 차이가 있으므로 실제 제품 문서를 확인해야 합니다.
| 옵션 | 동작 | 사용 예 |
|---|---|---|
| RESTRICT | 참조 행이 있으면 삭제/수정 거부 | 주문이 있는 고객 삭제 방지 |
| CASCADE | 참조 행도 함께 삭제/수정 | 게시글 삭제 시 댓글도 삭제 |
| SET NULL | 외래키를 NULL로 설정 | 담당자 삭제 시 업무의 담당자를 비움 |
| SET DEFAULT | 외래키를 기본값으로 설정 | 카테고리 삭제 시 미분류로 변경 |
| NO ACTION | 보통 RESTRICT와 유사하나, 지연 제약이면 늦게 검사 가능 | SQL 표준 동작 |
CREATE TABLE orders (
id NUMBER PRIMARY KEY,
customer_id NUMBER REFERENCES customers(id) ON DELETE CASCADE,
product_id NUMBER REFERENCES products(id) ON DELETE SET NULL
);각 옵션의 동작 시나리오
실무에서의 CASCADE 사용 주의
CASCADE는 편리하지만 위험할 수 있습니다. 부모 행 하나를 삭제했는데 연쇄적으로 수천 개의 행이 삭제될 수 있습니다.
users → orders → order_items → order_item_options
↓
payments → payment_details
users에서 1명 삭제 시 CASCADE가 걸려 있다면:
→ orders 10건 삭제
→ order_items 50건 삭제
→ order_item_options 200건 삭제
→ payments 10건 삭제
→ payment_details 30건 삭제
총 300건 연쇄 삭제!실무에서는 CASCADE 대신 소프트 삭제(Soft Delete) 방식을 많이 사용합니다.
-- 물리적 삭제 대신 deleted_at 컬럼으로 논리적 삭제
ALTER TABLE users ADD deleted_at TIMESTAMP NULL;
-- 삭제 시
UPDATE users SET deleted_at = CURRENT_TIMESTAMP WHERE id = 1;
-- 조회 시
SELECT * FROM users WHERE deleted_at IS NULL;도메인 무결성 (Domain Integrity)
속성 값은 정의된 도메인에 속해야 합니다. 나이에 음수가 들어가거나, 성별에 XYZ가 들어가면 안 됩니다.
CHECK 제약조건
CREATE TABLE employees (
id NUMBER PRIMARY KEY,
name VARCHAR2(100) NOT NULL,
age NUMBER CHECK (age >= 0 AND age <= 150),
gender CHAR(1) CHECK (gender IN ('M', 'F')),
email VARCHAR2(200) UNIQUE,
salary NUMBER CHECK (salary >= 0)
);
INSERT INTO employees VALUES (1, '김철수', -5, 'M', 'a@t.co', 5000);
-- 에러! CHECK 위반: age >= 0
INSERT INTO employees VALUES (1, '김철수', 25, 'X', 'a@t.co', 5000);
-- 에러! CHECK 위반: gender IN ('M', 'F')현재 날짜와 비교하는 규칙처럼 실행 시점에 따라 결과가 달라지는 조건은 DBMS별 제약이 있습니다. 이런 규칙은 CHECK보다 트리거나 애플리케이션 검증으로 처리하는 경우가 많습니다.
NOT NULL 제약조건
CREATE TABLE products (
id NUMBER PRIMARY KEY,
name VARCHAR2(100) NOT NULL, -- 상품명은 반드시 필요
price NUMBER NOT NULL, -- 가격도 반드시 필요
description VARCHAR2(500) -- 설명은 선택 (NULL 허용)
);UNIQUE 제약조건
UNIQUE는 해당 컬럼의 값이 테이블 전체에서 유일해야 합니다. PRIMARY KEY와 달리 NULL 처리 방식은 DBMS별로 다릅니다. 후보키나 대체키처럼 “값이 반드시 있으면서 유일해야 하는” 컬럼이라면 NOT NULL과 UNIQUE를 함께 사용합니다.
CREATE TABLE users (
id NUMBER PRIMARY KEY,
email VARCHAR2(200) UNIQUE, -- 이메일은 유일해야
phone VARCHAR2(20) UNIQUE, -- 전화번호도 유일해야
nickname VARCHAR2(50) UNIQUE -- 닉네임도 유일해야
);
-- 대부분의 DBMS에서는 NULL을 서로 다른 미확정 값처럼 취급하지만, 세부 동작은 DBMS별로 다름
INSERT INTO users VALUES (1, NULL, NULL, '철수');
INSERT INTO users VALUES (2, NULL, NULL, '영희'); -- OK (NULL은 중복이 아님)DEFAULT 제약조건
CREATE TABLE posts (
id NUMBER PRIMARY KEY,
title VARCHAR2(200) NOT NULL,
status VARCHAR2(20) DEFAULT 'draft', -- 기본값: 초안
views NUMBER DEFAULT 0, -- 기본값: 0
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO posts (id, title) VALUES (1, '첫 게시물');
-- status = 'draft', views = 0, created_at = 현재시간 (자동 설정)사용자 정의 무결성 (비즈니스 규칙)
위 세 가지 외에도 비즈니스 로직에 따른 규칙을 정의할 수 있습니다. CHECK 제약조건이나 트리거(Trigger)를 사용합니다.
-- 1. 시작일은 종료일보다 이전이어야 함
CREATE TABLE projects (
id NUMBER PRIMARY KEY,
name VARCHAR2(100),
start_date DATE,
end_date DATE,
CHECK (start_date <= end_date)
);
-- 2. 할인율은 0% ~ 50% 사이
CREATE TABLE promotions (
id NUMBER PRIMARY KEY,
discount_rate NUMBER CHECK (discount_rate >= 0 AND discount_rate <= 50)
);트리거를 사용한 복잡한 규칙
CHECK로 표현할 수 없는 복잡한 규칙은 트리거(Trigger)로 구현합니다.
-- 재고보다 많은 수량 주문 방지
CREATE OR REPLACE FUNCTION check_stock()
RETURNS TRIGGER AS $
BEGIN
IF NEW.quantity > (SELECT stock FROM products WHERE id = NEW.product_id) THEN
RAISE EXCEPTION '재고 부족: 요청 수량(%)이 재고(%)를 초과',
NEW.quantity,
(SELECT stock FROM products WHERE id = NEW.product_id);
END IF;
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER trg_check_stock
BEFORE INSERT ON order_items
FOR EACH ROW
EXECUTE FUNCTION check_stock();무결성 제약과 성능
무결성 제약은 데이터 품질을 보장하지만 성능에 영향을 줍니다.
-- Oracle
ALTER TABLE orders DISABLE CONSTRAINT fk_customer;
-- 대량 INSERT 실행
ALTER TABLE orders ENABLE CONSTRAINT fk_customer;
-- PostgreSQL
ALTER TABLE orders DISABLE TRIGGER ALL;
-- 대량 INSERT 실행
ALTER TABLE orders ENABLE TRIGGER ALL;제약 조건을 일시적으로 비활성화하는 방식은 권한과 DBMS 동작 차이가 크며, 잘못 사용하면 깨진 데이터가 그대로 적재될 수 있습니다. 운영 환경에서는 통제된 절차로만 사용하고, 적재 후 반드시 재검증해야 합니다.
무결성 제약 확인과 관리
-- Oracle: 테이블의 제약 조건 목록
SELECT constraint_name, constraint_type, search_condition
FROM user_constraints
WHERE table_name = 'EMPLOYEES';
-- PostgreSQL: 제약 조건 목록
SELECT conname, contype, pg_get_constraintdef(oid) AS definition
FROM pg_constraint
WHERE conrelid = 'employees'::regclass;
-- MySQL: 제약 조건 목록
SELECT constraint_name, constraint_type
FROM information_schema.table_constraints
WHERE table_name = 'employees';-- 제약 조건 추가
ALTER TABLE employees ADD CONSTRAINT chk_salary CHECK (salary >= 0);
ALTER TABLE orders ADD CONSTRAINT fk_customer
FOREIGN KEY (customer_id) REFERENCES customers(id);
-- 제약 조건 삭제
ALTER TABLE employees DROP CONSTRAINT chk_salary;
ALTER TABLE orders DROP CONSTRAINT fk_customer;
-- 제약 조건 이름 변경
ALTER TABLE employees RENAME CONSTRAINT chk_salary TO chk_emp_salary;무결성 제약과 애플리케이션의 역할
무결성 검증을 어디에서 해야 하는지는 중요한 설계 결정입니다.
데이터 품질은 모든 시스템의 기반이므로, 핵심 규칙은 가능한 한 DB 레벨 제약 조건으로 보장하고 애플리케이션에서도 보조적으로 검증해야 합니다.
무결성 제약 정리
| 무결성 유형 | 대상 | 보장하는 것 | 구현 방법 |
|---|---|---|---|
| 개체 무결성 | 기본키 | 튜플의 유일 식별 | PRIMARY KEY |
| 참조 무결성 | 외래키 | 테이블 간 관계 정합성 | FOREIGN KEY |
| 도메인 무결성 | 속성 값 | 유효한 값만 저장 | CHECK, NOT NULL, UNIQUE, DEFAULT |
| 키 무결성 | 릴레이션 | 후보키 구조 설계 | PRIMARY KEY, UNIQUE |
| 사용자 정의 | 비즈니스 규칙 | 업무 규칙 준수 | CHECK, TRIGGER |
핵심 정리
다음 절에서는 SQL의 이론적 기반인 관계 대수를 다루겠습니다.