흩어진 데이터를 하나로 모으기
관계형 데이터베이스의 꽃인 JOIN을 활용하여 여러 테이블에 흩어진 유저, 게시글, 댓글 데이터를 하나로 연결하는 방법을 배웁니다.
관계형 데이터베이스(RDBMS)의 진정한 힘은 데이터를 중복 없이 여러 테이블에 쪼개어 담아두었다가, 필요할 때 순식간에 연결하여 하나의 거대한 표로 만들어내는 데 있습니다. 커뮤니티 게시판을 생각해 봅시다. 화면에는 '작성자 이름', '게시글 제목', '댓글 내용'이 한 번에 보이지만, 실제 데이터베이스 안에는 이 정보들이 '유저 테이블', '게시글 테이블', '댓글 테이블' 세 곳에 뿔뿔이 흩어져 있습니다. 이 조각난 퍼즐들을 정확한 규칙에 따라 하나로 맞춰주는 가장 중요한 마법의 주문, `JOIN`에 대해 알아봅시다.
퍼즐 조각의 이음새: `id` (식별자)
흩어진 데이터를 연결하려면 반드시 두 테이블을 이어주는 공통의 '연결 고리'가 있어야 합니다. 예를 들어 `users` (유저) 테이블에는 각 유저의 고유 번호인 `id`가 있습니다. 홍길동 유저의 `id`가 1번이라고 해봅시다. 홍길동이 게시글을 작성하면, `posts` (게시글) 테이블에 글의 제목과 내용을 저장할 때 "이 글은 1번 유저가 썼다"라는 증거( `user_id = 1` )를 함께 남겨둡니다.
나중에 게시글을 화면에 뿌려줄 때 데이터베이스에게 이렇게 명령하는 것입니다. "게시글 테이블의 `user_id` 숫자와, 유저 테이블의 `id` 숫자가 똑같이 일치하는 애들끼리 가로로 착착 이어 붙여서 가져와 줘!" 이 과정을 수행하는 명령어가 바로 `JOIN`입니다.
`JOIN`의 기본 문법
가장 많이 사용되는 기본형(내부 조인, INNER JOIN)의 문법은 다음과 같습니다.
- `FROM` 뒤에 기준이 되는 첫 번째 테이블을 적습니다.
- `JOIN` 뒤에 붙여다 끌어올 두 번째 테이블을 적습니다.
- `ON` 뒤에 두 테이블을 연결할 정확한 수학적 조건을 적습니다. (퍼즐의 이음새)
유저와 게시글 테이블 합치기
SELECT posts.title, users.name
FROM posts
JOIN users
ON posts.user_id = users.id;위 코드를 사람의 언어로 번역하면: "게시글(`posts`) 테이블을 기준으로 유저(`users`) 테이블을 가져와서 합쳐라(`JOIN`). 단, 게시글에 적힌 작성자 번호(`user_id`)와 유저의 고유 번호(`id`)가 정확히 일치하는(`ON`) 데이터끼리만 짝을 지어라. 짝이 지어지면, 게시글의 '제목'과 유저의 '이름'만 화면에 보여줘(`SELECT`)."
교집합과 부분 집합: `INNER JOIN` vs `LEFT JOIN`
바이브코딩으로 서비스를 만들 때 AI가 가장 헷갈려 하고 에러를 많이 내는 부분이 바로 `JOIN`의 종류를 잘못 선택하는 것입니다. 데이터를 합치는 방식에는 크게 두 가지가 있습니다.
1. `INNER JOIN` (교집합, 기본 `JOIN`)
위에서 배운 기본 형태입니다. 양쪽 테이블에 모두 조건이 일치하는 짝이 있을 때만 결과물을 보여줍니다. 만약 탈퇴한 유저가 쓴 게시글이 남아있다고 칩시다. 게시글 테이블에는 있지만, 유저 테이블에는 해당 번호가 지워지고 없습니다. 이때 `INNER JOIN`을 걸면 짝을 찾지 못한 그 게시글은 결과 화면에서 아예 사라져 버립니다(누락).
2. `LEFT JOIN` (기준 테이블은 무조건 다 살리기)
이런 누락을 막기 위해 실무에서 더 안전하게 자주 쓰이는 방식입니다. 왼쪽(`FROM` 뒤에 쓴 기준 테이블)에 있는 데이터는 짝이 맞든 안 맞든 무조건 100% 다 결과에 보여주고, 오른쪽(`JOIN` 테이블) 데이터는 짝이 맞으면 붙여주고 안 맞으면 '없음(NULL)'으로 표시합니다.
INNER JOIN과 LEFT JOIN의 실전 차이
| 상황 | INNER JOIN 사용 시 | LEFT JOIN 사용 시 |
|---|---|---|
| 게시글과 유저 정보를 합쳐서 게시판 목록 띄우기 (탈퇴한 유저의 글이 존재함) | 탈퇴한 유저가 쓴 글은 짝이 없으므로 목록에서 아예 증발해 버립니다. (게시글 수 줄어듦) | 게시글은 모두 정상적으로 출력되며, 탈퇴한 유저 이름 자리에만 'NULL'이 뜹니다. 앱 코드에서 이를 '알 수 없음'으로 처리하면 됩니다. |
| 모든 유저 목록을 뽑으면서, 그 사람이 쓴 최신 글 제목 하나를 옆에 붙여서 보기 (글을 한 번도 안 쓴 신규 가입자 존재함) | 글을 한 번도 안 쓴 유저들은 짝이 없으므로 유저 목록에서 사라집니다. | 모든 유저가 정상 출력되고, 신규 가입자의 글 제목 칸만 비워져 나옵니다. |
3단 콤보 엮기: 유저 + 게시글 + 댓글
`JOIN`의 진정한 무서움은 두 개뿐만 아니라 세 개, 네 개의 테이블도 기차를 연결하듯 계속해서 이어 붙일 수 있다는 점입니다. 우리가 커뮤니티 게시글 하나를 클릭해서 들어갔을 때(상세 페이지) 일어나는 일이 바로 이 3단 콤보입니다.
SELECT posts.title, users.name, comments.content
FROM posts
LEFT JOIN users
ON posts.user_id = users.id
LEFT JOIN comments
ON comments.post_id = posts.id
WHERE posts.id = 15;"15번 게시글을 가져오고, 거기에 쓴 사람(유저) 정보를 붙인 다음, 다시 그 게시글에 달린 댓글들까지 몽땅 다 붙여서 한 판에 보여줘!" 라는 명령입니다. 데이터베이스는 이 복잡한 명령을 단 0.01초 만에 수행하여 프론트엔드로 쏘아 보내줍니다.
개념 퀴즈
퀴즈 답을 맞춰야 학습 완료가 됩니다.우리 서비스의 가입자 1만 명 전체 목록을 관리자 페이지에 띄우려고 합니다. 이때 각 유저 옆에 '작성한 프로필 소개글(profile_text)'을 함께 보여주려 하는데, 소개글을 아예 안 쓴 유저들도 명단에서 누락되지 않고 1만 명이 전부 표시되게 하려면 어떤 JOIN을 써야 할까요?
3줄 요약
- 1JOIN은 여러 테이블에 쪼개져 있는 관계형 데이터를 '공통된 식별자(id)'를 기준으로 한데 묶어 하나의 거대한 표로 만들어주는 문법입니다.
- 2INNER JOIN은 양쪽에 모두 짝이 존재하는 교집합 데이터만 가져오며, 짝이 없는 데이터는 결과에서 누락시킵니다.
- 3LEFT JOIN은 기준이 되는 첫 번째 테이블의 데이터를 100% 보존하므로, 실무에서 정보 누락(탈퇴 회원 글 사라짐 등)을 막기 위해 가장 많이 쓰입니다.