Devin.KR

모델링 판단 문제 - 식별 관계와 비식별 관계를 고르는 기준

개발자KR 조회 16

이 장에서 배우는 것

이 책은 데이터 모델링과 SQL 기본을 마친 독자가 실무에서 부딪히는 판단 문제를 다룬다. 데이터 모델링 단계에서 가장 먼저, 그리고 가장 자주 틀리는 지점이 식별 관계와 비식별 관계를 고르는 기준이다. 두 관계는 문법상 차이가 아니라 "자식 엔터티가 부모 없이 독립적으로 존재하고 식별될 수 있는가"라는 업무 판단의 차이이며, 이 판단이 잘못되면 기본키 설계와 참조 무결성 전체가 흔들린다. 이 장에서는 온라인 서점 스키마의 주문상세와 리뷰 테이블을 예로 들어 판단 기준을 정리하고, 주식별자가 갖춰야 할 조건과 인조 식별자의 장단점을 실제 DDL로 확인한다.

  • 자식 엔터티가 부모 없이 독립적으로 존재할 수 있는지를 기준으로 식별 관계와 비식별 관계를 구분한다
  • 주식별자의 네 조건(유일성·최소성·불변성·존재성)을 스키마 설계에 적용한다
  • 인조 식별자를 쓸 때 빠뜨리기 쉬운 자연키 유일성 제약을 챙긴다
  • 온라인 서점 스키마의 주문상세·리뷰 테이블로 판단 근거를 DDL과 제약 조건으로 직접 확인한다

문제 상황

온라인 서점 개발팀이 주문상세와 리뷰 테이블 설계를 리뷰하던 중 의견이 갈렸다. 한 사람은 "모든 관계를 비식별로 통일하고 모든 테이블에 인조 기본키를 두자"고 주장했고, 다른 사람은 "자식이 부모 없이는 업무적으로 아무 의미가 없다면 부모 식별자를 그대로 기본키에 포함하는 식별 관계로 둬야 한다"고 반박했다. 실제로 두 테이블은 성격이 다르다. 주문상세의 라인번호는 특정 주문 안에서만 의미를 가지는 값이고, 리뷰는 회원과 도서라는 두 부모를 참조하지만 리뷰 자체는 독립적인 업무 단위로 취급하는 경우가 많다.

아래 표는 주문상세 샘플 데이터다. 라인번호가 주문마다 1부터 다시 시작하는 것을 볼 수 있다. 이 값은 주문번호 없이는 아무것도 가리키지 못한다.

주문상세 샘플 데이터로 본 라인번호의 의존성
주문번호라인번호도서번호수량
100111011
100121022
100211013

1001번 주문과 1002번 주문 모두 라인번호 1이 존재한다. 라인번호만으로는 어떤 행인지 특정할 수 없고 주문번호와 함께 있어야 유일해진다. 이 특징이 식별 관계 판단의 출발점이다.

식별 관계와 비식별 관계를 고르는 기준

식별 관계(identifying relationship)는 부모 엔터티의 식별자를 자식 엔터티의 기본키 일부로 그대로 물려받는 관계다. 비식별 관계(non-identifying relationship)는 부모 식별자를 자식의 일반 속성(외래키)으로만 두고, 자식은 별도의 기본키로 식별하는 관계다. 두 관계 중 무엇을 고를지는 아래 두 가지 질문으로 판단한다.

자식이 부모 없이 존재할 수 있는가

주문상세는 주문이 없으면 존재 이유가 없다. 주문을 취소하면 주문상세도 함께 사라지는 것이 자연스럽다. 이런 종속 엔터티는 부모의 생명주기를 그대로 따라가므로 식별 관계로 설계한다. 반대로 리뷰는 주문이나 회원 탈퇴와 무관하게 독립적인 게시물로 남기는 경우가 많고, 도서 상세 화면에서 리뷰번호 하나로 바로 조회·수정하는 업무 흐름이 있다면 비식별 관계 쪽이 자연스럽다.

