데이터베이스 정규화와 반정규화 완벽 가이드

데이터베이스 정규화와 반정규화 완벽 가이드

데이터베이스를 설계할 때 가장 먼저 마주하는 핵심 질문이 있다. “테이블을 어떻게 나눌 것인가?” 정규화(Normalization)는 이 질문에 대한 체계적인 답을 제공하는 이론이다. 데이터의 중복을 제거하고 이상 현상(Anomaly)을 방지하여 데이터 무결성을 보장하는 것이 목적이다.

그러나 정규화가 항상 정답은 아니다. 과도한 정규화는 조인(JOIN)을 증가시켜 조회 성능을 저하시킨다. 이때 반정규화(Denormalization)를 통해 의도적으로 중복을 허용하고 조회 성능을 개선할 수 있다.

이 글은 정규화의 이론적 배경(함수 종속성)부터 각 정규형(1NF~BCNF)의 정확한 정의와 분해 과정, 이상 현상의 구체적인 사례, 반정규화 기법과 판단 기준, 그리고 JPA/Spring 환경에서의 실무 적용까지를 다룬다.


1. 왜 정규화가 필요한가 — 이상 현상(Anomaly)

정규화를 이해하기 전에, 정규화를 하지 않으면 어떤 문제가 발생하는지 살펴보자.

다음과 같은 비정규화된 테이블이 있다고 하자.

1
2
3
4
5
6
7
8
9
10
11
[수강 테이블] — 학생, 과목, 교수 정보를 하나의 테이블에 저장

┌─────────┬──────┬──────────┬───────────┬───────┬──────────┐
│ 학생ID   │ 이름  │ 학과      │ 과목코드    │ 과목명  │ 교수      │
├─────────┼──────┼──────────┼───────────┼───────┼──────────┤
│ S001    │ 홍길동 │ 컴퓨터공학 │ CS101     │ 자료구조 │ 김교수    │
│ S001    │ 홍길동 │ 컴퓨터공학 │ CS201     │ 알고리즘 │ 이교수    │
│ S002    │ 김영희 │ 전자공학   │ CS101     │ 자료구조 │ 김교수    │
│ S002    │ 김영희 │ 전자공학   │ EE101     │ 회로이론 │ 박교수    │
│ S003    │ 이철수 │ 컴퓨터공학 │ CS201     │ 알고리즘 │ 이교수    │
└─────────┴──────┴──────────┴───────────┴───────┴──────────┘

이 테이블에서 세 가지 이상 현상이 발생한다.

1.1 삽입 이상 (Insertion Anomaly)

새로운 과목 “운영체제(CS301)”를 개설하고 싶은데, 아직 수강 신청한 학생이 없다. 학생ID가 기본키의 일부이므로, 학생 없이는 과목 데이터를 삽입할 수 없다.

1
2
3
INSERT INTO 수강 VALUES (?, ?, ?, 'CS301', '운영체제', '최교수');
                          ↑
                     학생ID를 넣을 수 없음 → 삽입 불가!

1.2 갱신 이상 (Update Anomaly)

“자료구조” 과목의 담당 교수가 김교수에서 정교수로 변경되었다. 자료구조를 수강하는 모든 행(S001, S002)을 찾아서 갱신해야 한다. 하나라도 누락하면 데이터 불일치가 발생한다.

1
2
3
4
5
UPDATE 수강 SET 교수 = '정교수' WHERE 과목코드 = 'CS101';

만약 S001 행만 업데이트하고 S002를 누락하면:
S001 → CS101 자료구조 정교수  ← 갱신됨
S002 → CS101 자료구조 김교수  ← 갱신 누락! → 누가 담당교수?

1.3 삭제 이상 (Deletion Anomaly)

이철수(S003)가 알고리즘(CS201) 수강을 취소한다. S003 행을 삭제하면, 이철수의 학생 정보(이름, 학과)도 함께 사라진다. 학생은 여전히 재학 중인데 학생 정보가 소실되는 것이다.

1
2
3
4
DELETE FROM 수강 WHERE 학생ID = 'S003' AND 과목코드 = 'CS201';

→ 이철수의 학생 정보(이름: 이철수, 학과: 컴퓨터공학)도 함께 삭제됨!
  다른 과목을 수강하지 않으면 학생 데이터 자체가 사라짐

이 세 가지 이상 현상의 근본 원인은 서로 다른 개체(학생, 과목, 수강관계)의 정보가 하나의 테이블에 혼재되어 있기 때문이다. 정규화는 이 문제를 체계적으로 해결한다.


2. 함수 종속성 (Functional Dependency)

정규화의 이론적 토대는 함수 종속성이다. 정규형의 조건을 이해하려면 함수 종속성을 먼저 정확히 이해해야 한다.

2.1 함수 종속성의 정의

릴레이션 R에서 속성 집합 X의 값이 속성 집합 Y의 값을 유일하게 결정하면, Y는 X에 함수적으로 종속된다고 한다. 이를 X → Y로 표기한다. X를 결정자(Determinant), Y를 종속자(Dependent)라 한다.

1
2
3
4
5
6
학생ID → 이름           학생ID를 알면 이름이 유일하게 결정됨
학생ID → 학과           학생ID를 알면 학과가 유일하게 결정됨
과목코드 → 과목명        과목코드를 알면 과목명이 유일하게 결정됨
과목코드 → 교수          과목코드를 알면 담당 교수가 유일하게 결정됨

{학생ID, 과목코드} → 성적  학생ID와 과목코드를 함께 알아야 성적이 결정됨

2.2 함수 종속성의 종류

1
2
3
4
5
6
7
8
9
10
11
12
13
< 완전 함수 종속 (Full FD) >
{학생ID, 과목코드} → 성적
  ↑ 기본키 전체에 종속 (학생ID만으로도 안 되고, 과목코드만으로도 안 됨)

< 부분 함수 종속 (Partial FD) >
{학생ID, 과목코드} → 학과
  ↑ 학생ID만으로 학과가 결정됨. 과목코드는 불필요
  = 기본키의 일부에만 종속 → 2NF 위반의 원인

< 이행 함수 종속 (Transitive FD) >
학생ID → 학과 → 학과사무실전화
  ↑ 학생ID가 학과를 결정하고, 학과가 학과사무실전화를 결정
  = 기본키가 비키 속성을 거쳐 다른 비키 속성을 결정 → 3NF 위반의 원인

2.3 Armstrong의 공리

함수 종속성을 추론하는 세 가지 기본 규칙이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 반사 규칙 (Reflexivity): Y ⊆ X → X → Y
   {학생ID, 이름} → 학생ID  (자명한 종속)

2. 증가 규칙 (Augmentation): X → Y → XZ → YZ
   학생ID → 이름  ⇒  {학생ID, 과목코드} → {이름, 과목코드}

3. 이행 규칙 (Transitivity): X → Y이고 Y → Z → X → Z
   학생ID → 학과이고 학과 → 학과전화  ⇒  학생ID → 학과전화

파생 규칙:
4. 합집합 규칙: X → Y이고 X → Z → X → YZ
5. 분해 규칙: X → YZ → X → Y이고 X → Z
6. 의사이행 규칙: X → Y이고 WY → Z → WX → Z

2.4 속성 폐쇄(Attribute Closure) 알고리즘

Armstrong의 공리를 사용하면 주어진 함수 종속성 집합으로부터 새로운 함수 종속성을 추론할 수 있다. 그런데 모든 가능한 함수 종속성을 일일이 추론하는 것은 비효율적이다. 실무와 시험에서 더 자주 사용하는 도구가 바로 속성 폐쇄(Attribute Closure)이다.

속성 폐쇄란 특정 속성 집합 X가 함수 종속성 집합 F를 통해 결정할 수 있는 모든 속성의 집합을 말한다. 이를 X⁺ 또는 X의 폐쇄(closure)라고 표기한다. 직관적으로 말하면, “X의 값을 알면 추론해낼 수 있는 모든 속성”이 X⁺이다.

속성 폐쇄가 중요한 이유는 두 가지다. 첫째, X → Y가 F로부터 추론 가능한지 판별할 수 있다. Y ⊆ X⁺이면 X → Y가 성립한다. 둘째, X가 슈퍼키인지, 후보키인지를 판별할 수 있다. X⁺가 릴레이션의 전체 속성 집합이면 X는 슈퍼키이고, X의 어떤 진부분집합도 슈퍼키가 아니면 X는 후보키다.

속성 폐쇄 계산 알고리즘

알고리즘은 단순하다. X⁺를 X로 초기화한 후, 함수 종속성 집합 F를 반복적으로 순회하면서 적용 가능한 종속성을 찾아 X⁺를 확장한다. 더 이상 확장되지 않으면 종료한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
알고리즘: ATTRIBUTE_CLOSURE(X, F)
────────────────────────────────
입력: 속성 집합 X, 함수 종속성 집합 F
출력: X의 폐쇄 X⁺

