문서 읽는 데 50분 · B4

B-4: 정규화와 반정규화 — 중복을 덜어내는 법과, 일부러 다시 더하는 법

목차 29
전체 24강 중 6강 · 데이터베이스
난이도 · 입문

ℹ️Oracle SQL로 SQLD 자격증을 대비하며 실전 SQL을 쌓는 트랙이에요. 프로그래밍 경험이 없어도 시작할 수 있고, 부트캠프에서는 자바 기초 다음·스프링 부트 직전에 배치하길 권해요.

안녕하세요, 데이터베이스의 세계를 함께 걷는 홍순구 튜터입니다. 다시 만나서 반가워요.

지난 시간엔 다섯 박스를 잇는 '선'과, 각 박스에서 한 행을 가리키는 '한 줄'을 확정했죠. 관계는 차수(1:1·1:N·M:N)와 선택성으로 읽고, 주식별자는 유일·최소·불변·존재 네 조건을 갖춰야 하며, 부모 키를 주식별자 안으로 끌어들이면 식별 관계라는 데까지 왔어요. 종이 위 인스타그램 설계도가 제법 단단해졌습니다.

그런데 그 끝에서 제가 한 가지 함정을 예고했어요. 욕심을 부려서 회원·게시물·좋아요를 한 표에 몽땅 욱여넣으면, 같은 회원 정보가 줄줄이 중복되고, 회원이 자기소개를 한 번 바꾸면 중복된 칸을 전부 고쳐야 한다고요. 한 군데라도 놓치면 같은 회원이 두 자기소개를 갖는 모순이 생긴다고도 했죠. 오늘은 그 중복이 부르는 골칫거리를 정면으로 마주하고, 그것을 단계별로 풀어내는 정규화를 배웁니다.

미리 귀띔하면, 오늘은 SQLD 1과목에서 매 회차 거의 빠지지 않는 ★빈출 중의 ★빈출이에요. 특히 1NF·2NF·3NF 단계 분해와 반정규화 트레이드오프는 단골 출제 포인트라, 오늘은 시험을 단단히 염두에 두고 갈게요. 난이도도 이번 카테고리에서 가장 높은 편이니, 한 단계씩 천천히 따라오시면 됩니다.

텍스트
 오늘의 여정 — 중복을 덜어내고, 필요하면 다시 더하기

  ① 한 표에 몽땅       — 중복이 부르는 이상현상(삽입·갱신·삭제) ★빈출
  ② 무엇이 무엇을 정하나 — 함수 종속(완전 · 부분)
  ③ 거쳐서 정해지면     — 함수 종속(이행)
  ④ 한 칸에 하나씩      — 제1정규형(원자값으로)
  ⑤ 일부에만 매달리면   — 제2정규형(부분 종속 떼기) ★빈출
  ⑥ 거쳐가는 건 분리    — 제3정규형(이행 종속 떼기) ★빈출
  ⑦ 결정자는 후보키     — BCNF(드문 마지막 손질)
  ⑧ 일부러 다시 더하기  — 반정규화의 트레이드오프 ★빈출
        │
        
   비정규 한 표  1NF  2NF  3NF 로 쪼개고, 필요하면 반정규화로 되돌린다

종이와 펜은 지난 시간 그대로 들고 오시면 됩니다. 오늘도 SQL을 새로 짜는 시간이 아니라, 표가 어떤 모양이어야 하는지를 다듬는 마지막 설계 시간이에요.

💡 오늘 수업의 핵심 — "중복이 부르는 이상현상을 함수 종속을 따라 1NF·2NF·3NF로 단계별로 풀어내고, 반대로 성능을 위해 일부러 중복을 더하는 반정규화의 트레이드오프까지 익힌다"

🎯 학습 목표

  • 한 표에 몰아넣었을 때 생기는 이상현상(삽입·갱신·삭제)을 설명하고, 그 원인이 중복임을 안다.
  • 함수 종속(완전·부분·이행)을 읽고, 그에 따라 비정규 표를 1NF·2NF·3NF로 단계별로 분해할 수 있다.
  • 반정규화가 무엇을 얻고 무엇을 잃는지(읽기 속도 ↔ 정합성) 트레이드오프를 설명하고, SQLD 1과목 정규화 출제 포인트를 정리할 수 있다.

Step 1: "한 표에 몽땅 — 중복이 부르는 이상현상" ★빈출

지난 시간 끝에 던진 질문으로 시작할게요. 회원·게시물·좋아요를 따로따로 나누지 않고 한 표에 몽땅 적으면 어떻게 될까요? 말로만 듣지 말고 직접 보는 게 빨라요. 게시물 하나를 한 행으로 잡고, 그 게시물의 작성자 정보와 좋아요 누른 사람까지 한 줄에 다 적어본 표예요.

post_id caption 작성자 작성자 자기소개 좋아요 누른 사람
1 첫 게시물입니다 김재훈 백엔드 공부 중 이민지, 박승우
2 오늘도 코딩 김재훈 백엔드 공부 중 정하루
3 주말 산책 김재훈 백엔드 공부 중 (없음)
4 아침 커피 한 잔 이민지 사진과 커피 김재훈, 최도연
11 운동 끝 최도연 (NULL) 강지수
12 제주도 여행 강지수 여행 좋아해요 (없음)

얼핏 편해 보여요. 게시물 한 줄만 보면 작성자도, 좋아요 누른 사람도 한눈에 보이니까요. 그런데 자세히 보면 김재훈이 게시물을 셋(1·2·3) 올렸다고 '김재훈'과 '백엔드 공부 중'이 세 줄에 똑같이 중복됐죠? 이 중복 하나 때문에 세 가지 골칫거리가 생겨요. 이걸 이상현상(anomaly)이라고 불러요.

텍스트
 한 표에 몰아넣으면 생기는 이상현상 세 가지

  삽입 이상   넣고 싶어도 못 넣는다
             윤태양·한나래는 게시물이 0개라 이 표에 등장조차 못 한다
             (post_id 가 있어야 한 행이 생기니까)

  갱신 이상   하나 바꾸려다 여러 줄을 다 고쳐야 한다
             김재훈이 자기소개를 바꾸면 1·2·3 세 줄을 전부 수정
             한 줄이라도 놓치면 같은 회원이 두 자기소개를 갖는 모순

  삭제 이상   지우려다 멀쩡한 정보까지 사라진다
             최도연의 게시물은 11번 하나뿐  그 글을 지우면
             '최도연'이라는 회원 정보까지 표에서 통째로 증발

세 글자로 삽입·갱신·삭제 이상, 입에 붙여두세요. 셋 다 뿌리는 하나예요. 한 표 안에 서로 다른 주제(회원·게시물·좋아요)가 섞여 중복이 생겼다는 것. 회원 정보가 게시물 줄마다 따라다니니, 회원만 따로 넣지도 못하고(삽입), 회원만 따로 고치지도 못하고(갱신), 회원만 따로 남기지도 못해요(삭제).

그럼 답은 정해졌죠. 섞인 주제를 잘 나눠 담으면 됩니다. 이렇게 중복을 줄이도록 표를 나누는 작업이 바로 오늘의 주인공 정규화(normalization)예요. 다만 무작정 나누는 게 아니라, 속성들이 서로 어떻게 의존하는지를 보고 단계적으로 나눠요. 그 '의존'을 읽는 법부터 Step 2에서 잡고 갑니다.

⚠️ 함정 — 이상현상의 원인을 "데이터가 많아서"로 착각하기 쉬워요. 아니에요. 원인은 중복이에요. 한 표에 한 주제만 담겨 중복이 없으면, 데이터가 아무리 많아도 이상현상은 안 생깁니다. "많아서"가 아니라 "섞여서"가 핵심이에요.

💡 한 줄 정리

