정규형과 MongoDB

발행: (2026년 2월 8일 오전 06:53 GMT+9)
16 분 소요
원문: Dev.to

Source: Dev.to

(번역할 텍스트를 제공해 주시면 한국어로 번역해 드리겠습니다.)

정규형과 현대 데이터 모델링

데이터베이스가 처음 설계될 때는 관계형 모델이 접근 패턴이 알려지기 전에 정의된 기업 전체의 엔터티에 초점을 맞추었습니다. 목표는 많은 미래 애플리케이션이 공유할 수 있는 안정적이고 정규화된 스키마를 만드는 것이었습니다.

오늘날 우리는 특정 애플리케이션이나 제한된 도메인을 위해 데이터베이스를 설계합니다. 전체 모델을 한 번에 만들기보다는 기능을 점진적으로 추가하고 피드백을 수집하며 스키마가 애플리케이션과 함께 진화하도록 합니다.

핵심 포인트: 정규형은 단순히 관계 이론이 아니라 실제 데이터 종속성을 설명합니다. MongoDB와 같은 문서 모델을 사용하더라도 정규화에 대해 고민해야 하며, 적용 방식에 있어 더 큰 유연성을 가질 뿐입니다.

MVP: 피자 하나, 매니저 하나, 종류 하나, 지역 하나

우리는 새로운 사업을 시작합니다: 다양한 피자를 제공하는 여러 지역에 걸친 대규모 피자 체인망.

MVP 가정

속성
name“A1 Pizza”
manager“Bob”
variety“Thick Crust”
area“Springfield”
{
  "name": "A1 Pizza",
  "manager": "Bob",
  "variety": "Thick Crust",
  "area": "Springfield"
}

반복 그룹이나 다중값 속성이 없으므로 제1정규형 (1NF) 에 이미 부합합니다.
MVP 데이터 모델은 속성당 하나의 값과 단일 키만을 가지고 있어, 고차 정규형을 위반하는 종속성이 존재하지 않습니다.

많은 설계가 처음부터 완전히 정규화된 형태로 시작하는데, 이는 설계자가 모든 정규형을 일일이 검토했기 때문이 아니라 초기 데이터셋이 너무 단순해 복잡한 종속성이 존재하지 않기 때문입니다.

정규화는 비즈니스 규칙이 발전하고 새로운 종류, 지역, 그리고 독립적인 속성이 추가되어 고차 정규형이 다루는 종속성이 등장할 때 필요해집니다.

여러 종류 추가 – 1NF 위반

피자 가게가 여러 종류를 제공할 수 있을 때, 순진한 접근 방식은 다음과 같을 수 있습니다:

{
  "name": "A1 Pizza",
  "manager": "Bob",
  "varieties": "Thick Crust, Stuffed Crust",
  "area": "Springfield"
}

왜 이것이 1NF를 위반하는가

  • Atomicity(원자성) – 각 필드는 단일하고 분할할 수 없는 데이터를 담아야 합니다.
  • 쉼표로 구분된 문자열은 개별 종류별로 효율적으로 조회, 인덱싱, 업데이트할 수 없습니다.

올바른 1NF 준수 표현

관계형 뷰 – 일대다 관계를 별도의 테이블에 저장합니다.

문서 뷰 – 구분 문자열 대신 배열을 사용합니다.

{
  "name": "A1 Pizza",
  "manager": "Bob",
  "email": "bob@a1-pizza.it",
  "varieties": ["Thick Crust", "Stuffed Crust"]
}
  • 각 배열 요소는 원자적이며 독립적으로 접근 가능 → 문서 지향형 1NF와 동등하게 만족합니다.
  • MongoDB는 관련 데이터를 함께 배치해 예측 가능한 성능을 제공하고, SQL은 논리‑물리 데이터 독립성을 제공합니다.

Introducing Prices – Moving Toward 2NF

Now we want to store the base price of each pizza variety.

Embedded price information

{
  "name": "A1 Pizza",
  "manager": "Bob",
  "email": "bob@a1-pizza.it",
  "varieties": [
    { "name": "Thick Crust", "basePrice": 10 },
    { "name": "Stuffed Crust", "basePrice": 12 }
  ]
}

