조인 전략(Joined Strategy)은 부모 테이블과 자식 테이블을 분리하고, 조인을 통해 데이터를 조회하는 방식이다. 가장 정석적이고 정규화된 형태라고 볼 수 있다.
테이블 설계
-- 기존 테이블 정리
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS book;
DROP TABLE IF EXISTS electronics;
DROP TABLE IF EXISTS clothing;
DROP TABLE IF EXISTS product;
-- 부모 테이블: 공통 속성을 가진다
CREATE TABLE product (
product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_type VARCHAR(20) NOT NULL, -- 구분자 컬럼
name VARCHAR(200) NOT NULL,
price INT NOT NULL,
stock_quantity INT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_product_type (product_type)
);
-- 자식 테이블: 도서
CREATE TABLE book (
product_id BIGINT PRIMARY KEY,
author VARCHAR(100) NOT NULL,
isbn VARCHAR(13),
publisher VARCHAR(100),
FOREIGN KEY (product_id) REFERENCES product(product_id)
);
-- 자식 테이블: 전자제품
CREATE TABLE electronics (
product_id BIGINT PRIMARY KEY,
manufacturer VARCHAR(100) NOT NULL,
warranty_months INT NOT NULL DEFAULT 12,
power_consumption INT,
FOREIGN KEY (product_id) REFERENCES product(product_id)
);
-- 자식 테이블: 의류
CREATE TABLE clothing (
product_id BIGINT PRIMARY KEY,
size VARCHAR(10) NOT NULL,
color VARCHAR(50) NOT NULL,
material VARCHAR(100),
FOREIGN KEY (product_id) REFERENCES product(product_id)
);
- 부모 테이블인 product에는 공통 속성만 있다. 자식 테이블인 book, electronics, clothing 에는 각각의 고유 속성만 있다.
- 부모 테이블의 기본 키와 자식 테이블의 기본 키가 같다.
- 자식 테이블의 기본 키는 부모 테이블의 기본 키를 참조하는 외래키이기도 하다.
- 부모와 자식은 PK를 공유하는 1:1 관계이다.
데이터 입력
데이터를 입력할 때는 부모 테이블에 먼저 입력하고, 자식 테이블에 추가 정보를 입력한다. 이 설계에서 주의할 점은 자식 테이블의 기본키는 반드시 부모 테이블의 기본 키를 이어받아 사용해야 한다는 점이다.
-- 도서 데이터 입력
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('BOOK', '클린 코드', 33000, 100);
INSERT INTO book (product_id, author, isbn, publisher)
VALUES (LAST_INSERT_ID(), '로버트 마틴', '9788966260959', '인사이트');
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('BOOK', '이펙티브 자바', 36000, 50);
INSERT INTO book (product_id, author, isbn, publisher)
VALUES (LAST_INSERT_ID(), '조슈아 블로크', '9788966262281','인사이트');
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('BOOK', 'JPA 프로그래밍', 43000, 30);
INSERT INTO book (product_id, author, isbn, publisher)
VALUES (LAST_INSERT_ID(), '김영한', '9788960777330', '에이콘');
-- 전자제품 데이터 입력
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('ELECTRONICS', '갤럭시', 1800000, 200);
INSERT INTO electronics (product_id, manufacturer, warranty_months, power_consumption)
VALUES (LAST_INSERT_ID(), '삼성전자', 24, 15);
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('ELECTRONICS', '맥북 프로 14', 2500000, 50);
INSERT INTO electronics (product_id, manufacturer, warranty_months, power_consumption)
VALUES (LAST_INSERT_ID(), '애플', 12, 96);
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('ELECTRONICS', 'LG 그램 17', 1800000, 80);
INSERT INTO electronics (product_id, manufacturer, warranty_months, power_consumption)
VALUES (LAST_INSERT_ID(), 'LG전자', 24, 65);
-- 의류 데이터 입력
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('CLOTHING', '오버핏 맨투맨', 35000, 500);
INSERT INTO clothing (product_id, size, color, material)
VALUES (LAST_INSERT_ID(), 'L', '블랙','면 100%');
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('CLOTHING', '슬림핏 청바지', 59000, 300);
INSERT INTO clothing (product_id, size, color, material)
VALUES (LAST_INSERT_ID(), 'M','인디고','데님');
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('CLOTHING', '패딩 점퍼', 189000, 100);
INSERT INTO clothing (product_id, size, color, material)
VALUES (LAST_INSERT_ID(), 'XL', '네이비','폴리에스터');
- "LAST_INSERT_ID()" 함수를 사용해서 방금 입력한 부모의 "product_id"를 자식 테이블에 입력한다.
데이터 확인
공통 속성 확인
SELECT * FROM product;

SELECT * FROM book;