1. X⁺ ← X                          // 초기화: 자기 자신은 항상 결정 가능
2. REPEAT
3.    old ← X⁺                     // 변화 감지를 위해 이전 상태 저장
4.    FOR EACH (A → B) IN F DO     // 모든 FD를 순회
5.       IF A ⊆ X⁺ THEN            // 결정자가 현재 폐쇄에 포함되면
6.          X⁺ ← X⁺ ∪ B           // 종속자를 폐쇄에 추가
7.       END IF
8.    END FOR
9. UNTIL X⁺ = old                  // 더 이상 변화가 없으면 종료
10. RETURN X⁺

이 알고리즘의 핵심 아이디어는 “이미 알고 있는 속성으로 새로운 속성을 알아낼 수 있으면, 그 새로운 속성도 알고 있는 것”이라는 점이다. 마치 연쇄적으로 문을 열어가는 것과 같다. 열쇠 A로 방을 열면 그 안에 열쇠 B가 있고, 열쇠 B로 또 다른 방을 열면 열쇠 C가 나오는 식이다.

속성 폐쇄 계산 예제

다음과 같은 릴레이션과 함수 종속성 집합이 주어졌다고 하자.

1
2
3
4
5
6
7
8
9
릴레이션: R(A, B, C, D, E, F)

함수 종속성 집합 F:
  ① A → B
  ② BC → D
  ③ E → C
  ④ D → A

문제: {B, E}⁺를 구하라.

단계별로 풀어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
[Step 0] 초기화
X⁺ = {B, E}

[Step 1] 첫 번째 순회 — F의 모든 FD를 확인

  ① A → B : A ⊆ {B,E}? → NO (A가 없음)  → 적용 불가
  ② BC → D : BC ⊆ {B,E}? → NO (C가 없음)  → 적용 불가
  ③ E → C : E ⊆ {B,E}? → YES!            → X⁺ = {B,E} ∪ {C} = {B,C,E}
  ④ D → A : D ⊆ {B,E}? → NO (D가 없음)  → 적용 불가

  X⁺ = {B, C, E}  (변화 발생! → 계속 반복)

[Step 2] 두 번째 순회

  ① A → B : A ⊆ {B,C,E}? → NO           → 적용 불가
  ② BC → D : BC ⊆ {B,C,E}? → YES!       → X⁺ = {B,C,E} ∪ {D} = {B,C,D,E}
  ③ E → C : E ⊆ {B,C,E}? → YES          → C는 이미 포함 → 변화 없음
  ④ D → A : D ⊆ {B,C,E}? → NO           → 적용 불가
     (이 시점에서 D가 추가되었으므로, 다시 순회하면 ④번이 적용될 수 있음)

  X⁺ = {B, C, D, E}  (변화 발생! → 계속 반복)

[Step 3] 세 번째 순회

  ① A → B : A ⊆ {B,C,D,E}? → NO         → 적용 불가
  ② BC → D : BC ⊆ {B,C,D,E}? → YES      → D 이미 포함 → 변화 없음
  ③ E → C : E ⊆ {B,C,D,E}? → YES        → C 이미 포함 → 변화 없음
  ④ D → A : D ⊆ {B,C,D,E}? → YES!       → X⁺ = {B,C,D,E} ∪ {A} = {A,B,C,D,E}

  X⁺ = {A, B, C, D, E}  (변화 발생! → 계속 반복)

[Step 4] 네 번째 순회

  ① A → B : A ⊆ {A,B,C,D,E}? → YES      → B 이미 포함 → 변화 없음
  ② BC → D : BC ⊆ {A,B,C,D,E}? → YES    → D 이미 포함 → 변화 없음
  ③ E → C : E ⊆ {A,B,C,D,E}? → YES      → C 이미 포함 → 변화 없음
  ④ D → A : D ⊆ {A,B,C,D,E}? → YES      → A 이미 포함 → 변화 없음

  X⁺ = {A, B, C, D, E}  (변화 없음 → 종료!)

최종 결과: {B, E}⁺ = {A, B, C, D, E}

여기서 전체 속성 R = {A, B, C, D, E, F}인데 F가 포함되지 않았으므로, {B, E}⁺ ≠ R이다. 따라서 {B, E}는 슈퍼키가 아니다.

속성 폐쇄를 이용한 후보키 판별

속성 폐쇄를 계산하면 특정 속성 집합이 슈퍼키인지 후보키인지를 체계적으로 판별할 수 있다. 판별 규칙은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌────────────────────────────────────────────────────────────┐
│              속성 폐쇄를 이용한 키 판별 규칙                    │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  ■ 슈퍼키 판별                                              │
│    X⁺ = R(전체 속성)  →  X는 슈퍼키                          │
│    X⁺ ≠ R            →  X는 슈퍼키가 아님                    │
│                                                            │
│  ■ 후보키 판별                                               │
│    X가 슈퍼키이고,                                           │
│    X의 모든 진부분집합 Y에 대해 Y⁺ ≠ R  →  X는 후보키          │
│    (= 더 작은 부분집합으로는 전체를 결정할 수 없음)              │
│                                                            │
│  ■ 기본키 선택                                               │
│    후보키가 여러 개이면 그 중 하나를 기본키로 선택               │
│    일반적으로 속성 수가 적고 의미가 명확한 것을 선택             │
└────────────────────────────────────────────────────────────┘

후보키 찾기 — 체계적인 절차

후보키를 체계적으로 찾는 방법은 다음과 같다. 모든 속성을 세 가지로 분류하는 것이 출발점이다.

1
2
3
4
5
속성 분류:
  L (Left-only)  : FD의 왼쪽(결정자)에만 등장하는 속성
  R (Right-only) : FD의 오른쪽(종속자)에만 등장하는 속성
  LR (Both)      : 양쪽 모두에 등장하는 속성
  N (Neither)    : 어느 FD에도 등장하지 않는 속성

핵심 원리는 이것이다. R에만 등장하는 속성은 절대 결정자가 될 수 없으므로, 어떤 후보키에도 포함되지 않는다. 반대로 L에만 등장하거나 어디에도 등장하지 않는 속성은 반드시 후보키에 포함되어야 한다. 이 원리를 활용하면 탐색 범위를 대폭 줄일 수 있다.

위의 예제로 후보키를 찾아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
R(A, B, C, D, E, F)
F = {A→B, BC→D, E→C, D→A}

[1단계] 속성 분류

  FD 왼쪽에 등장: A, B, C, E, D      →  {A, B, C, D, E}
  FD 오른쪽에 등장: B, D, C, A        →  {A, B, C, D}

  L (왼쪽만)  : E    (오른쪽에 등장하지 않음)
  R (오른쪽만) : 없음
  LR (양쪽)    : A, B, C, D
  N (미등장)   : F    (어디에도 없음)

[2단계] 필수 속성 결정

  L과 N은 반드시 후보키에 포함 → {E, F}는 모든 후보키에 반드시 포함

[3단계] 필수 속성의 폐쇄 계산

  {E, F}⁺ 계산:
    초기: {E, F}
    E → C 적용: {C, E, F}
    BC → D: B 없음 → 불가
    A → B: A 없음 → 불가
    D → A: D 없음 → 불가
    → {E, F}⁺ = {C, E, F}

  {C, E, F} ≠ R  →  {E, F}만으로는 슈퍼키가 아님
  → LR 속성 중 일부를 추가해야 함

[4단계] LR 속성을 하나씩 추가하여 확인

  {A, E, F}⁺ 계산:
    초기: {A, E, F}
    A → B: {A, B, E, F}
    E → C: {A, B, C, E, F}
    BC → D: {A, B, C, D, E, F}  ← 전체!
    → {A, E, F}⁺ = R  →  슈퍼키!

  {B, E, F}⁺ 계산:
    초기: {B, E, F}
    E → C: {B, C, E, F}
    BC → D: {B, C, D, E, F}
    D → A: {A, B, C, D, E, F}  ← 전체!
    → {B, E, F}⁺ = R  →  슈퍼키!

  {C, E, F}⁺ = {C, E, F} (이미 계산)  →  슈퍼키 아님
  {D, E, F}⁺ 계산:
    초기: {D, E, F}
    D → A: {A, D, E, F}
    A → B: {A, B, D, E, F}
    E → C: {A, B, C, D, E, F}  ← 전체!
    → {D, E, F}⁺ = R  →  슈퍼키!

[5단계] 최소성 검증

  {A, E, F}: {E,F}⁺ ≠ R, {A,F}⁺ = {A,B,F} ≠ R, {A,E}⁺ = {A,B,C,D,E} ≠ R
    → 어떤 진부분집합도 슈퍼키가 아님 → 후보키!

  {B, E, F}: {E,F}⁺ ≠ R, {B,F}⁺ = {B,F} ≠ R, {B,E}⁺ = {A,B,C,D,E} ≠ R
    → 어떤 진부분집합도 슈퍼키가 아님 → 후보키!

  {D, E, F}: {E,F}⁺ ≠ R, {D,F}⁺ = {A,B,D,F} ≠ R, {D,E}⁺ = {A,B,C,D,E} ≠ R
    → 어떤 진부분집합도 슈퍼키가 아님 → 후보키!

