SQL JOIN으로 N:M 관계 풀기 | 학생-동아리 연결 테이블 예제

학생 1명이 여러 동아리에 가입할 수 있고, 동아리 1개에도 여러 학생이 가입할 수 있다면 두 테이블만으로는 관계를 깔끔하게 표현하기 어렵다. 이런 구조는 N:M 관계(Many-to-Many) 라고 부르고, 관계형 데이터베이스에서는 보통 연결 테이블을 하나 더 만들어서 해결한다.

실무에서 JOIN이 자꾸 헷갈리는 이유도 여기서 시작되는 경우가 많다. 학생 테이블과 동아리 테이블만 보고 바로 연결하려고 하면 구조가 꼬이고, 중복 데이터가 늘어나고, 나중에 조회 쿼리도 애매해진다. 이 글에서는 MySQL 기준으로 N:M 관계를 왜 연결 테이블로 풀어야 하는지, 그리고 PRIMARY KEY, FOREIGN KEY, AUTO_INCREMENT가 왜 같이 등장하는지를 학생-동아리 예제로 정리한다.

N:M 관계가 왜 문제일까

예를 들어 학생과 동아리를 생각해보자.

  • 학생 1명은 여러 동아리에 가입할 수 있다.
  • 동아리 1개에는 여러 학생이 가입할 수 있다.

이걸 단순히 두 테이블만으로 표현하려고 하면 금방 문제가 생긴다. 예를 들어 학생 테이블에 동아리명을 넣으면 한 학생이 동아리 2개 이상 가입했을 때 컬럼을 여러 개 만들어야 하거나, 같은 학생 정보를 여러 줄로 반복 저장하게 된다. 반대로 동아리 테이블에 학생 이름을 넣어도 똑같은 문제가 생긴다.

즉 N:M 관계는 한쪽 테이블에 값을 우겨 넣는 방식으로 처리하면 안 되고, 관계 자체를 따로 저장하는 테이블이 필요하다.

연결 테이블이 필요한 이유

N:M 관계를 풀 때는 보통 이렇게 3개 테이블로 나눈다.

테이블 역할

stdtbl 학생 기본 정보 저장
clubtbl 동아리 기본 정보 저장
stdclubtbl 어떤 학생이 어떤 동아리에 가입했는지 저장

핵심은 stdclubtbl이다.

이 테이블은 “학생”도 아니고 “동아리”도 아니라, 학생과 동아리의 연결 정보를 담당한다.

즉 N:M 관계를 이렇게 바꾸는 거다.

  • 학생 ↔ 동아리 (직접 연결) → 학생 ↔ 연결 테이블 ↔ 동아리

이렇게 구조를 바꾸면 학생 정보와 동아리 정보는 중복 없이 관리할 수 있고, 가입 정보만 별도로 늘어나게 된다.

테이블 구조 예제

아래는 학생, 동아리, 연결 테이블을 만드는 MySQL 예제다.

CREATE TABLE stdtbl
(
    stdName VARCHAR(10) NOT NULL PRIMARY KEY,
    addr    CHAR(4) NOT NULL
);

CREATE TABLE clubtbl
(
    clubName VARCHAR(10) NOT NULL PRIMARY KEY,
    roomNo   CHAR(4) NOT NULL
);

CREATE TABLE stdclubtbl
(
    num      INT AUTO_INCREMENT NOT NULL PRIMARY KEY,
    stdName  VARCHAR(10) NOT NULL,
    clubName VARCHAR(10) NOT NULL,
    FOREIGN KEY(stdName) REFERENCES stdtbl(stdName),
    FOREIGN KEY(clubName) REFERENCES clubtbl(clubName)
);

이 구조를 보면 역할이 분명하다.

  • stdtbl은 학생 이름과 주소를 저장한다.
  • clubtbl은 동아리 이름과 동아리방 번호를 저장한다.
  • stdclubtbl은 학생 이름과 동아리 이름을 연결해서 가입 정보를 저장한다.