SELECT * FROM electronics;

SELECT * FROM clothing;

- 각 테이블에 필요한 데이터만 있다. NULL 값이 없다!
- 부모 테이블의 ID와 자식 테이블의 ID가 일치한다
전체 상품 조회
전체 상품의 공통 정보만 조회할 때는 부모 테이블만 조회하면 된다.
SELECT product_id, product_type, name, price, stock_quantity
FROM product
ORDER BY product_id DESC;

유형별 상세 조회
특정 유형의 상품을 상세 정보와 함께 조회하려면 부모 테이블과 해당 자식 테이블의 조인이 필요하다.
-- 도서 상세 조회
SELECT
p.product_id,
p.name,
p.price,
p.stock_quantity,
b.author,
b.isbn,
b.publisher
FROM product p
JOIN book b ON p.product_id = b.product_id;

-- 전자제품 상세 조회
SELECT
p.product_id,
p.name,
p.price,
p.stock_quantity,
e.manufacturer,
e.warranty_months,
e.power_consumption
FROM product p
JOIN electronics e ON p.product_id = e.product_id;

상품 ID로 상세 조회
특정 상품의 모든 정보를 조회하려면 "product_type"을 확인하고 해당 자식 테이블과 조인해야 한다.
-- 상품 ID가 2인 상품 조회 (도서인 경우)
SELECT
p.product_id,
p.product_type,
p.name,
p.price,
b.author,
b.isbn,
b.publisher
FROM product p
LEFT JOIN book b ON p.product_id = b.product_id
WHERE p.product_id = 2;

자식의 타입을 모르는 경우에는 모든 자식 테이블을 "LEFT JOIN"으로 연결해서 찾을 수 있다.
-- 모든 자식 테이블과 LEFT JOIN
SELECT
p.product_id,
p.product_type,
p.name,
p.price,
b.author,
b.isbn,
b.publisher,
e.manufacturer,
e.warranty_months,
c.size,
c.color
FROM product p
LEFT JOIN book b ON p.product_id = b.product_id
LEFT JOIN electronics e ON p.product_id = e.product_id
LEFT JOIN clothing c ON p.product_id = c.product_id
WHERE p.product_id = 5;

모든 자식 테이블 조인(Join)의 동작 원리
이와 같은 쿼리가 가능한 이유는 데이터베이스 설계 시 부모와 자식 테이블이 동일한 기본 키(PK)를 공유하도록 설계했기 때문이다.
- PK 공유와 1:1 관계: book, electronics, clothing 테이블의 PK이자 FK인 "product_id"는 부모 테이블 product의 "product_id"와 동일한 값을 가진다.
- 즉, 논리적으로 하나의 상품 정보가 여러 테이블에 쪼개져 저장된 형태다.
- LEFT JOIN의 특성: "LEFT JOIN"은 조인 조건이 일치하지 않는 경우 데이터를 누락시키는 것이 아니라 NULL로 채워서 반환한다.
- 배타적 관계: 일반적으로 하나의 상품은 "책"이면서 동시에 "전자제품"일 수 없다.
- 따라서 특정 "product_id"에 대해 자식 테이블 중 하나에만 데이터가 존재하고, 나머지 자식 테이블과의 조인 결과는 모두 "NULL"이 된다.
결과를 살펴보면 product 테이블의 공통 정보와 electronics 테이블의 상세 정보(manufacturer, warranty_months)는 정상적으로 조회되었지만, 해당 상품과 관련이 없는 book, clothing 테이블의 컬럼(author, size 등)은 모두 NULL로 표시된 것을 확인할 수 있다.
모든 상품 상세 조회
자식을 포함한 모든 컬럼을 한 번에 조회해 보자. 앞서 사용한 쿼리에서 "product_id" 조건만 제거하면 된다
-- 모든 자식 테이블과 LEFT JOIN
SELECT
p.product_id,
p.product_type,
p.name, p.price,
b.author,
b.isbn,
b.publisher,
e.manufacturer,
e.warranty_months,
c.size,
c.color
FROM product p
LEFT JOIN book b ON p.product_id = b.product_id
LEFT JOIN electronics e ON p.product_id = e.product_id
LEFT JOIN clothing c ON p.product_id = c.product_id;