여러 주제를 한 표에 몰아넣으면 중복이 생기고, 그 중복이 삽입·갱신·삭제 이상을 부른다. 원인은 데이터의 양이 아니라 한 표에 주제가 섞인 것이며, 해법은 표를 잘 나누는 정규화다.

🙋 학생 질문 — "튜터님, 이상현상이 그렇게 위험하면 왜 한 표로 쓰는 사람이 있나요?"

처음엔 한 표가 더 쉬워 보이거든요. 표 하나만 보면 모든 정보가 다 있으니, 만들 때나 처음 읽을 때는 편해요. 문제는 데이터가 쌓이고 바뀌기 시작할 때 드러나요.

회원 한 명이 게시물을 100개 올렸다고 해봐요. 그 회원이 자기소개를 바꾸는 순간 100줄을 다 고쳐야 하고, 한 줄이라도 놓치면 데이터가 어긋나죠. 처음의 작은 편함이 나중에 큰 골칫거리로 돌아오는 거예요. 그래서 설계 단계에서 미리 주제별로 나눠두는 거고, 그게 정규화입니다.


Step 2: "무엇이 무엇을 정하나 — 함수 종속(완전·부분)"

정규화는 표를 나누는 작업인데, 아무 데서나 자르면 안 돼요. "이 속성은 저 속성을 따라간다"는 의존 관계를 보고, 그 의존을 따라 잘라야 하죠. 이 의존 관계를 함수 종속(functional dependency)이라고 불러요. 이름이 어렵지, 뜻은 단순해요.

X를 알면 Y가 하나로 정해진다. 이때 "Y는 X에 함수적으로 종속된다"고 하고, X → Y로 적어요.

예를 들어 member_id를 알면 그 회원의 nickname이 딱 하나로 정해지죠. 그래서 member_id → nickname이에요. 반대로 nickname을 안다고 member_id가 하나로 정해지진 않아요(동명이인이 있을 수 있으니까). 그래서 화살표엔 방향이 있어요. 결정하는 쪽(X)을 결정자라고 불러요.

이제 한 걸음 더. 지난 시간에 좋아요는 (member_id, post_id) 두 속성을 묶은 복합 식별자였죠? 결정자가 이렇게 둘 이상일 때, 종속이 두 갈래로 나뉘어요.

텍스트
 좋아요 한 행을 가리키는 키:  (post_id, member_id)

  완전 함수 종속   키 '전체'가 있어야 정해진다
                  (post_id, member_id)  liked_at
                  "어느 회원이, 어느 게시물에 누른 좋아요인지"
                  둘 다 있어야 '언제 눌렀나'가 정해짐

  부분 함수 종속   키의 '일부'만으로 정해진다
                  post_id  caption
                  게시물 내용은 post_id 만 알면 정해짐 (member_id 불필요)

liked_at(좋아요 누른 시각)은 (post_id, member_id) 둘 다 있어야 정해져요. 어느 좋아요인지 콕 집어야 그 시각이 나오니까요. 이렇게 키 전체에 매달린 걸 완전 함수 종속이라고 해요.

반면 caption(게시물 내용)은 post_id만 알면 정해져요. 누가 좋아요를 눌렀든(member_id) 게시물 내용은 그대로니까요. 이렇게 키의 일부에만 매달린 걸 부분 함수 종속이라고 해요. 기억해 두세요. 이 부분 종속이 곧 Step 5(2NF)에서 도려낼 표적이에요.

비유하면 "몇 학년 몇 반"으로 한 교실이 정해지는 것과 같아요. '교실 담임'은 학년·반 둘 다 있어야 정해지지만(완전), '학년 교장선생님' 같은 건 학년만 알면 정해지죠(부분). 키 전체가 필요한지, 일부면 되는지를 가리는 거예요.

⚠️ 함정 — 부분 함수 종속은 결정자가 복합키(두 개 이상)일 때만 따질 수 있어요. 키가 속성 하나(단일 식별자)면 "일부"라는 게 없으니 부분 종속도 없죠. 그래서 2NF 문제는 거의 항상 복합키 표에서 나와요. 단일키 표를 주고 "2NF 위반을 찾아라"라고 하면, 답은 보통 "이미 2NF다"입니다.

💡 한 줄 정리

함수 종속은 "X를 알면 Y가 하나로 정해진다(X → Y)"는 의존 관계다. 결정자가 복합키일 때, 키 전체가 있어야 정해지면 완전 함수 종속, 키의 일부만으로 정해지면 부분 함수 종속이다. 부분 종속은 2NF에서 떼어낼 표적이다.

🙋 학생 질문 — "튜터님, 함수 종속이라는 말이 너무 수학 같아요. 꼭 이 용어를 알아야 하나요?"

이름은 수학에서 빌려왔지만, 뜻은 우리가 이미 쓰던 거예요. "회원 번호를 알면 그 회원이 누군지 정해진다" — 이걸 멋있게 부른 게 함수 종속이에요. 새로 외울 개념이라기보다, 이미 아는 걸 부르는 이름이라고 보면 돼요.

그래도 이 용어를 짚고 가는 건, 정규화 단계(2NF·3NF)가 전부 "어떤 종속을 떼느냐"로 정의되기 때문이에요. 2NF는 부분 종속을, 3NF는 이행 종속을 떼는 거라, 종속의 종류를 구분할 줄 알면 정규화가 그냥 따라와요. SQLD에서도 "이건 무슨 종속인가?"를 직접 묻습니다.


Step 3: "거쳐서 정해지면 — 함수 종속(이행)"

종속이 한 가지 더 있어요. 결정자가 복합키냐 아니냐와는 다른 축이에요. 바로 거쳐서 정해지는 경우예요.

게시물을 예로 들게요. post_id를 알면 그 게시물의 작성자(writer_id)가 정해지죠. 그리고 작성자(writer_id)를 알면 그 작성자의 닉네임이 정해져요. 그럼 post_id로 닉네임이 정해지긴 하는데, 작성자를 한 번 거쳐서 정해지는 거예요.

텍스트
 직접 정해지나, 거쳐서 정해지나

  직접 종속   post_id  caption
             게시물 번호로 내용이 바로 정해짐

  이행 종속   post_id  writer_id  작성자 닉네임
             게시물 번호로 작성자가 정해지고(AB)
             작성자로 닉네임이 정해진다(BC)
             그래서 post_id  닉네임은 writer_id 를 거쳐 가는 간접 경로

이렇게 A → B이고 B → C라서 A → C가 따라오는 걸 이행 함수 종속(이행 종속)이라고 해요. '이행'은 '거쳐서 넘어간다'는 뜻이에요. 여기서 중요한 건 가운데 B(writer_id)가 키가 아닌 일반 속성이라는 점이에요. 키가 아닌 속성이 또 다른 키 아닌 속성을 정하고 있는 거죠. 이 이행 종속이 Step 6(3NF)에서 도려낼 표적이에요.

비유하면 "우편번호를 알면 동네가 정해지고, 동네를 알면 그 동네 주민센터가 정해진다"와 같아요. 우편번호로 주민센터가 정해지긴 하지만, 동네를 한 번 거쳐서죠. 동네라는 중간 다리가 끼어 있는 거예요.

⚠️ 함정 — 이행 종속은 중간 다리가 키가 아닐 때만 따져요. post_id → writer_id → 닉네임에서 writer_id는 게시물 표의 키가 아니라 일반 속성(작성자 참조)이라 이행이에요. 만약 중간 속성이 그 표의 주식별자였다면 그건 정상이고 이행이 아니에요. "키가 아닌 것을 거쳐 가느냐"가 판정 기준입니다.

💡 한 줄 정리

이행 함수 종속은 A → B, B → C라서 A → C가 따라오는, 중간 속성을 거쳐 가는 간접 종속이다. 가운데 다리(B)가 키가 아닌 일반 속성일 때 성립하며, 3NF에서 떼어낼 표적이다.

