주문(orders)과 상품(product)의 다대다 관계를 해결하는 order_item 테이블을 예로 들어보자.
비즈니스 규칙
"하나의 주문에는 동일한 상품이 중복으로 들어가면 안 된다"는 아주 일반적인 비즈니스 규칙이 있다고 생각해보자. 예를 들어, "주문번호 100번(order_id)"에 "청바지(product_id)" 상품이 두 번 기록되면 안 된다. 데이터가 중복으로 쌓이면 나중에 통계를 낼 때나 재고를 계산할 때 심각한 오류를 유발할 수 있다.
식별 관계 (Identifying)
DROP TABLE IF EXISTS order_item_identifying;
DROP TABLE IF EXISTS product_identifying;
DROP TABLE IF EXISTS orders_identifying;
-- 상품 테이블 생성
CREATE TABLE product_identifying (
product_id BIGINT,
name VARCHAR(100) NOT NULL,
price INT NOT NULL,
PRIMARY KEY (product_id)
);
-- 주문 테이블 생성
CREATE TABLE orders_identifying (
order_id BIGINT NOT NULL, -- 주문id 직접 입력
order_date DATE,
PRIMARY KEY (order_id)
);
CREATE TABLE order_item_identifying (
order_id BIGINT NOT NULL, -- PK의 일부 + FK
product_id BIGINT NOT NULL, -- PK의 일부 + FK
order_price INT NOT NULL,
order_quantity INT NOT NULL,
PRIMARY KEY (order_id, product_id), -- 복합 기본 키
CONSTRAINT fk_oi_iden_orders FOREIGN KEY (order_id) REFERENCES
orders_identifying (order_id),
CONSTRAINT fk_oi_iden_product FOREIGN KEY (product_id) REFERENCES
product_identifying (product_id)
);

