데이터 베이스/설계 1편, 현대적 데이터 모델링 완전 정복

Ch06. 논리적 모델링(3) - 다대다 관계 연결 테이블

webmaster 2026. 6. 26. 19:57

해결책: 연결 테이블(Junction Table)

다대다 관계를 푸는 표준적인 해법은 중간에 새로운 테이블을 하나 만들어서, 기존의 다대다 관계를 개의 일대다 관계로 풀어내는 것이다.  중간 테이블을 연결 테이블(Junction Table) 또는 개념적 모델링에서는 연관 엔티티(Associative Entity)라고 부른다.

 

우리 쇼핑몰의 orders product 관계를 예로 들면, order_product라는 연결 테이블을 만드는 것이다. 참고로 여기서는 orders product 연결하는 의미로 order_product라는 이름을 사용했다.

ERD

  • 기존 "M:N" 관계
    • orders (M) <---> (N) product
  • 연결 테이블로 분해
    • orders (1) <---> (N) order_product (N) <---> (1) product

이렇게 하면 다음과 같이 다대다 관계가 개의 일대다 관계로 나뉜다.

  • 하나의 주문(orders)은 여러 주문 상품(order_product)을 가질 수 있다. (1:N)
  • 하나의 상품(product)은 여러 주문 상품(order_product)에 포함될 수 있다. (1:N)

이렇게 하면 관계형 데이터베이스가 완벽하게 다대다 관계를 이해하고 처리할 있는 구조가 된다.

 

 

연결 테이블 - 일대다, 다대일 관계

연결 테이블 그림

 

 

연결 테이블 예시

DROP TABLE IF EXISTS order_item; -- 다른 예제 충돌 예방
DROP TABLE IF EXISTS order_product;
DROP TABLE IF EXISTS product;
DROP TABLE IF EXISTS orders;

-- 상품 테이블 생성
CREATE TABLE product (
	product_id BIGINT NOT NULL, -- 상품id 직접 입력
    name VARCHAR(100) NOT NULL,
	price INT NOT NULL,
	PRIMARY KEY (product_id)	
);
-- 주문 테이블 생성
CREATE TABLE orders (
	order_id BIGINT NOT NULL, -- 주문id 직접 입력
    order_date DATE,
	PRIMARY KEY (order_id)
);

-- 연결 테이블(주문-상품) 생성
CREATE TABLE order_product (
	order_product_id BIGINT NOT NULL AUTO_INCREMENT,
	order_id BIGINT NOT NULL, -- orders 테이블의 FK
	product_id BIGINT NOT NULL, -- product 테이블의 FK
	PRIMARY KEY (order_product_id),
	-- 한 주문에 동일한 상품이 중복으로 들어가는 것을 방지
	CONSTRAINT uq_order_product UNIQUE (order_id, product_id),
    
	CONSTRAINT fk_order_product_orders FOREIGN KEY (order_id)
		REFERENCES orders (order_id),
	CONSTRAINT fk_order_product_product FOREIGN KEY (product_id)
		REFERENCES product (product_id)
);

erd

  • order_product 테이블이 핵심이다.
    • 이 테이블은 orders의 기본 키인 "order_id"와 product의 기본 키인 "product_id"를 각각 외래 키(FK)로 가진다.
    • 이 두 외래 키가 orders와 product를 연결하는 다리 역할을 한다.
  • CONSTRAINT uq_order_product UNIQUE (order_id, product_id)
    • 유니크 제약조건을 통해 같은 주문에 같은 상품이 중복 선택되지 않도록 막는다.

 

데이터 삽입

이제 실제 데이터를 넣어보자. 다음과 같은 가지 주문 시나리오를 가정한다.

-- 상품 데이터 삽입
INSERT INTO product(product_id, name, price) VALUES(1, '청바지' , 50000);
INSERT INTO product(product_id, name, price) VALUES(2, '티셔츠', 25000);

-- 주문 100 생성
INSERT INTO orders(order_id, order_date) VALUES(100, '2025-09-01');

-- 연결 테이블 데이터 삽입
-- 주문 100에 대한 상품 정보
INSERT INTO order_product(order_id, product_id) VALUES(100, 1); -- 주문 100번, 청바지
INSERT INTO order_product(order_id, product_id) VALUES(100, 2); -- 주문 100번, 티셔츠

-- 주문 101 생성
INSERT INTO orders(order_id, order_date) VALUES(101, '2025-09-02');
-- 주문 101에 대한 상품 정보
INSERT INTO order_product(order_id, product_id) VALUES(101, 1); -- 주문 101번, 청바지 1개
  • 주문 100 (order_id: 100)
    • "청바지"와 "티셔츠"를 함께 주문한다.
  • 주문 101 (order_id: 101)
    • "청바지"를 주문한다.

 

결과 확인

orders 테이블

SELECT * FROM orders;

쿼리 결과

 

product 테이블

SELECT * FROM product;

쿼리 결과

 

order_product 테이블

SELECT * FROM order_product;

쿼리 결과

  • 이 테이블이 다대다 관계의 모든 정보를 담고 있다.
  • "order_id" "100" 주문은 "product_id" "1(청바지)" "2(티셔츠)" 포함한다.
    • (orders : order_product = 1 : N)
  • "product_id" "1" 상품(청바지) "order_id" "100번"과 "101번" 주문에 모두 포함된다.
    • (product : order_product = 1 : N)