🙋 학생 질문 — "튜터님, 부분 종속이랑 이행 종속이 자꾸 헷갈려요. 어떻게 구분하나요?"

보는 곳이 달라요. 부분 종속은 키 쪽을 봐요. "복합키 중 일부만으로 정해지나?" — 그렇다면 부분 종속이에요. 이행 종속은 키가 아닌 속성들 사이를 봐요. "키가 아닌 게 또 다른 키 아닌 걸 정하나?" — 그렇다면 이행 종속이고요.

한 문장으로 정리하면, 부분 종속은 "키의 일부에 매달림"이고 이행 종속은 "키 아닌 것에 거쳐서 매달림"이에요. 그래서 2NF(부분 떼기)를 먼저 하고 3NF(이행 떼기)를 나중에 해요. 키와의 관계부터 정리하고, 그다음 키 아닌 것들 사이를 정리하는 순서죠.


Step 4: "한 칸에 하나씩 — 제1정규형(1NF)"

자, 이제 도구(함수 종속)를 손에 넣었으니 실제로 표를 나눠볼 차례예요. 정규화는 1단계부터 차례로 올라가요. 첫 단계, 제1정규형(1NF)부터요.

1NF의 규칙은 딱 하나예요. 모든 칸에는 값이 하나만 들어간다. 더 쪼갤 수 없는 값 하나, 이걸 원자값(atomic value)이라고 해요. 그런데 Step 1의 표를 다시 보면 규칙을 어긴 칸이 있죠?

post_id caption 작성자 좋아요 누른 사람
1 첫 게시물입니다 김재훈 이민지, 박승우
4 아침 커피 한 잔 이민지 김재훈, 최도연

'좋아요 누른 사람' 칸에 여러 명이 쉼표로 욱여넣어져 있어요. 한 칸에 값 하나라는 규칙 위반이죠. 지난 시간 끝에 "좋아요 누른 회원을 한 칸에 쉼표로 나열하면 안 되나?"라는 질문, 기억하세요? 바로 그게 1NF 위반이에요. 한 칸에 목록이 들어가면 "박승우가 좋아요를 취소"하나만 처리하려 해도 그 문자열을 뜯어고쳐야 하고, "좋아요를 가장 많이 받은 게시물"을 세려면 쉼표를 일일이 세야 해요.

해법은 한 칸 한 값이 되도록 행을 펼치는 거예요. 좋아요 한 건을 한 행으로 풀어요.

텍스트
 1NF — 한 칸의 목록을 여러 행으로 펼친다

  [정규화 전]  post_id=1 | 좋아요 누른 사람 = "이민지, 박승우"    한 칸에 둘

         좋아요 한 건 = 한 행으로 펼친다

  [정규화 후]  (post_id, member_id) 가 키가 된다
               post_id=1, member_id=이민지
               post_id=1, member_id=박승우

펼치고 나면 이런 표가 돼요. 좋아요 한 건이 한 행이 되었죠.

post_id member_id (좋아요 누른 회원) caption 작성자 작성자 자기소개
1 이민지 첫 게시물입니다 김재훈 백엔드 공부 중
1 박승우 첫 게시물입니다 김재훈 백엔드 공부 중
2 정하루 오늘도 코딩 김재훈 백엔드 공부 중
4 김재훈 아침 커피 한 잔 이민지 사진과 커피
4 최도연 아침 커피 한 잔 이민지 사진과 커피

여기서 두 가지를 짚을게요. 첫째, 이제 한 행을 가리키려면 (post_id, member_id) 둘이 필요해요. 어느 게시물에, 어느 회원이 누른 좋아요인지 둘 다 있어야 하니까요. 복합키가 생긴 거예요. Step 2에서 미리 봐둔 그 키죠. 둘째, caption과 작성자 정보가 좋아요 건마다 또 중복되기 시작했어요(post 1이 두 줄이 되며 '첫 게시물'·'김재훈'이 두 번). 1NF는 한 칸의 목록은 풀었지만, 중복은 오히려 늘었어요. 이 새 중복을 다음 단계에서 잡습니다.

⚠️ 함정 — 1NF에서 가장 자주 틀리는 게 "값이 하나처럼 보이는데 사실 여러 의미"인 경우예요. 예를 들어 주소 칸에 "서울시 강남구 역삼동"을 통째로 넣으면, 칸은 하나지만 시·구·동이 섞여 있죠. 나중에 "강남구 회원만 찾기"가 어려워져요. 한 칸엔 더 쪼갤 수 없는 값 하나가 1NF의 정신입니다.

💡 한 줄 정리

제1정규형(1NF)은 모든 칸에 값이 하나만(원자값) 들어가야 한다. 한 칸의 목록(쉼표 나열)을 여러 행으로 펼치면 1NF가 되며, 좋아요 표에서는 (post_id, member_id) 복합키가 생긴다. 단, 펼치는 과정에서 다른 속성의 중복이 늘어난다.

🙋 학생 질문 — "튜터님, 1NF로 펼쳤더니 중복이 더 늘었는데, 이게 정말 나아진 건가요?"

좋은 관찰이에요. 1NF 하나만 보면 오히려 손해처럼 느껴질 수 있어요. 하지만 정규화는 계단이에요. 1NF는 "한 칸에 값 하나"라는 가장 기본 약속을 지키게 만드는 첫 계단일 뿐, 여기서 멈추지 않아요.

핵심은 1NF가 만들어준 복합키 (post_id, member_id) 덕분에 다음 계단(2NF)이 가능해진다는 거예요. 한 칸에 목록이 들어 있을 땐 "이 속성이 키의 어디에 매달렸나"를 따질 수조차 없었죠. 행을 펼쳐 키를 또렷이 만든 다음에야, 부분 종속을 도려내는 2NF로 넘어갈 수 있어요. 늘어난 중복은 바로 다음 Step에서 잡으니 조금만 기다려 주세요.


Step 5: "일부에만 매달리면 — 제2정규형(2NF)" ★빈출

1NF 표를 다시 볼게요. 키는 (post_id, member_id)인데, caption과 작성자 정보가 좋아요 건마다 중복됐죠. 왜 그럴까요? Step 2의 도구를 꺼낼 차례예요. 부분 함수 종속 때문이에요.

captionpost_id만 알면 정해져요. 누가 좋아요를 눌렀든(member_id) 상관없죠. 즉 caption은 키 (post_id, member_id)일부post_id에만 매달려 있어요. 작성자 정보도 마찬가지고요. 이 부분 종속이 중복의 범인이에요. 같은 게시물에 좋아요가 여럿 달리면, post_id에만 매달린 caption이 좋아요 수만큼 따라 적히니까요.

제2정규형(2NF)의 규칙이 바로 이거예요. 키의 일부에만 매달린 속성(부분 종속)을 떼어내, 그 일부를 키로 하는 별도의 표로 옮긴다.

텍스트
 2NF — 부분 종속을 떼어 별도 표로 옮긴다

 [1NF 표]   PK = (post_id, member_id)
   post_id           ┐ 키
   member_id         ┘
   caption            post_id 만으로 정해짐    (부분 종속!)
   작성자             post_id 만으로 정해짐    (부분 종속!)
   작성자 자기소개    post_id 만으로 정해짐    (부분 종속!)

         post_id 에만 매달린 것들을 '게시물' 표로 분리

 [좋아요]   PK = (post_id, member_id)      "누가 어느 글에 좋아요"
 [게시물]   PK = post_id                   caption · 작성자 · 작성자 자기소개

이렇게 나누면 표가 둘이 돼요. 좋아요 표는 "누가 어느 게시물에 좋아요를 눌렀나"라는 사실만 담고(키 전체에 완전 종속인 것만 남죠), 게시물 정보는 게시물 표로 빠져요. 이제 게시물 정보는 게시물 표에 게시물 하나당 딱 한 줄이에요. 좋아요가 백 개 달려도 caption은 한 번만 적히죠. 부분 종속이 부르던 중복이 사라진 거예요.