- order_item_identifying 테이블의 PK는 "(order_id, product_id)" 복합키다.
- 이 구조는 데이터베이스 레벨에서 '하나의 주문에 동일한 상품은 한 번만 포함될 수 있다'는 비즈니스 규칙을 PK를 통해 강제한다.
장점
- 비즈니스 규칙 강제: 복합 키(PK) 자체가 중복을 막아주므로 데이터 정합성을 확실히 보장한다.
- 인덱스 효율: "order_id"와 "product_id"를 함께 조회하는 쿼리에서 복합키 인덱스가 효율적으로 동작할 수 있다.
단점
- 유연성 부족: 만약 "하나의 주문에 옵션이 다른 동일 상품을 여러 개 담을 수 있다"는 요구사항이 생기면 이 모델은 즉시 한계에 부딪힌다.
- PK에 "option_id" 같은 컬럼을 또 추가해야만 한다.(PK는 불변해야 한다.)
- 이것은 PK를 바꾸는 매우 심각한 변경이다.
- 복잡성 증가: 복합키는 관리 및 사용이 불편하고, ORM 매핑도 더 복잡해진다.
비즈니스 규칙 만족
식별 관계의 가장 큰 특징은 기본 키(PK) 자체가 비즈니스 규칙을 강제한다는 점이다. "PRIMARY KEY (order_id, product_id)" 설정은 "order_id"와 "product_id"의 조합이 테이블 전체에서 유일해야 함을 의미한다.
정상 데이터 추가
-- 상품 데이터 삽입
INSERT INTO product_identifying(product_id, name, price) VALUES(1, '청바지', 50000);
INSERT INTO product_identifying(product_id, name, price) VALUES(2, '티셔츠', 25000);
-- 주문 100 생성
INSERT INTO orders_identifying(order_id, order_date) VALUES(100,'2025-09-01');
-- 100번 주문에 1번 상품(청바지) 추가
INSERT INTO order_item_identifying(order_id, product_id, order_price, order_quantity) VALUES (100, 1, 30000, 1);
-- 100번 주문에 2번 상품(티셔츠) 추가
INSERT INTO order_item_identifying(order_id, product_id, order_price,order_quantity) VALUES (100, 2, 15000, 2);
- 100번 주문에 1번 상품(청바지)과 2번 상품(티셔츠)을 하나씩 추가한다.
중복 데이터 추가 시도
-- 100번 주문에 1번 상품(청바지)을 중복 추가 시도
INSERT INTO order_item_identifying(order_id, product_id, order_price,order_quantity)
VALUES (100, 1, 30000, 5); -- 수량을 5개로 다시 주문 시도
- 100번 주문에 1번 상품(청바지)을 한 번 더 추가하려고 시도해 보자.
- 데이터베이스는 이 요청을 거부하고 오류를 발생시킨다.
- Error Code: 1062. Duplicate entry '100-1' for key 'order_item_identifying.PRIMARY'
- 이 오류 메시지는 order_item_identifying 테이블의 기본 키에 "100-1"이라는 값이 이미 존재하여 중복된다는 의미다.
이처럼 식별 관계 모델은 애플리케이션 로직에 실수가 있더라도 데이터베이스의 PK 제약조건이 최후의 보루가 되어 데이터의 중복을 원천적으로 차단한다. 요구사항을 매우 강력하고 확실하게 만족시키는 방법이다.
비식별 관계 (Non-identifying)
DROP TABLE IF EXISTS order_item_non_identifying;
DROP TABLE IF EXISTS product_non_identifying;
DROP TABLE IF EXISTS orders_non_identifying;
-- 상품 테이블 생성
CREATE TABLE product_non_identifying (
product_id BIGINT NOT NULL, -- 상품id 직접 입력
name VARCHAR(100) NOT NULL,
price INT NOT NULL,
PRIMARY KEY (product_id)
);
-- 주문 테이블 생성
CREATE TABLE orders_non_identifying (
order_id BIGINT NOT NULL, -- 주문id 직접 입력
order_date DATE,
PRIMARY KEY (order_id)
);
CREATE TABLE order_item_non_identifying (
order_item_id BIGINT NOT NULL AUTO_INCREMENT, -- 독립적인 PK
order_id BIGINT NOT NULL, -- 일반 컬럼 + FK
product_id BIGINT NOT NULL, -- 일반 컬럼 + FK
order_price INT NOT NULL,
order_quantity INT NOT NULL,
PRIMARY KEY (order_item_id),
CONSTRAINT fk_oi_non_orders FOREIGN KEY (order_id)
REFERENCES orders_non_identifying(order_id),
CONSTRAINT fk_oi_non_product FOREIGN KEY (product_id)
REFERENCES product_non_identifying(product_id)
);