부모 식별자가 자식의 업무적 의미에 포함되는가

주문상세의 라인번호는 "이 주문의 몇 번째 줄"이라는 뜻이며 주문번호 없이는 그 자체로 의미가 없다. 즉 부모 식별자가 자식의 업무 의미 안에 포함된다. 반면 리뷰번호는 "이 회원이 이 책에 쓴 글"이라는 의미를 회원번호·도서번호 없이도 리뷰번호 하나로 대체할 수 있다. 회원번호와 도서번호는 리뷰의 속성이지 리뷰를 정의하는 값은 아니다.

이 두 질문에 모두 "그렇다"로 답하면 식별 관계, 하나라도 "아니다"이면 비식별 관계로 두는 것이 일반적인 기준이다. 아래 그림은 두 관계가 스키마에서 어떻게 달라지는지를 보여 준다.

주문상세는 식별 관계로 부모 키를 기본키에 포함하고 리뷰는 비식별 관계로 인조키를 쓴다

실무에서는 이 판단을 다음 순서로 정리하면 흔들리지 않는다. 첫째, 자식 행이 부모 행 삭제와 함께 사라져야 하는지 확인한다. 둘째, 자식을 부모 식별자 없이 단독으로 지칭하는 업무 화면이나 API가 있는지 확인한다. 셋째, 두 조건이 모두 "예"이면 식별 관계, 그렇지 않으면 비식별 관계로 정한다. 주문상세는 첫째·둘째 조건이 모두 "예"이므로 식별 관계이고, 리뷰는 둘째 조건이 "아니다"에 가까우므로 비식별 관계로 판단한다.

주식별자 조건 - 유일성·최소성·불변성·존재성

어떤 관계로 설계하든 자식 기본키가 실제로 주식별자 자격을 갖추는지는 별도로 확인해야 한다. 주식별자는 네 조건을 모두 만족해야 한다.

  • 유일성: 테이블 안의 모든 행에서 값이 겹치지 않아야 한다. 회원번호는 회원마다 하나씩 부여하므로 유일성을 만족한다.
  • 최소성: 유일성을 유지하는 데 꼭 필요한 컬럼만 포함해야 한다. 주문상세는 주문번호와 라인번호 두 컬럼만으로 유일성이 보장되므로 도서번호까지 기본키에 넣을 이유가 없다.
  • 불변성: 한 번 정해지면 잘 바뀌지 않는 값이어야 한다. 이메일은 회원이 바꿀 수 있는 값이므로 기본키로 삼기에 적합하지 않다.
  • 존재성: 모든 행에 값이 있어야 하며 NULL을 허용하지 않는다. 기본키 컬럼은 정의상 NOT NULL이어야 한다.
주식별자는 유일성·최소성·불변성·존재성을 모두 만족해야 한다

네 조건 중 하나라도 깨지면 그 컬럼(또는 컬럼 조합)은 주식별자로 쓸 수 없다. 예를 들어 isbn은 유일성·존재성은 만족하지만, 리퍼브판처럼 같은 책이 새 isbn을 받는 사례가 있다면 불변성이 흔들린다. 이런 값은 대체 식별자(alternate key)로 UNIQUE 제약만 걸고, 기본키는 인조 식별자로 두는 절충이 자주 쓰인다.

인조 식별자의 장단점

인조 식별자(surrogate key, 이하 인조키)는 업무적 의미가 없는 일련번호를 기본키로 쓰는 방식이다. 반대로 업무 속성을 그대로 기본키로 쓰는 방식을 자연키(natural key)라 부른다. 리뷰 테이블을 자연키 복합키(회원번호+도서번호)로 설계할 수도 있고, 인조키(리뷰번호)에 UNIQUE 제약을 얹어 설계할 수도 있다. 두 방식의 차이는 다음과 같다.