덤으로 Step 4에서 좋아요가 0개라 사라졌던 게시물(post 3·12) 기억하세요? 게시물을 따로 떼니, 좋아요가 없어도 게시물 표엔 멀쩡히 남아요. 삽입 이상 하나가 여기서 또 풀립니다.

⚠️ 함정 — 2NF는 "주식별자가 복합키일 때"만 의미가 있어요. 지난 시간 배운 주식별자 4조건 중 최소성 기억하시죠? 단일키(속성 하나)거나 인조키 하나로 잡은 표는 부분 종속이 생길 수가 없어서 자동으로 2NF예요. 그래서 시험에서 2NF 위반은 거의 항상 복합키 표(좋아요·주문상세 같은)에서 나옵니다.

💡 한 줄 정리

제2정규형(2NF)은 복합키의 일부에만 매달린 부분 종속을 떼어, 그 일부를 키로 하는 별도 표로 옮긴다. 좋아요 표에서 post_id에만 매달린 게시물 정보를 분리하면, 게시물 정보의 중복이 사라진다. 단일키 표는 자동으로 2NF다.

🙋 학생 질문 — "튜터님, 그럼 처음부터 모든 표에 인조키 하나만 쓰면 2NF 걱정이 없는 거 아닌가요?"

영리한 생각이에요. 실제로 좋아요에 like_id 같은 인조 번호 하나를 키로 얹으면, 키가 단일이라 부분 종속이 안 생기긴 해요. 그런데 그건 2NF를 '지킨' 게 아니라 '문제를 가린' 거예요.

like_id로 키를 바꿔도 captionpost_id를 따라간다는 사실 자체는 그대로거든요. 게시물 정보는 여전히 좋아요 줄마다 중복돼요. 키만 바꿨을 뿐 중복은 그대로인 거죠. 그래서 인조키로 덮기 전에, 먼저 "이 표 안에 서로 다른 주제가 섞여 있나"를 봐야 해요. 섞여 있으면 키 모양과 상관없이 표를 나누는 게 정규화의 본질입니다.


Step 6: "거쳐가는 건 분리 — 제3정규형(3NF)" ★빈출

2NF까지 했더니 좋아요 표와 게시물 표로 깔끔하게 나뉘었어요. 그런데 게시물 표 안에 아직 중복의 씨앗이 남아 있어요. 이번엔 Step 3의 도구, 이행 함수 종속을 꺼낼 차례예요.

2NF를 마친 게시물 표를 자세히 적으면 이래요. post_id가 키이고, 작성자(writer_id)와 작성자 자기소개가 들어 있죠.

텍스트
 3NF — 이행 종속을 떼어 별도 표로 옮긴다

 [게시물]   PK = post_id
   caption
   writer_id           post_id 로 정해짐
   작성자 자기소개     writer_id 로 정해짐
                        post_id  writer_id  자기소개  (이행 종속!)

         키가 아닌 writer_id 를 거쳐 가는 것을 '회원' 표로 분리

 [게시물]   PK = post_id        caption · writer_id (작성자 참조)
 [회원]     PK = member_id      nickname · bio

보세요. '작성자 자기소개'는 post_id로 바로 정해지는 게 아니라, writer_id를 거쳐서 정해져요(post_id → writer_id → 자기소개). 그리고 writer_id는 게시물 표의 키가 아니라 일반 속성(작성자 참조)이죠. 키가 아닌 속성을 거쳐 가는 이 이행 종속이 또 중복을 불러요. 같은 회원이 게시물을 여럿 쓰면, writer_id에 매달린 자기소개가 게시물마다 따라 적히니까요.

제3정규형(3NF)의 규칙이 이거예요. 키가 아닌 속성을 거쳐 가는 이행 종속을 떼어, 그 중간 속성을 키로 하는 별도 표로 옮긴다. 작성자 정보를 회원 표로 빼면, 회원 정보는 회원 하나당 딱 한 줄이 돼요. 게시물 표엔 작성자를 가리키는 writer_id만 남고요.

여기까지 오면 비정규 한 표가 세 표로 깔끔하게 갈렸어요. 그리고 재미있는 게 보여요.

텍스트
 정규화를 마친 세 표 — 회원을 두 곳에서 가리킨다

  [회원]    member_id · nickname · bio
  [게시물]  post_id · caption · writer_id     writer_id 로 회원을 가리킴 (글 쓴 사람)
  [좋아요]  post_id · member_id               member_id 로 회원을 가리킴 (좋아요 누른 사람)

좋아요를 누른 회원도, 글을 쓴 회원도 결국 같은 회원 표를 가리켜요. 한 표를 두 곳에서 참조하는 거죠. 이렇게 키로 이어진 표들을 나중에 다시 맞춰보는 게 바로 조인(JOIN)인데, 그건 다음 시간에 만나요.

⚠️ 함정 — 시험에서 2NF와 3NF를 섞어 묻습니다. 둘을 가르는 한마디. 2NF는 키와의 관계(부분 종속), 3NF는 키 아닌 것들 사이(이행 종속)예요. "비-키 속성이 다른 비-키 속성을 결정하면?" → 3NF 위반이에요. 흔히 "주식별자가 아닌 속성에 종속되는 속성을 제거"라고 표현하는 게 정확히 이 3NF입니다.

💡 한 줄 정리

제3정규형(3NF)은 키가 아닌 속성을 거쳐 가는 이행 종속을 떼어, 중간 속성을 키로 하는 별도 표로 옮긴다. 게시물 표에서 작성자 정보를 회원 표로 분리하면 비정규 한 표가 회원·게시물·좋아요 세 표로 갈리며, 이 표들은 키로 이어져 나중에 조인된다.

🙋 학생 질문 — "튜터님, 1NF·2NF·3NF를 매번 순서대로 다 거쳐야 하나요?"

개념을 배울 땐 순서대로 보는 게 좋아요. 각 단계가 앞 단계를 전제로 정의되거든요(2NF는 1NF인 표에서, 3NF는 2NF인 표에서). 그래서 "1NF → 2NF → 3NF" 계단으로 이해해 두면 머릿속이 정리돼요.

하지만 실제 설계할 땐, 경험이 쌓이면 "이 표는 주제가 섞였네" 하고 처음부터 3NF 모양으로 바로 그리는 경우가 많아요. 단계를 의식하지 않아도 결과가 3NF가 되는 거죠. 그래도 왜 그렇게 나눠야 하는지 설명하려면 단계별 근거(부분 종속·이행 종속)를 알아야 하니, 지금은 계단을 또렷이 익혀 두는 게 좋습니다.


Step 7: "결정자는 후보키 — BCNF"

3NF까지 오면 사실 대부분의 중복은 다 잡혀요. 그런데 아주 드물게, 3NF를 만족하는데도 한 군데 더 손볼 곳이 남는 경우가 있어요. 그걸 다듬는 게 BCNF(Boyce-Codd 정규형)예요. 3NF를 살짝 더 엄격하게 만든 버전이라고 보면 돼요.

BCNF의 규칙은 한 문장이에요. 모든 결정자가 후보키여야 한다. Step 2에서 결정자는 "X → Y에서 화살표를 쏘는 X"라고 했죠. 그 모든 결정자가 그 표에서 한 행을 가리킬 수 있는 키(후보키)여야 한다는 뜻이에요. 키가 아닌 게 무언가를 결정하고 있으면 BCNF 위반이고요.

말로만 보면 3NF랑 비슷한데, 차이는 아주 좁은 틈에서 나요. 예를 하나 볼게요. 인스타에 이런 규칙이 있다고 해봐요. "한 해시태그(#여행)는 공식 운영자가 딱 한 명이고, 한 운영자는 한 해시태그만 맡는다."

