사전 제약조건
개념

제약조건

gabury1고친 사람 github-actions[bot]

제약조건은 테이블에 들어가면 안 되는 데이터를 데이터베이스가 직접 막게 해 줍니다. 규칙을 미리 적어 두면 그 규칙을 어기는 쓰기는 저장되지 않습니다. 그래서 어느 프로그램이 쓰든 테이블에는 규칙을 지킨 데이터만 남습니다. 같은 이름이 다른 분야에도 쓰입니다. 이 항목은 데이터베이스의 제약조건을 다룹니다.

쉽고 빠른 이해

제약조건은 「이 값은 비면 안 된다」 「수량은 0보다 커야 한다」 같은 규칙을 데이터베이스에 맡겨 둡니다. 주문 테이블의 수량 열에 「0보다 커야 한다」를 걸어 두면 수량이 -3 인 주문은 저장되지 않습니다.

같은 테이블에 쓰는 곳은 서버 코드, 관리자 화면, 배치 작업처럼 여럿입니다. 검사를 코드에만 두면 한 곳만 빠뜨려도 잘못된 값이 들어옵니다. 들어온 값은 한참 뒤에야 드러납니다.

어떻게 도는가:

  1. 테이블을 만들 때 규칙을 함께 적어 둡니다
  2. 행을 넣거나 고치거나 지울 때마다 데이터베이스가 그 규칙에 비춰 봅니다
  3. 어기면 그 쓰기를 오류로 돌려보내고 아무것도 저장하지 않습니다

대가도 있습니다. 쓰기마다 검사가 붙어 조금 느려집니다. 이미 들어 있는 데이터가 새 규칙을 어기면 그 규칙을 걸 수 없어서 데이터부터 정리해야 합니다.

못 쓰는 경우도 있습니다. 여러 행을 함께 세어야 하는 규칙은 제약조건으로 적지 못합니다. 「한 회원의 진행 중인 주문은 셋까지」가 그렇습니다. 이런 규칙은 트리거나 애플리케이션 코드가 맡습니다.

상세

은행 창구의 입금표를 떠올려 봅시다. 입금표에는 칸마다 규칙이 인쇄돼 있습니다. 계좌번호 칸은 비우면 안 됩니다. 금액 칸에는 0보다 큰 숫자를 씁니다. 창구 직원은 누가 가져왔든 규칙에 안 맞는 입금표를 받지 않고 돌려줍니다.

데이터베이스는 데이터를 테이블에 담습니다. 테이블은 같은 모양의 데이터를 줄지어 담는 표입니다. 줄 하나를 행, 칸 하나를 열이라고 부릅니다.

제약조건은 테이블의 열이나 행이 지켜야 할 규칙을 테이블 정의에 적어 둔 것입니다. 데이터베이스는 행을 넣고 고치고 지울 때마다 이 규칙에 비춰 봅니다. 어기는 쓰기는 저장하지 않고 오류로 돌려보냅니다. 입금표를 돌려주던 창구 직원의 일을 데이터베이스가 하는 셈입니다.

주문 테이블의 수량 열에 「0보다 커야 한다」를 겁니다. 회원 번호 열에는 「비면 안 된다」를 겁니다. 그러면 수량이 0 인 주문이나 주인이 없는 주문은 테이블에 들어가지 못합니다.

규칙을 데이터베이스에 두는 까닭

규칙을 코드에 둘 때와 데이터베이스에 둘 때는 한 테이블에 여러 프로그램이 쓰는 상황에서 결과가 갈립니다.

한 테이블에 쓰는 곳은 대개 여럿입니다. 주문을 받는 서버 코드가 씁니다. 운영자는 관리자 화면에서 고칩니다. 밤마다 도는 배치 작업은 한꺼번에 옮겨 넣습니다. 검사를 코드에 두면 이 셋이 모두 같은 검사를 갖춰야 합니다.

한 곳만 검사를 빠뜨려도 잘못된 값이 들어옵니다. 검사를 넣은 나머지 프로그램은 그 사실을 모릅니다. 잘못된 값은 그 행을 읽는 다른 기능이 엉뚱하게 돌 때에야 드러납니다.

규칙이 테이블에 붙어 있으면 어느 길로 들어오든 같은 검사를 지납니다. 아래 그림이 그 모습입니다.

flowchart TD
    A["서버 코드"] --> C{"제약조건 검사"}
    B["관리자 화면"] --> C
    D["배치 작업"] --> C
    C -->|지킴| E["테이블에 저장"]
    C -->|어김| F["오류로 돌려보냄"]