최종 결과: 후보키 = {A,E,F}, {B,E,F}, {D,E,F}

이처럼 속성 폐쇄 알고리즘은 정규화 과정에서 후보키를 식별하고, 함수 종속성을 검증하는 데 핵심적인 도구다. 면접에서 “후보키를 어떻게 찾나요?”라는 질문에 이 절차를 설명하면 이론적 깊이를 보여줄 수 있다.


3. 정규형 (Normal Forms)

정규화는 낮은 정규형에서 높은 정규형으로 단계적으로 진행한다. 각 정규형은 이전 정규형의 조건을 포함하며, 추가 조건을 만족해야 한다.

1
2
3
비정규형 → 1NF → 2NF → 3NF → BCNF → 4NF → 5NF
                                ↑
                          실무에서 대부분 여기까지

3.1 제1정규형 (1NF) — 원자값

정의: 릴레이션의 모든 속성 값이 원자값(Atomic Value)을 가져야 한다. 즉, 하나의 셀에 하나의 값만 들어가야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
< 1NF 위반 >
┌─────────┬──────┬──────────────────────┐
│ 학생ID   │ 이름  │ 수강과목               │
├─────────┼──────┼──────────────────────┤
│ S001    │ 홍길동 │ 자료구조, 알고리즘       │  ← 다중 값!
│ S002    │ 김영희 │ 자료구조, 회로이론       │  ← 다중 값!
└─────────┴──────┴──────────────────────┘

< 1NF 적용 후 >
┌─────────┬──────┬──────────┐
│ 학생ID   │ 이름  │ 수강과목   │
├─────────┼──────┼──────────┤
│ S001    │ 홍길동 │ 자료구조   │
│ S001    │ 홍길동 │ 알고리즘   │
│ S002    │ 김영희 │ 자료구조   │
│ S002    │ 김영희 │ 회로이론   │
└─────────┴──────┴──────────┘
기본키: {학생ID, 수강과목}

1NF를 만족하면 반복 그룹이나 다중 값이 제거되지만, 여전히 이상 현상이 발생할 수 있다.

3.2 제2정규형 (2NF) — 부분 종속 제거

정의: 1NF를 만족하고, 모든 비키(non-key) 속성이 기본키에 완전 함수 종속해야 한다. 즉, 부분 함수 종속이 없어야 한다.

2NF 위반은 기본키가 복합키(2개 이상의 속성으로 구성)일 때만 발생한다. 기본키가 단일 속성이면 부분 종속이 불가능하므로 자동으로 2NF를 만족한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
< 2NF 위반 — 부분 함수 종속 존재 >

[수강 테이블]
기본키: {학생ID, 과목코드}

┌─────────┬───────────┬──────┬──────────┬───────┬───────┐
│ 학생ID   │ 과목코드    │ 이름  │ 학과      │ 과목명  │ 성적   │
├─────────┼───────────┼──────┼──────────┼───────┼───────┤
│ S001    │ CS101     │ 홍길동 │ 컴퓨터공학 │ 자료구조│ A+    │
│ S001    │ CS201     │ 홍길동 │ 컴퓨터공학 │ 알고리즘│ B+    │
│ S002    │ CS101     │ 김영희 │ 전자공학   │ 자료구조│ A     │
└─────────┴───────────┴──────┴──────────┴───────┴───────┘

함수 종속성 분석:
{학생ID, 과목코드} → 성적          ✅ 완전 함수 종속
학생ID → 이름, 학과               ❌ 부분 함수 종속 (과목코드 불필요)
과목코드 → 과목명                  ❌ 부분 함수 종속 (학생ID 불필요)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
< 2NF 적용 — 부분 종속 제거를 위해 테이블 분해 >

[학생 테이블]          [과목 테이블]        [수강 테이블]
┌───────┬──────┬────┐ ┌───────┬──────┐  ┌───────┬───────┬────┐
│학생ID │ 이름  │학과 │ │과목코드│과목명 │  │학생ID │과목코드│성적 │
├───────┼──────┼────┤ ├───────┼──────┤  ├───────┼───────┼────┤
│S001   │홍길동 │컴공 │ │CS101 │자료구조│  │S001   │CS101  │A+  │
│S002   │김영희 │전자 │ │CS201 │알고리즘│  │S001   │CS201  │B+  │
└───────┴──────┴────┘ └───────┴──────┘  │S002   │CS101  │A   │
                                        └───────┴───────┴────┘

각 테이블의 비키 속성이 기본키에 완전 함수 종속 → 2NF 만족!
학생ID → {이름, 학과}       ✅
과목코드 → {과목명}         ✅
{학생ID, 과목코드} → {성적}  ✅

3.3 제3정규형 (3NF) — 이행 종속 제거

정의: 2NF를 만족하고, 모든 비키 속성이 기본키에 대해 이행적 함수 종속이 없어야 한다. 즉, 비키 속성이 다른 비키 속성을 결정하면 안 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
< 3NF 위반 — 이행 함수 종속 존재 >

[학생 테이블]
기본키: 학생ID

┌─────────┬──────┬──────────┬────────────┐
│ 학생ID   │ 이름  │ 학과코드   │ 학과사무실    │
├─────────┼──────┼──────────┼────────────┤
│ S001    │ 홍길동 │ CS       │ 공학관 301  │
│ S002    │ 김영희 │ EE       │ 전자관 201  │
│ S003    │ 이철수 │ CS       │ 공학관 301  │  ← 중복!
└─────────┴──────┴──────────┴────────────┘

함수 종속성:
학생ID → 학과코드              ✅ 직접 종속
학과코드 → 학과사무실            (비키 → 비키)
학생ID → 학과코드 → 학과사무실   ❌ 이행 함수 종속!

문제: 학과사무실이 변경되면 해당 학과의 모든 학생 행을 갱신해야 함
1
2
3
4
5
6
7
8
9
10
11
12
< 3NF 적용 — 이행 종속 제거를 위해 테이블 분해 >

[학생 테이블]               [학과 테이블]
┌───────┬──────┬──────┐    ┌──────┬──────────┐
│학생ID │ 이름  │학과코드│    │학과코드│ 학과사무실 │
├───────┼──────┼──────┤    ├──────┼──────────┤
│S001   │홍길동 │CS    │    │CS    │ 공학관 301│
│S002   │김영희 │EE    │    │EE    │ 전자관 201│
│S003   │이철수 │CS    │    └──────┴──────────┘
└───────┴──────┴──────┘

학과사무실 변경 → 학과 테이블 1행만 수정

3.4 BCNF (Boyce-Codd Normal Form) — 결정자가 후보키

정의: 3NF를 만족하고, 모든 결정자(Determinant)후보키(Candidate Key)여야 한다.

3NF와 BCNF의 차이는 미묘하지만 중요하다. 3NF는 “비키 속성이 기본키에 이행 종속하면 안 된다”이고, BCNF는 “모든 함수 종속의 결정자가 후보키여야 한다”이다. BCNF가 더 엄격하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
< BCNF 위반 — 후보키가 아닌 결정자 존재 >

[수강지도 테이블]
한 학생은 과목당 한 교수만 수강 가능
한 교수는 한 과목만 담당

┌─────────┬───────┬───────┐
│ 학생ID   │ 과목   │ 교수   │
├─────────┼───────┼───────┤
│ S001    │ 자료구조│ 김교수 │
│ S001    │ 알고리즘│ 이교수 │
│ S002    │ 자료구조│ 김교수 │
│ S003    │ 알고리즘│ 이교수 │
└─────────┴───────┴───────┘

후보키: {학생ID, 과목} 또는 {학생ID, 교수}

함수 종속성:
{학생ID, 과목} → 교수     ✅ 후보키 → OK
{학생ID, 교수} → 과목     ✅ 후보키 → OK
교수 → 과목              ❌ 교수는 후보키가 아닌데 결정자! → BCNF 위반

문제: 김교수가 자료구조→운영체제로 담당 과목 변경 시,
      김교수가 포함된 모든 행(S001, S002)을 갱신해야 함 → 갱신 이상
1
2
3
4
5
6
7
8
9
10
11
12
13
14
< BCNF 적용 — 테이블 분해 >

[수강 테이블]              [교수담당 테이블]
┌───────┬──────┐          ┌──────┬───────┐
│학생ID │ 교수  │          │ 교수  │ 과목   │
├───────┼──────┤          ├──────┼───────┤
│S001   │김교수 │          │김교수 │자료구조│
│S001   │이교수 │          │이교수 │알고리즘│
│S002   │김교수 │          └──────┴───────┘
│S003   │이교수 │
└───────┴──────┘

