모든 글

요구사항과 객체 관계로 정규화와 반정규화를 선택하기

정규화 수준을 점수처럼 높이기보다 값의 변경 의미, 조회 비용, 데이터가 보존해야 할 객체 관계를 기준으로 테이블을 설계합니다.

출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다. gpt-5.6-sol 모델을 사용해 생성·편집했습니다.

데이터베이스를 설계할 때 정규화를 얼마나 해야 하는지부터 묻지는 않습니다. 그 질문만으로는 시스템이 해야 할 일과 너무 멀리 떨어져 있습니다. 서비스 안의 개념과 그 관계를 먼저 보고, 요구사항이 그 관계의 변경 방식을 어떻게 정하는지 살펴봅니다.

저는 테이블을 값의 묶음이라기보다 개념 객체를 뒷받침하는 구조로 보기 때문에 기본 설계가 비교적 정규화되는 편입니다. 그렇다고 반정규화가 곧 편법이나 실수라는 뜻은 아닙니다. 어떤 경우에는 요구사항 자체가 값을 복제하라고 말합니다.

같은 쿠폰명도 의미가 다를 수 있다

coupon 테이블에 ID와 이름이 있고, issued_coupon 테이블에는 어떤 쿠폰을 어떤 사용자에게 발급했는지 기록한다고 해보겠습니다. 정규화된 형태에서는 user_idcoupon_id를 저장하고, 이름이 필요할 때 쿠폰을 조인합니다.

여기에 실제 요구사항을 하나씩 추가해 보겠습니다.

발급 쿠폰 조회가 아주 잦고 화면마다 쿠폰명이 필요하며, 조인이나 별도 조회가 성능 문제를 일으키고 있다면 issued_coupon에 이름을 복제하는 선택을 할 수 있습니다. 익숙한 성능 목적의 반정규화입니다. 하지만 성능만이 이유는 아닙니다.

기획자가 “쿠폰명이 나중에 바뀌더라도 이미 발급된 쿠폰의 이름은 바뀌면 안 됩니다”라고 정할 수 있습니다. 이때 발급 레코드의 쿠폰명은 캐시가 아닙니다. 발급 시점의 상태를 보존하는 스냅숏입니다. 그 값은 발급 쿠폰이 소유해야 하므로 발급 테이블에 저장하는 편이 요구사항과 맞습니다.

반대 요구사항이라면 답도 달라집니다. 발급된 모든 쿠폰이 언제나 최신 쿠폰명을 보여줘야 한다면 coupon_id만 저장하고 원본을 조회하는 방식이 단순합니다. 이름을 복제한 뒤 원본이 바뀔 때마다 모든 발급 레코드를 갱신할 수도 있지만, 필요 없던 동기화 작업을 새로 만든 셈입니다.

데이터 규모가 커지면 그 선택의 대가가 드러납니다. 특정 쿠폰 하나가 1억 번 발급되었다고 해보겠습니다. 이름을 바꾸는 순간 복제된 1억 건을 갱신해야 합니다. 첫날에는 편해 보였던 컬럼 하나가 큰 쓰기 작업과 정합성 문제를 만들 수 있습니다.

따라서 “반정규화를 해야 하나?“보다 “이 값은 발급 시점의 스냅숏인가, 현재 값을 따라가는 참조인가, 조회를 빠르게 하기 위한 복사본인가?“를 묻는 편이 낫습니다. 화면에는 똑같은 문자열로 보여도 세 경우의 변경 규칙은 다릅니다.

테이블 관계는 개발자가 데이터를 쓰는 방식도 만든다

조회 성능만큼 조심해야 할 문제가 하나 더 있습니다. 반정규화가 개념 사이의 높낮이를 지울 수 있습니다.

제가 유지보수했던 주문 구조에는 대략 주문 헤더, 주문 상세, 주문 아이템이 있었습니다. 아래 단계의 레코드가 위 단계의 ID, 이름과 여러 정보를 함께 들고 있었습니다. 정확한 과거 스키마보다 중요한 것은 여러 개발자의 손을 거친 뒤 코드가 어떻게 변했느냐입니다. 거의 모든 곳에서 주문 아이템만 불러오기 시작했습니다.

주문 전체의 이름이 필요한 코드도 아이템을 조회했습니다. 상세 수준의 정보가 필요할 때도 아이템을 가져왔습니다. 헤더와 상세 테이블은 남아 있었지만 애플리케이션의 극히 일부에서만 사용됐습니다. 아이템이 모든 것을 대표하는 만능 객체처럼 코드 안을 돌아다녔습니다.

처음에는 편합니다. 그러나 데이터베이스는 세 개의 개념과 계층을 표현하는데 애플리케이션은 가장 아래 레코드 하나가 전부인 것처럼 행동합니다. 다음 개발자는 어떤 값이 주문 전체에 속하는지, 상세에 속하는지, 아이템에 속하는지 구분하기 어려워집니다. 컬럼 복제는 조인 하나만 없애는 일이 아닙니다. 개발자가 어떤 객체를 가져오고 어디에 새 동작을 붙이는지도 바꿉니다.

그래서 저는 컬럼뿐 아니라 테이블 사이의 위상도 봅니다. 주문 아이템이 정말 헤더의 모든 정보를 알아야 할까요? 어디까지 결합을 유지하고 어디서 느슨하게 풀어야 할까요? 어떤 값들이 같은 이유로 함께 바뀌며, 어떤 개념이 규칙을 소유할까요?

정규화라는 용어만으로는 답할 수 없습니다. 객체 관계를 봐야 합니다.

복제 전에 변경 규칙부터 적기

컬럼을 복제하기 전에 다음 내용을 분명히 하려고 합니다.

  • 기존 레코드는 생성 시점의 값을 보존해야 하는가?
  • 원본의 최신 값을 계속 따라가야 하는가?
  • 읽기 빈도와 비용이 별도 표현을 둘 만큼 실제로 큰가?
  • 원본이 바뀌면 복사본 몇 건을 고쳐야 하며 일부 갱신이 실패하면 어떻게 되는가?
  • 복제된 필드 때문에 호출자가 필요한 상위 개념 대신 하위 객체만 쓰게 되지는 않는가?

답에 따라 스냅숏 컬럼, 조인, 별도 조회 모델, 통제된 동기화 작업을 선택할 수 있습니다. 더 큰 시스템에는 다른 선택지도 있을 겁니다. 선택지가 늘어도 데이터의 의미를 먼저 정해야 한다는 사실은 바뀌지 않습니다.

누구도 설명하지 못하는 영리한 스키마보다 관계가 선명한 소박한 정규화 설계가 낫습니다. 구체적인 요구사항이나 측정된 비용 때문에 복제가 필요하면 의도적으로 추가하고 변경 규칙을 남기면 됩니다. 테이블 설계는 다음 개발자에게 어느 복사본이 진짜인지 추측하게 만들지 않아야 합니다.