Insights·2026-09-30

다대다(N:M) 관계는 어떻게 푸나 — 중간 테이블, 조인, 머메이드 ERD

다대다(N:M) 관계는 칸 하나에 목록을 넣지 않고, 두 테이블의 id(각 줄을 가리키는 고유 값)를 한 줄씩 적는 중간 테이블을 만들어 1:N(한쪽 하나에 다른 쪽 여럿이 달리는 관계) 두 개로 푼다. 모바일 청첩장 서비스에 부관리자가 생겨 청첩장 하나를 관리자 여럿이 맡게 되는 순간이 그 예다. 중간 테이블에는 owner·sub 같은 역할도 적을 수 있다. 조인은 내 테이블에 없는 정보를 id로 다른 테이블에서 끌어와 붙이는 일이다. 테이블과 그 사이 관계를 그린 설계도인 ERD는 글자로 적은 코드를 그림으로 바꿔 주는 머메이드(Mermaid)의 erDiagram 형식으로 AI에게 그려 달라고 하면 눈으로 확인할 수 있다.

다대다(N:M)는 중간 테이블로 1:N 두 개로 푼다 — 글의 요약 도식

부관리자가 생기면 1:N이 N:M이 된다

앞 편까지 모바일 청첩장 서비스의 관계는 1:N이었다. 관리자 한 명이 청첩장 여러 개를 만들고, 청첩장 하나는 자기를 만든 관리자 한 명만 바라본다. 그래서 청첩장 테이블에는 admin_id 컬럼(열) 하나면 충분했다. 이 칸에는 관리자 테이블에서 그 관리자의 줄을 가리키는 id가 들어간다.

서비스가 잘돼서 관리자도 여러 명 뽑고 청첩장도 많아졌다고 하자. 청첩장을 만든 관리자만 고칠 수 있게 해 두었더니, 그 관리자에게 연락이 닿아 수정할 때까지 시간이 너무 오래 걸린다. 더 빨리 대응할 부관리자가 필요해진다.

이제 청첩장 하나를 관리자 여러 명이 맡는다. 관리자 한 명도 여전히 청첩장 여러 개를 맡는다. 양쪽 모두 여럿인 이 관계를 다대다라고 하고 N:M으로 적는다. 1:N에서는 '여럿'이 한쪽에만 붙었다면, N:M에서는 양쪽에 다 붙는다.

이 연재에서 본 관계는 이로써 세 가지다. 기본이 되는 1:N, 가끔 쓰는 선택적 1:1, 그리고 이번의 N:M이다.

칸 하나에 목록을 넣지 않는다 — 중간 테이블로 푼다

가장 먼저 떠오르는 방법은 admin_id 칸에 관리자 여러 명을 목록으로 넣는 것이다. 청첩장 1번의 칸에 관리자 1, 2, 3을 한꺼번에 적는 식이다. 뜻으로는 맞지만 테이블은 그렇게 쓰지 않는다. 표를 나눠 id로 잇는 관계형 데이터베이스는 칸 하나에 값 하나를 넣는 것을 기본으로 설계한다.

대신 관계를 한 줄에 하나씩 적는다. 청첩장 1과 관리자 1, 청첩장 1과 관리자 2, 청첩장 1과 관리자 3, 청첩장 2와 관리자 4처럼. 이렇게 관계만 적는 테이블을 따로 하나 만든다. 강의 예시에서는 이름이 '청첩장 관리'이고, 칸은 자기 id와 청첩장 id, 관리자 id다.

그러면 N:M 하나가 1:N 두 개로 바뀐다. 청첩장 하나에 청첩장 관리 줄이 여러 개 달리고, 관리자 하나에도 청첩장 관리 줄이 여러 개 달린다. 앞 편에서 본 1:N을 두 번 쓰는 것뿐이라 새로 익힐 규칙이 없다.

이 중간 테이블을 부르는 이름이 따로 있다. 강의에서는 정션 오브젝트(junction object)라고 불렀다. 두 테이블 사이의 관계를 설명하려고 만든 객체라는 뜻이다. 정션 테이블, 연결 테이블이라는 이름도 같은 것을 가리킨다.

관계를 한 줄에 하나씩 적는 중간 테이블
invitations       admins            invitation_admins
───────────       ──────────        ──────────────────────────────
id  groom_name    id  name          id  invitation_id  admin_id
1   민수          1   지은          1   1              1
2   준호          2   서준          2   1              2
                  3   하린          3   1              3
                  4   도윤          4   2              4

invitations 1 : N invitation_admins
admins      1 : N invitation_admins

중간 테이블에는 역할을 적을 수 있다

관계를 따로 빼면 덤이 생긴다. 중간 테이블에 컬럼을 더 붙일 수 있다. 관계 자체에 대한 정보를 적을 자리가 생긴 것이다.

강의 예시는 role(역할) 컬럼이다. 청첩장 1에 대해 관리자 1은 처음 만든 사람이니 owner, 나머지는 주인은 아니지만 대응은 할 수 있는 사람이니 sub로 적는다. 같은 관리자라도 청첩장마다 역할이 다를 수 있으니, 이 값은 관리자 테이블도 청첩장 테이블도 아닌 둘 사이의 줄에 있어야 맞다.

그래서 N:M을 만나면 중간 테이블을 하나 만들고 양쪽을 각각 1:N으로 잇는 것이 주된 해결책이다. 관리자와 청첩장 둘뿐이던 단순한 ERD가 기능이 붙을 때마다 이렇게 넓어진다.

AI에게 이 구조를 요청할 때는 모양을 말로 정해 주면 된다. 아래 요청문처럼 중간 테이블의 이름과 칸을 먼저 적어 주면 관리자 목록을 칸 하나에 몰아넣는 답이 나올 여지가 줄어든다. 이미 쌓인 청첩장의 admin_id 값을 새 테이블로 옮기는 일은 마이그레이션, 곧 기존 데이터를 새 구조로 옮기는 변경이라 따로 확인이 필요하다. 이 부분은 04편에서 다룬다.