모든 결정자가 후보키 → BCNF 만족!
교수 → 과목: 교수가 교수담당 테이블의 기본키(후보키) ✅

3.5 정규형 요약

1
2
3
4
5
6
7
8
9
10
┌──────┬────────────────────────────┬──────────────────────────┐
│정규형 │          조건               │       제거하는 문제        │
├──────┼────────────────────────────┼──────────────────────────┤
│ 1NF  │ 모든 속성이 원자값            │ 반복 그룹, 다중 값         │
│ 2NF  │ 1NF + 부분 함수 종속 제거     │ 복합키의 일부에 대한 종속   │
│ 3NF  │ 2NF + 이행 함수 종속 제거     │ 비키→비키 종속            │
│ BCNF │ 모든 결정자가 후보키          │ 후보키가 아닌 결정자       │
│ 4NF  │ 다치 종속(MVD) 제거          │ 다중 값 종속              │
│ 5NF  │ 조인 종속(JD) 제거           │ 무손실 분해가 불가한 종속   │
└──────┴────────────────────────────┴──────────────────────────┘

3.6 무손실 분해(Lossless Decomposition) 검증

정규화의 목적은 이상 현상을 제거하기 위해 테이블을 분해하는 것이다. 그런데 테이블을 아무렇게나 분해하면 정보가 손실될 수 있다. 분해된 두 테이블을 다시 조인했을 때 원래 없던 튜플이 생기거나(가짜 튜플, spurious tuple), 원래 있던 튜플이 사라지는 문제가 발생할 수 있는 것이다. 이것을 손실 분해(lossy decomposition)라 하고, 정보 손실 없이 분해하는 것을 무손실 분해(lossless decomposition) 또는 무손실 조인 분해(lossless-join decomposition)라 한다.

정규화에서 테이블을 분해할 때는 반드시 무손실 분해가 보장되어야 한다. 이것은 정규화의 전제 조건이다. 아무리 높은 정규형을 달성하더라도, 분해 과정에서 정보가 손실되면 의미가 없다.

손실 분해 vs 무손실 분해

구체적인 예제로 두 가지 분해의 차이를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[원본 테이블] R(학생, 과목, 교수)
┌──────┬───────┬──────┐
│ 학생  │ 과목   │ 교수  │
├──────┼───────┼──────┤
│ 홍길동 │ DB    │ 김교수│
│ 홍길동 │ OS    │ 이교수│
│ 김영희 │ DB    │ 이교수│
└──────┴───────┴──────┘

< 손실 분해 — 잘못된 분해 >
R1(학생, 과목)과 R2(학생, 교수)로 분해

R1(학생, 과목)          R2(학생, 교수)
┌──────┬───────┐      ┌──────┬──────┐
│ 학생  │ 과목   │      │ 학생  │ 교수  │
├──────┼───────┤      ├──────┼──────┤
│ 홍길동 │ DB    │      │ 홍길동 │ 김교수│
│ 홍길동 │ OS    │      │ 홍길동 │ 이교수│
│ 김영희 │ DB    │      │ 김영희 │ 이교수│
└──────┴───────┘      └──────┴──────┘

R1 ⋈ R2 (학생으로 자연 조인):
┌──────┬───────┬──────┐
│ 학생  │ 과목   │ 교수  │
├──────┼───────┼──────┤
│ 홍길동 │ DB    │ 김교수│  ← 원래 있던 튜플 ✅
│ 홍길동 │ DB    │ 이교수│  ← 가짜 튜플! ❌ (홍길동은 DB를 이교수에게 안 배움)
│ 홍길동 │ OS    │ 김교수│  ← 가짜 튜플! ❌ (홍길동은 OS를 김교수에게 안 배움)
│ 홍길동 │ OS    │ 이교수│  ← 원래 있던 튜플 ✅
│ 김영희 │ DB    │ 이교수│  ← 원래 있던 튜플 ✅
└──────┴───────┴──────┘

→ 원본 3행인데 조인 결과 5행 → 가짜 튜플 2개 생성! → 손실 분해!

위 예제에서 볼 수 있듯이, 잘못된 속성 조합으로 분해하면 조인 시 원래 없던 데이터가 만들어진다. 이를 방지하려면 분해 시 공통 속성이 최소한 한쪽 릴레이션의 키(슈퍼키)가 되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
< 무손실 분해 — 올바른 분해 >

교수 → 과목 이라는 FD가 존재한다고 가정하면:
R1(학생, 교수)과 R2(교수, 과목)로 분해

R1(학생, 교수)          R2(교수, 과목)
┌──────┬──────┐       ┌──────┬───────┐
│ 학생  │ 교수  │       │ 교수  │ 과목   │
├──────┼──────┤       ├──────┼───────┤
│ 홍길동 │ 김교수│       │ 김교수 │ DB    │
│ 홍길동 │ 이교수│       │ 이교수 │ OS    │
│ 김영희 │ 이교수│       └──────┴───────┘
└──────┴──────┘
(주의: 이교수가 DB도 가르친다면 R2에 행 추가 필요)

R1 ∩ R2의 공통 속성: {교수}
교수 → 과목이므로 {교수}는 R2의 슈퍼키 → 무손실 분해!

R1 ⋈ R2:
┌──────┬──────┬───────┐
│ 학생  │ 교수  │ 과목   │
├──────┼──────┼───────┤
│ 홍길동 │ 김교수│ DB    │  ✅
│ 홍길동 │ 이교수│ OS    │  ✅
│ 김영희 │ 이교수│ OS    │  ✅
└──────┴──────┴───────┘

→ 원본과 동일! → 무손실 분해!

무손실 분해 검증 조건