프로그램이 같은 주문 코드가 이미 있는지 먼저 조회해 보고, 없을 때만 넣는 방식도 중복을 못 막을 때가 있습니다. 같은 코드를 든 요청 둘이 거의 같은 순간에 오면 둘 다 조회에서 그 코드가 없음을 확인하고 둘 다 넣습니다. 이렇게 작업 순서가 엇갈려 결과가 틀어지는 상황을 경쟁 상태라고 부릅니다.

주문 코드 열에 「겹치면 안 된다」는 규칙을 걸어 두면 데이터베이스가 저장하는 순간에 검사합니다. 두 번째로 저장하려는 쪽은 같은 코드가 이미 있어서 거절됩니다.

다섯 가지 제약조건

관계형 데이터베이스가 흔히 갖춘 제약조건은 다섯입니다. 자세한 동작은 제 항목이 따로 다룹니다.

NULL은 열에 값이 없다는 표시입니다. 숫자 0 이나 빈 문자열과 달리 「모른다」 또는 「아직 없다」를 뜻합니다. 다섯 가운데 첫째인 NOT NULL 이 이 표시를 막습니다.

나머지 넷은 유니크 제약 · 기본 키 · 외래 키 · CHECK 제약(Check Constraint)입니다. CHECK 제약은 조건식을 직접 적는 제약조건입니다. 아래 표가 다섯을 한 줄씩 보입니다.

제약조건 적는 법 지키는 규칙 예
NOT NULL NOT NULL 이 열은 NULL 이면 안 된다 주문의 회원 번호
유니크 제약 UNIQUE 이 열의 값은 행마다 달라야 한다 로그인 아이디
기본 키 PRIMARY KEY 행 하나를 가리키는 값이다. 비지 않고 겹치지 않는다 주문 번호
외래 키 FOREIGN KEY 다른 테이블에 있는 값만 가리킨다 주문이 가리키는 회원 번호
CHECK 제약 CHECK 적어 둔 조건식이 참이어야 한다 수량은 0보다 크다

앞의 셋은 한 테이블 안에서 끝나는 규칙입니다. 외래 키는 다른 테이블을 들여다봅니다. CHECK 제약은 개발자가 조건식을 직접 적으므로 쓰임이 가장 넓습니다.

선언하는 법

제약조건은 테이블을 만들 때 SQL(Structured Query Language, 구조화 질의 언어)로 함께 적습니다. SQL 은 관계형 데이터베이스에 테이블을 만들고 데이터를 넣고 읽는 언어입니다. 아래는 위 다섯을 한 테이블에 모두 건 모습입니다.

SQL
CREATE TABLE orders (
  id      bigint PRIMARY KEY,
  user_id bigint NOT NULL
          REFERENCES users (id),
  code    text UNIQUE,
  qty     int CHECK (qty > 0)
);

열 이름과 타입 뒤에 붙은 낱말이 제약조건입니다. REFERENCES users (id) 가 외래 키를 적는 꼴입니다. user_id 에는 users 테이블의 id 열에 있는 값만 들어간다는 뜻입니다.

이제 쓰기 다섯 개를 보냅니다. users 테이블에는 7번 회원 하나만 있다고 합시다. 값은 차례로 주문 번호, 회원 번호, 주문 코드, 수량입니다.

SQL
INSERT INTO orders VALUES
  (1, 7, 'A1', 2);    -- 저장됨
INSERT INTO orders VALUES
  (2, 7, 'A2', 0);    -- 거절 · CHECK
INSERT INTO orders VALUES
  (3, 7, 'A1', 1);    -- 거절 · UNIQUE
INSERT INTO orders VALUES
  (4, 99, 'A3', 1);   -- 거절 · 외래 키
INSERT INTO orders VALUES
  (5, NULL, 'A4', 1); -- 거절 · NOT NULL

첫 쓰기만 저장됩니다. 나머지 넷은 규칙을 하나씩 어겼습니다.

둘째 쓰기는 수량이 0 입니다. 셋째는 주문 코드가 첫 행과 겹칩니다. 넷째는 없는 회원을 가리킵니다. 다섯째는 회원 번호가 비었습니다.

테이블의 모양과 규칙을 적은 정의 전체를 스키마라고 부릅니다. 제약조건은 스키마의 일부입니다. 그래서 스키마만 읽어도 이 테이블에 어떤 데이터가 들어올 수 없는지 알 수 있습니다.