invitation_idadmin_idrole
11owner
12sub
13sub
24owner
AI에게 줄 요청문
청첩장 하나를 관리자 여러 명이 관리할 수 있게 바꾸고 싶어.
청첩장 테이블의 admin_id 칸에 관리자 목록을 넣지 말고,
중간 테이블 invitation_admins(id, invitation_id, admin_id, role)를 만들어
invitations와 admins를 각각 1:N으로 이어 줘.
role에는 owner 또는 sub만 들어가게 해 줘.
바꾸기 전에 기존 청첩장 데이터를 어떻게 옮길지 먼저 설명해 줘.

조인 — 내 테이블에 없는 정보를 id로 끌어온다

이번에는 청첩장 목록 화면을 만든다고 하자. 신랑, 신부 같은 청첩장 정보와 함께 이 청첩장이 어떤 템플릿을 쓰는지, 그 템플릿의 URL(웹 주소)과 썸네일(작은 미리보기 그림)도 한눈에 보고 싶다.

그런데 청첩장 테이블에는 템플릿 URL이 없다. 앞 편에서 템플릿을 별도 테이블로 뺐으니 청첩장 줄에는 template_id만 있고, URL과 썸네일은 템플릿 테이블의 줄에 있다.

그래서 청첩장 줄의 template_id를 들고 템플릿 테이블에서 같은 id의 줄을 찾는다. 그 줄의 URL을 가져와 청첩장 줄 옆에 template_url이라는 이름으로 붙인다. 내 테이블에는 없지만 부모 테이블, 곧 여러 청첩장이 바라보는 템플릿 테이블에 있는 정보를 id로 끌어와 붙이는 것, 이것이 조인이다.

목록 화면을 AI에게 맡길 때 '템플릿 URL과 썸네일은 템플릿 테이블에서 조인해서 가져와 줘'라고 한 줄 붙이면 된다. 같은 값을 청첩장 테이블에 한 번 더 복사해 두는 방식보다, id 하나로 끌어오는 쪽이 템플릿을 고쳤을 때 값이 어긋나지 않는다.

template_id로 템플릿 줄을 찾아 붙인다
invitations                                templates
───────────────────────────────────────    ────────────────────────────────
id  groom_name  bride_name  template_id    id    url              thumbnail
1   민수        서연        t-03           t-03  /t/spring        spring.png

목록 화면에 보이는 한 줄 (조인 결과)
────────────────────────────────────────────────────────────
id  groom_name  bride_name  template_url  template_thumbnail
1   민수        서연        /t/spring     spring.png

머메이드로 AI에게 ERD를 그려 달라고 한다

관계가 이만큼 늘면 말로만 따라가기 어렵다. 이때 쓰는 것이 머메이드(Mermaid)다. 이름은 영어로 인어라는 뜻이고, 글자로 적은 코드를 그림으로 바꿔 주는 도구다.

AI와 작업하면 마크다운 문서를 많이 쓰게 된다. 마크다운은 #이나 - 같은 기호로 제목과 목록을 적는 문서 형식이고, README.md처럼 이름 끝이 .md인 파일이 그것이다. 이 문서에서 백틱(`) 세 개로 코드 블록(코드를 따로 담는 구역)을 열고 mermaid라고 적은 뒤, 다음 줄에 erDiagram과 테이블·관계를 적으면 머메이드를 지원하는 화면이 ERD를 알아서 그려 준다.

코드를 직접 쓸 필요는 없다. 강의가 권하는 방법은 AI에게 '머메이드로 ERD를 그려 줘'라고 요청하는 것이다. 아래는 이 편에 나온 네 테이블을 그렇게 적은 코드다. ||--o{ 같은 기호가 02편에서 본 까마귀발 표기, 곧 선 끝 모양으로 하나인지 여럿인지를 나타내는 표시이고, PK는 그 줄을 가리키는 고유 id(기본 키), FK는 다른 테이블의 id를 가리키는 칸(외래 키)이다.

그려진 그림을 보면서 관계를 하나씩 직접 짚어 보는 것이 강의가 말하는 요령이다. 청첩장과 관리자 사이에 중간 테이블이 있는지, 목록 화면에 필요한 정보가 어느 테이블에서 조인으로 오는지를 그림으로 확인한다. 다음 편에서는 이렇게 그린 구조를 AI에게 맡겼을 때 사고가 나는 네 가지를 '목록이 느리다', '가입은 됐는데 포인트가 안 들어왔다' 같은 증상으로 알아보는 법을 본다.

AI에게 줄 요청문
지금 이 프로젝트의 데이터베이스 구조를 머메이드 erDiagram으로 그려 줘.
테이블마다 컬럼 이름과 데이터 타입, PK·FK 표시를 넣고,
관계는 까마귀발 표기로 1:N인지 N:M인지 보이게 해 줘.
N:M이 있으면 어떤 중간 테이블로 풀었는지도 설명해 줘.
청첩장 서비스 ERD (Mermaid)
```mermaid
erDiagram
    admins ||--o{ invitation_admins : manages
    invitations ||--|{ invitation_admins : "managed by"
    templates ||--o{ invitations : "used by"

    admins {
        uuid id PK
        varchar name
    }
    invitations {
        uuid id PK
        varchar groom_name
        varchar bride_name
        uuid template_id FK
    }
    invitation_admins {
        uuid id PK
        uuid invitation_id FK
        uuid admin_id FK
        varchar role "owner or sub"
    }
    templates {
        uuid id PK
        varchar url
        varchar thumbnail
    }
```