릴레이션 R을 R1과 R2로 분해할 때, 다음 조건 중 하나라도 만족하면 무손실 분해이다. 이를 Heath의 정리라 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌────────────────────────────────────────────────────────────┐
│            무손실 분해 검증 (Heath's Theorem)                  │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  R을 R1과 R2로 분해할 때, 다음 중 하나를 만족하면 무손실:       │
│                                                            │
│  (R1 ∩ R2) → (R1 - R2)                                    │
│  또는                                                       │
│  (R1 ∩ R2) → (R2 - R1)                                    │
│                                                            │
│  즉, 공통 속성이 R1 또는 R2의 나머지 속성을 결정하면 된다.       │
│  다시 말해, 공통 속성이 R1 또는 R2의 슈퍼키이면 무손실이다.      │
│                                                            │
│  직관: 공통 속성(조인 키)이 한쪽의 키이면,                      │
│        조인 시 한 행이 여러 행과 매칭되어                       │
│        가짜 튜플이 생기는 일이 없다.                            │
└────────────────────────────────────────────────────────────┘

무손실 분해 검증 예제

앞서 BCNF 분해에서 다룬 수강지도 테이블을 검증해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
원본: R(학생ID, 과목, 교수)
FD: {학생ID, 과목} → 교수, 교수 → 과목

분해: R1(학생ID, 교수), R2(교수, 과목)

검증:
  R1 ∩ R2 = {교수}           (공통 속성)
  R2 - R1 = {과목}           (R2에만 있는 속성)

  교수 → 과목 이 FD가 존재하므로:
  (R1 ∩ R2) → (R2 - R1) 성립!
  즉, {교수} → {과목}

  ∴ 무손실 분해 ✅

3NF 합성 알고리즘 — 무손실 + 종속성 보존 보장

BCNF 분해는 무손실을 보장하지만, 함수 종속성 보존(dependency preservation)을 보장하지 못할 수 있다. 종속성 보존이란 분해된 테이블들만으로 원래의 모든 함수 종속성을 검증할 수 있어야 한다는 것이다. 종속성이 보존되지 않으면, 데이터 무결성을 검증하기 위해 여러 테이블을 조인해야 하는 비용이 발생한다.

3NF 합성 알고리즘은 무손실 분해종속성 보존동시에 보장하는 알고리즘이다. 이것이 실무에서 3NF가 BCNF보다 자주 선택되는 이유 중 하나다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
알고리즘: 3NF_SYNTHESIS(R, F)
────────────────────────────────
입력: 릴레이션 R, 함수 종속성 집합 F
출력: 3NF를 만족하는 무손실, 종속성 보존 분해

1. F의 최소 커버(Minimal Cover) Fc를 구한다.
   - 오른쪽을 단일 속성으로 분해 (A → BC를 A → B, A → C로)
   - 왼쪽에서 불필요한 속성 제거
   - 불필요한 FD 제거

2. Fc의 각 FD X → A에 대해 릴레이션 Ri = XA를 생성한다.
   (같은 X를 가진 FD들은 하나의 릴레이션으로 합침)

3. 생성된 릴레이션 중 R의 후보키를 포함하는 것이 없으면,
   R의 후보키를 속성으로 가지는 릴레이션을 추가한다.
   → 이 단계가 무손실을 보장하는 핵심!

4. 다른 릴레이션에 포함되는 릴레이션을 제거한다.

이 알고리즘에서 3단계가 특히 중요하다. 후보키를 포함하는 릴레이션이 반드시 존재해야 전체를 무손실로 복원할 수 있다.

BCNF 분해 알고리즘 — 무손실 보장, 종속성 보존은 미보장

BCNF 분해는 BCNF를 위반하는 FD를 찾아 반복적으로 분해하는 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
알고리즘: BCNF_DECOMPOSITION(R, F)
────────────────────────────────────
입력: 릴레이션 R, 함수 종속성 집합 F
출력: 모든 릴레이션이 BCNF를 만족하는 무손실 분해

1. result ← {R}
2. WHILE result에 BCNF를 만족하지 않는 Ri가 존재
3.    Ri에서 BCNF를 위반하는 FD X → Y를 찾는다
      (X가 Ri의 슈퍼키가 아닌 FD)
4.    Ri를 다음과 같이 분해:
      - R1 = X ∪ Y         (위반 FD의 속성들)
      - R2 = Ri - Y + X    (원래에서 Y를 빼고 X는 유지)
5.    result에서 Ri를 제거하고 R1, R2를 추가
6. END WHILE
7. RETURN result
1
2
3
4
5
6
7
8
9
10
11
12
┌──────────────────────────────────────────────────────────────┐
│       3NF 합성 vs BCNF 분해 비교                               │
├────────────────┬──────────────────┬──────────────────────────┤
│     항목        │   3NF 합성        │    BCNF 분해             │
├────────────────┼──────────────────┼──────────────────────────┤
│ 달성 정규형     │ 3NF               │ BCNF                     │
│ 무손실 보장     │ ✅ 보장            │ ✅ 보장                   │
│ 종속성 보존     │ ✅ 보장            │ ❌ 보장하지 못할 수 있음    │
│ 알고리즘 방식   │ 합성(bottom-up)    │ 분해(top-down)            │
│ 실무 선택 기준  │ 종속성 보존이       │ 더 강한 정규화가          │
│                │ 중요할 때           │ 필요할 때                 │
└────────────────┴──────────────────┴──────────────────────────┘

면접에서 “BCNF가 항상 3NF보다 좋은가?”라는 질문이 나오면, 이 트레이드오프를 설명하면 된다. BCNF는 더 엄격한 정규화를 달성하지만 종속성 보존을 잃을 수 있고, 3NF 합성은 약간 느슨하지만 무손실과 종속성 보존을 모두 보장한다. 실무에서는 대부분 3NF로 충분하며, BCNF가 반드시 필요한 경우(후보키가 아닌 결정자로 인한 이상 현상이 실제로 문제가 될 때)에만 BCNF 분해를 적용하고, 잃어버린 종속성은 애플리케이션 레벨에서 검증한다.


4. 4NF와 5NF (간략)

4NF와 5NF는 실무에서 직접 적용하는 경우가 드물지만, 면접에서 간혹 물어보므로 개념을 알아두자.

4.1 제4정규형 (4NF) — 다치 종속 제거

정의: BCNF를 만족하고, 다치 종속(MVD, Multi-Valued Dependency)이 없어야 한다.

다치 종속이란 하나의 속성이 다른 속성의 값 집합을 결정하는 관계다. X →→ Y로 표기한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
< 4NF 위반 — 다치 종속 >

[교수 테이블]
한 교수가 여러 과목을 가르칠 수 있고, 여러 취미를 가질 수 있다.
과목과 취미 사이에는 관련이 없다.

┌──────┬───────┬──────┐
│ 교수  │ 과목   │ 취미  │
├──────┼───────┼──────┤
│김교수 │자료구조│ 등산  │
│김교수 │자료구조│ 독서  │
│김교수 │알고리즘│ 등산  │
│김교수 │알고리즘│ 독서  │  ← 모든 조합이 존재해야 함 (중복 폭발)
└──────┴───────┴──────┘

교수 →→ 과목 (교수가 과목 집합을 결정)
교수 →→ 취미 (교수가 취미 집합을 결정)

< 4NF 적용 >

[교수과목]            [교수취미]
┌──────┬───────┐    ┌──────┬──────┐
│ 교수  │ 과목   │    │ 교수  │ 취미  │
├──────┼───────┤    ├──────┼──────┤
│김교수 │자료구조│    │김교수 │ 등산  │
│김교수 │알고리즘│    │김교수 │ 독서  │
└──────┴───────┘    └──────┴──────┘

4.2 제5정규형 (5NF) — 조인 종속 제거

5NF는 릴레이션을 더 이상 정보 손실 없이 분해할 수 없는 상태이다. 3개 이상의 테이블로 분해해야만 이상 현상을 제거할 수 있는 특수한 경우에 해당한다. 실무에서 5NF까지 고려하는 경우는 거의 없다.


5. 정규화 종합 예제

실제 테이블을 비정규형에서 BCNF까지 단계적으로 정규화해보자.

원본 테이블 (비정규형)

온라인 서점의 주문 데이터이다.

1
2
3
4
5
6
7
8
[주문 테이블]
┌──────┬──────┬────────┬──────────────────────────────────────┐
│주문ID│고객명 │고객전화  │ 주문내역                               │
├──────┼──────┼────────┼──────────────────────────────────────┤
│O001  │홍길동 │010-1234│ {자바의 정석, 2권, 30000원},            │
│      │      │        │ {스프링 인 액션, 1권, 35000원}          │
│O002  │김영희 │010-5678│ {자바의 정석, 1권, 30000원}             │
└──────┴──────┴────────┴──────────────────────────────────────┘

Step 1: 1NF (원자값)

1
2
3
4
5
6
7
8
9
10
[주문 테이블] — 1NF
┌──────┬──────┬────────┬──────────┬────┬──────┐
│주문ID│고객명 │고객전화  │ 도서명     │수량│ 단가  │
├──────┼──────┼────────┼──────────┼────┼──────┤
│O001  │홍길동 │010-1234│ 자바의 정석│ 2  │30000 │
│O001  │홍길동 │010-1234│ 스프링인액션│ 1  │35000 │
│O002  │김영희 │010-5678│ 자바의 정석│ 1  │30000 │
└──────┴──────┴────────┴──────────┴────┴──────┘

기본키: {주문ID, 도서명}

Step 2: 2NF (부분 종속 제거)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
함수 종속성 분석:
주문ID → 고객명, 고객전화       ❌ 부분 종속 (도서명 불필요)
도서명 → 단가                   ❌ 부분 종속 (주문ID 불필요)
{주문ID, 도서명} → 수량          ✅ 완전 종속

분해:

[주문 테이블]                [도서 테이블]         [주문상세 테이블]
┌──────┬──────┬────────┐  ┌──────────┬──────┐  ┌──────┬──────────┬────┐
│주문ID│고객명 │고객전화  │  │ 도서명    │ 단가  │  │주문ID│ 도서명    │수량│
├──────┼──────┼────────┤  ├──────────┼──────┤  ├──────┼──────────┼────┤
│O001  │홍길동 │010-1234│  │자바의 정석│30000 │  │O001  │자바의 정석│ 2  │
│O002  │김영희 │010-5678│  │스프링인액션│35000 │  │O001  │스프링인액션│ 1  │
└──────┴──────┴────────┘  └──────────┴──────┘  │O002  │자바의 정석│ 1  │
                                               └──────┴──────────┴────┘

Step 3: 3NF (이행 종속 제거)

현재 주문 테이블에서 이행 종속이 있는지 확인한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
주문ID → 고객명       직접 종속
주문ID → 고객전화     직접 종속

만약 고객이 여러 주문을 할 수 있고, 고객 정보가 중복된다면?
주문ID → 고객ID → 고객명, 고객전화  ← 이행 종속!

더 정확한 모델:

[주문 테이블]          [고객 테이블]          [도서]  [주문상세]
┌──────┬──────┐     ┌──────┬──────┬────────┐
│주문ID│고객ID │     │고객ID│고객명 │고객전화  │  (이전과 동일)
├──────┼──────┤     ├──────┼──────┼────────┤
│O001  │C001  │     │C001  │홍길동 │010-1234│
│O002  │C002  │     │C002  │김영희 │010-5678│
└──────┴──────┘     └──────┴──────┴────────┘

→ 고객 정보 변경 시 고객 테이블 1행만 수정 → 갱신 이상 해결

최종 ERD

1
2
3
4
5
6
[고객] 1 ──── N [주문] 1 ──── N [주문상세] N ──── 1 [도서]

고객 (고객ID PK, 고객명, 고객전화)
주문 (주문ID PK, 고객ID FK, 주문일시)
주문상세 (주문ID PK/FK, 도서ID PK/FK, 수량)
도서 (도서ID PK, 도서명, 단가)

6. 반정규화 (Denormalization)

정규화가 데이터 무결성을 위한 것이라면, 반정규화는 조회 성능 최적화를 위한 의도적인 정규화 역행이다.

6.1 반정규화가 필요한 상황

1
2
3
4
5
6
7
8
9
10
11
12
13
14
정규화된 구조의 문제:
[주문] ← JOIN → [고객] ← JOIN → [주문상세] ← JOIN → [도서]

"최근 주문 목록과 고객명, 도서명을 한 번에 조회"하려면 4개 테이블 JOIN이 필요

SELECT o.주문ID, c.고객명, b.도서명, od.수량, b.단가
FROM 주문 o
JOIN 고객 c ON o.고객ID = c.고객ID
JOIN 주문상세 od ON o.주문ID = od.주문ID
JOIN 도서 b ON od.도서ID = b.도서ID
ORDER BY o.주문일시 DESC
LIMIT 20;

→ 데이터가 수천만 건이면 JOIN 비용이 매우 높음

6.2 반정규화 판단 기준

반정규화는 트레이드오프다. 무분별한 반정규화는 오히려 문제를 키운다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌────────────────────────────────────────────────────────────┐
│                반정규화 판단 체크리스트                       │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  반정규화를 고려해야 하는 경우:                                │
│  ✅ 조회 빈도가 매우 높고, 갱신 빈도가 낮은 데이터             │
│  ✅ JOIN 비용이 쿼리 성능의 병목인 경우                       │
│  ✅ 인덱스 최적화로도 충분한 성능을 얻지 못한 경우              │
│  ✅ 집계 쿼리(SUM, COUNT 등)가 빈번한 경우                   │
│                                                            │
│  반정규화를 피해야 하는 경우:                                  │
│  ❌ 아직 인덱스 최적화를 시도하지 않은 경우                    │
│  ❌ 데이터 변경이 빈번한 테이블                               │
│  ❌ 데이터 정합성이 매우 중요한 도메인 (금융, 의료 등)          │
│  ❌ 현재 성능에 문제가 없는 경우 (조기 최적화 금지)             │
│                                                            │
│  순서: 정규화 → 인덱스 최적화 → 쿼리 튜닝 → 캐시 → 반정규화   │
│                                                 ↑ 마지막 수단│
└────────────────────────────────────────────────────────────┘

6.3 반정규화 기법

기법 1: 중복 칼럼 추가

자주 조회되는 다른 테이블의 칼럼을 중복 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
정규화 상태:
[주문] → JOIN → [고객] (고객명 조회)

반정규화:
[주문] 테이블에 고객명 칼럼 추가

┌──────┬──────┬──────┬────────┐
│주문ID│고객ID │고객명  │주문일시  │  ← 고객명 중복 저장
└──────┴──────┴──────┴────────┘

장점: 고객명 조회 시 JOIN 불필요
단점: 고객이 이름을 변경하면 주문 테이블도 갱신 필요
적합: 이름 변경이 거의 없고, 조회가 매우 빈번한 경우

기법 2: 파생 칼럼 추가 (계산 결과 저장)

매번 계산해야 하는 집계 값을 미리 저장한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
정규화 상태:
주문의 총 금액 = SUM(주문상세.수량 * 도서.단가)  ← 매번 JOIN + 집계

반정규화:
[주문] 테이블에 총금액 칼럼 추가

┌──────┬──────┬────────┬────────┐
│주문ID│고객ID │총금액    │주문일시  │  ← 계산 결과 저장
└──────┴──────┴────────┴────────┘

주문 시: 총금액을 계산하여 저장
수정 시: 주문상세 변경 시 총금액도 함께 갱신

주의: 애플리케이션에서 총금액 동기화를 보장해야 함
      → 트리거(Trigger) 또는 애플리케이션 로직으로 관리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// JPA에서 파생 칼럼 관리
@Entity
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;

    private int totalAmount;  // 파생 칼럼

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderItem> items = new ArrayList<>();

    // 주문 항목 추가 시 총금액 동기화
    public void addItem(OrderItem item) {
        items.add(item);
        item.setOrder(this);
        recalculateTotalAmount();
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
        item.setOrder(null);
        recalculateTotalAmount();
    }

    private void recalculateTotalAmount() {
        this.totalAmount = items.stream()
            .mapToInt(item -> item.getQuantity() * item.getUnitPrice())
            .sum();
    }
}