ERD
결과

 

 

연결 테이블과 조인

 

주문 "100번"에 대한 상품 목록 조회 (orders, order_product, product  테이블을 조인해서 100 주문에 포함된 상품들의 이름과 가격을 조회해 보자.)

SELECT
    o.order_id,
    p.name,
    p.price
FROM orders o
JOIN order_product op ON o.order_id = op.order_id
JOIN product p ON op.product_id = p.product_id
WHERE o.order_id = 100;

결과

  • FROM orders o: 조인의 시작점을 orders 테이블로 잡고, "o"라는 별칭(alias)을 붙였다.
  • JOIN order_product op ON o.order_id = op.order_id : orders 테이블과 order_product 테이블을 "order_id"를 기준으로 연결한다.
    • 이때 일대다 관계의 조인이 발생한다. (조인 뻥튀기 발생)
  • JOIN product p ON op.product_id = p.product_id : 위에서 연결된 결과에 다시 product 테이블을 "product_id"를 기준으로 연결한다.
    • 이때 다대일 관계의 조인이 발생한다.
  • WHERE o.order_id = 100: 이렇게 3개의 테이블이 모두 연결된 거대한 가상의 테이블에서 "order_id"가 100인 데이터만 필터링한다.

"청바지" 포함된 주문 목록 조회

SELECT
    p.name,
    o.order_id,
    o.order_date
FROM product p
JOIN order_product op ON p.product_id = op.product_id
JOIN orders o ON op.order_id = o.order_id
WHERE p.product_id = 1;

쿼리 결과

  • JOIN order_product op ON p.product_id = op.product_id : 일대다 조인이다. (조인 뻥튀기 발생)
  • JOIN orders o ON op.order_id = o.order_id : 다대일 조인이다.

이처럼 연결 테이블(order_product) 있었기 때문에, 특정 상품이 어떤 주문들에 포함되어 있는지도 쉽게 조회할 수 있다. 이것이 바로 관계형 데이터베이스가 관계를 다루는 방식이다. 다대다 관계를 직접 표현할 없으므로, 중간에 연결 테이블을 두어 개의 일대다 관계로 풀어내는 것이다. 구조를 이해하는 것이 관계형 데이터베이스 설계의 핵심 하나다.

 

이처럼 연결 테이블 order_product  통해 기존의 복잡했던 다대다 관계가 개의 명확한 일대다 관계로 완벽하게 해결되었다. 이렇게 중간에 연결 테이블을 두는 방식이 관계형 데이터베이스에서 다대다 관계를 다루는 표준적인 방법이다.

 

연결 테이블의 본질: 관계를 모델링하다

그렇다면 연결 테이블은 근본적으로 어떻게 다대다 관계의 문제를 해결하는 것일까? 단순히 테이블을 하나 추가했기 때문일까? 핵심은 관계 자체를 하나의 독립된 데이터로 보고, 그것을 테이블로 모델링했다는 있다.

 

 

다대다
일대다, 다대일 관계
ERD
조인 결과

  • order_product 테이블의 첫 번째 행 (order_id: 100, product_id: 1)은 "100번 주문은 1번 상품 (청바지)을 포함한다"라는 하나의 사실 또는 사건을 의미한다.
  • 두 번째 행 (order_id: 100, product_id: 2)는 "100번 주문은 2번 상품(티셔츠)을 포함한다"라는 또 다른 하나의 사실을 의미한다.

즉, 연결 테이블은 orders와 product 사이에서 발생할 수 있는 수많은 관계의 경우의 수를 하나하나의 독립된 데이터로 저장하는 공간이다.

이렇게 관계를 구체적인 데이터로 만들어 테이블에 담는 순간, 기존의 복잡했던 다대다(M:N) 관계는 아주 명확한 일대다(1:N) 관계 자연스럽게 해소된다.

  • orders와 order_product의 관계 (1:N)
    • 하나의 주문(orders)은 여러 개의 주문-상품 내역(order_product)을 가질 수 있다.
    • "order_id" "100번" 주문은 order_product 테이블에서 2개의 행(청바지, 티셔츠)을 가진다.
    • 이는 완벽한 일대다(1:N) 관계이며, "다(N)" 쪽에 외래 키(order_id)가 있으므로 아무런 문제가 없다.
  • product와 order_product의 관계 (1:N)
    • 하나의 상품(product)은 여러 개의 주문-상품 내역(order_product)에 포함될 수 있다.
    • "product_id" 1번 상품(청바지)은 order_product 테이블에서 2개의 행(100번 주문, 101번 주문)을 가진다.
    • 이 또한 완벽한 일대다(1:N) 관계이며, "다(N)" 쪽에 외래 키(product_id)가 있으므로 아무런 문제가 없다.

결론적으로, 연결 테이블은 엔티티 사이의 추상적인 "관계" "구체적인 데이터" 변환하는 것이다. 변환을 통해 관계형 데이터베이스가 가장 처리할 있는 단순한 일대다 관계 개로 문제를 바꾸어 해결하는 것이다. 이것이 바로 관계형 데이터베이스에서 다대다 관계를 해결하는 방식이다.