텍스트
 BCNF — 3NF 보다 한 끗 더 엄격하게

  표: (post_id, hashtag, moderator)     게시물에 달린 해시태그와 그 운영자

  규칙:  (post_id, hashtag)  moderator     게시물·태그로 운영자가 정해짐
         moderator  hashtag               운영자로 담당 해시태그가 정해짐

   'moderator' 가 hashtag 를 결정하는데, moderator 는 후보키가 아니다
   3NF 는 통과(걸린 게 키의 일부라서)하지만, BCNF 는 위반
   moderator·hashtag 를 별도 표로 떼어내면 BCNF 완성

moderatorhashtag를 결정하는데, moderator는 이 표의 키가 아니에요. 그래서 BCNF 위반이죠. 이런 건 결정자(moderator)와 그가 정하는 값(hashtag)을 작은 표로 따로 떼면 풀려요.

그런데 솔직히 말할게요. 이런 상황은 실무에서 정말 드물어요. 대부분의 표는 3NF까지만 하면 충분하고, BCNF까지 따로 신경 쓸 일이 거의 없어요. SQLD에서도 BCNF는 "결정자가 모두 후보키여야 한다"는 한 줄 정의와, "3NF보다 엄격하다"는 관계만 알면 대개 충분해요. 그러니 이 Step은 가볍게, 정의 한 줄만 손에 쥐고 넘어가면 됩니다.

🌟 정규화는 보통 3NF까지가 실무의 목표예요. 그 위로 BCNF·4NF·5NF도 있지만, 갈수록 만나기 힘든 특수한 경우라 입문 단계에선 "3NF까지 탄탄히 + BCNF는 정의만"으로 잡으면 충분합니다.

💡 한 줄 정리

BCNF는 "모든 결정자가 후보키여야 한다"는, 3NF를 한 단계 더 엄격하게 만든 정규형이다. 키가 아닌 속성이 무언가를 결정하면 위반이며, 실무·시험 모두 대개 3NF까지가 핵심이고 BCNF는 정의와 관계만 알면 된다.

🙋 학생 질문 — "튜터님, 그럼 BCNF나 4NF는 안 배워도 되는 거예요?"

"안 배워도 된다"기보다 "지금은 정의만 알면 된다"가 맞아요. BCNF는 방금처럼 정의 한 줄(결정자=후보키)과 3NF와의 관계만 잡아두면, SQLD 수준에선 거의 다 커버돼요. 4NF·5NF는 다중값 종속 같은 더 특수한 개념인데, 입문·자격증 단계에선 이름만 들어봐도 충분해요.

중요한 건 "왜 정규화하는가"의 감각이에요. 중복을 줄여 이상현상을 막는다는 큰 그림과, 1NF~3NF를 종속을 보고 단계별로 나누는 능력. 이 둘이 탄탄하면 BCNF 이상은 필요할 때 찾아봐도 금방 따라옵니다.


Step 8: "일부러 다시 더하기 — 반정규화의 트레이드오프" ★빈출

오늘 내내 중복을 덜어내는 정규화를 배웠어요. 그런데 마지막에 정반대 이야기를 할게요. 때로는 일부러 중복을 다시 더하는 게 나을 때가 있어요. 이걸 반정규화(denormalization)라고 해요. 정규화를 거꾸로 돌리는 거죠.

왜 애써 나눈 걸 되돌릴까요? 정규화에는 그림자가 하나 있거든요. 표를 잘게 나누면 중복은 줄지만, 흩어진 정보를 다시 모으는 일이 잦아져요. 예를 들어 "각 게시물이 좋아요를 몇 개 받았나"를 보여주려면, 좋아요 표에서 그 게시물의 행을 매번 일일이 세야 해요. 인기 게시물 목록을 띄울 때마다요.

텍스트
 정규화 vs 반정규화 — 좋아요 수를 어떻게 보여줄까

  정규화한 채로:   게시물의 좋아요 수 = 좋아요 표에서 그 post_id 행을 매번 센다
                   중복 없음(정합성 ) · 단, 목록 띄울 때마다 세야 함(읽기 느림)

  반정규화:       게시물 표에 like_count 칸을 더해 그 수를 미리 저장
                   셀 필요 없이 바로 읽음(읽기 빠름)
                   단, 좋아요가 추가·취소될 때마다 like_count 도 같이 고쳐야 함
                    (의도한 중복  갱신 이상의 위험을 일부러 떠안음)

like_count 같은 칸은 사실 좋아요 표를 세면 나오는 값이라, 굳이 따로 저장하면 중복이에요. 우리가 그토록 없애려던 그 중복이요. 하지만 그 중복을 일부러 떠안는 대가로 읽기 속도를 크게 얻어요. 좋아요가 수만 개라도 매번 세지 않고 칸 하나만 읽으면 되니까요.

공짜는 아니에요. like_count를 저장해 두면, 좋아요가 하나 추가되거나 취소될 때마다 그 칸도 같이 +1, -1 해줘야 해요. 깜빡하면 실제 좋아요 수와 like_count가 어긋나죠. 바로 갱신 이상의 위험을 스스로 들이는 거예요. 그래서 반정규화는 트레이드오프예요. 무엇을 얻고 무엇을 잃는지 따져 고르는 결정이죠.

정규화 반정규화
중복 없음 일부러 더함
읽기(조회) 모아야 해서 느릴 수 있음 미리 저장해 빠름
쓰기(수정) 한 곳만 고치면 됨 여러 곳을 맞춰야 함
정합성 위험 낮음 높음(동기화 필요)

언제 반정규화를 고를까요? 읽기가 압도적으로 잦고 쓰기는 드물 때가 후보예요. 좋아요 수처럼 "보여주는 일(읽기)은 수없이 많은데 바뀌는 일(쓰기)은 가끔"인 값이 딱이죠. 반대로 자주 바뀌는 값을 반정규화하면, 매번 여기저기 맞추느라 오히려 손해예요. 기본은 정규화로 탄탄히 설계하고, 성능이 정말 발목을 잡는 곳에서만 신중히 반정규화하는 게 정석이에요.

⚠️ 함정 — "반정규화 = 정규화를 안 한 것"이 아니에요. 순서가 중요해요. 먼저 3NF까지 제대로 정규화한 다음, 성능 때문에 의도적으로 일부를 되돌리는 게 반정규화예요. 정규화를 모르고 한 표에 몰아넣은 비정규 설계와는 완전히 달라요. SQLD에서 이 차이("의도된 중복이냐, 설계 실수냐")를 묻습니다.

💡 한 줄 정리

반정규화는 읽기 성능을 위해 일부러 중복을 더하는 것(예: 게시물에 like_count 저장)으로, 읽기는 빨라지지만 쓰기마다 동기화해야 하고 정합성 위험을 떠안는 트레이드오프다. 정규화로 탄탄히 설계한 뒤, 읽기가 잦고 쓰기가 드문 곳에서만 의도적으로 적용한다.

🙋 학생 질문 — "튜터님, 그럼 처음부터 반정규화로 설계하면 빠르고 좋은 거 아닌가요?"

빠른 건 맞지만 위험해요. 정규화를 건너뛰고 처음부터 중복을 깔면, 그건 반정규화가 아니라 Step 1에서 본 비정규 설계예요. 의도한 중복과 실수로 생긴 중복은 결과가 같아도 관리가 천지차이거든요.

정규화부터 한 설계는 "원본은 좋아요 표, like_count는 그걸 빠르게 보여주려는 사본"이라는 구조가 명확해요. 어긋나면 원본을 세서 맞추면 되죠. 반면 처음부터 중복을 깔면 어느 게 원본인지조차 흐려져서, 데이터가 틀어졌을 때 무엇이 맞는지 알 수가 없어요. 그래서 "정규화 먼저, 반정규화는 필요한 곳에 신중히"가 기본 순서입니다.


