정규화는 데이터의 사실을 한곳에서 관리해 변경 이상을 줄인다. 역정규화는 확인된 조회 병목을 줄이기 위해 일부 데이터를 의도적으로 중복한다. 둘은 반대 진영이 아니라 데이터의 기준과 읽기 비용을 어디에 둘지 결정하는 도구다. 한 테이블에 사용자와 수강 과목을 함께 저장한다고 하자. 이 구조에서는 이름이 여러 행에 반복된다. 이름을 바꿀 때 한 행만 수정하면...
정규화는 데이터의 사실을 한곳에서 관리해 변경 이상을 줄인다. 역정규화는 확인된 조회 병목을 줄이기 위해 일부 데이터를 의도적으로 중복한다. 둘은 반대 진영이 아니라 데이터의 기준과 읽기 비용을 어디에 둘지 결정하는 도구다.
한 테이블에 사용자와 수강 과목을 함께 저장한다고 하자.
| student_id | student_name | subject |
|---|---|---|
| 1 | 민수 | 데이터베이스 |
| 1 | 민수 | 운영체제 |
이 구조에서는 이름이 여러 행에 반복된다. 이름을 바꿀 때 한 행만 수정하면 같은 학생의 이름이 서로 달라지는 갱신 이상이 생긴다. 아직 수강 과목이 없는 학생을 저장하기 어렵고, 마지막 수강 기록을 지우면서 학생 정보도 함께 사라질 수 있다.
정규화는 서로 다른 사실을 별도 릴레이션으로 나눈다.
student(student_id, student_name)
subject(subject_id, subject_name)
enrollment(student_id, subject_id)학생 이름은 student가 한 번만 소유한다. 수강 관계는 enrollment가 관리한다. 수정해야 할 위치가 분명해지고 기본 키와 외래 키로 무결성을 표현할 수 있다.
한 칸에는 하나의 값만 둔다. 쉼표로 연결한 목록이나 반복 컬럼을 별도 행과 테이블로 분리한다.
잘못된 예: member(id, phone1, phone2, phone3)
분리한 예: member_phone(member_id, phone_number)복합 키를 사용할 때 일반 컬럼이 키 전체에 의존해야 한다. 키 일부만으로 결정되는 값은 별도 테이블로 옮긴다.
enrollment(student_id, subject_id, student_name, grade)여기서 student_name은 student_id만으로 결정된다. student 테이블이 소유해야 한다.
일반 컬럼이 다른 일반 컬럼을 통해 간접적으로 결정되지 않게 한다.
employee(employee_id, department_id, department_name)department_name은 employee_id가 아니라 department_id에 의존한다. 부서 정보를 별도 테이블로 분리하면 부서명 변경 위치가 하나로 줄어든다.
정규형의 이름보다 중요한 질문은 이것이다.
정규화된 모델은 무결성을 관리하기 쉽지만 읽을 때 조인이 늘 수 있다. 조인이 많다는 사실만으로 역정규화를 선택하지는 않는다. 인덱스와 실행 계획이 적절하면 여러 조인도 충분히 빠를 수 있다.
다음 순서로 병목을 확인한다.
매번 집계하던 값을 원본 행에 함께 저장한다.
UPDATE post
SET comment_count = comment_count + 1
WHERE post_id = ?;목록 조회는 빨라지지만 댓글 생성과 삭제 때 집계값도 함께 갱신해야 한다. 원본과 파생 값의 트랜잭션 경계를 정하지 않으면 값이 어긋난다.
화면이나 검색에 필요한 모양으로 별도 읽기 모델을 만든다. RDB 테이블, Materialized View나 검색 문서가 될 수 있다.
쓰기 모델 ── 이벤트·배치 ──> 조회 전용 모델
│ │
└── 기준 데이터 └── 빠른 조회이 방식은 조회 모델 재생성과 지연 허용 범위를 함께 설계해야 한다. 동기화가 실패했을 때 원본에서 다시 만들 수 있어야 운영이 단순해진다.
다른 서비스가 자주 필요한 식별자나 표시 값을 로컬에 복제할 수 있다. 네트워크 호출은 줄지만 변경 전파 지연과 중복 저장 비용이 생긴다.
| 질문 | 정규화 유지에 가까움 | 역정규화 검토에 가까움 |
|---|---|---|
| 변경 빈도 | 자주 바뀜 | 드물게 바뀜 |
| 무결성 요구 | 즉시 일치가 중요함 | 짧은 지연을 허용함 |
| 조회 빈도 | 보통 | 매우 높음 |
| 조회 비용 | 인덱스와 조인으로 충족 | 반복 측정에서 목표 초과 |
| 복구 경로 | 중복 데이터 없음 | 원본에서 재생성 가능 |
역정규화를 선택했다면 중복 데이터를 만든 사실보다 유지 전략을 먼저 기록한다.
실행 계획을 확인하지 않고 컬럼을 복제하면 쓰기 경로만 복잡해질 수 있다. 먼저 읽은 행 수, 인덱스 사용 여부와 실제 응답 시간을 측정한다.
Redis나 검색 문서를 기준 데이터처럼 수정하면 장애 뒤 복구 기준이 사라진다. 파생 데이터는 지워도 원본에서 다시 만들 수 있어야 한다.
애플리케이션 트랜잭션, 이벤트 발행과 소비 사이에는 실패 구간이 있다. 재시도만으로 충분한지, Outbox와 멱등 소비가 필요한지 판단한다.
역정규화는 읽기를 빠르게 만드는 설계이면서 동시에 새로운 정합성 문제를 만드는 설계다. 조회 성능 개선치와 그 정합성 비용을 함께 측정해야 한다.