즉, 학생 정보와 동아리 정보는 각각 한 번만 저장하고, 실제 가입 관계만 stdclubtbl에 쌓는 방식이다.

PRIMARY KEY는 왜 필요한가

PRIMARY KEY는 각 행을 유일하게 식별하기 위한 기본 키다.

예제에서 보면

  • stdtbl.stdName
  • clubtbl.clubName
  • stdclubtbl.num

이렇게 각 테이블마다 기본 키가 있다.

학생 이름과 동아리 이름은 예제상 유일하다고 가정해서 기본 키로 쓴 것이고, 연결 테이블에서는 num이라는 숫자 키를 따로 만들었다. 연결 테이블은 같은 학생이 여러 동아리에 가입할 수도 있고, 같은 동아리에 여러 학생이 들어갈 수도 있기 때문에, 단순히 이름 하나로는 행을 고유하게 구분하기 어렵다. 그래서 별도의 식별자 컬럼이 필요하다.

FOREIGN KEY는 왜 필요한가

FOREIGN KEY는 다른 테이블의 데이터를 참조하는 키다.

쉽게 말하면 “이 값은 저 테이블에 실제로 존재해야 한다”는 제약이다.

FOREIGN KEY(stdName) REFERENCES stdtbl(stdName),
FOREIGN KEY(clubName) REFERENCES clubtbl(clubName)

이 두 줄 때문에 stdclubtbl에는

  • 학생 테이블에 없는 학생 이름
  • 동아리 테이블에 없는 동아리 이름

을 마음대로 넣을 수 없다.

이게 중요한 이유는 데이터 무결성 때문이다. 연결 테이블에 존재하지 않는 학생이나 동아리를 넣어버리면, JOIN 결과가 깨지고 실제 관계도 신뢰할 수 없게 된다. FOREIGN KEY는 이런 잘못된 입력을 막아준다.

AUTO_INCREMENT는 왜 쓰는가

연결 테이블의 num 컬럼에는 AUTO_INCREMENT가 들어가 있다.

num INT AUTO_INCREMENT NOT NULL PRIMARY KEY

이건 새로운 가입 정보가 들어올 때마다 번호를 자동으로 1씩 증가시키는 옵션이다.

예를 들어 첫 번째 가입 정보는 1, 두 번째는 2, 세 번째는 3처럼 자동으로 값이 들어간다. 연결 테이블은 보통 관계가 계속 누적되는 구조라서, 사람이 직접 번호를 넣기보다 자동 증가 키를 쓰는 편이 훨씬 편하고 안전하다.

즉 AUTO_INCREMENT는 “행 식별용 번호를 자동으로 관리하는 방식”이라고 보면 된다.

데이터 입력 예제

이제 실제 데이터를 넣어보자.

INSERT INTO stdtbl VALUES
('김범수','경남'),
('성시경','서울'),
('조용필','경기'),
('은지원','경북'),
('바비킴','서울');

INSERT INTO clubtbl VALUES
('수영','101호'),
('바둑','102호'),
('축구','103호'),
('봉사','104호');

INSERT INTO stdclubtbl VALUES
(NULL, '김범수','바둑'),
(NULL, '김범수','축구'),
(NULL, '조용필','축구'),
(NULL, '은지원','축구'),
(NULL, '은지원','봉사'),
(NULL, '바비킴','봉사');

여기서 stdclubtbl을 보면 학생 한 명이 여러 동아리에 들어가는 구조가 자연스럽게 표현된다.

  • 김범수 → 바둑, 축구
  • 은지원 → 축구, 봉사

그리고 동아리 입장에서도 여러 학생을 받을 수 있다.

  • 축구 → 김범수, 조용필, 은지원
  • 봉사 → 은지원, 바비킴