Second Normal Form (2NF) builds on 1NF: every non‑key attribute must depend on the entire primary key, not just part of it. This only matters when we have composite keys.

  • Composite key for each array element: (pizzeria, variety).
  • If each pizzeria can set its own price, basePrice depends on the full composite key → 2NF satisfied.

When prices are standardized

If the same variety costs the same everywhere, basePrice depends only on variety. That’s a partial dependency2NF violation.

Normalizing the price data

Create a separate collection (or table) for pricing:

{ "variety": "Thick Crust", "basePrice": 10 }
{ "variety": "Stuffed Crust", "basePrice": 12 }

Remove basePrice from the pizzeria document and retrieve it via a lookup when needed.

Example: MongoDB view that joins pricing

db.createView(
  "pizzeriasWithPrices",
  "pizzerias",
  [
    { $unwind: "$varieties" },
    {
      $lookup: {
        from: "pricing",
        localField: "varieties.name",
        foreignField: "variety",
        as: "priceInfo"
      }
    },
    { $unwind: "$priceInfo" },
    {
      $addFields: {
        "varieties.basePrice": "$priceInfo.basePrice"
      }
    },
    { $project: { priceInfo: 0 } }
  ]
);

Alternatively, the pricing collection can be referenced directly from the varieties array (using the variety name as a foreign key) and joined at query time.

요약

정규형적용 내용피자 모델에의 적용 방법
1NF원자값, 반복 그룹 없음여러 종류는 배열(또는 별도 테이블)로 사용
2NF복합키에 대한 부분 종속성 없음가격이 종류 자체의 속성일 때 basePrice를 분리
3NF (다루지 않음)이행 종속성 없음예를 들어 varietycategorytaxRate와 같이 추가 분리가 필요할 경우

스키마를 단계적으로 발전시켜—단순하고 1NF를 만족하는 문서로 시작하고 비즈니스 규칙이 요구될 때만 정규화—함으로써 피자 체인이 성장함에 따라 데이터 모델을 유연하면서도 구조적으로 잘 정리된 상태로 유지할 수 있습니다.

Source:

문서 데이터베이스에서 가격 업데이트하기

애플리케이션이 가격을 조회하면 pizzeria 문서에 저장해 읽기를 빠르게 합니다.
업데이트 이상 현상을 방지하기 위해, 품종의 가격이 변하면 애플리케이션이 영향을 받는 모든 문서를 업데이트합니다:

const session = db.getMongo().startSession();
const sessionDB = session.getDatabase(db.getName());
session.startTransaction();

sessionDB.getCollection("pricing").updateOne(
  { variety: "Thick Crust" },
  { $set: { basePrice: 11 } }
);

sessionDB.getCollection("pizzerias").updateMany(
  { "varieties.name": "Thick Crust" },
  { $set: { "varieties.$[v].basePrice": 11 } },
  { arrayFilters: [{ "v.name": "Thick Crust" }] }
);

session.commitTransaction();

SQL 데이터베이스는 직접적인 최종 사용자 접근을 위해 설계되었기 때문에 이러한 다중 업데이트를 피합니다. 종속성을 별도 테이블로 분리하지 않고(정규화하지 않고) 중복된 데이터를 간과하기 쉽습니다. 문서 데이터베이스에서는 일관성을 유지하는 책임이 애플리케이션 서비스에 있습니다.

2NF 로 정규화하는 것이 가능하지만, 도메인‑주도 설계에서는 항상 최선의 선택은 아닙니다. 가격을 각 피자 가게 문서에 포함시켜 두면:

  • 비동기 업데이트가 가능해집니다.
  • 일부 피자 가게가 다른 가격을 제공하는 미래 요구사항도 무결성을 깨뜨리지 않고 지원할 수 있습니다—애플리케이션이 원자적 업데이트를 강제하기 때문입니다.

실제로 많은 애플리케이션이 가격 변경이 드물고 단일 문서 읽기가 빠른 것을 선호하기 때문에, 이와 같은 통제된 중복을 허용하고 완벽하게 정규화된 쓰기보다 빠른 읽기를 선택합니다.

