DDIA 2장 — 데이터 모델과 질의 언어
책: 데이터 중심 애플리케이션 설계 (Designing Data-Intensive Applications), Martin Kleppmann, O'Reilly 챕터: 2장 — Data Models and Query Languages
학습 개요
2장의 핵심 질문은 다음 하나로 요약됩니다:
"데이터를 어떻게 표현하고, 어떻게 질의할 것인가?"
그리고 이것은 단순한 기술 선택의 문제가 아니라 도메인을 바라보는 관점의 문제입니다. 같은 데이터라도 트리, 테이블, 그래프 중 어떤 모델로 보느냐에 따라 쿼리 패턴과 확장성이 완전히 달라집니다.
2장 구조
- 2.1 관계형 모델 vs 문서 모델 — NoSQL의 등장과 역사
- 2.2 데이터를 위한 질의 언어 — 선언형 vs 명령형
- 2.3 그래프형 데이터 모델 — 복잡한 관계를 다루는 방법
2.1 관계형 모델과 문서 모델
NoSQL의 탄생 배경
1970년대 에드거 코드가 관계형 모델을 제안한 이후 약 30년간 "DB = 관계형 DB"였으나, 2000년대 후반 NoSQL 운동이 등장합니다. 주요 동기는 네 가지입니다.
- 확장성: 대용량 데이터와 높은 쓰기 처리량
- 오픈소스 선호: 상용 DB에 대한 반발
- 특수 질의 지원: 그래프, 시계열 등
- 스키마 유연성: 관계형 스키마 제약에 대한 불만
이 시대 이후, 애플리케이션은 여러 기술을 혼합해 쓰는 다중 저장소 지속성(polyglot persistence) 방향으로 진화하고 있습니다.
객체-관계 불일치 (Object-Relational Mismatch)
대부분의 애플리케이션 코드는 객체지향 언어로 작성되지만, 데이터는 관계형 테이블에 저장됩니다. 이 두 세계 사이의 구조적 차이가 객체-관계 불일치입니다.
예를 들어 링크드인 프로필을 관계형 DB에 저장하면 users, positions, education, contact_info 등 여러 테이블로 쪼개야 하고, 애플리케이션에서 다시 하나의 객체로 조립해야 합니다. 이 번거로움 때문에 ORM(Hibernate, ActiveRecord 등) 프레임워크가 등장했습니다.
문서 모델의 대안
문서형 DB(MongoDB 등)에서는 프로필 전체를 하나의 JSON 문서로 저장할 수 있습니다.
{
"user_id": 251,
"first_name": "Bill",
"last_name": "Gates",
"positions": [
{"job_title": "Co-chair", "organization": "Gates Foundation"},
{"job_title": "Co-founder", "organization": "Microsoft"}
],
"education": [
{"school_name": "Harvard", "start": 1973, "end": 1975}
]
}
문서 모델의 핵심 장점: 자기 완결적(self-contained), 지역성(locality), 일대다 관계의 자연스러운 표현.
정규화와 ID 참조
같은 값이 여러 문서에 반복될 때 중요한 선택이 생깁니다. 문자열로 중복 저장할지, ID로 참조할지.
ID 참조의 장점 (정규화):
- 일관성: 단일 진실 공급원(Single Source of Truth)
- 업데이트 용이성: 한 곳만 수정하면 모든 곳에 반영
- 지역화: 표시 이름을 언어별로 관리 가능
- 검색 개선: ID 기반 인덱스의 효율성
DRY 원칙("Don't Repeat Yourself")에 따라, 사람에게 의미 있는 정보를 중복하는 것은 정규화에 위배됩니다.
다대일과 다대다 관계의 도전
- 다대일(N:1): 여러 사용자가 같은 지역에 거주
- 다대다(N:N): 여러 사람이 여러 회사에서 일하고, 한 회사는 여러 사람을 고용
문서형 DB는 트리 구조이기 때문에 다대일/다대다 관계 처리가 약합니다. 조인 지원이 제한적이라 두 가지 선택지만 남습니다.
- 애플리케이션 코드에서 여러 번 쿼리 (N+1 문제 발생)
- 비정규화 (업데이트 지옥)
관계가 복잡해질수록 관계형 모델의 조인 능력이 빛을 발합니다.
문서 DB는 역사를 반복하는가?
클레프만은 흥미로운 관찰을 제시합니다. 1960~70년대에 이미 비슷한 논쟁이 있었다는 것입니다.
계층형 모델 (IMS, IBM): 1968년 아폴로 프로그램용으로 개발. JSON처럼 트리 구조. 일대다는 잘 다루지만 다대다에 약함.
이 문제를 해결하려는 두 모델이 경쟁했습니다.
- 네트워크 모델 (CODASYL): 트리 대신 그래프 허용. 하지만 접근 경로를 수동 관리해야 함.
- 관계형 모델 (Codd): 테이블로만 표현하고, 접근 경로는 쿼리 최적화기가 자동 결정.
결과적으로 관계형 모델이 승리했습니다. 그리고 오늘날 문서형 DB는 여러모로 계층형 모델과 유사한 특징을 보입니다. 트리 구조, 일대다 친화, 다대다 약함, 조인 약함.
다만 현대 문서 DB는 과거의 실수를 반복하지 않았습니다.
- 물리적 포인터 대신 논리적 ID 사용 → 데이터 독립성 유지
- 선언형 쿼리와 인덱스 지원
- 스키마리스(schemaless)의 유연성
세 가지 비교 관점
1. 애플리케이션 코드 복잡도
- 문서형 유리: 자기 완결적 데이터(블로그 포스트, 사용자 프로필, 주문서)
- 관계형 유리: 다대다 관계가 많은 경우
2. 스키마 유연성
가장 자주 오해되는 부분입니다. "NoSQL은 스키마가 없다"는 말은 반쪽짜리 진실입니다.
| 유형 | 설명 | 예시 |
|---|---|---|
| Schema-on-write | DB가 쓸 때 스키마를 강제 | PostgreSQL, MySQL |
| Schema-on-read | 읽는 코드가 구조를 해석 | MongoDB, JSON 파일 |
스키마는 사라지지 않고, 관리 책임이 DB에서 애플리케이션 코드로 이동할 뿐입니다. 검증을 안 하면, 검증 책임이 코드 모든 곳에 흩어집니다.
// 스키마 관리를 안 하면 생기는 버전별 대응 지옥
if (user.address && typeof user.address === 'string') {
// 구버전: 주소가 문자열
} else if (user.address && user.address.city) {
// 중간 버전: 객체
} else if (user.addresses && Array.isArray(user.addresses)) {
// 신버전: 배열
}
3. 데이터 지역성 (Locality)
지역성이란? 함께 읽고 쓰이는 데이터가 물리적으로(디스크 상에서) 가까이 모여 있는 정도를 말합니다. 데이터가 한 곳에 모여 있을수록 디스크 I/O 횟수가 줄어들어 읽기 성능이 좋아집니다.
이 관점에서 두 모델을 비교하면, 문서형은 하나의 문서가 디스크에 연속 저장되어 읽기가 빠릅니다. 반면 관계형은 여러 테이블을 조인하면서 디스크를 여기저기 읽습니다.
다만 지역성은 한 번에 많은 부분을 필요로 할 때만 이득입니다. 문서의 일부만 필요한 경우에는 오히려 낭비가 됩니다. 문서는 "같이 읽히는 데이터"를 모아둘 때 진가를 발휘합니다.
수렴 (Convergence)
두 모델은 서로의 강점을 흡수하며 수렴하고 있습니다.
- PostgreSQL의 JSONB: 컬럼에 JSON 저장, 인덱싱과 쿼리 가능
- MongoDB의 $lookup: 조인 흉내내기
- MongoDB의 트랜잭션: 4.0부터 멀티 문서 트랜잭션
- MongoDB의 스키마 검증:
$jsonSchema
"Mongo vs Postgres" 선택은 이제 이분법이 아닌 적합성의 문제입니다.
2.2 데이터를 위한 질의 언어
선언형 vs 명령형
SQL이 50년간 살아남은 이유는 선언형(declarative) 이라는 특성에 있습니다.
명령형 (Imperative) — "어떻게(How)"를 단계별로 지시:
function getCats(animals) {
const result = [];
for (let i = 0; i < animals.length; i++) {
if (animals[i].family === 'Felidae') {
result.push(animals[i]);
}
}
return result;
}
선언형 (Declarative) — "무엇(What)"만 말함:
SELECT * FROM animals WHERE family = 'Felidae';
선언형의 네 가지 장점
- 구현 세부사항 은폐: DB가 인덱스 구조를 바꿔도 쿼리는 그대로 동작
- 자동 최적화: 쿼리 최적화기가 인덱스 선택, 조인 순서 등을 결정
- 간결함: 복잡한 작업도 적은 코드로 표현
- 병렬 실행 용이성: 순서 제약이 없어 멀티코어/분산 환경에 유리
의외의 선언형 예시: CSS
CSS도 선언형 쿼리 언어의 한 종류입니다.
li.selected > p {
background-color: blue;
}
"선택된 li의 자식 p에 파란 배경을 적용하라"만 선언하고, 브라우저가 알아서 DOM을 순회하고 적용합니다. 만약 JavaScript로 명령형으로 작성한다면 훨씬 장황하고, DOM이 바뀔 때마다 재실행 관리까지 직접 해야 합니다.
통찰: 선언형은 DB만의 개념이 아닙니다. 복잡성을 관리하는 일반적인 방법론입니다. React의 UI 패러다임(jQuery의 명령형 DOM 조작 vs React의 선언형)도 같은 맥락입니다.
MapReduce
2004년 구글이 발표한 분산 데이터 처리 모델입니다. 선언형과 명령형의 중간에 위치합니다.
- Map 함수: 각 문서에 대해 호출, 키-값 쌍 방출
- Reduce 함수: 같은 키의 값들을 모아 집계
db.observations.mapReduce(
function map() {
const year = this.observationTimestamp.getFullYear();
const month = this.observationTimestamp.getMonth() + 1;
emit(`${year}-${month}`, 1);
},
function reduce(key, values) {
return Array.sum(values);
},
{ query: { family: "Sharks" }, out: "monthlySharkReport" }
)
장점: 유연함, 분산 처리 쉬움 단점: 함수가 순수해야 함, 디버깅 어려움, 표현력 한계
MongoDB의 Aggregation Pipeline
MapReduce의 단점 때문에 MongoDB는 나중에 aggregation pipeline을 도입했습니다. 데이터가 여러 단계를 순서대로 통과하며 변형되는 구조입니다.
db.orders.aggregate([
{ $match: { status: "completed" } }, // 1단계: 필터
{ $group: { // 2단계: 그룹
_id: "$user_id",
total: { $sum: "$amount" }
}},
{ $sort: { total: -1 } }, // 3단계: 정렬
{ $limit: 5 } // 4단계: 제한
])
| Stage | 역할 | SQL 대응 |
|---|---|---|
$match | 필터링 | WHERE |
$group | 그룹화 + 집계 | GROUP BY |
$project | 필드 선택 | SELECT |
$sort | 정렬 | ORDER BY |
$limit | 개수 제한 | LIMIT |
$lookup | 다른 컬렉션과 조인 | JOIN |
이것이 "두 모델이 수렴하고 있다" 의 구체적인 사례입니다. MongoDB는 점점 관계형 DB의 기능을 흡수 중입니다.
2.3 그래프형 데이터 모델
그래프가 유용한 경우
관계가 데이터의 핵심이고 복잡하게 얽혀 있는 경우에는 그래프 모델이 자연스럽습니다.
- 소셜 그래프 (Facebook, LinkedIn)
- 웹 그래프 (Google)
- 도로/철도 네트워크 (길찾기)
- 추천 시스템 (Netflix, Amazon)
- 지식 그래프 (Wikipedia, Google Knowledge Graph)
그래프의 두 가지 강점:
- 깊이를 알 수 없는 연결 탐색: "친구의 친구의 친구가 좋아한 상품"
- 이종(heterogeneous) 데이터 통합: 사람, 페이지, 지역, 이벤트 등을 하나의 그래프에
속성 그래프 (Property Graph)
- 정점(Vertex): 고유 ID, 들어오는/나가는 간선 목록, 속성들
- 간선(Edge): 고유 ID, 시작/끝 정점, 레이블, 속성들
관계형 DB로 구현한다면 단 두 개의 테이블로 충분합니다.
CREATE TABLE vertices (
vertex_id INTEGER PRIMARY KEY,
properties JSON
);
CREATE TABLE edges (
edge_id INTEGER PRIMARY KEY,
tail_vertex INTEGER REFERENCES vertices (vertex_id),
head_vertex INTEGER REFERENCES vertices (vertex_id),
label TEXT,
properties JSON
);
Cypher 쿼리 언어
Neo4j의 선언형 쿼리 언어입니다.
데이터 삽입:
CREATE
(USA:Location {name:'United States', type:'country'}),
(Idaho:Location {name:'Idaho', type:'state'}),
(Lucy:Person {name:'Lucy'}),
(Idaho) -[:WITHIN]-> (USA),
(Lucy) -[:BORN_IN]-> (Idaho)
가변 길이 경로 쿼리 — Cypher의 핵심 강점:
MATCH
(person) -[:BORN_IN]-> () -[:WITHIN*0..]-> (us:Location {name:'United States'}),
(person) -[:LIVES_IN]-> () -[:WITHIN*0..]-> (eu:Location {name:'Europe'})
RETURN person.name
[:WITHIN*0..]는 "WITHIN 관계를 0번 이상 반복해서 따라가라"는 뜻입니다. 지역 계층의 깊이를 미리 알 필요가 없습니다.
SQL로 그래프 쿼리하기
재귀 공통 테이블 표현식(Recursive CTE)으로 가능하지만 훨씬 장황합니다. Cypher 한 줄이 SQL 20줄 이상이 됩니다. 그래서 그래프에 최적화된 전용 DB와 쿼리 언어가 존재하는 것입니다.
트리플 스토어 (Triple Store)
속성 그래프보다 더 단순한 모델입니다. 모든 정보를 (주어, 서술어, 목적어) 세 개짜리 문장으로 저장합니다.
(Lucy, born_in, Idaho)
(Lucy, age, 33)
(Idaho, within, USA)
속성과 관계의 구분이 없고, 전부 트리플로 통일됩니다.
Turtle 표기법 (사람이 읽을 수 있는 RDF):
_:lucy a :Person; :name "Lucy"; :bornIn _:idaho.
_:idaho a :Location; :name "Idaho"; :within _:usa.
시맨틱 웹과 RDF
팀 버너스리의 비전: "웹페이지를 사람뿐만 아니라 기계도 읽을 수 있게 하자." RDF(Resource Description Framework) 는 이를 위한 표준입니다.
원래 비전대로는 성공하지 못했지만, 부분적으로는 살아남았습니다.
- Google Knowledge Graph
- Wikidata
- schema.org
SPARQL
트리플 스토어를 위한 W3C 표준 쿼리 언어입니다.
PREFIX : <urn:example:>
SELECT ?personName WHERE {
?person :name ?personName.
?person :bornIn / :within* / :name "United States".
?person :livesIn / :within* / :name "Europe".
}
Cypher와 사실 꽤 비슷합니다. 둘 다 "패턴 매칭" 방식의 선언형 쿼리입니다.
속성 그래프 vs 트리플 스토어
| 측면 | 속성 그래프 | 트리플 스토어 |
|---|---|---|
| 대표 DB | Neo4j | Datomic, AllegroGraph |
| 쿼리 언어 | Cypher, Gremlin | SPARQL (W3C 표준) |
| 데이터 표현 | 정점+간선 (분리) | 트리플 (균일) |
| 주 용도 | 애플리케이션 개발 | 지식 그래프, 시맨틱 웹 |
| 표준화 | 벤더별로 다름 | W3C 표준 |
Datalog
1970년대 후반 등장한, Cypher와 SPARQL의 이론적 기반이 된 언어입니다.
# 기본 사실
name(lucy, "Lucy").
born_in(lucy, idaho).
within(idaho, usa).
# 규칙: 재귀적 포함 관계 정의
within_recursive(Location, Name) :- name(Location, Name).
within_recursive(Location, Name) :- within(Location, Via),
within_recursive(Via, Name).
Datalog의 차별점: 패턴 매칭을 넘어 규칙 기반 추론이 가능합니다. 기본 사실로부터 새 사실을 유도합니다.
실무 사용은 드물지만 이념은 여러 시스템에 녹아 있습니다: Datomic, Cascalog, LogicBlox, Rust 타입 체커 등.
2장의 핵심 통찰
1. 데이터 모델은 "사고방식"을 결정한다
기술 선택처럼 보이지만 실제로는 도메인을 어떻게 바라볼지를 결정합니다. 같은 링크드인 프로필도 문서, 관계, 그래프 중 어떤 모델로 보느냐에 따라 다르게 보입니다.
2. 어떤 모델도 완벽하지 않다
각 모델은 특정 종류의 데이터에 최적화되어 있습니다. "NoSQL이 낫다" 또는 "SQL이 낫다"는 이분법은 틀렸습니다. 도메인의 성격이 모델을 결정해야 합니다.
| 데이터 특성 | 적합한 모델 |
|---|---|
| 관계가 단순하고 트리 구조 | 문서 모델 |
| 관계가 복잡한 다대다 | 관계형 모델 |
| 관계 깊이를 알 수 없고 관계 자체가 중심 | 그래프 모델 |
3. 역사는 반복되지만 조금씩 진화한다
1970년대 계층형 vs 관계형 논쟁이 2010년대 문서 vs 관계형 논쟁으로 돌아왔습니다. 그러나 현대 문서 DB는 과거의 실수를 대부분 피했고, 두 모델은 수렴 중입니다.
4. 선언형 쿼리 언어가 승리한다
SQL, Cypher, SPARQL 모두 선언형입니다. 데이터 규모, 분산 시스템, 최적화 기술이 발전할수록 선언형의 가치는 커집니다.
더 깊이 논의할 질문들
1. 우리 서비스는 MongoDB를 사용 중인데, 왜 저장된 형태는 관계형 DB처럼 되어 있는 건가요?
부분 답변 (2장 맥락에서):
실제 비즈니스 데이터는 거의 예외 없이 다대일/다대다 관계를 포함합니다(사용자↔상품, 게시물↔태그, 주문↔결제 등). 이런 관계를 문서 안에 중첩하면 중복과 업데이트 지옥이 발생하므로, 많은 팀이 MongoDB에서도 ID 참조 기반 구조를 채택하게 됩니다. 이 결과가 사실상 관계형 스키마의 모습입니다.
이것은 잘못된 선택이 아니며, 오히려 "데이터의 관계가 복잡하면 어떤 DB를 쓰든 관계형스러운 구조로 귀결된다" 는 것이 2장의 핵심 통찰 중 하나입니다.
다만 한 번 점검해볼 만한 질문들:
- 우리 데이터는 정말 MongoDB의 장점(자기 완결 문서, 스키마 유연성, 지역성)을 활용하고 있는가?
- 관계형 스키마처럼 쓰고 있다면, PostgreSQL + JSONB가 더 적합하지 않은가?
- MongoDB를 쓰더라도, 어떤 컬렉션은 문서를 더 적극적으로 중첩시킬 여지가 있지 않은가?
이것은 좋고 나쁨이 아니라 적합성의 문제입니다.
모델 선택 가이드 — 개념 요약
애플리케이션 요구사항에 가장 적합한 데이터 모델을 찾기 위한 간단한 판단 기준입니다.
Step 1. 데이터의 관계 모양을 먼저 확인
모든 결정의 출발점은 "이 데이터의 관계는 어떤 모양인가?" 입니다. 기술을 먼저 고르지 말고, 데이터를 먼저 보세요.
| 관계 모양 | 적합한 모델 | 대표 DB |
|---|---|---|
| 트리 구조 / 자기 완결적 (1:N 중심) | 문서 모델 | MongoDB, CouchDB, PostgreSQL+JSONB |
| 다대다 관계가 많음 / 일관성 중요 | 관계형 모델 | PostgreSQL, MySQL |
| 깊이 모를 연결 탐색 / 관계 자체가 데이터 | 그래프 모델 | Neo4j, ArangoDB |
Step 2. 각 모델의 체크리스트
다음 질문에 대부분 YES라면 해당 모델이 적합합니다.
문서형이 적합한가?
- 데이터가 자기 완결적인가? (한 번에 통째로 읽고 쓰는가)
- 관계가 대부분 1:N 트리 구조인가?
- 항목마다 필드 구조가 다른가? (이질적 데이터)
- 참조되는 데이터가 거의 변하지 않는가?
- 트랜잭션이 단일 문서 내에서 끝나는가?
전형적 사례: 로그·이벤트, CMS 콘텐츠, 사용자 프로필, 이질적 상품 카탈로그, 게임 상태
관계형이 적합한가? (기본값)
- 데이터 엔티티 간 관계가 복잡한가?
- 다대다 관계가 존재하는가?
- 데이터 일관성이 비즈니스적으로 중요한가?
- 여러 엔티티에 걸친 트랜잭션이 필요한가?
- 여러 팀/서비스가 같은 데이터를 다루는가?
- 분석/리포팅 쿼리가 중요한가?
전형적 사례: 전자상거래, 금융 시스템, 예약 시스템, SaaS의 핵심 비즈니스 데이터
그래프가 적합한가? (대부분 보조 DB)
- 관계 탐색이 쿼리의 핵심인가?
- N단계 연결을 찾아야 하는가? ("친구의 친구의 친구")
- 최단 경로, 공통 연결 같은 그래프 알고리즘이 필요한가?
- 관계형 DB에서 재귀 쿼리 성능 문제를 이미 겪고 있는가?
전형적 사례: 추천 엔진, 사기 탐지, 소셜 네트워크 분석, 지식 그래프
Step 3. 판단이 서지 않을 때의 기본 원칙
확신이 없으면 관계형 DB(PostgreSQL)로 시작한다.
- 대부분의 비즈니스 데이터는 시간이 지나면 관계형 패턴이 됨
- JSONB로 문서형 기능도 흡수 가능
NoSQL은 명확한 이유가 있을 때만 선택한다.
- "확장성"은 대부분 조기 최적화
- "스키마 유연성"은 대부분 설계 책임 회피
- 진짜 이유가 있을 때만: 로그/이벤트, 고도로 이질적 데이터, 거대한 쓰기 처리량
하나의 DB로 모든 걸 해결하려 하지 않는다.
- 주 DB는 관계형, 보조 DB는 용도별로 선택
- 다중 저장소 지속성(polyglot persistence)은 일반적 패턴
스키마는 반드시 어딘가에 관리된다.
- DB에서 관리할지, 애플리케이션(TypeScript, Zod/Joi 등)에서 관리할지 의식적으로 결정
- "스키마 없음"은 선택지가 아님
측정한 뒤에 최적화한다.
- 비정규화, 캐시, 다중 저장소는 실제 병목이 측정된 뒤에 도입
- 조기 최적화는 복잡성만 낳음
Step 4. 역할별 DB 선택 치트시트
현대 웹 서비스에서 자주 등장하는 역할별 추천입니다.
| 역할 | 추천 DB | 이유 |
|---|---|---|
| 핵심 비즈니스 데이터 | PostgreSQL, MySQL | 일관성, 트랜잭션, 조인 |
| 캐시 / 세션 | Redis | 초고속, TTL 지원 |
| 실시간 카운터 | Redis | 원자적 증감 연산 |
| 로그 / 이벤트 | MongoDB, Cassandra | 쓰기 처리량, 시간 기반 |
| 전문(full-text) 검색 | Elasticsearch | 형태소 분석, 랭킹 |
| 추천 / 그래프 분석 | Neo4j | 깊은 관계 탐색 |
| 분석 / BI | BigQuery, Snowflake | 컬럼 지향, 대용량 집계 |
| 시계열 데이터 | InfluxDB, TimescaleDB | 시간 기반 최적화 |
Step 5. 프로젝트 시작 전 10가지 질문
새 프로젝트나 새 기능을 시작할 때의 체크리스트입니다.
- 이 데이터의 가장 중요한 관계 유형은 무엇인가? (1:N / N:N / 깊은 연결)
- 읽기와 쓰기 중 어느 쪽이 더 많은가? (비율)
- 일관성이 비즈니스적으로 얼마나 중요한가? (금융인가, 로그인가)
- 스키마가 얼마나 자주 바뀔 것 같은가?
- 데이터는 얼마나 자주 업데이트되는가? (추가 위주인가, 수정 위주인가)
- 여러 엔티티에 걸친 트랜잭션이 필요한가?
- 검색이나 분석 요구사항이 있는가?
- 팀이 익숙한 기술은 무엇인가? (운영 부담)
- 1-2년 후에도 이 선택이 유효할 것 같은가?
- 지금 필요한 것이 단일 DB인가, 다중 DB인가?
피해야 할 안티패턴
- "NoSQL은 빠르다"는 미신 — 대부분의 서비스는 PostgreSQL만으로 수년간 충분히 빠름
- 관계가 복잡한데 문서형 선택 — 시간이 지나면
$lookup지옥에 빠짐 - 스키마 관리 포기 — 스키마는 사라지지 않고 코드로 이동함
- 트렌드 따라가기 — 팀이 감당 못 하는 기술은 부채
- 미래 과대평가 — 대부분의 서비스는 초기 선택 그대로 유지되지 않음
한 문장 요약
"데이터의 관계 모양을 먼저 이해하고, 확신이 없으면 PostgreSQL로 시작하라. 특수한 요구가 실제로 측정되었을 때만 전용 DB를 추가하라. 모든 결정에서 '지금의 편함'과 '미래의 관계 복잡도' 사이의 트레이드오프를 의식하라."
실전 적용: CMP의 CMDB 설계
2장에서 배운 내용을 실제 엔터프라이즈 도메인에 적용해본 예시입니다. 클라우드 관리 플랫폼(CMP)의 CMDB 구축을 가정합니다.
1. 도메인 분석
엔티티
- User — 로그인 주체
- Customer — 고객사 (테넌트)
- Organization — 고객사 내 조직
- Application — 트리 구조 (부모-자식 계층)
- Cloud Resource — EC2, RDS, S3 등
- 리소스는 리프 Application에만 매핑됨
관계 요약
| 관계 | 유형 |
|---|---|
| Customer → Organization | 1:N |
| Organization → Application | 1:N |
| Application → Application | 재귀 트리 (1:N) |
| User ↔ Organization | N:N |
| Application(leaf) ↔ Resource | N:N |
| Resource ↔ Resource | 그래프 (의존성) |
특징: 계층 구조 + 다대다 + 트리 제약(리프 전용 매핑) + 선택적 그래프가 공존하는 전형적 엔터프라이즈 도메인.
2. 데이터 특성
| 데이터 | 관계 복잡도 | 쓰기 빈도 | 일관성 요구 | 스키마 안정성 |
|---|---|---|---|---|
| User, Customer, Org | 중 | 낮음 | 매우 높음 | 안정적 |
| Application (트리) | 중-높 | 낮음 | 높음 | 안정적 |
| Cloud Resource | 중 | 중간 | 중간 | 가변적 |
| 권한/역할 | 중 | 낮음 | 매우 높음 | 안정적 |
| 메트릭/비용 시계열 | 없음 | 매우 높음 | 낮음 | 안정적 |
| 감사 로그 | 없음 | 높음 | 낮음 | 안정적 |
핵심 포인트
- 핵심 도메인은 관계와 일관성이 중요 → 관계형 적합
- 리소스 속성은 타입마다 달라서 유연성 필요 → JSONB 활용
- 메트릭은 쓰기 폭주 → 별도 DB로 분리
3. 기능별 DB 매핑
| 기능 | DB | 역할 |
|---|---|---|
| 로그인, 인증 | PostgreSQL + Redis | PG에 계정, Redis에 세션 |
| 사용자/조직/앱 관리 | PostgreSQL | 계층 구조, 관계, 일관성 |
| 권한 검사 | Redis (캐시) + PostgreSQL (원본) | 매 요청 속도 확보 |
| Application 트리 조회 | PostgreSQL | 재귀 CTE 또는 Closure Table |
| 리소스 메타데이터 저장 | PostgreSQL (JSONB) | 가변 스키마는 JSONB로 |
| 앱별 리소스 목록 | PostgreSQL | JOIN으로 조회 |
| 복잡한 리소스 검색/필터 | Elasticsearch | 태그, 전문 검색, 복합 조건 |
| 대시보드 요약 | Redis (캐시) + PostgreSQL | 집계 결과 캐싱 |
| 메트릭/비용 추이 | TimescaleDB (또는 InfluxDB) | 시계열 최적화 |
| 감사 로그 검색 | Elasticsearch | 시간 + 텍스트 검색 |
| 리소스 의존성 분석 | PostgreSQL → 규모 커지면 Neo4j | 초기엔 PG로 충분 |
4. 단계별 도입 전략
- Phase 1 (MVP): PostgreSQL + Redis만
- Phase 2 (성장기): + Elasticsearch, + TimescaleDB
- Phase 3 (성숙기): + Neo4j (의존성 분석이 차별화 요소가 될 때)
그래프 DB는 PostgreSQL 재귀 쿼리가 실제로 병목이 되었을 때 도입하면 됩니다.
5. DDIA 2장 개념과의 연결
- 다대다와 트리 구조 → 관계형이 기본값
- 리소스 속성의 가변성 → JSONB로 수렴 전략 활용
- 쓰기 특성이 다른 데이터(메트릭) → 전용 DB로 분리 (다중 저장소 지속성)
- 그래프 DB는 실제 필요가 측정되었을 때만 도입
다음 단계
3장 "저장소와 검색"으로 진행 예정.
3장에서는 "데이터를 디스크에 실제로 어떻게 저장하는가"라는 훨씬 낮은 레벨의 이야기가 나옵니다.
- LSM 트리
- B-트리
- 인덱스 구조
- 트랜잭션 처리 vs 분석 처리
- 컬럼 지향 저장소
2장을 잘 이해했다면 3장이 훨씬 잘 읽힐 것입니다.