인조키는 참조가 단순해지지만 유일성 보장을 위해 별도 제약이 필요하다
자연키와 인조키의 장단점 비교
구분복합 자연키(회원번호+도서번호)인조키(리뷰번호)+UNIQUE
중복 방지기본키 자체가 중복을 막는다별도 UNIQUE 제약이 있어야 막는다
참조 전파자식 테이블마다 두 컬럼을 물려받는다한 컬럼만 물려받는다
단독 식별회원번호·도서번호 둘 다 있어야 한다리뷰번호 하나로 조회·수정한다

인조키를 쓰기로 했다면 "한 회원은 한 책에 리뷰를 하나만 쓴다"는 업무 규칙이 기본키만으로는 지켜지지 않는다는 점을 반드시 기억해야 한다. 이 규칙은 UNIQUE 제약으로 따로 걸어야 하며, 이를 빠뜨리면 인조키의 편의성이 데이터 정합성을 깎아먹는 결과로 이어진다.

완성 코드

CREATE TABLE 회원 (
    회원번호      INT             NOT NULL,
    이메일        VARCHAR(100)    NOT NULL,
    이름          VARCHAR(50)     NOT NULL,
    가입일        DATE            NOT NULL,
    CONSTRAINT pk_회원 PRIMARY KEY (회원번호),
    CONSTRAINT uq_회원_이메일 UNIQUE (이메일)
);

CREATE TABLE 도서 (
    도서번호      INT             NOT NULL,
    isbn          CHAR(13)        NOT NULL,
    제목          VARCHAR(200)    NOT NULL,
    정가          INT             NOT NULL,
    CONSTRAINT pk_도서 PRIMARY KEY (도서번호),
    CONSTRAINT uq_도서_isbn UNIQUE (isbn)
);

CREATE TABLE 주문 (
    주문번호      INT             NOT NULL,
    회원번호      INT             NOT NULL,
    주문일시      DATETIME        NOT NULL,
    상태          VARCHAR(20)     NOT NULL,
    CONSTRAINT pk_주문 PRIMARY KEY (주문번호),
    CONSTRAINT fk_주문_회원 FOREIGN KEY (회원번호) REFERENCES 회원 (회원번호)
);

CREATE TABLE 주문상세 (
    주문번호      INT             NOT NULL,
    라인번호      INT             NOT NULL,
    도서번호      INT             NOT NULL,
    수량          INT             NOT NULL,
    판매단가      INT             NOT NULL,
    CONSTRAINT pk_주문상세 PRIMARY KEY (주문번호, 라인번호),
    CONSTRAINT fk_주문상세_주문 FOREIGN KEY (주문번호)
        REFERENCES 주문 (주문번호) ON DELETE CASCADE,
    CONSTRAINT fk_주문상세_도서 FOREIGN KEY (도서번호) REFERENCES 도서 (도서번호)
);

CREATE TABLE 리뷰 (
    리뷰번호      INT             NOT NULL AUTO_INCREMENT,
    회원번호      INT             NOT NULL,
    도서번호      INT             NOT NULL,
    별점          INT             NOT NULL,
    내용          VARCHAR(1000),
    작성일시      DATETIME        NOT NULL,
    CONSTRAINT pk_리뷰 PRIMARY KEY (리뷰번호),
    CONSTRAINT uq_리뷰_회원_도서 UNIQUE (회원번호, 도서번호),
    CONSTRAINT fk_리뷰_회원 FOREIGN KEY (회원번호) REFERENCES 회원 (회원번호),
    CONSTRAINT fk_리뷰_도서 FOREIGN KEY (도서번호) REFERENCES 도서 (도서번호),
    CONSTRAINT ck_리뷰_별점 CHECK (별점 BETWEEN 1 AND 5)
);

INSERT INTO 회원 (회원번호, 이메일, 이름, 가입일) VALUES
    (1, 'yuna@example.com', '김유나', '2025-01-10'),
    (2, 'minho@example.com', '박민호', '2025-02-20');