기법 3: 테이블 병합 (1:1 관계)

1:1 관계의 두 테이블을 하나로 합친다.

1
2
3
4
5
6
7
8
9
10
정규화 상태:
[사용자] 1 ──── 1 [사용자 프로필]

반정규화:
┌──────┬──────┬──────┬──────┬────────┬────────┐
│유저ID │이름   │이메일 │ 소개  │프로필사진│가입일   │
└──────┴──────┴──────┴──────┴────────┴────────┘

적합: 프로필 조회 시 항상 함께 조회되는 경우
     두 테이블의 생명주기가 동일한 경우

기법 4: 테이블 분할

하나의 테이블을 수직 또는 수평으로 분할한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
< 수직 분할 (Vertical Partitioning) >
자주 조회되는 칼럼과 드물게 조회되는 칼럼을 분리

[게시글_기본]                         [게시글_상세]
┌──────┬──────┬──────┬──────┐       ┌──────┬──────────────┐
│게시글ID│제목  │작성자 │작성일│       │게시글ID│본문 (BLOB)   │
└──────┴──────┴──────┴──────┘       └──────┴──────────────┘
  ↑ 목록 조회 시 이 테이블만 스캔        ↑ 상세 조회 시에만 접근
    (I/O 감소)

< 수평 분할 (Horizontal Partitioning / Sharding) >
데이터를 행 기준으로 분리

[주문_2025]  ← 2025년 주문 데이터
[주문_2026]  ← 2026년 주문 데이터

또는 MySQL 파티셔닝:
CREATE TABLE orders (
    id BIGINT,
    order_date DATE,
    ...
) PARTITION BY RANGE (YEAR(order_date)) (
    PARTITION p2025 VALUES LESS THAN (2026),
    PARTITION p2026 VALUES LESS THAN (2027)
);

기법 5: 집계 테이블 (요약 테이블)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
매일 반복되는 통계 쿼리:
SELECT DATE(order_date) AS day, COUNT(*), SUM(total_amount)
FROM orders
GROUP BY DATE(order_date);
→ 주문 수가 수천만 건이면 매번 풀 스캔

반정규화: 집계 테이블 생성

[일별주문통계]
┌──────────┬──────┬──────────┬──────────┐
│ 날짜      │주문건수│총주문금액  │평균주문금액│
├──────────┼──────┼──────────┼──────────┤
│2026-03-09│ 1523 │45690000  │ 29999    │
│2026-03-10│ 1847 │55410000  │ 29999    │
└──────────┴──────┴──────────┴──────────┘

갱신 방법:
- 배치 스케줄러로 매일/매시간 집계
- 이벤트 기반으로 실시간 갱신
- 트리거로 자동 갱신
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Spring Scheduler를 활용한 집계 테이블 갱신
@Service
@RequiredArgsConstructor
public class OrderStatisticsService {
    private final OrderRepository orderRepository;
    private final DailyOrderStatsRepository statsRepository;

    @Scheduled(cron = "0 0 1 * * *")  // 매일 새벽 1시
    @Transactional
    public void aggregateDailyStats() {
        LocalDate yesterday = LocalDate.now().minusDays(1);

        OrderStatsProjection stats = orderRepository.aggregateByDate(yesterday);

        DailyOrderStats dailyStats = statsRepository.findByDate(yesterday)
            .orElse(new DailyOrderStats(yesterday));

        dailyStats.update(
            stats.getOrderCount(),
            stats.getTotalAmount(),
            stats.getAverageAmount()
        );

        statsRepository.save(dailyStats);
    }
}

7. JPA/Spring 환경에서의 정규화 실무

7.1 정규화와 엔티티 설계

JPA 엔티티를 설계할 때 정규화 원칙이 자연스럽게 적용된다. 연관관계 매핑이 정규화된 테이블 간의 관계를 표현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 3NF 기반 엔티티 설계 예시

@Entity
public class Customer {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    private String phone;

    @OneToMany(mappedBy = "customer")
    private List<Order> orders = new ArrayList<>();
}

@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id", nullable = false)
    private Customer customer;

    @Column(nullable = false)
    private LocalDateTime orderDate;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();
}

@Entity
public class OrderItem {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id", nullable = false)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "book_id", nullable = false)
    private Book book;

    @Column(nullable = false)
    private int quantity;

    @Column(nullable = false)
    private int unitPrice;  // 주문 시점의 가격 (도서 가격 변경에 독립적)
}

@Entity
public class Book {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String title;

    @Column(nullable = false)
    private int price;
}

