콘텐츠로 이동

DB 정규화와 연결 표

11/9(월)

한 줄 요약: 같은 정보는 한 곳에만 두고, 다른 곳에서는 ID로 가리키게 표를 나누는 원칙이다. 여러 개끼리 이어지는 관계(N:M)는 연결 표로 담는다.

📍 만난 상황

  • 2026-11-09 — 2번 물음 → 그날 일지
  • 15:41 처음 말한 메타정보가 ERD의 표 6개에 나뉘어 들어 있다는 설명을 들었다. ERD를 보다가 물었다. "dataset_tag에 tag의 kind·name을 다 써도 될 것 같은데, 왜 두 개로 나눴나?"
  • 15:42 태그 표를 나눈 이유(DB 정규화)를 듣고, 이어서 표 13개를 나눈 기준 전체를 설명해 달라고 했다.
  • 같은 날 오전에 "H3 정규화"라는 말을 계속 썼다. 이름은 같지만 전혀 다른 뜻이라는 점도 함께 짚었다.

🧩 쉽게 말하면

학교 반 명단을 떠올리면 된다. 학생 카드마다 담임 이름과 전화번호를 적어 두면, 담임 번호가 바뀔 때 카드 30장을 다 고쳐야 한다. 한 장이라도 빠뜨리면 번호가 두 개가 된다.

그래서 담임 정보는 교사 명단 한 장에만 쓰고, 학생 카드에는 "담임: 교사 3번"이라고만 적는다. 이것이 정규화다.

📖 제대로 알기

정의

DB 정규화(normalization)는 중복을 없애도록 표를 나누는 설계 원칙이다. 같은 정보는 한 곳에만 두고, 다른 표는 FK(외래 키: 다른 표의 행을 가리키는 열)로 가리킨다.

일부러 중복을 허용하는 설계는 비정규화(denormalization)라고 한다. 틀린 게 아니라 선택이다.

중복이 부르는 세 가지 문제(이상 현상)

이름 무슨 일이 생기나
갱신 이상 이름 하나를 바꾸려면 여러 줄을 고쳐야 하고, 빠뜨리면 값이 갈린다
삽입 이상 아직 연결이 없는 값(쓰이지 않은 태그)은 넣을 곳이 없다
삭제 이상 마지막 연결 줄을 지우면 그 값 자체가 사라진다

정규형(참고)

교과서에서는 단계로 부른다. - 1정규형: 칸 하나에 값 하나. "즐겨찾기1, 즐겨찾기2" 같은 반복 열을 두지 않는다. - 2·3정규형: 키가 아닌 열이 키에만 의존하게 한다. 태그 이름은 dataset_tag의 키가 아니라 태그에 딸린 정보이므로 tag 표로 뺀다.

관계별로 담는 법

관계 처리
1:N N쪽 표에 FK 열 하나 (inquiry.user_id)
N:M 연결 표를 새로 만듦 (favorite, dataset_tag)
1:1 보통 합침. 나눌 이유가 있을 때만 나눔

연결 표(junction table)는 두 표의 ID 쌍만 담는 표다. 줄 하나가 "이것과 저것이 이어져 있다"는 사실 하나다.

⚙️ 어떻게 동작하나

그날 들은 표 나누는 기준 중 핵심 네 가지다.

  1. 기능 문장에서 명사를 찾는다. "사람이 데이터셋을 즐겨찾기한다" → 사람(app_user), 데이터셋(dataset). 동사(즐겨찾기한다)는 보통 표가 아니라 관계가 된다.
  2. 둘 사이의 개수를 센다. 사람 한 명은 여러 데이터셋을, 데이터셋 하나는 여러 사람이 즐겨찾기한다 → N:M → 연결 표 favorite.
  3. 반복되는 값은 목록 표로 뺀다. "침수"가 여러 데이터셋에 반복된다 → tag 표. "강남구 = 11680"도 같은 이유로 region_code.
  4. 쓰는 주체가 다르면 나눈다. dataset은 동기화 프로그램이, dataset_meta는 사람이 쓴다. 1:1이지만 나눠서, 동기화가 사람이 쓴 설명을 덮어쓰는 일을 구조적으로 막는다.
