데이터 베이스/설계 2편, 실무에서 반드시 마주치는 9가지 설계 패턴

Ch06. 상속 관계 설계 - 조인 전략

webmaster 2026. 7. 23. 21:54

조인 전략(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 제약조건 사용 가능  모든 상세 정보 조회 시 쿼리 복잡
외래 키 설정 가능  
유연한 확장 가능  

 

조인 전략을 선택하면 좋은 경우

  • 상품 유형이 많거나 앞으로 계속 늘어날 예정일 때
  • 각 유형별 고유 속성이 많을 때
  • 데이터 무결성이 중요할 때
  • 정규화된 구조를 원할 때