마무리

오늘 수고 많으셨어요! 지난 시간까지 다섯 박스를 잇는 선과 각 행을 가리키는 키를 정했다면, 오늘은 그 박스 안에 어떤 속성을 어떻게 나눠 담아야 하는지를 다뤘어요. 한 표에 몰아넣었을 때 생기는 이상현상을, 함수 종속을 따라 1NF·2NF·3NF로 차근차근 풀어냈고, 반대로 성능을 위해 일부러 중복을 더하는 반정규화까지 봤죠. 정규화는 SQLD 1과목의 심장이라, 오늘 흐름을 잘 익혀두면 시험장에서 크게 든든할 거예요.

오늘 배운 핵심 세 가지

  • 💡 하나 — 한 표에 여러 주제를 몰아넣으면 중복이 생기고, 그 중복이 삽입·갱신·삭제 이상을 부른다. 원인은 데이터의 양이 아니라 주제가 섞인 것이다.
  • 💡 — 정규화는 함수 종속을 보고 단계별로 표를 나눈다. 1NF는 한 칸 한 값(원자값), 2NF는 부분 종속 떼기, 3NF는 이행 종속 떼기. BCNF는 "결정자는 모두 후보키"라는 한 단계 더 엄격한 버전이다.
  • 💡 — 반정규화는 읽기 성능을 위해 일부러 중복을 더하는 트레이드오프다. 정규화로 탄탄히 설계한 뒤, 읽기가 잦고 쓰기가 드문 곳에서만 의도적으로 적용한다.

다음 시간 예고

오늘로 종이 위 인스타그램 설계도가 거의 완성됐어요. 다섯 박스를 잇고, 각 행을 키로 가리키고, 속성을 정규화로 잘 나눠 담았으니까요. 그런데 정규화로 표를 잘게 나눴더니 한 가지 숙제가 생겼죠. 회원·게시물·좋아요가 따로 흩어져 있으니, "이 게시물을 누가 썼고 누가 좋아요를 눌렀나"를 한눈에 보려면 흩어진 표들을 다시 맞춰봐야 해요. 오늘 본 것처럼 게시물 표의 writer_id와 좋아요 표의 member_id가 모두 회원 표를 가리키고 있었잖아요. 이 키들을 맞대어 표를 다시 잇는 작업, 그게 바로 조인(JOIN)이에요.

다음 시간엔 이 조인의 기초가 되는 식별자 관계를 정리하고, 표 하나를 다루는 트랜잭션과 빈칸(NULL)을 어떻게 봐야 하는지까지 짚으며 모델링 카테고리를 마무리합니다. 오늘 나눈 세 표가 다음 시간에 어떻게 다시 만나는지 기대하고 오세요.


과제

오늘 배운 이상현상·함수 종속·정규화를 직접 종이 위에서 따져보는 과제예요. SQL을 짜는 게 아니라, 표를 보고 종속을 읽고 단계별로 나누는 손을 익히는 게 목적이에요.

[기초] 비정규 표에서 이상현상 세 가지 찾기

인스타에 '댓글'을 한 표에 몰아넣은 비정규 설계가 있다고 해봐요. 한 행이 댓글 하나이고, 그 댓글이 달린 게시물 정보와 댓글 단 회원 정보까지 한 줄에 다 들어 있어요(예: comment_id, 내용, post_id, 게시물 내용, 회원 닉네임, 회원 자기소개). 이 표에서 삽입 이상·갱신 이상·삭제 이상이 각각 어떤 상황에서 생길지 한 가지씩 구체적인 예를 들어 적어보세요. 그리고 세 이상현상의 공통 원인이 무엇인지 한 줄로 정리해 보세요.

[응용] 비정규 표를 3NF까지 단계별로 분해하기

위 [기초]의 댓글 비정규 표를 1NF → 2NF → 3NF 순서로 나눠보세요. (1) 1NF는 이미 만족한다면 그 이유를, 어긴 칸이 있다면 어떻게 펼칠지 적고, (2) 어떤 부분 종속·이행 종속이 있는지 화살표(A → B)로 표시한 뒤, (3) 최종적으로 어떤 표 몇 개로 갈리는지, 각 표의 주식별자와 함께 그려보세요.

[심화] 반정규화 결정 내려보기

인스타 프로필 화면에 "이 회원의 게시물 수"를 보여줘야 한다고 해봐요. 정규화한 설계라면 게시물 표에서 그 회원의 행을 매번 세야 하죠. 여기에 회원 표에 post_count 칸을 더하는 반정규화를 고민합니다. (1) 이 반정규화로 무엇을 얻고 무엇을 잃는지 읽기·쓰기·정합성 관점에서 적고, (2) post_count가 실제 게시물 수와 어긋나지 않으려면 언제 이 칸을 같이 고쳐야 하는지, (3) 결국 이 반정규화를 도입할지 말지를 "읽기 빈도 vs 쓰기 빈도" 기준으로 판단하고 이유를 붙여보세요.


생각해볼 주제

1. 정규화는 어디까지 해야 할까

오늘 1NF부터 BCNF까지 봤지만, 실무에선 보통 3NF에서 멈춰요. 더 높은 정규형(BCNF·4NF)도 있는데 왜 거기까지 잘 안 갈까요? 반대로 "이왕이면 최대한 높은 정규형까지 가는 게 항상 옳은 것 아닌가?"라고 누가 묻는다면 어떻게 답할지 생각해 보세요. 정규화를 더 할수록 표가 늘어나고, 정보를 모으려면 그만큼 더 많이 맞춰봐야 한다는 점을 함께 떠올려 보세요.

2. 반정규화한 값이 어긋나면 누가 책임지나

게시물에 like_count를 반정규화로 저장해 뒀어요. 그런데 어느 날 like_count는 100인데 실제 좋아요 표를 세어 보니 97이었어요. 셋이 비는 거죠. 이런 불일치는 왜 생기고, 어떻게 막거나 바로잡을 수 있을까요? "원본은 무엇이고 사본은 무엇인가"를 구분하는 관점에서, 그리고 "언제 두 값을 맞추는가"라는 시점의 관점에서 생각해 보세요.

3. 정규화하면 표가 많아져 조인이 늘어난다 — 그래도 정규화가 기본인 이유

정규화를 잘할수록 표가 잘게 쪼개지고, 흩어진 정보를 모으려면 표들을 다시 맞춰보는 일(조인)이 늘어나요. "그러면 차라리 한 표로 두는 게 빠르지 않나?"라는 반론이 자연스럽게 나오죠. 그럼에도 정규화를 설계의 기본으로 삼는 이유는 무엇일지, 데이터가 오래 쌓이고 자주 바뀌는 서비스를 떠올리며 생각해 보세요. 이 고민은 다음 시간 조인으로 곧장 이어집니다.

✅ 예시 답안정답 보기

과제와 생각해볼 주제의 예시답안입니다. 오늘 과제도 SQL을 짜는 게 아니라 표를 보고 종속을 읽고 단계별로 나누는 데 초점이 있고, 생각해볼 주제는 자격증·실무 양쪽 관점을 함께 담았어요. 정답을 맞히는 것보다 "왜 그렇게 나눴는지"를 설명할 수 있는지가 핵심이라, 아래 예시는 여러 모범 답 중 하나일 뿐입니다.


🎯 [과제 1 예시답안] 비정규 표에서 이상현상 세 가지 찾기

채점 포인트

채점 포인트 확인
삽입 이상의 구체적 예를 들었는가
갱신 이상의 구체적 예를 들었는가
삭제 이상의 구체적 예를 들었는가
세 이상현상의 공통 원인(중복)을 한 줄로 정리했는가

풀이 예시