7.2 반정규화와 JPA — @Formula, @SecondaryTable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// @Formula — 파생 칼럼을 SQL로 계산 (읽기 전용)
@Entity
@Table(name = "orders")
public class Order {
    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Customer customer;

    @OneToMany(mappedBy = "order")
    private List<OrderItem> items;

    // 서브쿼리로 총금액 계산 (DB에 칼럼이 없어도 됨)
    @Formula("(SELECT COALESCE(SUM(oi.quantity * oi.unit_price), 0) " +
             "FROM order_item oi WHERE oi.order_id = id)")
    private int totalAmount;
}

// @SecondaryTable — 수직 분할된 테이블을 하나의 엔티티로 매핑
@Entity
@Table(name = "article")
@SecondaryTable(name = "article_content",
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "article_id"))
public class Article {
    @Id @GeneratedValue
    private Long id;

    private String title;
    private String author;
    private LocalDateTime createdAt;

    // article_content 테이블의 칼럼
    @Column(table = "article_content", columnDefinition = "TEXT")
    private String content;

    @Column(table = "article_content")
    private String summary;
}

7.3 읽기 전용 뷰와 DTO Projection

반정규화 대신 DB 뷰(View)나 DTO Projection을 활용하면 데이터 무결성을 유지하면서 조회 성능을 개선할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// DTO Projection — JOIN 결과를 직접 매핑 (N+1 문제 방지)
public interface OrderSummary {
    Long getOrderId();
    String getCustomerName();
    LocalDateTime getOrderDate();
    int getTotalAmount();
}

@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("SELECT o.id AS orderId, c.name AS customerName, " +
           "o.orderDate AS orderDate, " +
           "SUM(oi.quantity * oi.unitPrice) AS totalAmount " +
           "FROM Order o " +
           "JOIN o.customer c " +
           "JOIN o.items oi " +
           "GROUP BY o.id, c.name, o.orderDate " +
           "ORDER BY o.orderDate DESC")
    Page<OrderSummary> findOrderSummaries(Pageable pageable);

    // Native Query + DB View 활용
    @Query(value = "SELECT * FROM v_order_summary ORDER BY order_date DESC",
           nativeQuery = true)
    Page<OrderSummary> findOrderSummariesFromView(Pageable pageable);
}
1
2
3
4
5
6
7
8
9
10
11
12
-- DB View 정의 (반정규화 없이 JOIN 결과를 재사용)
CREATE VIEW v_order_summary AS
SELECT
    o.id AS order_id,
    c.name AS customer_name,
    o.order_date,
    COUNT(oi.id) AS item_count,
    SUM(oi.quantity * oi.unit_price) AS total_amount
FROM orders o
JOIN customer c ON o.customer_id = c.id
JOIN order_item oi ON o.id = oi.order_id
GROUP BY o.id, c.name, o.order_date;

7.4 Redis 캐시 — 반정규화의 대안

자주 조회되는 집계 데이터는 반정규화 대신 캐시로 처리하는 것이 더 안전할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
@RequiredArgsConstructor
public class OrderQueryService {
    private final OrderRepository orderRepository;

    @Cacheable(value = "orderSummary", key = "#pageable.pageNumber")
    public Page<OrderSummary> getOrderSummaries(Pageable pageable) {
        return orderRepository.findOrderSummaries(pageable);
    }

    @CacheEvict(value = "orderSummary", allEntries = true)
    @Transactional
    public Order createOrder(OrderCreateDto dto) {
        // 주문 생성 시 캐시 무효화
        return orderRepository.save(dto.toEntity());
    }
}

8. 정규화 vs 반정규화 — 의사결정 프레임워크

실무에서 “어디까지 정규화하고, 언제 반정규화할 것인가”를 판단하는 프레임워크이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌──────────────────────────────────────────────────────────────┐
│                   의사결정 흐름도                               │
│                                                              │
│  1. 논리적 설계: 최소 3NF(가능하면 BCNF)까지 정규화             │
│     │                                                        │
│  2. 물리적 설계: 예상 쿼리 패턴 분석                            │
│     │                                                        │
│  3. 성능 문제 발생?                                           │
│     │                                                        │
│     ├─ NO → 정규화 유지 (끝)                                  │
│     │                                                        │
│     └─ YES → 순서대로 시도:                                   │
│              ① 인덱스 최적화 (커버링 인덱스, 복합 인덱스)        │
│              ② 쿼리 튜닝 (실행 계획 분석, 서브쿼리→JOIN 변환)    │
│              ③ 캐시 적용 (Redis, 애플리케이션 캐시)             │
│              ④ DB 뷰 / Materialized View                     │
│              ⑤ 반정규화 (마지막 수단)                          │
│                                                              │
│  반정규화 적용 시 반드시 문서화:                                 │
│  - 어떤 정규형을 의도적으로 위반하는지                           │
│  - 데이터 정합성 유지 방안 (트리거/애플리케이션 로직)             │
│  - 성능 개선 측정 결과                                        │
└──────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────────────┬─────────────────┬────────────────────┐
│        항목           │    정규화        │    반정규화         │
├──────────────────────┼─────────────────┼────────────────────┤
│ 데이터 무결성         │ 높음             │ 관리 필요           │
│ 데이터 중복           │ 최소             │ 의도적 중복         │
│ 쓰기 성능            │ 좋음 (단일 갱신)  │ 복잡 (다중 갱신)    │
│ 읽기 성능            │ JOIN 필요        │ JOIN 감소           │
│ 저장 공간            │ 효율적           │ 중복으로 증가        │
│ 스키마 변경 유연성    │ 높음             │ 낮음               │
│ 개발 복잡도          │ 낮음             │ 동기화 로직 필요     │
│ 주로 적합한 시스템    │ OLTP             │ OLAP / 조회 위주   │
│                      │ (트랜잭션 위주)   │ (분석/리포팅)       │
└──────────────────────┴─────────────────┴────────────────────┘

9. 실무 설계에서 자주 헷갈리는 포인트

9.1 정규화는 논리 설계이고, 인덱스는 물리 설계다

면접에서 자주 나오는 실수는 정규화와 인덱스를 같은 층위에서 설명하는 것이다. 정규화는 테이블 구조를 어떻게 분해할지에 대한 논리 모델링이고, 인덱스는 특정 접근 패턴을 빠르게 만들기 위한 물리 저장 최적화다.

  • 정규화는 중복 제거와 무결성 확보가 목적이다.
  • 인덱스는 조회 성능 개선이 목적이다.
  • 정규화를 잘했다고 성능이 자동으로 좋아지지는 않는다.
  • 반대로 인덱스를 잘 잡아도 이상 현상 자체가 해결되지는 않는다.

9.2 정규화와 무결성 제약은 함께 가야 한다

정규화를 했더라도 PK, FK, UNIQUE, NOT NULL이 제대로 없으면 설계 품질이 떨어진다. 면접에서는 “정규형만 맞추면 끝이냐”를 자주 묻는데, 답은 아니다. 정규화는 구조를 잘게 나누는 것이고, 무결성 제약은 그 구조를 실제로 안전하게 유지하는 장치다.

예를 들어 사용자 이메일이 유일해야 한다면 단순히 user 테이블을 정규화하는 것만으로는 부족하고, UNIQUE(email)이 반드시 필요하다.

9.3 OLTP와 OLAP는 출발점이 다르다

  • OLTP는 짧고 빈번한 트랜잭션, 높은 정합성, 작은 단위 갱신이 중요해서 3NF/BCNF가 유리하다.
  • OLAP는 대용량 집계와 분석이 중요해서 스타 스키마 같은 반정규화 구조가 유리하다.

즉 “정규화가 항상 좋은가?”라는 질문에는 시스템의 목적을 먼저 구분해야 한다. 면접에서 이 관점을 먼저 꺼내면 설계 레벨이 높아 보인다.

9.4 정규화했다고 JOIN이 무조건 나쁜 것은 아니다

JOIN 자체가 나쁜 것이 아니라, 잘못된 인덱스 설계와 대량 스캔이 문제인 경우가 많다. 따라서 성능 이슈가 생겼을 때 바로 반정규화로 뛰면 안 되고, 다음 순서로 봐야 한다.

  1. 실행 계획이 예상대로 나오는가
  2. 조건절/정렬 기준에 맞는 인덱스가 있는가
  3. 불필요한 컬럼 조회가 많은가
  4. 캐시나 projection으로 해결 가능한가
  5. 그래도 안 되면 반정규화가 필요한가

9.5 면접 답변 프레임: “왜 그렇게 분해했나요?”

면접에서는 특정 정규형 정의보다도 “왜 이 테이블을 이렇게 분리했는가”를 묻는 경우가 많다. 그때는 아래 순서로 답하면 안정적이다.

  1. 어떤 함수 종속성이 있는지 식별했다.
  2. 그로 인해 어떤 이상 현상이 발생할 수 있는지 설명했다.
  3. 이를 제거하기 위해 어떤 기준으로 테이블을 분해했다.
  4. 분해 후 JOIN 비용이나 조회 패턴을 어떻게 보완할지까지 설명했다.