- order_item_non_identifying 테이블은 비즈니스 의미가 없는 대리 키 "order_item_id"를 PK로 사용한다.
- "order_id"와 "product_id"는 각각 부모 테이블을 참조하는 FK다.
문제 상황
비즈니스 규칙을 다시 확인해보자. "하나의 주문에는 동일한 상품이 중복으로 들어가면 안 된다" 예를 들어, "주문번호 100번에 청바지 상품이 두 번 기록되면 안 된다." 데이터가 중복으로 쌓이면 나중에 통계를 낼 때 나 재고를 계산할 때 심각한 오류를 유발할 수 있다.
하지만 우리가 방금 설계한 order_item 테이블은 "order_item_id"라는 대리 키를 PK로 사용하기 때문에, "order_id"와 "product_id"의 조합이 중복되어도 데이터베이스는 오류를 발생시키지 않고 그대로 INSERT를 허용하게 된다. 이것은 잠재적인 데이터 부정합 문제로 이어질 수 있다.
중복 데이터 입력 예시
order_item 테이블의 현재 구조에서는 "order_item_id"만 유일성이 보장된다. 따라서 "order_id"와 "product_id"가 완전히 동일한 데이터가 여러 번 삽입될 수 있다
-- 상품 데이터 삽입
INSERT INTO product_non_identifying(product_id, name, price) VALUES(1, '청바지',50000);
INSERT INTO product_non_identifying(product_id, name, price) VALUES(2, '티셔츠', 25000);
-- 주문 100 생성
INSERT INTO orders_non_identifying(order_id, order_date) VALUES(100, '2025-09-01');
-- 100번 주문에 1번 상품(청바지) 추가
INSERT INTO order_item_non_identifying(order_id, product_id, order_price, order_quantity) VALUES (100, 1, 30000, 1);
-- 실수로 동일한 상품을 한 번 더 추가
INSERT INTO order_item_non_identifying(order_id, product_id, order_price,order_quantity) VALUES (100, 1, 30000, 1);
- 100번 주문에 1번 상품(청바지)을 두 번 추가해 보자.
- "order_item_id"는 "1"과 "2"로 서로 다르기 때문에 데이터베이스는 아무런 문제 없이 데이터를 추가했다.
- 하지만 "order_id"와 "product_id"는 "(100, 1)"로 완전히 중복된다.
- 이 테이블의 데이터만 보면 100번 고객은 청바지를 두 번 주문한 셈이 되어버린다.
- 이것이 바로 우리가 해결해야 할 데이터 정합성 문제다.
해결 방안: UNIQUE 제약조건 추가
이 문제를 어떻게 해결할 수 있을까? 바로 PK와는 별개로, 비즈니스적으로 중복되면 안 되는 컬럼 조합에 UNIQUE 제약조건을 추가하면 된다.
DROP TABLE IF EXISTS order_item_non_identifying;
CREATE TABLE order_item_non_identifying (
order_item_id BIGINT NOT NULL AUTO_INCREMENT, -- 독립적인 PK
order_id BIGINT NOT NULL, -- 일반 컬럼 + FK
product_id BIGINT NOT NULL, -- 일반 컬럼 + FK
order_price INT NOT NULL,
order_quantity INT NOT NULL,
PRIMARY KEY (order_item_id),
UNIQUE KEY uq_order_item (order_id, product_id), -- 이 조합은 유일해야 한다.
CONSTRAINT fk_oi_non_orders FOREIGN KEY (order_id)
REFERENCES orders_non_identifying(order_id),
CONSTRAINT fk_oi_non_product FOREIGN KEY (product_id)
REFERENCES product_non_identifying(product_id)
);
- order_item 테이블은 비즈니스 의미가 없는 대리 키 "order_item_id"를 PK로 사용한다.
- "order_id"와 "product_id"는 각각 부모 테이블을 참조하는 FK다.
- "UNIQUE KEY uq_order_item (order_id, product_id)": 이 부분이 핵심이다.
- PRIMARY KEY 와는 별도로 "order_id"와 "product_id"의 조합이 테이블 내에서 항상 유일한 값을 갖도록 데이터베이스 레벨에서 강제한다.
- 만약 중복된 조합의 데이터 삽입을 시도하면, 데이터베이스는 즉시 오류를 발생시켜 데이터의 정합성을 지켜준다.
장점
- 유연성과 데이터 정합성을 모두 확보
- 비식별 관계의 장점인 단순하고 유연한 구조는 그대로 가져가면서, 식별 관계의 장점인 데이터 정합성 보장(중복 방지)까지 UNIQUE 제약조건을 통해 얻을 수 있다.
- 실무에서 가장 권장되는 방식이다.
- ORM 친화적이다
- ORM에서 OrderItem이라는 엔티티를 다른 엔티티들과 동일한 방식으로 단순하게 다룰 수 있다.
UNIQUE 제약조건 동작 확인
-- 100번 주문에 1번 상품(청바지)을 중복 추가 시도
INSERT INTO order_item_non_identifying(order_id, product_id, order_price,order_quantity)
VALUES (100, 1, 30000, 1);
- 데이터베이스는 이 요청을 거부하고 UNIQUE 제약조건 위반 오류를 발생시킨다.
- Error Code: 1062. Duplicate entry '100-1' for key 'order_item_non_identifying.uq_order_item'
- 오류 메시지를 보면 PK(기본 키)가 아닌 uq_order_item라는 이름의 UNIQUE KEY 제약조건 때문에 중복이 발생했음을 명확히 알려준다
이처럼 대리 키를 PK로 사용하는 비식별 관계에서도 UNIQUE 제약조건을 활용하면, 비즈니스 규칙에 따른 데이터의 정합성을 완벽하게 보장할 수 있다. 이것이 바로 "M:N 관계"를 모델링할 때 가장 권장되는 현대적인 설계 방식이다.
식별 관계 vs 비식별 관계: 다대다 관계에서의 확장성 문제
새로운 요구사항: "고객이 장바구니에 같은 티셔츠를 담더라도, L 사이즈, 흰색과 M 사이즈, 검은색처럼 옵션이 다르면 별개의 상품으로 주문할 수 있게 해라."
이 요구사항은 기존의 (주문, 상품) 관계에 '옵션'이라는 새로운 차원을 추가해야 함을 의미한다. 이제 중복을 판단하는 기준이 "(order_id, product_id)"에서 "(order_id, product_id, option_id)"로 변경되어야 한다.
비식별 관계의 경우 - 유연한 확장
비식별 관계에선 order_item 테이블의 정체성(PK)이 "order_item_id"라는 독립적인 값에 의해 결정된다. 따라서 새로운 비즈니스 규칙을 추가하는 작업이 훨씬 간단하고 안전하다.
변경 작업
- order_item 테이블에 "option_id" 컬럼을 추가한다.
- 기존의 "(order_id, product_id)" 조합에 대한 UNIQUE 제약조건을 삭제한다.
- 새로운 비즈니스 규칙에 맞는 "(order_id, product_id, option_id)" 조합에 대한 UNIQUE 제약조건을 추가한다
-- 1. 새로운 컬럼 추가 (상품에 옵션이 없는 경우도 있으므로 NULL 허용)
ALTER TABLE order_item_non_identifying ADD COLUMN option_id BIGINT NULL;
-- 2. 기존 UNIQUE 제약조건 삭제
ALTER TABLE order_item_non_identifying DROP KEY uq_order_item;
-- 3. 새로운 UNIQUE 제약조건 추가
ALTER TABLE order_item_non_identifying
ADD UNIQUE KEY uq_order_item_option (order_id, product_id, option_id);
-- MySQL 전용 --
-- 외래 키(Foreign Key) 제약조건 삭제 (MySQL에서는 인덱스 변경 전 FK를 먼저 삭제해야 한다)
ALTER TABLE order_item_non_identifying DROP FOREIGN KEY fk_oi_non_orders;
ALTER TABLE order_item_non_identifying DROP FOREIGN KEY fk_oi_non_product;
-- 1. 새로운 컬럼 추가 (상품에 옵션이 없는 경우도 있으므로 NULL 허용)
ALTER TABLE order_item_non_identifying ADD COLUMN option_id BIGINT NULL;
-- 2. 기존 UNIQUE 제약조건 삭제
ALTER TABLE order_item_non_identifying DROP INDEX uq_order_item;
-- 3. 새로운 UNIQUE 제약조건 추가, option_id 추가
ALTER TABLE order_item_non_identifying ADD UNIQUE KEY uq_order_item_option (order_id, product_id, option_id);
-- 외래 키 제약조건 다시 추가
ALTER TABLE order_item_non_identifying ADD CONSTRAINT fk_oi_non_orders FOREIGN KEY (order_id) REFERENCES orders_non_identifying(order_id);
ALTER TABLE order_item_non_identifying ADD CONSTRAINT fk_oi_non_product FOREIGN KEY (product_id) REFERENCES product_non_identifying(product_id);
- MySQL은 NULL을 서로 다른 값으로 취급하므로 동일 주문, 상품, NULL이 중복 허용된다는 점을 주의하자.
- 중요한 점은 테이블의 PK인 "order_item_id"는 전혀 변경되지 않았다는 것이다.
- 테이블의 정체성은 그대로 유지한채, 비즈니스 규칙을 담당하는 UNIQUE 제약조건만 유연하게 교체했다
요구사항 반영 확인
-- order_id=100, product_id=1 (티셔츠)에 대해 옵션만 다르게 주문
INSERT INTO order_item_non_identifying(order_id, product_id, option_id, order_price, order_quantity)
VALUES (100, 1, 10, 15000, 2); -- option_id 10: L 사이즈, 흰색
INSERT INTO order_item_non_identifying(order_id, product_id, option_id, order_price, order_quantity)
VALUES (100, 1, 15, 15000, 1); -- option_id 15: M 사이즈, 검은색