- 이처럼 모든 자식 테이블을 "LEFT JOIN"으로 묶어서 조회하면, 상품의 타입이 무엇인지 미리 알지 못해도 한 번의 쿼리로 필요한 모든 상세 정보를 가져올 수 있다.
- 애플리케이션 계층에서는 이 결과를 받아 "product_type" 여부를 확인하여 적절한 객체로 매핑하면 된다.
- 이 방식은 조회 쿼리가 단순해진다는 장점이 있지만, 자식 테이블이 늘어날수록 조인 횟수가 많아져 성능에 영향을 줄 수 있으므로 트래픽이 많은 서비스에서는 주의해서 사용해야 한다.
외래 키와 주문 연동
주문 테이블은 부모 테이블인 product를 참조한다. 주문 정보와 상품 정보를 함께 조회하는 것도 가능하다.
SELECT
o.order_id,
o.quantity,
p.product_type,
p.name,
p.price,
(o.quantity * p.price) AS total_price
FROM orders o
JOIN product p ON o.product_id = p.product_id;

- order_id 1번: "BOOK" 타입이므로 book 테이블의 author(로버트 마틴) 정보가 출력되고, 전자제품이나 의류 관련 컬럼은 NULL이다.
- order_id 2번: "ELECTRONICS" 타입이므로 manufacturer(삼성전자) 정보가 출력된다.
- order_id 3번: "CLOTHING" 타입이므로 size(L)와 color(블랙) 정보가 출력된다.
장점
- 정규화된 구조이다
- 테이블이 정규화되어 있어 데이터 중복이 없다.
- 각 테이블에 필요한 컬럼만 있어서 저장 공간을 효율적으로 사용한다.
- NULL 값이 없다
- 자식 테이블에 해당 유형에 필요한 컬럼만 있으므로 NULL이 발생하지 않는다.
- NOT NULL 제약조건을 사용할 수 있다
- 각 자식 테이블에서 해당 유형에 필수인 컬럼에 "NOT NULL" 제약조건을 걸 수 있다.
- 데이터 무결성을 데이터베이스 레벨에서 보장할 수 있다.
- 외래 키 설정이 가능하다
- 부모 테이블을 참조하는 외래 키를 설정할 수 있다.
- 주문, 장바구니 등 다른 테이블에서 상품을 참조하기 편리하다.
- 유연한 확장이 가능하다
- 새로운 상품 유형을 추가할 때 기존 테이블을 수정하지 않고 새 자식 테이블만 추가하면 된다.
단점
조인이 필요하다
- 상세 정보를 조회하려면 부모 테이블과 자식 테이블을 조인해야 한다.
- 조인은 매우 빠르지만, 너무 많은 데이터를 조인하면 성능에 영향을 줄 수 있다
INSERT 시 두 번의 쿼리가 필요하다
-- 1. 부모 테이블에 INSERT
INSERT INTO product (product_type, name, price, stock_quantity)
VALUES ('BOOK', '새로운 책', 25000, 50);
-- 2. 자식 테이블에 INSERT
INSERT INTO book (product_id, author, isbn, publisher)
VALUES (LAST_INSERT_ID(), '저자명', '1234567890123','출판사');
- 데이터를 입력할 때 부모 테이블과 자식 테이블에 각각 "INSERT" 해야 한다.
쿼리가 복잡해질 수 있다
-- 모든 상품의 모든 정보 조회 (복잡한 쿼리)
SELECT
p.*,
b.author, b.isbn, b.publisher,
e.manufacturer, e.warranty_months, e.power_consumption,
c.size, c.color, c.material
FROM product p
LEFT JOIN book b ON p.product_id = b.product_id
LEFT JOIN electronics e ON p.product_id = e.product_id
LEFT JOIN clothing c ON p.product_id = c.product_id;
- 모든 상품의 상세 정보를 한 번에 조회하려면 여러 테이블을 "LEFT JOIN"해야 한다.
조인 전략 정리
| 장점 | 단점 |
| 정규화된 구조 | 조인이 필요함 |
| NULL 값 없음 | INSERT 시 두 번의 쿼리 필요 |
| NOT NULL 제약조건 사용 가능 | 모든 상세 정보 조회 시 쿼리 복잡 |
| 외래 키 설정 가능 | |
| 유연한 확장 가능 |
조인 전략을 선택하면 좋은 경우
- 상품 유형이 많거나 앞으로 계속 늘어날 예정일 때
- 각 유형별 고유 속성이 많을 때
- 데이터 무결성이 중요할 때
- 정규화된 구조를 원할 때
'데이터 베이스 > 설계 2편, 실무에서 반드시 마주치는 9가지 설계 패턴' 카테고리의 다른 글
| Ch07. 동적 설계 EAV - EAV 패턴 (0) | 2026.07.27 |
|---|---|
| Ch07. 동적 설계 EAV - 기존 방식의 한계와 새로운 접근법 (0) | 2026.07.27 |
| Ch06. 상속 관계 설계 - 단일 테이블 전략 (0) | 2026.07.23 |
| Ch06. 상속 관계 설계 - 구현 클래스마다 테이블 전략 (0) | 2026.07.23 |
| Ch06. 상속 관계 설계 - 문제 상황 (0) | 2026.07.23 |