INSERT INTO 도서 (도서번호, isbn, 제목, 정가) VALUES
    (101, '9791100000019', 'SQL 실전 노트', 25000),
    (102, '9791100000026', '데이터 모델링 입문', 28000);

INSERT INTO 주문 (주문번호, 회원번호, 주문일시, 상태) VALUES
    (1001, 1, '2025-03-05 10:20:00', '결제완료');

INSERT INTO 주문상세 (주문번호, 라인번호, 도서번호, 수량, 판매단가) VALUES
    (1001, 1, 101, 1, 25000),
    (1001, 2, 102, 2, 28000);

INSERT INTO 리뷰 (회원번호, 도서번호, 별점, 내용, 작성일시) VALUES
    (1, 101, 5, '설명이 친절하다', '2025-03-10 09:00:00');

줄별 해설

주문상세의 pk_주문상세는 주문번호와 라인번호 두 컬럼으로 구성된다. 주문번호는 부모 식별자를 그대로 물려받은 것이므로 이 관계는 식별 관계이며, fk_주문상세_주문이 동시에 기본키의 일부를 이룬다. ON DELETE CASCADE는 주문이 삭제되면 딸린 주문상세도 함께 삭제되게 해, "부모 없이 존재할 수 없다"는 업무 규칙을 스키마로 강제한다.

리뷰의 리뷰번호는 업무 의미가 없는 AUTO_INCREMENT 인조키이고, 회원번호와 도서번호는 기본키가 아닌 일반 외래키다. 이 관계는 비식별 관계다. 다만 "한 회원은 한 책에 리뷰 하나만"이라는 규칙은 기본키만으로 지켜지지 않으므로 uq_리뷰_회원_도서 UNIQUE 제약을 별도로 걸었다. 이 제약이 없으면 같은 회원이 같은 책에 리뷰를 여러 건 남길 수 있다.

ck_리뷰_별점은 존재성과는 다른 문제인 값의 범위를 제한하는 CHECK 제약이며, MySQL 8.0.16 이상에서 실제로 강제된다. uq_회원_이메일, uq_도서_isbn은 각각 이메일과 isbn이 주식별자 후보이지만 불변성이 약해 대체키(UNIQUE)로만 남겨 두고, 실제 주식별자는 인조키인 회원번호·도서번호를 쓴 결과다.

실행 결과

정상 조인 조회는 다음과 같이 나온다.

mysql> SELECT 주문상세.주문번호, 주문상세.라인번호, 도서.제목, 주문상세.수량
    -> FROM 주문상세 JOIN 도서 ON 주문상세.도서번호 = 도서.도서번호
    -> WHERE 주문상세.주문번호 = 1001;
+--------------+--------------+--------------------+--------+
| 주문번호     | 라인번호     | 제목               | 수량   |
+--------------+--------------+--------------------+--------+
| 1001         | 1            | SQL 실전 노트      | 1      |
| 1001         | 2            | 데이터 모델링 입문 | 2      |
+--------------+--------------+--------------------+--------+
2 rows in set (0.00 sec)

같은 회원이 같은 책에 리뷰를 한 번 더 남기려 하면 UNIQUE 제약이 막는다.

mysql> INSERT INTO 리뷰 (회원번호, 도서번호, 별점, 내용, 작성일시)
    -> VALUES (1, 101, 3, '중복 리뷰 시도', '2025-03-11 09:00:00');
ERROR 1062 (23000): Duplicate entry '1-101' for key '리뷰.uq_리뷰_회원_도서'

존재하지 않는 주문에 주문상세를 붙이려 하면 외래키 제약이 막는다. 식별 관계로 설계했다고 해서 이 제약이 저절로 생기는 것은 아니며, 외래키 선언이 별도로 필요하다는 점에 주의한다.

mysql> INSERT INTO 주문상세 (주문번호, 라인번호, 도서번호, 수량, 판매단가)
    -> VALUES (9999, 1, 101, 1, 25000);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key