이게 바로 연결 테이블이 필요한 이유다. 학생 테이블이나 동아리 테이블 하나만으로는 이런 구조를 깔끔하게 담기 어렵다.

JOIN은 이렇게 연결된다

N:M 관계를 실제로 조회할 때는 결국 JOIN을 사용하게 된다.

예를 들어 “학생 이름, 주소, 가입한 동아리, 동아리방 번호”를 한 번에 보고 싶다면 이렇게 쓸 수 있다.

SELECT
    S.stdName,
    S.addr,
    SC.clubName,
    C.roomNo
FROM stdtbl S
INNER JOIN stdclubtbl SC
    ON S.stdName = SC.stdName
INNER JOIN clubtbl C
    ON SC.clubName = C.clubName;

이 쿼리의 흐름은 간단하다.

  1. 학생 테이블과 연결 테이블을 학생 이름으로 연결
  2. 연결 테이블과 동아리 테이블을 동아리 이름으로 연결

즉 N:M 관계에서는 JOIN도 직접 두 테이블만 붙이지 않고, 연결 테이블을 통해 단계적으로 연결하는 구조가 된다.

왜 연결 테이블 방식이 실전에서 중요한가

N:M 관계를 연결 테이블로 풀어두면 장점이 많다.

첫째, 중복 저장이 줄어든다.

학생 정보나 동아리 정보를 여러 번 반복해서 저장하지 않아도 된다.

둘째, 관계를 유연하게 관리할 수 있다.

학생이 동아리에 추가 가입하거나 탈퇴해도 연결 테이블만 수정하면 된다.

셋째, 조회가 명확해진다.

어떤 학생이 어떤 동아리에 가입했는지, 어떤 동아리에 몇 명이 속해 있는지 같은 질문을 JOIN으로 자연스럽게 풀 수 있다.

즉 연결 테이블은 단순한 보조 테이블이 아니라, N:M 관계를 관계형 데이터베이스 방식으로 표현하는 핵심 구조다.

실전에서 자주 하는 실수

N:M 관계를 처음 설계할 때 자주 나오는 실수도 같이 정리해두면 좋다.

1) 한쪽 테이블에 여러 값을 억지로 넣는 경우

예를 들어 학생 테이블에 club1, club2, club3 같은 컬럼을 만드는 방식은 확장성이 떨어진다. 나중에 동아리가 더 늘어나면 구조부터 바꿔야 한다.

2) 학생 정보 자체를 중복 저장하는 경우

학생이 동아리 2개에 가입했다고 해서 학생 행을 두 번 넣으면 안 된다. 이러면 주소 같은 기본 정보가 반복 저장되고, 수정 시 불일치가 생긴다.

3) FOREIGN KEY 없이 연결 테이블을 만드는 경우

이렇게 하면 존재하지 않는 학생 이름이나 동아리 이름이 연결 테이블에 들어갈 수 있다. JOIN은 되더라도 데이터 신뢰도가 무너진다.

정리

N:M 관계는 두 테이블만으로 표현하려고 하면 구조가 금방 꼬인다. 학생과 동아리처럼 서로 여러 개씩 연결될 수 있는 관계는 반드시 연결 테이블을 두고 풀어야 한다. 이때 PRIMARY KEY는 각 행을 고유하게 식별하고, FOREIGN KEY는 잘못된 연결을 막고, AUTO_INCREMENT는 연결 테이블의 식별자를 자동으로 관리하는 역할을 한다.

N:M 관계 설계에서 가장 중요한 건 데이터 중복을 줄이고, 관계를 별도 테이블로 분리하는 것이다. JOIN이 자꾸 헷갈린다면 문법보다 먼저 관계 구조가 어떻게 설계되어 있는지부터 보는 게 훨씬 도움이 된다.


N:M 관계는 학생과 동아리를 직접 연결하는 것이 아니라, 연결 테이블을 통해 관계 자체를 저장해야 깔끔하게 설계할 수 있다.