제약조건에 붙이는 이름

거절된 쓰기는 코드 쪽에 오류로 돌아옵니다. 오류에는 대개 어느 제약조건에 걸렸는지 그 이름이 실려 옵니다.

이름을 적지 않으면 데이터베이스가 알아서 붙입니다. 직접 붙이려면 제약조건 앞에 CONSTRAINT 와 이름을 적습니다.

SQL
CONSTRAINT orders_qty_positive
  CHECK (qty > 0)

이름을 붙여 두면 코드가 오류 속 이름을 보고 무엇을 어겼는지 가릴 수 있습니다. 「수량을 확인해 주세요」와 「이미 쓰는 주문 코드입니다」처럼 서로 다른 안내를 줄 수 있습니다. 나중에 그 제약조건만 골라 지울 때도 이 이름으로 부릅니다.

검사가 일어나는 시점

제약조건 검사는 보통 쓰기 문장 하나를 실행할 때 바로 일어납니다. 규칙을 어긴 문장은 실패합니다. 그 문장이 바꾸려던 행은 하나도 남지 않습니다. 행 백 개를 한 번에 넣다가 하나가 어겨도 나머지 아흔아홉 개까지 들어가지 않습니다.

검사를 뒤로 미뤄야 하는 경우도 있습니다. 두 테이블이 서로를 외래 키로 가리키면 어느 쪽을 먼저 넣어도 상대가 아직 없습니다.

트랜잭션은 여러 쓰기를 한 덩어리로 묶은 단위입니다. 덩어리 안의 쓰기는 전부 반영되거나 전부 취소됩니다. 두 테이블에 넣는 쓰기 둘도 한 트랜잭션으로 묶을 수 있습니다.

트랜잭션을 확정해 그 안의 쓰기를 모두 반영하는 순간을 커밋이라고 부릅니다. 몇몇 데이터베이스는 제약조건 검사를 커밋 때까지 미루게 해 줍니다. 그러면 두 행을 다 넣은 뒤에 한꺼번에 검사합니다.

미룰 수 있게 선언한 제약조건을 지연 가능 제약(deferrable constraint)이라고 부릅니다. SQL 로는 DEFERRABLE 이라고 적습니다. 미뤄 둔 검사가 커밋 때 실패하면 그 트랜잭션의 쓰기는 전부 취소됩니다.

무결성과의 관계

무결성은 데이터가 믿고 쓸 만한 상태로 유지되는 성질입니다. 제약조건은 그 성질을 지키려고 데이터베이스에 적어 두는 수단입니다.

무결성은 흔히 셋으로 나눕니다. 셋마다 그것을 지키는 제약조건이 짝지어져 있습니다.

무결성 지키려는 것 짝이 되는 제약조건
개체 무결성 모든 행을 하나씩 가려낼 수 있다 기본 키
참조 무결성 가리키는 값은 실제로 있는 행을 가리킨다 외래 키
도메인 무결성 열의 값이 허용된 범위 안에 있다 데이터 타입 · NOT NULL · CHECK

위 표의 데이터 타입은 열에 담을 값의 종류입니다. 숫자 열에 글자를 넣지 못하게 막는 것도 넓게 보면 이 범위 검사에 들어갑니다.

제약조건으로 못 거는 규칙

제약조건에도 닿지 못하는 규칙이 있습니다. 여러 행을 함께 봐야 하는 규칙이 대표입니다.

대부분의 데이터베이스에서 CHECK 제약은 지금 넣거나 고치는 행 하나만 봅니다. 「한 회원이 동시에 진행 중인 주문은 셋까지」 같은 규칙은 다른 행을 세야 판정됩니다. 행 하나만 보는 CHECK 제약으로는 이 규칙을 적을 수 없습니다.

이런 규칙은 다른 수단이 맡습니다. 하나는 트리거입니다. 테이블에 쓰기가 일어날 때마다 데이터베이스가 미리 적어 둔 SQL 을 스스로 실행하게 하는 장치입니다. 트리거 안에서 다른 행을 세어 보고 어기면 쓰기를 막습니다. 다른 하나는 애플리케이션 코드가 검사하는 것입니다.

두 수단 모두 제약조건만큼 단단하지 않습니다. 트리거는 동시에 들어온 쓰기 둘이 아직 확정되지 않은 서로의 행을 못 보고 함께 통과할 수 있습니다. 코드 검사는 앞에서 본 것처럼 쓰는 곳마다 갖춰야 합니다.

