블로그 포스트와 태그로 알아보는 SQL의 다대다 관계 | M:N 관계

수정일: 2025. 9. 27.

개요

이번 글에서는 블로그 포스트와 태그의 관계를 를 통해 SQL의 M:N 관계를 알아봅니다.

들어가며

보통 블로그 포스트는 여러 기능들을 가집니다. 그 중에서도 태그와 카테고리, 좋아요, 댓글 등 다양한 기능과 편의성을 제공하는데요, 이들은 모두 SQL로 구조화 될 수 있습니다.

그 중에서도 포스트와 태그에 대해 알아보겠습니다. 하나의 포스트는 글의 내용안에 여러 주제를 다룰 수 있습니다. 예를 들어 맛집 관련 글을 쓰는 블로거의 포스트에는 맛집의 지역, 위치, 휴무일, 가격, 맛에 대한 평가, 점수, 한식, 일식, 중식 등등의 내용이 들어갈 수 있습니다.

이들 중 태그로 만들만한 것들을 뽑아보면 한식, 일식, 중식등의 카테고리성의 성격을 띄는 내용을 태그로 만들어도 좋습니다.

포스트와 태그의 관계

하나의 포스트는 여러 태그를 가질 수 있습니다. 예를 들어보면 맛집 포스트는 한식, 가성비, 위치등의 태그를 가질 수 있습니다. 이 때, 1 대 N의 관계가 성립합니다. 이는 포스트 입장에서 바라본 포스트와 태그와의 관계입니다.

태그입장에서 보게 되면, 한 태그는 여러 포스트에 속할 수 있습니다. 한식 태그는 비빔밥, 매운탕, 삼계탕, 보쌈등의 각 포스트에 모두 속할 수 있습니다. 따라서 하나의 태그는 여러 포스트에 속할 수 있습니다. 여기서도 다시 1 대 N의 관계가 성립함을 보입니다.

하나의 포스트는 여러개의 태그를 가집니다. 하나의 태그는 여러개의 포스트에 속합니다. 라는 사실을 알게 되었습니다.

이를 SQL로 표현하면서 구체화 해보겠습니다.

SQL 스키마 예제

우선 포스트 테이블부터 만들어봅시다.

CREATE TABLE posts(
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	title TEXT NOT NULL,
	content TEXT NOT NULL,
	created_at DATETIME
);

이어서 tags 테이블을 생성합니다.

CREATE TABLE tags (
	id INT PRIMARY KEY AUTOINCREMENT,
	name VARCHAR(50) UNIQUE
);

이렇게 두 테이블이 생성되고 나서는 서로의 존재를 알지 못하는 상태입니다. 이제 두 테이블을 연결해주는 posts_tags 테이블을 생성해줍니다.

CREATE TABLE posts_tags (
	post_id INT,
	tag_id INT,
	PRIMARY KEY (post_id, tag_id),
	FOREIGN KEY (post_id) REFERENCES posts(id),
	FOREIGN KEY (tag_id) REFERENCES tags(id)
);

이 때, 왜 위와 같은 테이블을 생성하지? 라고 의문을 가질 수도 있습니다. “포스트 테이블에 tags 컬럼을 생성하고 그 곳에 태그 내용을 추가해도 되는것 아닌가?” 이렇게 되면 검색이나 수정 삭제가 곤란해집니다. 태그 테이블의 내용을 변경하더라도 포스트 테이블을 찾아가 수정하기가 난감해지게 되는 것이죠.

post_tags 테이블

post_tags는 이번 내용의 핵심입니다. 아래의 sql문을 하나씩 뜯어보겠습니다.

CREATE TABLE posts_tags (
	post_id INT, -- post_id를 받습니다.
	tag_id INT, -- tag_id
	PRIMARY KEY (post_id, tag_id), -- post_id와 tag_id를 함께 받아 묶어서 하나의 PK로 설정합니다.
	FOREIGN KEY (post_id) REFERENCES posts(id), -- post_id를 FK로 지정하고 posts 테이블의 id임을 지정합니다.
	FOREIGN KEY (tag_id) REFERENCES tags(id) -- tag_id를 FK로 지정하고 tags 테이블의 id임을 지정합니다.
);