댓글을 한 표에 몰아넣으면 이렇게 생겨요. 한 행이 댓글 하나이고, 게시물 정보와 회원 정보가 줄마다 따라붙어요.

comment_id 내용 post_id 게시물 내용 회원 닉네임 회원 자기소개
1 멋져요! 1 첫 게시물입니다 이민지 사진과 커피
2 저도요 1 첫 게시물입니다 박승우 (NULL)
3 잘 봤어요 4 아침 커피 한 잔 이민지 사진과 커피

세 이상현상을 하나씩 짚어볼게요.

  • 삽입 이상 — 댓글을 한 번도 안 단 회원은 이 표에 넣을 수가 없어요. 이 표는 comment_id가 있어야 한 행이 생기는데, 댓글이 없으면 회원 정보만 따로 넣을 곳이 없으니까요. "회원 정보를 기록하고 싶어도 댓글이 없으면 못 넣는다"가 삽입 이상이에요.
  • 갱신 이상 — 이민지가 댓글을 여럿 달면(1번·3번) '이민지'와 '사진과 커피'가 여러 줄에 중복돼요. 이민지가 자기소개를 바꾸면 그 줄을 전부 고쳐야 하고, 한 줄이라도 놓치면 같은 회원이 두 자기소개를 갖는 모순이 생겨요.
  • 삭제 이상 — 어떤 게시물에 달린 댓글이 딱 하나뿐인데 그 댓글을 지우면, 그 게시물 정보(post_id·게시물 내용)까지 표에서 함께 사라져요. "댓글만 지웠을 뿐인데 게시물 정보가 증발"하는 게 삭제 이상이에요.

세 이상현상의 공통 원인은 한 줄로 이래요. 한 표에 댓글·게시물·회원이라는 서로 다른 주제가 섞여 중복이 생겼기 때문이에요. 주제를 표마다 따로 담으면 셋 다 사라져요.

💡 튜터의 한마디

이 과제의 핵심은 세 이상현상을 "구체적인 상황"으로 그려내는 거예요. "삽입 이상이 생긴다"가 아니라 "댓글 안 단 회원은 넣을 수가 없다"처럼요. 그리고 셋의 원인을 "데이터가 많아서"가 아니라 "주제가 섞여 중복돼서"로 짚었는지 꼭 확인해 보세요. 원인을 중복으로 정확히 본 사람만 다음 과제(정규화)를 제대로 풀 수 있어요.


🎯 [과제 2 예시답안] 비정규 표를 3NF까지 단계별로 분해하기

채점 포인트

채점 포인트 확인
1NF 만족 여부를 이유와 함께 판단했는가
부분 종속·이행 종속을 화살표로 표시했는가
최종 분해된 표와 각 주식별자를 그렸는가

풀이 예시

[기초]의 댓글 비정규 표를 단계별로 나눠볼게요. 이 표엔 댓글 단 회원을 가리키는 member_id도 함께 있다고 볼게요(닉네임만으론 회원을 제대로 가리킬 수 없으니까요).

(1) 1NF — 이미 만족

모든 칸에 값이 하나씩만 들어 있어요. 한 칸에 쉼표로 여러 값을 욱여넣은 곳이 없으니 1NF는 이미 만족이에요. 펼칠 목록이 없죠.

(2) 종속 읽기 — 화살표로 표시

텍스트
  주식별자 = comment_id (단일키)

  comment_id  내용              (직접)
  comment_id  post_id  게시물 내용         (이행: post_id 를 거침)
  comment_id  member_id  닉네임, 자기소개  (이행: member_id 를 거침)

부분 종속은 없어요. 주식별자가 comment_id 하나뿐(단일키)이라 "키의 일부"라는 게 없으니까요. 그래서 2NF도 자동으로 만족이에요. 이 표의 문제는 전부 이행 종속이에요. 게시물 내용post_id를 거쳐, 회원 정보는 member_id를 거쳐 정해지죠.

(3) 3NF — 이행 종속을 떼어 세 표로

텍스트
 [댓글]    PK = comment_id     내용 · post_id(게시물 참조) · member_id(작성자 참조)
 [게시물]  PK = post_id        게시물 내용
 [회원]    PK = member_id      닉네임 · 자기소개

이행 종속 두 개를 각각 떼어내면 댓글·게시물·회원 세 표로 갈려요. 게시물 내용은 게시물 표에 게시물당 한 줄, 회원 정보는 회원 표에 회원당 한 줄이라 중복이 사라졌어요. 댓글 표엔 두 부모를 가리키는 post_id·member_id만 남고요.

💡 튜터의 한마디

이 과제의 함정은 2NF예요. "1NF 다음은 2NF니까 뭔가 떼어내야 하지 않나?" 하고 억지로 나누면 틀려요. 주식별자가 단일키(comment_id)면 부분 종속이 생길 수 없어서 2NF는 그냥 통과예요. 단일키 표는 1NF에서 곧장 3NF로 가는 셈이죠. "2NF는 이미 만족"이라고 정확히 짚었다면 정규화의 원리를 제대로 이해한 거예요.


🎯 [과제 3 예시답안] 반정규화 결정 내려보기

채점 포인트

채점 포인트 확인
얻는 것·잃는 것을 읽기·쓰기·정합성으로 나눠 적었는가
post_count를 언제 같이 고쳐야 하는지 짚었는가
읽기/쓰기 빈도 기준으로 도입 여부를 판단했는가

풀이 예시

(1) 얻는 것과 잃는 것

텍스트
  회원 표에 post_count 칸을 더하면

  얻는 것   읽기 빠름 — 프로필 열 때마다 게시물을 세지 않고 칸 하나만 읽음
  잃는 것   쓰기 번거로움 — 게시물이 추가·삭제될 때마다 post_count 도 고쳐야 함
            정합성 위험 — 깜빡 놓치면 실제 게시물 수와 post_count 가 어긋남

post_count는 사실 게시물 표를 세면 나오는 값이라, 따로 저장하면 의도된 중복이에요. 읽기는 빨라지지만 그 대가로 쓰기가 복잡해지고 정합성 위험을 떠안죠.

(2) 언제 같이 고쳐야 하나

회원이 게시물을 새로 올릴 때 post_count를 +1, 게시물을 지울 때 -1 해줘야 해요. 즉 게시물 표가 바뀌는 모든 순간에 회원 표의 post_count도 함께 손봐야 실제 수와 어긋나지 않아요. 둘을 항상 함께 바꿔주는 게 핵심이에요.

(3) 도입 여부 판단

프로필의 게시물 수는 읽기는 아주 잦고(누가 프로필을 열 때마다) 쓰기는 드문(게시물을 올리는 일은 가끔) 값이에요. 읽기 빈도가 쓰기 빈도를 압도하니, 반정규화 후보로 적합해요. 저라면 도입하겠어요. 매번 세는 비용을 아끼는 이득이 크고, 동기화할 시점(게시물 추가·삭제)도 명확해서 관리할 만하거든요. 다만 게시물 추가·삭제 로직에서 post_count 갱신을 빠뜨리지 않도록 한 곳에 묶어두는 게 조건이에요.

💡 튜터의 한마디

반정규화 문제엔 정해진 정답이 없어요. 핵심은 "읽기 빈도 vs 쓰기 빈도"를 근거로 들었는지예요. 게시물 수처럼 읽기가 압도적이면 도입이 합리적이고, 반대로 자주 바뀌는 값이면 동기화 부담이 커서 말리는 게 맞아요. "빠르니까 무조건 한다"가 아니라 "무엇을 얻고 무엇을 잃는지" 저울질한 답이 좋은 답이에요.


🤔 [생각해볼 주제 1] 정규화는 어디까지 해야 할까

문제 상황 요약

1NF부터 BCNF까지 배웠지만 실무에선 보통 3NF에서 멈춰요. 더 높은 정규형도 있는데 왜 거기까지 잘 안 갈까요? 반대로 "이왕이면 최대한 높은 정규형까지 가는 게 항상 옳은 것 아닌가?"라는 반론에는 어떻게 답해야 할까요.