- 이제 동일한 주문과 상품에 대해 옵션만 다르게 하여 여러 데이터를 추가할 수 있다.
식별 관계의 경우 - 깨지기 쉬운 구조
식별 관계에선 "(order_id, product_id)" 자체가 PK이므로, "하나의 주문에 동일한 상품은 하나만"이라는 규칙이 PK 레벨에 딱 붙어 있다. 새로운 요구사항은 이 PK 제약조건을 정면으로 위반한다.
변경 작업
이 문제를 해결하려면 테이블의 PK, 즉 정체성 자체를 변경해야 한다.
- 테이블에 "option_id" 컬럼을 추가한다.
- 기존의 "(order_id, product_id)" 복합 키(PK)를 삭제한다.
- "option_id"까지 포함된 새로운 3 컬럼 복합 키(PK) "(order_id, product_id, option_id)"를 생성한다
문제점
이것은 단순히 컬럼 하나를 추가하거나 제약조건을 바꾸는 수준이 아니다. 테이블의 근간이자 식별자인 PRIMARY KEY를 변경하는 매우 위험하고 복잡한 작업이다. 만약 다른 테이블에서 이 복합키를 참조하고 있었다면, 그곳까지 변경의 여파가 미친다. 서비스가 운영 중인 상황에서 PK를 변경하는 것은 상당한 부담과 위험을 동반한다.
이 예시는 "M:N 관계"에서도 비식별 관계가 비즈니스 변화에 훨씬 유연하고 확장성 있게 대응할 수 있음을 명확하게 보여준다.
다대다 정리
결론적으로, 다대다 관계를 해결하는 연결 테이블은 비식별 관계로 설계하는 것이 훨씬 더 유연하고 확장성이 높다.
- 비식별 관계
- 독립적인 대리 키를 사용하므로, 비즈니스 규칙이 변경되어도 테이블의 PK를 건드릴 필요가 없다.
- 새로운 정보가 필요하면 컬럼만 추가하고, 비즈니스 규칙의 변경은 UNIQUE 제약조건을 수정하여 대응하면 된다.
- 이는 기존 테이블을 참조하는 다른 관계에 영향을 주지 않아 훨씬 안전하다.
- 식별 관계
- 복합키 자체가 비즈니스 규칙을 강제하기 때문에, 해당 규칙이 변경되면 PK 구조 자체를 바꿔야 한다.
- 이는 매우 위험하고 번거로운 작업이며, 테이블의 확장성을 심각하게 저해한다.
실무에서는 M:N 관계를 해소하는 연결 테이블에 비식별 관계를 사용하는 것이 최고의 선택이다. PK는 별도의 대리 키를 사용하고 외래키"(FK1, FK2,...)"조합에조합에 UNIQUE 제약조건을 걸어 비즈니스 중복을 제거하자.
대리 키 + 비식별 관계
현대 애플리케이션 개발에서는 "기본적으로 모든 관계는 비식별 관계로 설계하고, PK는 비즈니스와 무관한 대리 키를 사용한다"는 원칙을 따르는 것이 현명한 선택이다. 이것이 변화에 유연하게 대처하고 유지보수하기 좋은 시스템을 만드는 현대적인 설계 방법이다.
'데이터 베이스 > 설계 1편, 현대적 데이터 모델링 완전 정복' 카테고리의 다른 글
| Ch08. 논리적 모델링 - 실습 준비 (0) | 2026.06.28 |
|---|---|
| Ch07. 논리적 모델링(4) - 현대적인 설계 트랜드 (0) | 2026.06.27 |
| Ch07. 논리적 모델링(4) - 식별, 비식별 관계의 일대일 (0) | 2026.06.27 |
| Ch07. 논리적 모델링(4) - 식별, 비식별 관계의 SQL 쿼리/성능 (0) | 2026.06.27 |
| Ch07. 논리적 모델링(4) - 식별 관계의 문제점 (0) | 2026.06.27 |