해당 테이블은 포스트 테이블과 태그 테이블의 관계만을 저장합니다. 예를 들어 1번 포스트는 한식, 부산, 맛집이라는 3개의 태그를 지정했다고 가정합시다.

우선 1번 포스트를 만들어보겠습니다.

INSERT INTO posts (title, content, created_at)
VALUES ('첫번째 포스트', '부산 맛집을 소개합니다. 이곳은...', DATETIME('now'); -- mysql의 경우 NOW()

이어서 한식, 부산, 맛집이라는 3개의 태그를 만들어보겠습니다.

INSERT INTO tags (name)
VALUES ('한식'), ('부산'), ('맛집');

마지막으로 posts_tags에 포스트 id와 포스트에 연결되어질 태그 id들을 넣어주면 됩니다.

INSERt INTO posts_tags (post_id, tag_id)
VALUES (1, 1), (1, 2), (1, 3);

이제 위 내용을 정리하면 포스트 테이블, 태그 테이블, 포스트_태그 테이블이 존재하고, 포스트 테이블과 태그 테이블은 서로 연관이 없음 포스트 테이블은 포스트_태그 테이블과 1:N 관계, 태그 테이블과 포스트_태그 테이블도 1:N 관계로 정리됩니다.

결과적으로 포스트_태그 테이블을 이용해 M:N 관계가 성립됩니다.

이제, 1번 포스트에 사용된 태그들이 뭔지 조회하고 싶다면,

SELECT * FROM posts_tags pt
WHERE post_id = 1;

이렇게 조회할 수 있습니다.

또한 포스트에 속한 태그들의 이름을 추출하려면 아래와 같이 join을 할 수 있습니다.

SELECT t.name FROM posts_tags pt
INNER JOIN tags t ON t.id = pt.tag_id
WHERE post_id = 1;
name
----
한식
부산
맛집

마치며

이렇게 블로그 포스트와 태그 테이블을 만들어보며 다대다 관계 M:N 관계를 구현해봤습니다. 위 SQL 문법은 sqlite를 따르지만 mysql도 거의 비슷한 형태로 작성 가능합니다. 이번 핵심 테이블은 posts_tags 테이블입니다.

해당 테이블은 중간 테이블이라고도 하며 서로 관계 없는 두 테이블을 연결해주는 역할을 합니다.

posts_tags 테이블은 두 테이블의 각 행의 PK를 FK로 가지며, 두 FK를 묶은 값을 본인의 PK로 가집니다. 그렇게 고유한 값을 PK로 가질 수 있고, REFERENCES를 통해 쓰레기 값이 들어오는 것을 방지합니다.

아래는 전체 SQL 예시입니다.

-- posts
SELECT * FROM posts;
CREATE TABLE posts(
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	title TEXT NOT NULL,
	content TEXT NOT NULL,
	created_at DATETIME
);



INSERT INTO 
posts (title, content, created_at)
VALUES ('첫번째 포스트', '부산의 맛집을 소개합니다. 이곳은...',DATETIME());

INSERT INTO
posts (title, content, created_at)
VALUES ('두번째 포스트', '퇴근하고 싶어', DATETIME());


--tags

SELECT * FROM tags;

CREATE TABLE tags (
	id INTEGER PRIMARY KEY AUTOINCREMENT,
	name VARCHAR(50) UNIQUE
);



INSERT INTO tags (name)
VALUES ('한식'), ('부산'), ('맛집');


-- posts_tags
SELECT * FROM posts_tags;

CREATE TABLE posts_tags (
	post_id INT,
	tag_id INT,
	PRIMARY KEY (post_id, tag_id),
	FOREIGN KEY (post_id) REFERENCES posts(id),
	FOREIGN KEY (tag_id) REFERENCES tags(id)
);

INSERT INTO posts_tags (post_id, tag_id)
VALUES (2, 1), (2, 2), (2, 3);

SELECT t.name FROM posts_tags pt
INNER JOIN tags t ON t.id = pt.tag_id
WHERE post_id = 1;

감사합니다.

블로그 | devxdev