코드 검증과 나눠 맡는 일

제약조건이 있으면 코드 쪽 검사는 필요 없을까요. 둘은 맡는 일이 다릅니다.

코드 쪽 검사는 사용자에게 친절한 안내를 주려고 둡니다. 저장하기 전에 걸러 「수량은 1 이상이어야 합니다」를 바로 보여 줍니다. 데이터베이스까지 가지 않으니 오류를 풀어 읽는 수고도 없습니다.

제약조건은 마지막 방어선입니다. 코드 쪽 검사를 빠뜨린 경로나 동시에 겹친 요청을 받아 냅니다. 그래서 흔히 둘을 함께 둡니다. 코드가 대부분을 먼저 거르고, 코드가 놓친 드문 경우를 제약조건이 막습니다.

제약조건의 대가

제약조건을 걸면 무거워지는 것이 둘 있습니다. 쓰기 비용과 규칙을 바꾸는 비용입니다.

쓰기마다 검사가 붙습니다. 외래 키는 가리키는 행이 있는지 다른 테이블을 찾아봐야 합니다. 유니크 제약과 기본 키는 같은 값이 있는지 빨리 찾으려고 대개 인덱스를 하나 둡니다. 인덱스는 열의 값을 정렬해 두고 그 값이 어느 행에 있는지 적어 둔 별도의 구조입니다. 행을 넣을 때마다 인덱스도 함께 고쳐야 합니다.

규칙을 바꾸는 일도 무거워집니다. 이미 만든 테이블에 제약조건을 새로 걸면 데이터베이스가 기존 행을 전부 검사합니다. 하나라도 어기는 행이 있으면 제약조건을 걸 수 없습니다. 어기는 행부터 찾아 정리해야 합니다.

많은 데이터를 한꺼번에 옮겨 넣을 때도 검사가 부담이 됩니다. 그래서 먼저 데이터를 다 넣고 제약조건은 나중에 거는 방법을 씁니다. 이때도 거는 순간 기존 행 검사가 한 번 돕니다.

데이터베이스 밖의 제약조건

제약조건이라는 이름은 다른 분야에도 쓰입니다. 백엔드 개발자가 마주칠 만한 셋을 표로 짧게 가릅니다.

분야 무엇에 거는 규칙인가 예
최적화 문제 답이 만족해야 할 식 재료는 100kg 이하로 쓴다
제네릭 타입 매개변수가 갖춰야 할 조건 이 타입은 크기를 비교할 수 있어야 한다
화면 배치 화면 요소 사이의 거리와 크기 버튼은 오른쪽 끝에서 16만큼 떨어진다

셋 모두 규칙을 먼저 적고, 그 규칙을 벗어나는 것은 답이나 값으로 받지 않습니다. 그 점에서 이 항목의 제약조건과 뼈대가 같습니다. 규칙을 거는 대상만 다릅니다.

관련 항목

제약조건의 하위 종류

NOT NULL · 유니크 제약 · 기본 키 · 외래 키 · CHECK 제약 · 지연 가능 제약

제약조건이 지키는 성질

무결성 · 개체 무결성 · 참조 무결성 · 도메인 무결성 · 일관성 · ACID

제약조건을 선언하는 언어와 스키마 구성 요소

SQL · DDL · 스키마 · 테이블 · 열 · 행 · 데이터 타입 · 기본값 · NULL

제약조건으로 못 거는 규칙을 맡는 수단

트리거 · 저장 프로시저 · 입력 검증 · 애플리케이션 계층

제약조건 검사를 떠받치는 인덱스

인덱스 · 유니크 인덱스 · B-tree · 해시 인덱스

제약조건이 쓰기를 막을 때 돌아오는 오류

무결성 제약 위반 · 중복 키 오류 · SQLSTATE · 외래 키 위반

코드 검사만으로는 못 막는 동시성 문제

경쟁 상태 · 트랜잭션 · 커밋 · 격리 수준 · 락

제약조건과 함께 설계하는 데이터 모델

데이터 모델링 · 정규화 · 관계형 데이터베이스 · ORM · 스키마 마이그레이션

같은 이름을 쓰는 다른 분야의 규칙

최적화 문제 · 제약 충족 문제 · 선형 계획법 · 제네릭 · 오토 레이아웃

다른 이름: 제약 조건 · constraint · integrity constraint · 무결성 제약조건 · 무결성 제약 조건