constraint fails (`서점`.`주문상세`, CONSTRAINT `fk_주문상세_주문`
FOREIGN KEY (`주문번호`) REFERENCES `주문` (`주문번호`))

실무에서 자주 틀리는 것

식별 관계를 습관적으로 비식별로 바꾸는 경우

주문상세에 인조키만 두면 라인번호의 유일성이 주문 단위로 보장되지 않는다.

-- 틀린 코드: 인조키만 있고 (주문번호, 라인번호) 유일성이 없다
CREATE TABLE 주문상세 (
    상세번호   INT NOT NULL AUTO_INCREMENT,
    주문번호   INT NOT NULL,
    라인번호   INT NOT NULL,
    도서번호   INT NOT NULL,
    PRIMARY KEY (상세번호)
);
-- 고친 코드: 부모 식별자를 포함해 유일성을 보장한다
CREATE TABLE 주문상세 (
    주문번호   INT NOT NULL,
    라인번호   INT NOT NULL,
    도서번호   INT NOT NULL,
    PRIMARY KEY (주문번호, 라인번호)
);

인조키만 두고 자연키 유일성 제약을 빠뜨리는 경우

리뷰번호만 기본키로 두고 UNIQUE 제약을 걸지 않으면 중복 리뷰를 막지 못한다.

-- 틀린 코드: 회원-도서 조합 유일성이 없다
CREATE TABLE 리뷰 (
    리뷰번호 INT NOT NULL AUTO_INCREMENT,
    회원번호 INT NOT NULL,
    도서번호 INT NOT NULL,
    PRIMARY KEY (리뷰번호)
);
-- 고친 코드: 업무 규칙을 UNIQUE 제약으로 강제한다
CREATE TABLE 리뷰 (
    리뷰번호 INT NOT NULL AUTO_INCREMENT,
    회원번호 INT NOT NULL,
    도서번호 INT NOT NULL,
    PRIMARY KEY (리뷰번호),
    CONSTRAINT uq_리뷰_회원_도서 UNIQUE (회원번호, 도서번호)
);

변할 수 있는 속성을 주식별자로 삼는 경우

이메일을 기본키로 쓰면 회원이 이메일을 바꿀 때 참조 무결성이 흔들린다.

-- 틀린 코드: 불변성이 없는 값을 PK로 사용
CREATE TABLE 회원 (
    이메일   VARCHAR(100) NOT NULL,
    이름     VARCHAR(50)  NOT NULL,
    PRIMARY KEY (이메일)
);
-- 고친 코드: 인조키를 PK로, 이메일은 UNIQUE 대체키로 둔다
CREATE TABLE 회원 (
    회원번호 INT          NOT NULL,
    이메일   VARCHAR(100) NOT NULL,
    이름     VARCHAR(50)  NOT NULL,
    PRIMARY KEY (회원번호),
    CONSTRAINT uq_회원_이메일 UNIQUE (이메일)
);

최소성을 어기고 기본키에 불필요한 컬럼을 넣는 경우

주문상세 기본키에 도서번호까지 넣으면 같은 책을 같은 주문에서 다른 조건으로 두 줄에 담을 수 없다.

-- 틀린 코드: 최소성 위반, 같은 책을 두 줄로 나눠 담지 못한다
CREATE TABLE 주문상세 (
    주문번호 INT NOT NULL,
    도서번호 INT NOT NULL,
    수량     INT NOT NULL,
    PRIMARY KEY (주문번호, 도서번호)
);
-- 고친 코드: 라인번호로 유일성을 보장하고 도서번호는 일반 FK로 둔다
CREATE TABLE 주문상세 (
    주문번호 INT NOT NULL,
    라인번호 INT NOT NULL,
    도서번호 INT NOT NULL,
    수량     INT NOT NULL,
    PRIMARY KEY (주문번호, 라인번호)
);

한눈에 보기