1NF → 2NF 예시: 매니저 이메일

원본 문서 (피자 가게당 하나의 이메일)

{
  "name": "A1 Pizza",
  "manager": "Bob",
  "email": "bob@a1-pizza.it",
  "varieties": [
    { "name": "Thick Crust", "basePrice": 10 },
    { "name": "Stuffed Crust", "basePrice": 12 }
  ]
}

3NF – 전이 종속성 제거

이 이메일은 실제로 매니저에게 속하며 피자 가게에 직접 속하지 않으므로 전이 종속성이 발생합니다:

pizzeria → manager → email

정규화된 문서:

{
  "name": "A1 Pizza",
  "manager": { "name": "Bob", "email": "bob@a1-pizza.it" },
  "varieties": [
    { "name": "Thick Crust", "basePrice": 10 },
    { "name": "Stuffed Crust", "basePrice": 12 }
  ]
}

피자 가게에 매니저가 여러 명인 경우, 서브‑문서 배열을 사용하세요.
관계형 모델에서는 별도의 테이블(pizzeria, manager, contact)이 되겠지만, 우리 도메인에서는 피자 가게 외부의 연락처를 관리하지 않으므로 포함시키는 것이 적절합니다.

4NF – 독립적인 다중값 종속성

원하는 배달 지역

{
  "name": "A1 Pizza",
  "manager": { "name": "Bob", "email": "bob@a1-pizza.it" },
  "offerings": [
    { "variety": { "name": "Thick Crust", "basePrice": 10 }, "area": "Springfield" },
    { "variety": { "name": "Thick Crust", "basePrice": 10 }, "area": "Shelbyville" }
  ]
}

다중값 종속성은 한 속성이 다른 속성의 값 집합을 결정하고, 다른 모든 속성과는 독립적일 때 존재합니다.

  • 만약 품종과 지역이 서로 의존적이라면(예: 특정 지역에서만 특정 품종을 제공) (variety, area) 쌍이 하나의 사실이 되며 4NF 위반이 발생하지 않습니다.

  • 우리 경우에는 피자 가게가 모든 품종을 모든 지역에 배달하므로 두 개의 독립적인 다중값 사실이 존재합니다:

    • pizzeria →→ variety
    • pizzeria →→ area

모든 조합을 저장하면 중복이 발생합니다.

정규화된 4NF 문서

{
  "name": "A1 Pizza",
  "manager": { "name": "Bob", "email": "bob@a1-pizza.it" },
  "varieties": [
    { "name": "Thick Crust", "basePrice": 10 },
    { "name": "Stuffed Crust", "basePrice": 12 }
  ],
  "deliveryAreas": ["Springfield", "Shelbyville"]
}

이제 varietiesdeliveryAreas가 독립적으로 저장되어 4NF 위반이 해소되었습니다.

2NF & 3NF – 지역 기반 가격 책정

배달 지역에 따라 가격이 달라지는 경우:

{
  "name": "A1 Pizza",
  "manager": { "name": "Bob", "email": "bob@a1-pizza.it" },
  "offerings": [
    { "variety": "Thick Crust", "area": "Springfield", "price": 10 },
    { "variety": "Thick Crust", "area": "Shelbyville", "price": 11 },
    { "variety": "Stuffed Crust", "area": "Springfield", "price": 12 },
    { "variety": "Stuffed Crust", "area": "Shelbyville", "price": 13 }
  ]
}
  • 각 제공 항목에 대한 복합 키: (pizzeria, variety, area).
  • price는 전체 키에 의존하므로 2NF(부분 종속 없음)와 3NF(이행 종속 없음)를 만족합니다.

BCNF – 지역 매니저 추가

Now each area has a single manager, independent of the pizzeria:

{
  "name": "A1 Pizza",
  "manager": { "name": "Bob", "email": "bob@a1-pizza.it" },
  "offerings": [
    { "variety": "Thick Crust", "area": "Springfield", "price": 10, "areaManager": "Alice" },
    { "variety": "Stuffed Crust", "area": "Springfield", "price": 12, "areaManager": "Alice" },
    { "variety": "Thick Crust", "area": "Shelbyville", "price": 11, "areaManager": "Eve" },
    { "variety": "Stuffed Crust", "area": "Shelbyville", "price": 13, "areaManager": "Eve" }
  ]
}

Boyce‑Codd Normal Form (BCNF) 은 모든 결정자가 슈퍼키가 되도록 요구합니다.
여기서 area → areaManagerarea 가 제공 문서의 슈퍼키가 아니기 때문에 BCNF를 위반합니다.

BCNF 해결책

Extract the area‑manager relationship into its own collection or sub‑document:

{
  "area": "Springfield",
  "manager": "Alice"
}

그리고 각 제공 항목에서 해당 지역을 참조합니다.

관게형 vs. 문서 모델링에서의 정규형

BCNF vs. 3NF

  • BCNF: 모든 결정자는 슈퍼키여야 합니다.
  • 3NF는 종속 속성이 후보키의 일부일 때 결정자를 후보키 속성으로 허용합니다.

예시: offerings 관계(pizzeria, variety, area)에서 함수 종속성 area → areaManagerarea만으로는 슈퍼키가 아니기 때문에 BCNF를 위반합니다.

실제 영향: 특정 지역의 관리자를 변경하려면 해당 지역의 모든 offering을 업데이트해야 합니다. 관계형 시스템에서는 지역 관리자를 별도의 테이블로 분리합니다.

MongoDB 접근 방식 (내장 구조):

db.pizzerias.updateMany(
  { "offerings.area": "Springfield" },
  { $set: { "offerings.$[o].areaManager": "Carol" } },
  { arrayFilters: [{ "o.area": "Springfield" }] }
);

트레이드‑오프: 엄격한 BCNF 준수를 포기하고 더 간단한 쿼리와 빠른 읽기를 얻으며, 일관성은 애플리케이션에서 보장합니다.

5NF (프로젝트‑조인 정규형)

다양성과 지역과 독립적인 여러 크기(Small, Medium, Large)를 제공할 때, 모든 조합을 저장하면 중복이 발생합니다.

5NF 준수 설계: 독립적인 사실을 별도로 저장합니다.

{
  "name": "A1 Pizza",
  "varieties": ["Thick Crust", "Stuffed Crust"],
  "sizes": ["Large", "Medium"],
  "deliveryAreas": ["Springfield", "Shelbyville"]
}

애플리케이션은 필요할 때 유효한 조합을 생성할 수 있어 수백 개의 명시적인 문서를 피할 수 있습니다.

6NF (여섯 번째 정규형)

감사 수준의 가격 이력을 위해:

{
  "offerings": [
    {
      "variety": "Thick Crust",
      "area": "Springfield",
      "currentPrice": 12,
      "priceHistory": [
        { "price": 10, "effectiveDate": ISODate("2024-01-01") },
        { "price": 11, "effectiveDate": ISODate("2024-03-15") },
        { "price": 12, "effectiveDate": ISODate("2024-06-01") }
      ]
    }
  ]
}

6NF 설계: 시계열 사실 컬렉션을 사용합니다.

{ "pizzeria": "A1 Pizza", "variety": "Thick Crust", "area": "Springfield",
  "price": 10, "effectiveDate": ISODate("2024-01-01") }
{ "pizzeria": "A1 Pizza", "variety": "Thick Crust", "area": "Springfield",
  "price": 11, "effectiveDate": ISODate("2024-03-15") }
{ "pizzeria": "A1 Pizza", "variety": "Thick Crust", "area": "Springfield",
  "price": 12, "effectiveDate": ISODate("2024-06-01") }

감사, 분석, 혹은 특정 날짜의 가격 조회 등에 사용합니다.

0 조회
Back to Blog

관련 글

더 보기 »

Power BI의 스키마 및 데이터 모델링

소개 데이터 모델링은 이름에서 알 수 있듯이 정리되고 구조화된 데이터를 재구성하고 통찰력 있는 시각화를 만드는 것을 포함합니다. Power BI에서…