튜터의 가이드 및 해설

정규화는 "높을수록 무조건 좋은" 게 아니라 얻는 것과 잃는 것이 같이 따라오는 작업이에요.

정규화를 더 할수록 표가 더 잘게 쪼개져요. 중복은 줄지만, 그만큼 흩어진 정보를 다시 모으는 일(조인)이 늘어나죠. 3NF까지는 대부분의 실용적인 중복이 사라지면서 표 수는 적당히 유지돼요. 가성비가 가장 좋은 지점이라, 실무는 보통 여기서 멈춰요.

BCNF 위로 가면 잡히는 이상현상은 점점 드물고 특수해지는데, 표는 더 늘고 조인은 더 많아져요. 얻는 건 작은데 치르는 비용은 커지는 거죠. 그래서 "최대한 높이"가 항상 옳은 게 아니라, "이 표에 실제로 그런 특수한 종속이 있나?"를 보고 필요한 만큼만 올리는 게 맞아요. 대부분의 표엔 그런 종속이 없어서 3NF면 충분하고요.

정리하면, 정규화의 목표는 "정규형 숫자를 높이는 것"이 아니라 "이상현상을 막는 것"이에요. 3NF에서 이미 이상현상이 거의 다 막히니, 그 위는 필요할 때만 선택적으로 가는 거예요.

🎯 SQLD는 이렇게 나온다

"실무에서 보통 어느 정규형까지 정규화하는가?"의 답이 3NF예요. 그리고 "정규화 수준이 높아질수록 무엇이 늘어나는가?"라는 물음의 답이 "표의 수와 조인"이라는 점이 출제 포인트죠. 정규화의 목적이 "정규형을 높이는 것"이 아니라 "이상현상(중복) 제거"라는 본질을 묻는 문제도 자주 나오니, 목적과 수단을 헷갈리지 마세요.

💡 실무에선

대부분 3NF를 기준으로 설계하고, 그 위는 거의 가지 않아요. 오히려 성능이 필요한 곳에서는 3NF에서 의도적으로 한 발 물러나는 반정규화를 더 자주 고민하죠. "어디까지 올리나"보다 "어디서 멈추고, 어디서 되돌리나"가 실무의 진짜 감각이에요.


🤔 [생각해볼 주제 2] 반정규화한 값이 어긋나면 누가 책임지나

문제 상황 요약

게시물에 like_count를 반정규화로 저장해 뒀는데, 어느 날 like_count는 100인데 실제 좋아요 표를 세어 보니 97이었어요. 이런 불일치는 왜 생기고, 어떻게 막거나 바로잡을 수 있을까요.

튜터의 가이드 및 해설

이 불일치의 정체는 사실 우리가 오늘 배운 갱신 이상이에요. 반정규화는 일부러 중복을 더하는 거라, 정규화로 없앴던 갱신 이상의 위험을 스스로 다시 들이는 셈이거든요.

왜 어긋날까요? like_count는 좋아요 표를 세면 나오는 값의 '사본'이에요. 원본(좋아요 표)이 바뀌었는데 사본(like_count)을 같이 안 고치면 둘이 벌어져요. 좋아요가 추가·취소되는 순간마다 like_count도 +1, -1 해줘야 하는데, 어느 한 경로에서 그 갱신을 빠뜨리면 그만큼 어긋나는 거죠.

막는 방법은 두 갈래예요. 첫째, 원본과 사본을 항상 함께 바꾸기. 좋아요를 추가·취소하는 모든 길목에서 like_count 갱신을 같이 묶어, 둘이 따로 노는 틈을 없애요. 둘째, 주기적으로 원본을 세어 사본을 맞추기. 그래도 어긋남이 쌓일 수 있으니, 가끔 좋아요 표를 직접 세서 like_count를 덮어써 바로잡아요. 여기서 분명히 해둘 건, 원본은 어디까지나 좋아요 표라는 점이에요. 둘이 다투면 좋아요 표를 진실로 보고 like_count를 고치지, 그 반대가 아니에요.

🎯 SQLD는 이렇게 나온다

반정규화의 단점을 묻는 문제에서 "데이터 불일치(정합성) 위험"이 핵심 답이에요. 그 불일치가 곧 갱신 이상이라는 연결, 그리고 "반정규화는 읽기 성능을 얻는 대신 정합성 관리 부담을 진다"는 트레이드오프가 단골이죠. 정규화가 이상현상을 없애는 방향이라면, 반정규화는 그 위험을 의도적으로 떠안는 반대 방향이라는 대비를 기억해 두세요.

💡 실무에선

반정규화한 값은 "원본을 빠르게 보여주려는 사본"으로 명확히 다뤄요. 갱신을 한곳에 묶어 두고, 그래도 틀어질 수 있으니 주기적으로 원본과 맞추는 장치를 함께 둬요. 어느 게 원본인지가 흐려지면 손쓸 수 없게 되니, 그 경계를 또렷이 지키는 게 핵심이에요.


🤔 [생각해볼 주제 3] 정규화하면 조인이 늘어난다 — 그래도 기본인 이유

문제 상황 요약

정규화를 잘할수록 표가 잘게 쪼개지고, 흩어진 정보를 모으려면 표들을 다시 맞춰보는 일(조인)이 늘어나요. "그러면 차라리 한 표로 두는 게 빠르지 않나?"라는 반론이 자연스럽게 나오는데, 그럼에도 정규화를 기본으로 삼는 이유는 무엇일까요.

튜터의 가이드 및 해설

한 표로 두면 읽기 한 번은 빠를 수 있어요. 조인을 안 해도 되니까요. 하지만 데이터가 오래 쌓이고 자주 바뀌는 서비스에서는 그 편함이 금세 빚으로 돌아와요.

핵심은 읽기는 다시 모으면 되지만, 어긋난 데이터는 되돌리기 어렵다는 거예요. 정규화된 표는 정보가 한 군데에만 있어서, 회원이 자기소개를 바꾸면 회원 표 한 줄만 고치면 끝이에요. 반면 한 표에 몰아넣으면 같은 정보가 수십 줄에 흩어져 있어, 하나만 놓쳐도 모순이 생기고 그 모순은 한참 뒤에야 발견돼요. 조인은 그때그때 다시 하면 그만이지만, 한 번 어긋난 데이터는 무엇이 맞는지조차 알기 어려워지죠.

그래서 정규화를 기본으로 깔아요. 데이터의 정확함(정합성)을 구조로 보장해 두는 거예요. 그리고 읽기 성능이 정말 문제가 되는 특정 곳에서만, 오늘 배운 반정규화로 신중히 되돌려요. "기본은 정규화로 정확하게, 필요한 곳만 반정규화로 빠르게"가 순서예요. 정규화로 쪼갠 표들을 다시 잇는 조인은 다음 시간에 본격적으로 배웁니다.

🎯 SQLD는 이렇게 나온다

"정규화의 장점은 정합성(중복 제거), 단점은 조인 증가로 인한 조회 성능 부담"이라는 트레이드오프가 정확히 출제돼요. 그리고 그 단점을 보완하는 게 반정규화라는 연결도 함께 묻죠. "정규화는 데이터 무결성을, 반정규화는 조회 성능을 우선한다"는 한 문장을 정리해 두면 관련 문제를 묶어서 잡을 수 있어요.

💡 실무에선

설계는 정규화로 시작하는 게 정석이에요. 데이터가 쌓일수록 정합성이 무너진 표는 손쓰기 어려워지거든요. 조인이 부담되는 일부 조회는 반정규화나 별도의 조회용 구조로 풀되, 원본 설계는 정규화를 유지하는 게 오래가는 방식이에요.

전체 목록 데이터베이스