데이터 모델링은 데이터베이스가 데이터를 어떻게 저장할지 정하기 전에 엔터티, 속성, 관계를 정리하는 과정입니다. 엔터티는 쉬운 부분입니다. 어떤 제품이든 자신의 사용자, 주문, 게시물이 무엇인지 압니다. 진짜 기술은 관계에 있습니다. 어떤 레코드가 어떤 레코드를 가리키는지, 몇 개나 가리키는지, 그 연결이 물리적으로 어디에 존재하는지입니다. 연결을 제대로 잡으면 쿼리는 저절로 써지고, 잘못 잡으면 모든 기능이 스키마와 싸우게 됩니다.
핵심 요약
| 질문 | 답변 |
|---|---|
| 세 가지 수준 | 개념(무엇) → 논리(구조) → 물리(엔진) |
| 세 가지 관계 유형 | 일대일 · 일대다 · 다대다 |
| 관계형 도구 | 외래 키, 다대다에는 연결 테이블 |
| 문서형 도구 | 임베딩, Pointer(참조), Relation/ID 배열 |
| 현대의 규칙 | 접근 패턴에 맞춰 모델링 – 함께 읽는 데이터는 함께 저장 |
같은 관계, 두 세계
먼저 관계형입니다. 외래 키가 일대다를 담당하고, 다대다는 연결 테이블을 통해 분해해야 합니다:
-- 1:N — 외래 키는 "다" 쪽에 둡니다
CREATE TABLE books (
id bigint PRIMARY KEY,
title text NOT NULL,
author_id bigint NOT NULL REFERENCES authors(id) ON DELETE CASCADE
);
-- M:N — FK 하나로는 표현할 수 없으므로 연결 테이블이 두 키를 모두 담습니다
CREATE TABLE book_genres (
book_id bigint REFERENCES books(id),
genre_id bigint REFERENCES genres(id),
added_at timestamptz DEFAULT now(), -- 연결 테이블도 속성을 가질 수 있음
PRIMARY KEY (book_id, genre_id) -- 복합 키: 하나의 연결은 한 번만
);
문서 세계에서도 모델은 같습니다. 일대다 연결은 타입이 지정된 Pointer이고, 다대다 연결은 Relation입니다. 따로 고안할 연결 테이블이 없으며, 연결은 애플리케이션 코드에서 선언합니다:
// JavaScript / Node.js — Back4app JS SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
const author = await new Parse.Query('Author').get(authorId);
const book = new Parse.Object('Book');
book.set('title', 'Dune');
book.set('author', author); // Pointer — typed one-to-many edge
await book.save();
const genres = book.relation('genres'); // Relation — many-to-many, unbounded
genres.add([sciFi, classics]);
await book.save(); // Flutter / Dart — Back4app Flutter SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
final book = ParseObject('Book')
..set('title', 'Dune')
..set('author', author.toPointer()); // Pointer — typed one-to-many edge
await book.save();
book.addRelation('genres', [sciFi, classics]); // Relation — many-to-many
await book.save(); // iOS / Swift — Back4app Swift SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
var book = Book()
book.title = "Dune"
book.author = try author.toPointer() // Pointer — typed one-to-many edge
let saved = try await book.save()
let relation = try saved.relation("genres")
try await relation.add([sciFi, classics]).save() // Relation — many-to-many // Android / Kotlin — Back4app Android SDK
// One-to-many: a Pointer. Many-to-many: a Relation.
val book = ParseObject("Book").apply {
put("title", "Dune")
put("author", author) // Pointer — typed one-to-many edge
}
book.save()
val genres = book.getRelation<ParseObject>("genres") // Relation — many-to-many
genres.add(sciFi)
genres.add(classics)
book.save() 개념 vs. 논리 vs. 물리: 세 가지 수준
| 구분 | 개념 | 논리 | 물리 |
|---|---|---|---|
| 답하는 질문 | 무엇이 존재하고 어떻게 연결되는지 | 어떤 필드, 어떤 키 | 어떻게 저장되고 얼마나 빠른지 |
| 대상 | 모든 사람 | 설계자 | 엔지니어 + 엔진 |
| 담는 내용 | 엔터티, 관계 | + 속성, 타입, 카디널리티 | + 인덱스, 제약 조건, 파티션 |
| 바뀌는 시점 | 비즈니스가 바뀔 때 | 요구 사항이 구체화될 때 | 엔진이나 규모가 바뀔 때 |
세 수준이 강제하는 규율은 단순합니다. 팀이 아직 “주문”이 무엇인지 합의하지 못했다면, 인덱스를 두고 논쟁할 때가 아닙니다.
모든 관계, 모든 구현
검색 결과 상위 문서들이 만들지 않은 표입니다. 각 연결 유형과 두 세계에서의 구현을 함께 정리했습니다:
| 관계 | 예시 | 관계형 구현 | 문서형 구현 |
|---|---|---|---|
| 일대일 | 사용자 ↔ 프로필 | UNIQUE가 붙은 FK, 또는 같은 행 | 임베딩 – 거의 항상 |
| 일대소수 | 사람 → 주소 | 자식 테이블 + FK | 임베딩된 배열(크기 제한) |
| 일대다 | 저자 → 책 | ”다” 쪽의 FK | ”다” 쪽의 Pointer |
| 일대초대량 | 디바이스 → 이벤트 | FK + 파티셔닝 | 자식 쪽의 Pointer, 배열은 절대 사용하지 않음 |
| 다대다 | 책 ↔ 장르 | 연결 테이블, 복합 PK | Relation 또는 Pointer 배열 |
| 자기 참조 | 직원 → 상사 | 자기 테이블을 가리키는 FK | 자기 클래스를 가리키는 Pointer |
다이어그램을 읽기 위한 카디널리티 표기법도 알아 둘 만합니다. 까마귀발(crow’s foot) 표기법은 “하나”를 막대로, “다수”를 세 갈래 포크로 그립니다. ER 전통은 선 위에 1과 N을 적고, UML은 1과 * 같은 다중성을 적습니다. 방언은 셋이지만 문법은 하나입니다.
정규화, 비정규화, 아니면 임베딩?
관계형 모델링은 정규화에서 출발합니다. 모든 사실이 한 곳에만 존재하도록 데이터를 나누어, 쓰기를 이상 현상(anomaly)으로부터 보호합니다. 분석 모델링과 문서 모델링은 이 원칙을 의도적으로 구부립니다. 비정규화는 읽기가 잦은 필드를 복제해 조인을 없애고, 임베딩은 비정규화의 문서 네이티브 사촌입니다. 임베딩 vs. 참조 체크리스트는 세 가지 질문으로 압축됩니다. 함께 읽힙니까? 하나의 부모가 소유합니까? 크기가 제한됩니까? 셋 다 “예”라면 임베딩하고, 하나라도 “아니요”라면 참조합니다. 그리고 이 모든 것 위에, 범용 가이드들이 건너뛰는 현대의 규칙이 있습니다. 쿼리부터 나열하는 것입니다. 모델이 처리해야 할 접근 패턴을 적은 비용으로 처리할 수 있을 때 그 모델은 올바르며, 엔터티만으로는 그것을 알 수 없습니다.
일반적인 사용 사례
- 새 백엔드 설계. 고전적인 절차입니다. 엔터티의 이름을 정하고, 연결을 그리고, 위 표에 따라 연결마다 구현을 고릅니다. 이 모든 일을 첫 줄의 코드를 쓰기 전에 합니다.
- 속성을 가진 연결 테이블 패턴. 수강 등록, 멤버십, 주문 항목처럼 연결 자체에 데이터(날짜, 수량, 역할)가 있다면, 연결 테이블(또는 Pointer 두 개를 가진 연결 클래스)은 일급 엔터티입니다.
- 커져 버린 스키마 정리. 증상이 연결을 가리킵니다. 중복된 행은 잘못 모델링된 다대다를, 비대해진 문서는 크기 제한 없는 임베딩을 드러냅니다.
- 세계 간 마이그레이션. 관계형에서 문서형으로 옮긴다는 것은 모든 FK를 임베딩으로 할지 Pointer로 할지 다시 결정한다는 뜻이며, 위의 표가 그 번역 사전입니다.
- AI와 분석 피드. 데이터 웨어하우스는 연결이 명시적이고 안정적이기를 원합니다. 모델링 부채는 데이터를 내보내려는 날 드러납니다.
각 연결은 어떻게 모델링해야 할까? 결정 매트릭스
| 선택… | 이런 경우… | 주의할 점… |
|---|---|---|
| 임베딩 | 함께 읽힘, 부모가 소유, 크기 제한 | 증가: 오늘의 “소수”가 내일은 수천 개가 됨 |
| Pointer / FK | 독립적인 생명 주기, 높은 카디널리티 | 인덱스 필수 – 모든 조회와 include가 대가를 치름 |
| Relation / 연결 테이블 | 다대다, 또는 연결 자체가 데이터를 가짐 | 쿼리 방향: 어느 쪽에서 질의할지 미리 파악 |
| 복제(비정규화) | 연결을 가로질러 자주 읽히는 필드 | 업데이트 팬아웃 – 복제는 이자가 붙는 부채 |
한계와 트레이드오프
- 모델은 가정을 고정합니다. “사용자는 주소가 하나” 같은 카디널리티 결정은 스키마가 됩니다. 종이 위에서는 바꾸기 쉽지만 출시 후에는 비쌉니다. 내일의 카디널리티를 기준으로 모델링해야 합니다.
- 두 세계 모두 인덱스 없는 연결에 대가를 치르게 합니다. FK 열과 Pointer 필드는 조인 경로입니다. 여기에 인덱스를 빠뜨리는 것이 가장 흔한, 그리고 드러나지 않는 성능 버그입니다.
- 임베딩은 무결성을 지역성과 맞바꿉니다. 임베딩된 사본이 원본과 일관성을 유지하도록 강제하는 엔진은 없습니다. 그 책임은 이제 애플리케이션의 업데이트 로직에 있습니다.
- 연결 테이블은 조인을 늘리고, Relation은 조인을 숨깁니다. 모든 다대다 읽기는 연결 저장소를 거칩니다. 어느 세계에 있든 쿼리 경로의 비용을 미리 계산해야 합니다.
- 접근 패턴 중심 모델링에도 비용이 있습니다. 오늘의 쿼리에 최적화하면 스키마가 오늘의 제품에 묶일 수 있습니다. 중립적인 기준점으로 개념 모델을 계속 유지하는 것이 좋습니다.
Back4app에서의 데이터 모델링
Back4app은 관리형 데이터베이스, 자동 생성되는 REST 및 GraphQL API, 인증, 파일 스토리지, Cloud Code 서버리스 함수를 결합한 오픈소스 Backend as a Service(BaaS) 플랫폼입니다. Back4app의 모델링 어휘는 위 표들의 문서형 열을 일급 기능으로 만든 것입니다. Pointer는 타입이 지정된 일대다 연결이고, Relation은 직접 만든 연결 테이블 없이 다대다를 담당하며, 배열은 크기가 제한된 소수 관계를 처리합니다. 선언한 모든 연결은 즉시 탐색할 수 있습니다. include()는 한 번의 요청으로 Pointer를 따라가고, GraphQL은 한 번의 쿼리로 Relation을 중첩해 가져옵니다. 스키마는 대시보드에서 늘 보이고 편집할 수 있으므로, 물리 모델이 개념 모델을 설계한 팀의 시야에서 벗어나는 일이 없습니다.
자주 묻는 질문
데이터 모델링이란 무엇입니까?
애플리케이션의 정보, 즉 엔터티와 그 속성, 엔터티 사이의 관계를 데이터베이스가 저장할 수 있는 설계로 옮기는 과정입니다. 보통 세 단계의 상세 수준을 거칩니다. 엔터티의 이름을 정하는 개념 모델, 속성과 키를 더하는 논리 모델, 특정 엔진의 테이블, 열, 인덱스로 확정하는 물리 모델입니다.
데이터 모델의 세 가지 유형은 무엇입니까?
개념, 논리, 물리 모델로, 같은 설계를 점점 확대해서 보는 것입니다. 개념 모델은 비즈니스 이해관계자를 위해 무엇이 존재하고 서로 어떻게 연결되는지에 답합니다. 논리 모델은 특정 기술에 얽매이지 않은 채 속성, 타입, 키를 더합니다. 물리 모델은 실제 엔진에 맞춰 테이블이나 컬렉션, 인덱스, 제약 조건을 확정합니다. 각 수준은 이전 수준에 결정을 더한 것입니다.
일대다 관계란 무엇입니까?
하나의 부모 레코드가 여러 자식과 연결되고, 각 자식은 정확히 하나의 부모에 속하는 관계입니다. 고객과 주문, 저자와 책이 그 예입니다. 어떤 스키마에서든 가장 흔한 관계입니다. 관계형 데이터베이스는 "다" 쪽에 외래 키를 두어 구현하고, 문서 데이터베이스는 작고 크기가 제한된 집합에는 임베딩된 배열을, 그 밖의 모든 경우에는 부모를 가리키는 포인터를 사용합니다.
다대다 관계란 무엇이며, 왜 연결 테이블이 필요합니까?
양쪽 모두가 상대편의 여러 레코드와 연결되는 관계입니다. 학생과 강좌, 책과 장르가 그 예입니다. 외래 키 열 하나는 한 행만 가리킬 수 있으므로, 관계형 데이터베이스는 다대다를 두 개의 일대다 관계로 분해하고, 두 키를 모두 담는 연결 테이블(종종 등록일 같은 관계 속성도 함께 담습니다)로 이어 줍니다. 문서 데이터베이스는 연결 테이블을 생략합니다. 참조 배열이나 전용 관계 타입이 연결을 직접 담습니다.
일대다와 다대다는 어떻게 구분합니까?
소유 관계를 양방향으로 질문하면 됩니다. "한 저자가 여러 책을 가질 수 있습니까?" 그렇습니다. "한 책이 여러 저자를 가질 수 있습니까?" 아니라면 일대다이며 외래 키는 책 쪽에 둡니다. 그렇다면 다대다이며 연결 테이블이나 관계 타입을 사용합니다. 이를 잘못 판단하는 것이 대표적인 모델링 실수입니다. 일대다로 모델링한 다대다는 행을 중복시키거나 연결을 조용히 잃어버립니다.
ER 다이어그램에서 카디널리티란 무엇입니까?
한 엔터티의 인스턴스가 다른 엔터티의 인스턴스와 몇 개까지 연결될 수 있는지를 나타냅니다. 일대일, 일대다, 다대다가 있습니다. 다이어그램은 세 가지 표기법 중 하나로 이를 표현합니다. 연결선 위의 숫자와 문자, 까마귀발(crow's foot) 기호(하나는 막대, 다수는 세 갈래 포크), 또는 1과 별표 같은 다중성 범위입니다. 의미는 같고 그리는 관례만 다릅니다.
문서 데이터베이스는 외래 키 없이 관계를 어떻게 모델링합니까?
세 가지 도구를 사용합니다. 임베딩(관련 데이터를 부모 문서 안에 중첩하는 방식으로, 함께 읽히고 부모가 소유하며 크기가 제한될 때 적합합니다), 포인터 또는 참조(관련 문서의 ID를 타입이 지정된 필드로 저장하는 방식으로, 생명 주기가 독립적이고 카디널리티가 높을 때 적합합니다), 그리고 다대다를 위한 관계 객체나 ID 배열입니다. 지침이 되는 규칙도 정규화에서 접근 패턴으로 바뀝니다. 함께 읽히는 데이터는 함께 저장해야 합니다.
가장 흔한 데이터 모델링 실수는 무엇입니까?
늘 같은 다섯 가지입니다. 다대다를 일대다로 모델링하는 것, 외래 키와 포인터 필드에 인덱스를 빠뜨리는 것(모든 조인과 조회가 그 대가를 치릅니다), 문서 스키마에 크기 제한이 없는 임베딩 배열을 두는 것, 느린 쿼리가 확인되기도 전에 섣불리 비정규화하는 것, 그리고 모델이 처리해야 할 쿼리, 즉 접근 패턴을 나열하지 않고 엔터티만으로 모델링하는 것입니다.