erDiagram
  DATASET ||--o{ DATASET_TAG : "하나에 태그 여러 개"
  TAG ||--o{ DATASET_TAG : "하나가 여러 데이터셋에"
  APP_USER ||--o{ FAVORITE : "한 사람이 여러 개"
  DATASET ||--o{ FAVORITE : "여러 사람이 찜"
  DATASET ||--|| DATASET_META : "설명은 하나"

가운데의 DATASET_TAG, FAVORITE가 연결 표다. 양쪽 표와 1:N으로 이어지고, 그 둘을 합치면 N:M이 된다.

🧪 예시

  • 예시 1 · 일상에 빗대어: 도서관에서 책 3권에 "재난" 분류 스티커를 손글씨로 붙였다. 분류 이름을 "재해·재난"으로 바꾸려면 3권을 다 찾아야 한다. 대신 스티커에 "분류 7번"만 쓰고 분류표 한 장에 "7번 = 재난"이라 적으면, 분류표 한 줄만 고치면 된다.

  • 예시 2 · 실제로는: 두 방식을 같은 데이터로 비교한다.

[방식 A] dataset_tag에 이름을 직접
dataset_id | kind     | name
A(침수흔적) | disaster | 침수
B(강우)     | disaster | 침수
C(지반함몰) | disaster | 지반함몰

[방식 B] tag 표를 따로 (지금 ERD)
tag:         id | kind     | name
             1  | disaster | 침수
             2  | disaster | 지반함몰
             3  | disaster | 교량붕괴
dataset_tag: (A,1) (B,1) (C,2)
① 이름 바꾸기: A는 "침수" 줄 2개를 고친다. B는 tag 1번 한 줄만 고친다. ② 오타: A는 "칩수"가 들어와도 못 막는다. B는 tag_id가 FK라 없는 번호를 넣을 수 없다. ③ 빈 태그: "교량붕괴" 데이터가 아직 없어도 B는 선택지(3번)에 미리 있다. A에는 어디에도 없다. ④ 확장: 태그에 색·표시 순서를 붙이려면 B는 tag에 열 하나를 더하면 된다.

필터 선택지는 SELECT name FROM app.tag로 바로 나온다.

⚖️ 비교 · 흔한 오해

이름 하는 일
H3 정규화 제각각인 위치·시간 형식을 같은 기준(H3 칸, 시각)으로 맞추는 일
DB 정규화 중복을 없애도록 표를 나누는 일
  • 헷갈렸던 점: "한 표에 다 쓰면 더 간단하다" — 표가 적을수록 쉬워 보이고, 지금 규모(데이터셋 수십 개, 태그 10개 정도)에서는 실제로 동작한다. 문제는 이름을 바꾸거나 오타가 들어올 때 드러난다. 그래서 처음부터 나눠 둔다.
  • 오해: "정규화는 많이 할수록 좋다" — 표가 너무 잘게 나뉘면 조회마다 JOIN(표 이어 붙이기)이 늘어난다. column_meta는 자동 값과 사람 값이 한 표에 섞여 있지만, 더 나누면 지나치다고 보고 두었다.
  • 비정규화를 일부러 쓰는 예: 정규화 파이프라인이 데이터 줄에 sgg_code = 11680과 함께 sgg_name = 강남구를 미리 적어 두는 안이 있었다. 플랫폼 SQL 창구는 JOIN을 못 하니, 화면에 이름을 바로 보여 주려고 중복을 허용하는 것이다.

🛠️ 실무에서는

  • 정규화가 맞는 곳: 사용자 데이터·설정처럼 자주 쓰고 고치는 서비스 DB(PostgreSQL 같은 OLTP).
  • 비정규화가 흔한 곳: 분석용 DB(ClickHouse 같은 OLAP). 한 번 쓰고 많이 읽으며, JOIN이 비싸다.
  • 체크리스트
  • 같은 글자가 여러 줄에 반복되고, 나중에 바뀔 수 있나 → 목록 표로 뺀다.
  • 연결 표의 PK는 두 FK의 조합인가(PRIMARY KEY (user_id, dataset_id)) → 같은 연결이 두 번 생기지 않는다.
  • 비정규화한 값은 원본이 바뀔 때 누가 같이 고치나 정해 두었나.
  • 문제가 생기면 먼저 볼 것: 같은 대상이 두 이름으로 갈려 있으면(“침수”, “침수 ”) 중복 저장이 원인이다. SELECT DISTINCT로 값 목록을 뽑아 본다.

❓ 자주 묻는 질문

  • Q. 연결 표에 다른 열을 넣어도 되나? A. 된다. 그 연결 자체의 정보라면 넣는다. 예: favorite.created_at(언제 찜했나)은 사람도 데이터셋도 아닌 "찜"의 정보다.
  • Q. 태그 표에 ID 대신 이름을 PK로 쓰면 안 되나? A. 가능하지만, 이름을 바꿀 때 연결 표의 값도 다 바뀌어야 한다. 바뀔 수 있는 값은 PK로 쓰지 않는 편이 안전하다.
  • Q. 1:1인데 왜 나누나? A. 쓰는 주체나 바뀌는 때가 다를 때다. dataset(프로그램)과 dataset_meta(사람)처럼 덮어쓰기를 막으려고 나눈다.

🔭 더 깊이 보기

  • JOIN은 어떻게 동작하나? 나눈 표를 다시 합쳐 읽는 법이다. 정규화의 비용을 이해하는 열쇠다.
  • 인덱스는 연결 표에 왜 필요한가? favorite에서 "이 데이터셋을 찜한 사람"을 빨리 찾으려면 반대 방향 인덱스가 필요하다.
  • OLTP와 OLAP의 차이는? PostgreSQL과 ClickHouse가 왜 다르게 설계되는지 알 수 있다.
  • 관련 주제: OIDC 로그인 흐름 — app_user와 favorite가 생기는 과정

📚 참고 자료

  • 데이터베이스 정규화 기본 사항 설명 (Microsoft Learn 한국어) — 1·2·3정규형을 예시 표로 설명 · 링크
  • 관계형 데이터 모델링 (생활코딩, opentutorials.org) — 개념·논리 모델링과 정규화 입문 강의. 사이트에서 제목으로 검색
  • PostgreSQL Documentation: Constraints (영문) — PK·FK·UNIQUE 제약 문법 · 링크

🗓️ 이력

  • 2026-11-09: 태그를 연결 표에 직접 쓰지 않고 나눈 이유와 표를 나누는 기준을 물음 → 일지

🕸️ 연결된 주제