예시 답변:

주문과 고객, 배송지 정보를 한 테이블에 두면 고객 정보 갱신 이상과 배송지 중복이 커집니다. 그래서 고객 엔터티와 주문 엔터티를 분리해 3NF 수준으로 설계했고, 주문 조회 성능이 필요한 화면은 DTO projection과 인덱스로 풀고, 정말 자주 쓰는 집계만 별도 read model로 분리하겠습니다.

10. 면접에서 자주 나오는 정규화 질문과 답변

Q1: 정규화란 무엇이고, 왜 필요한가요?

정규화는 관계형 데이터베이스에서 데이터의 중복을 최소화하고 이상 현상(삽입/갱신/삭제 이상)을 방지하기 위해 테이블을 분해하는 과정입니다. 함수 종속성 이론을 기반으로 1NF, 2NF, 3NF, BCNF 등의 단계를 거치며, 각 단계에서 특정 유형의 종속성을 제거합니다. 정규화를 통해 데이터 무결성을 보장하고, 저장 공간을 절약하며, 데이터 변경 시 일관성을 유지할 수 있습니다.

Q2: 1NF, 2NF, 3NF의 차이를 설명하세요.

1NF는 모든 속성이 원자값을 가져야 합니다. 하나의 셀에 여러 값이나 반복 그룹이 없어야 합니다. 2NF는 1NF를 만족하면서 부분 함수 종속을 제거합니다. 복합키의 일부에만 종속되는 비키 속성이 없어야 하며, 기본키가 단일 속성이면 자동으로 2NF입니다. 3NF는 2NF를 만족하면서 이행 함수 종속을 제거합니다. 비키 속성이 다른 비키 속성을 거쳐 기본키에 종속되면 안 되며, 해당 중간 속성을 별도 테이블로 분리합니다.

Q3: BCNF는 3NF와 어떻게 다른가요?

3NF는 “비키 속성이 기본키에 이행 종속하지 않아야 한다”는 조건이고, BCNF는 “모든 함수 종속의 결정자가 후보키여야 한다”는 더 엄격한 조건입니다. 3NF를 만족하지만 BCNF를 만족하지 않는 경우는, 후보키가 아닌 속성이 후보키의 일부를 결정하는 경우입니다. 예를 들어 {학생, 과목} → 교수이고 교수 → 과목인 경우, 교수는 후보키가 아니지만 결정자이므로 BCNF를 위반합니다.

Q4: 반정규화는 언제 사용하나요?

반정규화는 조회 성능이 중요하고, 인덱스 최적화와 쿼리 튜닝만으로는 성능 요구를 충족하지 못할 때 마지막 수단으로 사용합니다. 조회 빈도가 높고 갱신 빈도가 낮은 데이터에 적합하며, 대표적으로 중복 칼럼 추가, 파생 칼럼(집계 결과) 저장, 테이블 병합(1:1 관계), 집계 테이블 생성 등의 기법이 있습니다. 반정규화 시에는 데이터 정합성 유지 방안(트리거, 애플리케이션 로직, 스케줄러)을 반드시 함께 설계해야 합니다.

Q5: 함수 종속성이란 무엇인가요?

함수 종속성(FD)은 릴레이션에서 속성 X의 값이 속성 Y의 값을 유일하게 결정하는 관계로, X → Y로 표기합니다. X를 결정자, Y를 종속자라 합니다. 완전 함수 종속은 기본키 전체에 종속되는 것이고, 부분 함수 종속은 기본키의 일부에만 종속되는 것이며, 이행 함수 종속은 X → Y → Z처럼 중간 속성을 거쳐 종속되는 것입니다. 정규화의 각 단계는 특정 유형의 함수 종속성을 제거하는 과정입니다.

Q6: 이상 현상(Anomaly)의 세 가지 유형을 설명하세요.

삽입 이상은 원하는 데이터를 삽입할 때 불필요한 데이터도 함께 삽입해야 하거나 삽입이 불가능한 경우입니다. 갱신 이상은 데이터를 변경할 때 여러 행을 동시에 수정해야 하며, 일부만 수정하면 데이터 불일치가 발생하는 경우입니다. 삭제 이상은 데이터를 삭제할 때 의도하지 않은 다른 정보까지 함께 삭제되는 경우입니다. 이 세 가지 이상 현상은 서로 다른 관심사의 데이터가 하나의 테이블에 혼재되어 있을 때 발생하며, 정규화를 통해 해결합니다.

Q7: 실무에서 보통 몇 정규형까지 적용하나요?

실무에서는 일반적으로 3NF 또는 BCNF까지 적용합니다. 4NF, 5NF는 다치 종속이나 조인 종속 같은 특수한 상황에서만 필요하며, 실무에서 직접 마주치는 경우가 드뭅니다. OLTP 시스템에서는 3NF/BCNF로 설계한 후 성능 이슈가 발생하면 선택적으로 반정규화합니다. OLAP/DW 시스템에서는 처음부터 스타 스키마나 스노우플레이크 스키마 같은 반정규화된 구조를 사용하는 경우가 많습니다.

Q8: 정규화와 반정규화의 트레이드오프를 설명하세요.

정규화는 데이터 무결성과 일관성을 보장하고 저장 공간을 절약하지만, 테이블 분해로 인해 조회 시 JOIN이 증가하여 읽기 성능이 저하될 수 있습니다. 반정규화는 중복을 허용하여 JOIN을 줄이고 조회 성능을 개선하지만, 데이터 정합성 유지를 위한 추가 로직이 필요하고 쓰기 성능이 저하될 수 있습니다. 따라서 쓰기 위주의 OLTP 시스템에서는 정규화가, 읽기 위주의 OLAP/리포팅 시스템에서는 반정규화가 유리합니다. 실무에서는 정규화를 기본으로 하되, 측정된 성능 문제에 대해서만 선택적으로 반정규화를 적용합니다.


Q9: 후보키, 기본키, 슈퍼키 차이를 설명하세요.

슈퍼키는 튜플을 유일하게 식별할 수 있는 속성 집합 전체를 말합니다. 후보키는 그중 최소성을 만족하는 키이고, 기본키는 후보키 중 대표로 선택한 키입니다. BCNF를 설명할 때 “모든 결정자는 후보키여야 한다”는 표현을 정확히 쓰려면 이 구분이 필요합니다.

Q10: 3NF인데도 이상 현상이 남을 수 있나요?

그럴 수 있습니다. 3NF는 일정 수준까지 이행 종속을 제거하지만, 후보키가 아닌 결정자가 남는 특수 케이스에서는 BCNF 위반으로 이상 현상이 남을 수 있습니다. 그래서 실무에서도 가능하면 BCNF까지 검토하되, 종속성 보존과 조인 비용을 함께 봅니다.

Q11: 정규화와 엔티티 분리는 같은 말인가요?

완전히 같은 말은 아닙니다. 정규화는 관계형 모델의 함수 종속성 기반 분해 이론이고, 엔티티 분리는 도메인 모델링 관점의 개념입니다. 다만 잘 설계된 엔티티 경계는 대개 정규화된 테이블 구조와 비슷한 방향으로 수렴합니다.

Q12: 반정규화 없이도 조회 성능을 높일 수 있는 방법은?

인덱스 최적화, 커버링 인덱스, projection, CQRS read model, 캐시, materialized view, 배치 집계 등이 있습니다. 면접에서는 반정규화를 최후 수단으로 둔다는 관점을 분명히 하는 것이 중요합니다.


정리

데이터베이스 정규화의 핵심을 정리하면 다음과 같다.

정규화의 본질은 함수 종속성에 기반한 테이블 분해이다. 각 정규형은 특정 유형의 종속성을 제거한다. 1NF는 원자값, 2NF는 부분 종속, 3NF는 이행 종속, BCNF는 후보키가 아닌 결정자를 제거한다.

정규화를 하지 않으면 삽입/갱신/삭제 이상이 발생하여 데이터 무결성이 깨진다. 이상 현상의 근본 원인은 서로 다른 개체의 정보가 하나의 테이블에 혼재되어 있기 때문이다.

반정규화는 조회 성능 최적화를 위한 의도적인 중복 허용이다. 중복 칼럼 추가, 파생 칼럼, 테이블 병합/분할, 집계 테이블 등의 기법이 있다. 그러나 반정규화는 마지막 수단이다. 인덱스 최적화 → 쿼리 튜닝 → 캐시 → 반정규화 순서로 시도해야 하며, 적용 시에는 데이터 정합성 유지 방안을 반드시 함께 설계해야 한다.

JPA 환경에서는 엔티티 연관관계 매핑이 자연스러운 정규화를, DTO Projection과 @Formula가 반정규화 없이도 성능을 개선할 수 있는 대안을 제공한다.