식별 관계와 비식별 관계 판단 기준
구분식별 관계비식별 관계
부모 없는 생존불가능(부모 삭제 시 함께 삭제)가능(부모와 독립적으로 유지)
부모 키 위치자식의 기본키 일부자식의 일반 외래키
예시주문 - 주문상세회원 - 리뷰
MySQL 8과 Oracle의 관련 구문 차이
항목MySQL 8Oracle
인조키 자동 증가컬럼에 AUTO_INCREMENTIDENTITY 컬럼 또는 시퀀스
CHECK 제약 실행8.0.16부터 실제로 강제버전과 무관하게 항상 강제
다국어 식별자utf8mb4에서 그대로 허용큰따옴표로 감싸야 할 수 있음

CHECK 제약과 자동 증가 컬럼의 정확한 동작 범위는 각 벤더 문서로 확인하는 편이 안전하다. MySQL CHECK 제약 문서와 Oracle CREATE TABLE 문서를 참고한다.

연습 문제

  1. 온라인 서점에 장바구니 기능을 추가한다. 장바구니항목(회원번호, 도서번호, 수량)을 설계할 때 기본키를 어떻게 정해야 하는지, 식별 관계인지 비식별 관계인지와 함께 근거를 서술하시오.
  2. 도서의 기본키를 isbn으로 쓰자는 의견과 인조키 도서번호를 쓰자는 의견이 있다. 리퍼브판이 새 isbn을 받는 경우가 있다는 점을 근거로, 주식별자 네 조건 중 어떤 조건이 문제가 되는지 판단하시오.
  3. 리뷰 테이블에서 uq_리뷰_회원_도서 제약을 빼면 어떤 문제가 생기는지, 그리고 이 제약이 비식별 관계 판단과 어떤 관계가 있는지 설명하시오.
  4. 주문상세의 기본키를 (주문번호, 도서번호)로 설계하면 안 되는 이유를, 한 회원이 같은 주문에서 같은 책을 사은품 포함/미포함 조건으로 두 줄에 나눠 담는 상황을 들어 설명하시오.

정답과 해설

1번: 장바구니항목은 회원이 장바구니를 비우거나 탈퇴하면 함께 사라지는 것이 자연스럽고, 회원번호 없이는 "누구의 장바구니 줄"인지 의미가 없다. 두 조건 모두 충족하므로 식별 관계로 두고, 기본키는 (회원번호, 도서번호)로 잡는다. 다만 같은 책을 여러 번 담을 상황(옵션이 다른 경우 등)이 있다면 라인번호를 추가해 최소성과 실제 업무 요구를 함께 맞춘다.

2번: isbn은 유일성과 존재성은 만족하지만, 리퍼브판이 새 isbn을 받으면 "같은 책인데 식별자가 바뀌는" 상황이 생겨 불변성이 흔들린다. 이런 경우 isbn은 UNIQUE 제약을 가진 대체키로 남기고, 기본키는 값이 절대 바뀌지 않는 인조키(도서번호)로 두는 쪽이 안전하다.

3번: 이 제약이 없으면 한 회원이 같은 책에 리뷰를 여러 건 남길 수 있어 "회원당 도서당 리뷰 하나"라는 업무 규칙이 깨진다. 비식별 관계로 설계해 회원번호·도서번호를 기본키 밖으로 뺀 대가로, 원래 식별 관계였다면 기본키가 자동으로 막아 줬을 유일성을 UNIQUE 제약으로 대신 챙겨야 한다.

4번: (주문번호, 도서번호)를 기본키로 두면 같은 주문에서 같은 책은 한 줄만 존재할 수 있다. 사은품 포함/미포함처럼 같은 책이 서로 다른 조건으로 두 줄에 나뉘어야 하는 업무 요구를 반영하지 못하므로, 라인번호를 기본키에 포함해 "몇 번째 줄인가"로 유일성을 잡아야 한다. 이는 최소성이 아니라 오히려 업무 요구를 반영할 만큼의 식별력을 기본키가 갖추지 못한 사례다.

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.