NoSQL 데이터 모델 선택: 주문 조회로 비교하는 키값·문서·컬럼·그래프


1장. 주문 상세은 빨라졌는데 왜 새 기능을 만들 때마다 구조를 다시 바꿔야 할까#

고객 7이 주문 501을 만들었습니다.

주문 내용은 다음과 같습니다.

주문ID 고객ID 상품ID 수량 단가
501 7 11 2 5,000
501 7 12 1 3,000

총 주문금액은:

2 × 5,000
+
1 × 3,000
=
13,000원

입니다.

초기 요구는 단순했습니다.

주문번호 501을 입력하면 주문 상세를 보여 주세요.

이 요구만 보면 주문 하나를 통째로 저장하는 구조가 편합니다.

그런데 서비스가 커지면서 요구가 추가됩니다.

고객 7의 최근 주문 20건을 보여 주세요.

상품 11을 구매한 고객을 찾아 주세요.

지난 7일 상품별 판매량을 집계해 주세요.

같은 상품을 구매한 고객끼리 연결해 주세요.

취소된 주문은 구매자 목록에서도 제거해 주세요.

처음에는 완벽해 보였던 저장 모델이 새로운 질문에서는 불편해질 수 있습니다.

데이터 모델은 단순히 데이터를 어떤 모양으로 저장할지 정하는 문제가 아닙니다.

어떤 질문을 적은 비용으로 답할 것인가?

를 결정하는 문제입니다.


2장. NoSQL은 하나의 데이터베이스 종류가 아니다#

NoSQL을 다음처럼 이해하면 부족합니다.

SQL을 사용하지 않는 데이터베이스

실제로 NoSQL이라는 이름 아래에는 서로 다른 데이터 모델이 포함됩니다.

대표적으로:

키값 모델

문서 모델

와이드 컬럼 모델

그래프 모델

이 있습니다.

각 모델은 데이터 접근 방식이 다릅니다.


3장. 같은 주문 하나도 네 가지 방식으로 다르게 표현할 수 있다#

고객 7의 주문 501을 기준으로 보겠습니다.

키값#

Key
order:501

Value
주문 전체 정보

문서#

orders 컬렉션
↓
주문 501 문서
↓
items 배열

와이드 컬럼#

Partition Key
customer_id = 7

Clustering
ordered_at
order_id

그래프#

고객 7
↓ 주문
주문 501
↓ 포함
상품 11
상품 12

같은 사실이지만 저장 관점이 다릅니다.


4장. 모델을 선택할 때 가장 먼저 써야 할 것은 조회 질문이다#

데이터베이스 제품부터 고르는 것보다 다음 질문부터 적어보는 편이 좋습니다.

Q1
주문번호로 주문 한 건 조회

Q2
고객별 최근 주문 조회

Q3
상품별 구매 고객 조회

Q4
상품별 매출 집계

Q5
고객과 상품 사이 다단계 관계 탐색

이 다섯 질문은 모두 같은 주문 데이터를 사용하지만 접근 경로는 다릅니다.


5장. 네 모델을 먼저 한눈에 비교하면#

모델 기본 접근 방식 자연스러운 질문 추가 비용이 생길 수 있는 질문
키값 키로 값 하나 조회 주문ID로 한 건 조회 상품별·기간별 조건 검색
문서 관련 데이터를 문서로 묶음 주문과 품목 함께 조회 여러 문서에 걸친 분석·중복 필드 갱신
와이드 컬럼 파티션 키와 정렬 키 고객별 시간순 주문 파티션 키 없는 임의 검색
그래프 노드와 관계 탐색 다단계 연결·경로 대량 단순 집계

이 표는 제품의 기능 한계를 뜻하는 것이 아닙니다.

보조 인덱스나 검색 기능으로 다른 질의도 지원할 수 있습니다.

핵심은 기본 데이터 배치가 어떤 질문에 유리한가입니다.


6장. 키값 모델은 키를 정확히 알고 있을 때 강하다#

가장 단순한 형태는 다음과 같습니다.

order:501
→ 주문 데이터

주문번호를 알고 있다면 한 번의 키 조회로 주문 전체를 가져올 수 있습니다.

개념적으로:

flowchart LR
    K["order:501"] --> V["주문 501 데이터"]

접근 경로가 매우 단순합니다.


7장. 주문 데이터를 값 하나로 저장할 수 있다#

예를 들어 JSON 형태로 저장할 수 있습니다.

{
  "order_id": 501,
  "customer_id": 7,
  "status": "paid",
  "items": [
    {
      "product_id": 11,
      "quantity": 2,
      "unit_price": 5000
    },
    {
      "product_id": 12,
      "quantity": 1,
      "unit_price": 3000
    }
  ],
  "order_total": 13000
}

조회는 매우 직관적입니다.

GET order:501

한 건을 바로 가져옵니다.


8장. 하지만 “상품 11을 산 주문”은 키만으로 찾기 어렵다#

현재 키는:

order:501
order:502
order:503
...

입니다.

새 요구:

상품 11을 포함한 모든 주문을 찾아라.

키에 상품ID 정보가 없습니다.

모든 주문 값을 열어보는 방식은 데이터가 커질수록 비효율적입니다.

따라서 새로운 역방향 구조를 만들 수 있습니다.

product:11:orders
→ [501, 508, 520, ...]

9장. 조회 구조 하나를 추가하면 쓰기 경로도 하나 늘어난다#

주문 501을 만들 때 이제 두 곳을 수정해야 합니다.

order:501
→ 원본 주문 저장

product:11:orders
→ 주문 501 추가

상품 12에도 별도 목록이 필요합니다.

product:12:orders
→ 주문 501 추가

읽기는 빨라졌지만 쓰기 책임이 늘었습니다.


10장. 취소가 들어오면 모든 사본을 다시 맞춰야 한다#

주문 501이 취소됐다고 하겠습니다.

원본만:

order:501
status = cancelled

로 바꾸면 부족할 수 있습니다.

상품별 구매자 목록에서도 취소 상태를 반영해야 합니다.

그렇지 않으면:

주문 상세
→ 취소됨

상품 11 구매자 목록
→ 여전히 고객 7 표시

처럼 서로 다른 결과를 보여줄 수 있습니다.


11장. 키값 저장소는 캐시와 세션에도 잘 맞는다#

예를 들어:

session:ABCD1234
→ 로그인 사용자 상태

처럼 명확한 키로 빠르게 접근하는 데이터에 자연스럽습니다.

TTL이 필요한 데이터에도 사용할 수 있습니다.

session
→ 30분 뒤 만료

하지만 주문 원본 같은 영속 데이터에 사용할 때는 다음도 확인해야 합니다.

내구성

복구

복제

트랜잭션 범위

검색 요구

키값이라는 모델 이름만으로 적합성을 판단하면 안 됩니다.


12장. Redis도 단순 문자열 키값 저장소만은 아니다#

Redis를 다음처럼만 설명하면 부족합니다.

Key → String

실제로 다양한 자료형을 지원할 수 있습니다.

String

Hash

List

Set

Sorted Set

Stream

따라서 같은 키값 계열이라도 제품 기능을 따로 확인해야 합니다.


13장. 문서 모델은 함께 읽는 데이터를 하나의 문서로 묶기 좋다#

주문 화면에서는 다음 데이터를 함께 보여줍니다.

주문 정보

배송지

품목

주문 당시 가격

주문 상태

이 정보를 하나의 주문 문서에 담으면 한 번에 가져오기 쉽습니다.

{
  "_id": 501,
  "customer_id": 7,
  "status": "paid",
  "items": [
    {
      "product_id": 11,
      "quantity": 2,
      "unit_price": 5000
    },
    {
      "product_id": 12,
      "quantity": 1,
      "unit_price": 3000
    }
  ],
  "order_total": 13000
}

14장. 문서에 품목을 넣는 것은 읽기 경계를 정하는 일이다#

관계형 모델에서는:

ORDER

ORDER_ITEM

으로 나눌 수 있습니다.

문서 모델에서는 주문 안에 품목 배열을 포함할 수 있습니다.

flowchart TD
    O["Order 501"] --> I1["Product 11 × 2"]
    O --> I2["Product 12 × 1"]

주문 화면에서 항상 함께 읽는다면 자연스러운 구조입니다.


15장. 하지만 모든 데이터를 한 문서에 넣으면 좋은 것은 아니다#

다음과 같은 구조를 생각할 수 있습니다.

Customer 7
↓
모든 주문
↓
모든 주문 품목
↓
모든 배송 이력

한 고객 문서에 모든 주문을 계속 추가하면 문서가 끝없이 커질 수 있습니다.

10개 주문

100개 주문

10,000개 주문

100만 개 주문

문서 크기와 변경 경합 문제가 생길 수 있습니다.

따라서:

함께 읽는다고 해서 무조건 모두 한 문서에 넣는 것은 아니다.

라는 원칙이 중요합니다.


16장. 문서 경계는 변경 경계와도 관련된다#

주문 한 건 안에서 다음 값이 함께 바뀐다고 하겠습니다.

품목 수량

주문 합계

상품 11 수량을 2개에서 3개로 변경합니다.

새 합계:

3 × 5,000
+
1 × 3,000
=
18,000

입니다.

두 값이 한 문서 안에 있다면 제품이 제공하는 단일 문서 원자적 변경 범위 안에서 함께 수정할 수 있는지 확인할 수 있습니다.


17장. order_total은 중복 데이터지만 이유가 있을 수 있다#

품목 배열만 있으면 주문 합계를 계산할 수 있습니다.

quantity × unit_price

를 모두 합하면 됩니다.

그런데 문서에:

order_total = 13000

도 저장했습니다.

중복입니다.

하지만 주문 목록 화면에서 합계를 매우 자주 읽는다면 계산을 줄이기 위해 저장할 수 있습니다.

대신 새로운 책임이 생깁니다.

품목 수정
↓
order_total도 반드시 변경

18장. 상품의 현재 가격과 주문 당시 가격은 다른 사실이다#

상품 11의 현재 가격이 6,000원으로 바뀌었다고 하겠습니다.

하지만 주문 501 당시 가격은 5,000원이었습니다.

주문 문서의:

unit_price = 5000

을 새 가격으로 6,000으로 바꾸면 과거 주문금액까지 달라집니다.

따라서 unit_price는 단순 중복이 아니라:

주문 당시 판매 단가

라는 역사적 사실일 수 있습니다.


19장. 상품명도 현재값인지 스냅샷인지 결정해야 한다#

주문 문서에 다음을 저장한다고 하겠습니다.

product_name = 무선 마우스

나중에 상품명이:

무선 마우스 Pro

로 바뀝니다.

주문서도 새 이름으로 보여야 할까요?

두 가지 정책이 가능합니다.

현재 이름#

상품명이 바뀌면 과거 주문에도 새 이름을 보여줍니다.

주문 당시 이름#

과거 주문에는 당시 표시명을 그대로 남깁니다.

같은 중복 필드라도 의미가 다릅니다.


20장. 문서형 데이터베이스도 스키마가 없는 것은 아니다#

문서형 DB를 다음처럼 말하기도 합니다.

스키마가 없다.

정확히는 관계형 테이블처럼 고정된 구조만 강제하지 않을 수 있다는 의미에 가깝습니다.

실제 서비스에서는 여전히 다음이 필요합니다.

필수 필드

자료형

버전

검증 규칙

인덱스

제품에 따라 스키마 검증 기능도 사용할 수 있습니다.


21장. 문서형 DB도 트랜잭션이 없다고 단정할 수 없다#

현대 문서 데이터베이스 중에는 단일 문서뿐 아니라 여러 문서에 대한 트랜잭션 기능을 제공하는 제품도 있습니다.

따라서:

RDBMS
→ 트랜잭션 있음

문서 DB
→ 트랜잭션 없음

처럼 나누면 부정확합니다.

확인해야 할 것은:

어떤 범위까지 원자적으로 변경할 수 있는가?

분산 환경에서는 비용이 어떻게 달라지는가?

입니다.


22장. 와이드 컬럼에서는 조회 질문에서 파티션 키를 거꾸로 정한다#

고객별 최근 주문 조회가 매우 중요하다고 하겠습니다.

질문:

고객 7의 최근 주문 20건을 보여 달라.

그러면 다음처럼 데이터를 모을 수 있습니다.

Partition Key
customer_id = 7

Clustering Key
ordered_at
order_id

개념적으로:

Customer 7 Partition

2026-09-30 | Order 550
2026-09-28 | Order 544
2026-09-20 | Order 530
...

입니다.


23장. 같은 고객의 주문을 한 파티션에 모으면 범위 조회가 자연스럽다#

고객 7의 주문은 같은 파티션에 있습니다.

flowchart TD
    C["customer_id = 7"] --> O1["2026-09-30 / O550"]
    C --> O2["2026-09-28 / O544"]
    C --> O3["2026-09-20 / O530"]

최근 주문 조회는 해당 파티션 안의 정렬 범위를 읽으면 됩니다.


24장. 그런데 상품 11 구매 고객을 찾는 질문은 어려워진다#

현재 데이터 배치는:

customer_id
→ 주문들

입니다.

새 질문은:

product_id = 11
→ 고객들

입니다.

접근 방향이 반대입니다.

고객ID를 모르는 상태에서 상품 11 구매자를 찾으려면 여러 고객 파티션을 조회해야 할 수 있습니다.


25장. 그래서 상품 중심 조회 테이블을 따로 만들 수 있다#

예를 들어:

orders_by_customer

orders_by_product

두 개를 유지합니다.

고객별#

Partition Key
customer_id

상품별#

Partition Key
product_id

같은 주문을 서로 다른 질의에 맞게 중복 저장합니다.


26장. 조회를 빠르게 만들수록 쓰기 증폭이 발생한다#

주문 하나를 저장할 때:

orders_by_customer

에 한 번 씁니다.

주문에 상품이 두 개라면:

orders_by_product

에도 상품별로 기록해야 할 수 있습니다.

즉 논리 주문 하나가 여러 물리 쓰기로 확장됩니다.

flowchart LR
    O["Order 501"] --> C["customer=7 조회표"]
    O --> P11["product=11 조회표"]
    O --> P12["product=12 조회표"]

읽기 성능의 대가가 쓰기 복잡도입니다.


27장. 취소 이벤트도 모든 조회 모델에 반영해야 한다#

주문 501을 취소합니다.

원본:

O501
status = cancelled

고객별 테이블에서도 취소 상태를 반영합니다.

상품별 테이블도 마찬가지입니다.

하나라도 실패하면 화면마다 다른 데이터를 보여줄 수 있습니다.


28장. 같은 취소 이벤트가 두 번 들어오면 어떻게 될까#

메시지 큐나 네트워크 재시도로 인해 같은 이벤트를 두 번 받을 수 있습니다.

event_id = E900

order_id = 501

status = cancelled

첫 처리에서 상품 11 판매량을 2개 줄였습니다.

같은 이벤트를 다시 처리하면서 또 2개를 줄이면 잘못됩니다.

원래 판매량
100

1차 취소
98

중복 이벤트
96

실제로는 98이어야 합니다.


29장. 사본을 비동기로 갱신한다면 멱등성을 설계해야 한다#

이벤트에 다음 정보를 둘 수 있습니다.

event_id

order_id

version

소비자는 이미 처리한 이벤트인지 확인하거나 최신 버전보다 오래된 이벤트인지 판정할 수 있습니다.

핵심은:

같은 이벤트 여러 번
→ 최종 결과 한 번 적용한 것과 같음

이 되도록 만드는 것입니다.


30장. 사본 지연을 어느 정도 허용할지도 먼저 정해야 한다#

상품별 구매자 목록이 주문 직후 반드시 정확해야 합니까?

두 가지 요구가 있을 수 있습니다.

실시간 정확성 필요#

지연 허용
0초에 가까움

더 강한 동기 변경 구조가 필요할 수 있습니다.

분석 화면#

지연 허용
5분

비동기 이벤트로 조회 모델을 갱신할 수 있습니다.

같은 데이터라도 업무 요구에 따라 구조가 달라집니다.


31장. 파티션 키가 너무 한쪽으로 몰리면 핫 파티션이 된다#

고객ID를 파티션 키로 정했다고 하겠습니다.

대부분 고객의 주문:

하루 1~3건

그런데 대형 기업 고객 999의 주문은:

하루 100만 건

입니다.

고객 999의 파티션에 트래픽이 집중됩니다.

따라서 파티션 키에서는 다음을 함께 봐야 합니다.

데이터 분포

요청 분포

시간에 따른 증가량

32장. 기간 버킷을 함께 사용할 수도 있다#

고객 하나의 주문 이력이 너무 커진다면:

customer_id
+
year_month

같은 복합 파티션 기준을 검토할 수 있습니다.

예:

customer 7 / 2026-08

customer 7 / 2026-09

customer 7 / 2026-10

한 파티션이 무한히 커지는 것을 줄일 수 있습니다.

대신 고객 전체 주문을 볼 때 여러 기간 파티션을 조회해야 합니다.


33장. 와이드 컬럼은 분석용 컬럼 저장 포맷과 같은 말이 아니다#

이름 때문에 혼동하기 쉽습니다.

Wide-column database

와:

columnar analytical storage

는 같은 개념이 아닙니다.

와이드 컬럼 모델은 파티션 키와 행 배치를 중심으로 분산 조회를 설계하는 계열입니다.

분석용 컬럼 지향 파일 포맷이나 컬럼형 웨어하우스 저장 방식과 구분해야 합니다.


34장. 그래프 모델에서는 관계 자체가 주요 데이터다#

다음 관계가 있다고 하겠습니다.

고객 7
→ 주문 501

주문 501
→ 상품 11

주문 501
→ 상품 12

그래프로 표현하면:

flowchart LR
    C7["고객 7"] --> O501["주문 501"]
    O501 --> P11["상품 11"]
    O501 --> P12["상품 12"]

관계 경로가 중요한 질문에서 자연스럽습니다.


35장. 같은 상품을 구매한 고객을 관계 경로로 찾을 수 있다#

고객 8도 상품 11을 구매했다고 하겠습니다.

flowchart LR
    C7["고객 7"] --> O501["주문 501"]
    O501 --> P11["상품 11"]
    C8["고객 8"] --> O601["주문 601"]
    O601 --> P11

관계는 다음처럼 읽을 수 있습니다.

고객 7
→ 구매 주문
→ 상품 11
← 구매 주문
← 고객 8

이런 다단계 연결이 그래프 모델의 대표적인 강점입니다.


36장. 그래프 탐색에서는 시작점과 깊이를 제한해야 한다#

다음 요청은 위험할 수 있습니다.

고객 7과 연결된 모든 것을 무제한으로 찾아라.

연결이 많아질수록 탐색 경로가 폭발할 수 있습니다.

따라서 다음을 정하는 것이 중요합니다.

시작 노드

관계 종류

방향

최대 홉 수

시간 범위

37장. 경로 수와 대상 수를 혼동하면 통계가 부풀려질 수 있다#

고객 7과 고객 8이 상품 11과 상품 12를 모두 함께 구매했다고 하겠습니다.

연결 경로는 두 개입니다.

고객7 → 상품11 ← 고객8

고객7 → 상품12 ← 고객8

질문:

공통 구매 상품은 몇 개인가?

답:

2개

하지만:

공통 상품을 하나 이상 구매한 다른 고객은 몇 명인가?

답:

1명

입니다.

그래프 경로 수와 고유 대상 수는 다릅니다.


38장. 관계가 있다고 곧바로 업무상 의미가 생기는 것도 아니다#

부정 사용 탐지를 생각해 보겠습니다.

두 계정이 같은 IP를 사용했습니다.

Account A
→ IP 1

Account B
→ IP 1

이 관계만으로 두 계정이 부정 사용자라고 판단하면 위험합니다.

같은 회사나 가족이 동일한 네트워크를 사용할 수 있기 때문입니다.

관계 탐색과 업무 판단은 구분해야 합니다.


39장. 관계에는 시점도 중요하다#

다음 두 관계는 의미가 다를 수 있습니다.

A와 B
5년 전 같은 IP 사용

A와 B
오늘 같은 기기에서 로그인

따라서 그래프 모델에서도 관계에 다음 속성이 필요할 수 있습니다.

발생 시각

유효 기간

관계 종류

신뢰도

그래프라고 해서 시간 개념이 사라지는 것은 아닙니다.


40장. 월별 매출 합계만 필요하다면 그래프가 항상 최선은 아니다#

질문이 단순히:

지난달 전체 매출은 얼마인가?

라면 대규모 관계 탐색보다 집계용 데이터 구조가 더 직접적일 수 있습니다.

그래프 모델은 관계 탐색이 핵심일 때 가치가 큽니다.

누가 누구와 연결되어 있는가?

몇 단계를 거치면 연결되는가?

공통 관계가 무엇인가?

와 같은 질문이 대표적입니다.


41장. 같은 서비스 안에서 여러 데이터 모델을 함께 쓸 수도 있다#

한 가지 데이터베이스로 모든 문제를 해결해야 하는 것은 아닙니다.

예:

주문 원장
→ 관계형 DB

세션
→ 키값 저장소

상품 카탈로그
→ 문서형 DB

추천 관계
→ 그래프 DB

고객별 대규모 이벤트
→ 와이드 컬럼

이런 방식을 흔히 여러 저장 기술을 목적에 따라 조합하는 폴리글랏 퍼시스턴스 관점으로 설명하기도 합니다.


42장. 하지만 저장소가 늘어나면 일관성 책임도 늘어난다#

주문 501 정보가 여러 저장소에 있다고 하겠습니다.

주문 원본 DB

Redis 캐시

상품별 조회 테이블

검색 인덱스

그래프 DB

주문이 취소되면 모두 갱신되어야 할 수 있습니다.

한 곳이라도 실패하면:

주문 원본
→ 취소

캐시
→ 결제완료

검색
→ 구매중

그래프
→ 여전히 구매 관계

같은 문제가 발생할 수 있습니다.


43장. 원본과 조회용 사본을 구분해야 한다#

여러 곳에 데이터가 있다면 반드시 정해야 합니다.

어느 저장소가 원본인가?

예:

주문 DB
→ Source of Truth

Redis
→ 캐시

상품별 조회 DB
→ 파생 조회 모델

검색 인덱스
→ 검색용 사본

사본이 틀렸을 때 원본에서 다시 만들 수 있어야 합니다.


44장. 조회용 사본은 재생성 가능하게 만드는 것이 중요하다#

상품별 판매 조회 테이블이 잘못됐다고 하겠습니다.

원본 주문 이벤트를 다시 재생해 조회 모델을 재구축할 수 있다면 복구가 가능합니다.

원본 주문
+
변경 이벤트
↓
상품별 조회 모델 재생성

반대로 어느 값이 원본인지 불분명하면 사본 오류를 고치기 어렵습니다.


45장. 신규 조회가 하나 늘어날 때 실제 비용을 계산해 보자#

현재 하루 주문량:

100만 건

기존 요구:

주문ID로 주문 조회

키값이나 문서 기반 구조로 잘 처리하고 있습니다.

새 요구:

최근 7일 상품 A를 산 고객을 보여 달라.

기존 주문만 사용하면 최대:

7일 × 100만
=
700만 주문

을 검사해야 할 수 있습니다.

상품 기준 조회 구조가 필요해질 수 있습니다.


46장. 상품별 조회 사본을 만들면 변경 사건마다 처리할 일이 생긴다#

주문 O1에 상품 A 두 개가 들어 있습니다.

결제 완료:

O1
A × 2
paid

상품별 조회 모델:

A
→ O1 추가

주문 취소:

O1
cancelled

조회 모델도 수정합니다.

같은 취소 이벤트 재전송:

이미 처리된 버전
→ 중복 차감 금지

조회 구조 하나가 추가될 때마다 이런 상태 변화까지 함께 설계해야 합니다.


47장. 새 조회의 허용 지연을 먼저 결정하면 설계가 쉬워진다#

질문:

상품 A를 구매한 고객 목록은 얼마나 최신이어야 하는가?

결제 직후 즉시#

지연 거의 불가

원본과 강하게 연결된 갱신 전략을 검토해야 합니다.

분석 페이지#

5분 지연 허용

비동기 이벤트 처리가 더 실용적일 수 있습니다.

야간 보고서#

하루 지연 가능

배치 처리도 가능할 수 있습니다.

최신성 요구는 데이터 모델 선택의 중요한 조건입니다.


48장. NoSQL에서 중복은 잘못이 아니라 의도적인 설계일 수 있다#

관계형 정규화에서는 중복 제거가 중요한 원칙입니다.

NoSQL 조회 모델에서는 읽기 효율을 위해 데이터를 여러 형태로 중복 저장하는 경우가 많습니다.

예:

주문별

고객별

상품별

각각 같은 주문 일부를 가지고 있을 수 있습니다.

문제는 중복 자체가 아닙니다.

중복된 데이터의 일관성을 누가 책임지는가?

가 핵심입니다.


49장. 주문 취소 하나로 중복 데이터의 비용을 확인할 수 있다#

주문 501을 취소했습니다.

변경 대상:

주문 원본

고객별 주문 목록

상품별 구매자 목록

판매량 집계

캐시

검색 인덱스

읽기 모델을 다섯 개 만들었다면 취소의 전파 경로도 그만큼 늘어날 수 있습니다.

따라서 모델 선택에서는:

읽기 비용 감소

뿐 아니라:

변경 전파 비용 증가

도 계산해야 합니다.


50장. ACID 대 BASE라는 단순 비교표만으로 제품을 고르면 위험하다#

흔히 다음처럼 설명합니다.

RDBMS
→ ACID

NoSQL
→ BASE

하지만 현실은 훨씬 복잡합니다.

관계형 DB도 복제와 분산 환경에서 다양한 일관성 설정을 가집니다.

NoSQL 제품도 트랜잭션, 스키마 검증, 강한 읽기 옵션을 제공할 수 있습니다.

따라서 확인해야 하는 것은 제품 분류보다 다음입니다.

원자성 범위

읽기 일관성

복제 방식

분산 장애 동작

트랜잭션 지원 범위

51장. 수평 확장도 NoSQL만의 전유물이 아니다#

다음 식의 구분도 단순화된 설명입니다.

RDBMS
→ 수직 확장

NoSQL
→ 수평 확장

현대 관계형 데이터베이스도:

복제

파티셔닝

샤딩

분산 SQL

구조를 사용할 수 있습니다.

반대로 NoSQL이라고 수평 확장이 자동으로 쉬운 것도 아닙니다.

파티션 키가 잘못되면 핫스팟이 생길 수 있습니다.


52장. 제품 이름보다 접근 경로를 그려보는 편이 정확하다#

예를 들어 다음 질문을 받았다고 하겠습니다.

고객 7의 최근 주문 20건.

먼저 실제 접근 경로를 그립니다.

customer_id = 7
↓
한 파티션
↓
ordered_at 역순
↓
20건

좋습니다.

이번에는:

상품 11을 구매한 모든 고객.

현재 구조:

customer_id 중심

이라면 직접적인 경로가 없습니다.

새로운 조회 모델이 필요한 이유가 명확해집니다.


53장. 모델별로 방문하는 데이터 수를 대략 적어보자#

키값#

주문ID 조회:

키 1개

문서#

주문 상세:

문서 1개

와이드 컬럼#

고객 최근 주문:

파티션 1개
+
상위 20행

그래프#

친구의 친구:

시작 노드
+
관계 여러 단계

이렇게 접근량을 적으면 추상적인 “빠르다”보다 비교가 쉬워집니다.


54장. 같은 모델에서도 인덱스가 추가되면 성격이 달라질 수 있다#

문서 DB에서 상품ID 보조 인덱스를 만들 수 있습니다.

그러면:

상품 11이 들어간 주문

검색이 쉬워질 수 있습니다.

하지만 인덱스를 만들면 쓰기 때 인덱스도 갱신해야 합니다.

즉:

새 인덱스
→ 읽기 접근 경로 추가
→ 쓰기 비용도 추가

입니다.

이 원리는 관계형 DB와 크게 다르지 않습니다.


55장. 하나의 모델이 모든 조회를 완벽하게 해결하기는 어렵다#

예를 들어 주문 시스템의 질문이 다음과 같다고 하겠습니다.

주문ID 한 건 조회

고객 최근 주문

상품별 구매자

월별 매출

추천 관계

각 질문의 최적 접근 경로는 다릅니다.

따라서 설계 목표는:

모든 질의를 하나의 저장 구조로 해결한다.

가 아니라:

핵심 질의는 자연스럽게 처리하고, 추가 질의의 비용을 감당 가능한 수준으로 관리한다.

에 가깝습니다.


56장. 모델을 결정하기 전에 변경 시나리오도 써봐야 한다#

조회만 테스트하면 부족합니다.

주문 501에 다음 변화가 발생할 수 있습니다.

결제

부분 취소

전체 취소

배송

반품

환불

품목 수량 정정

각 사건이 발생할 때:

몇 개 저장소를 수정해야 하는가?

중간 실패하면 어떻게 되는가?

다시 처리해도 안전한가?

를 확인해야 합니다.


57장. 주문 501의 수량 변경으로 모델을 검증해 보자#

현재:

상품 11
2개 × 5,000

상품 12
1개 × 3,000

합계
13,000

상품 11을 3개로 수정합니다.

새 합계:

3 × 5,000
+
1 × 3,000
=
18,000

이때 확인할 것은 다음입니다.

주문 문서 수량
→ 3

order_total
→ 18,000

상품별 판매량
→ +1 반영

캐시
→ 무효화 또는 갱신

어느 한 곳이 빠지면 데이터가 어긋날 수 있습니다.


58장. 이벤트 버전을 사용하면 오래된 변경을 구분하는 데 도움이 된다#

주문 상태가 다음 순서로 변경됐다고 하겠습니다.

version 1
paid

version 2
cancelled

네트워크 지연으로 이벤트가 뒤집혀 도착합니다.

먼저 version 2

나중에 version 1

아무 생각 없이 순서대로 적용하면 최종 상태가 다시 paid로 돌아갈 수 있습니다.

버전 번호를 비교하면:

현재 version = 2

수신 version = 1

→ 무시

같은 정책을 사용할 수 있습니다.


59장. 델타 이벤트와 상태 이벤트도 구분해야 한다#

상태 이벤트:

재고 = 8
version = 10

은 버전 비교로 오래된 상태를 무시하기 쉽습니다.

델타 이벤트:

재고 -2

는 같은 이벤트를 두 번 처리하면 결과가 달라집니다.

따라서 이벤트 ID를 통한 중복 처리 방지가 더 중요합니다.

데이터 동기화에서는 이벤트의 의미까지 명확히 해야 합니다.


60장. 모델 선택표에는 조회뿐 아니라 변경과 실패도 넣자#

질문 키값 문서 와이드 컬럼 그래프
주문ID 한 건 매우 자연스러움 자연스러움 키 설계에 따라 가능 가능하지만 과할 수 있음
주문과 품목 함께 값 구조에 따라 가능 매우 자연스러움 별도 행 설계 필요 관계 탐색 가능
고객 최근 주문 별도 키 필요 인덱스 필요 매우 자연스러움 가능
상품 구매자 역방향 구조 필요 인덱스 필요 상품 기준 조회표 필요 자연스러운 관계 탐색
다단계 관계 직접 구조 설계 필요 반복 조회 가능 비자연적일 수 있음 강점
취소 사본 동기화 필요 필요 필요 관계 갱신 필요

절대적인 우열표가 아니라 요구와 구조의 적합성을 보는 표입니다.


61장. NoSQL을 선택할 때 확인해야 할 질문#

  1. 가장 자주 실행하는 조회는 무엇인가?
  2. 결과 한 건의 자연스러운 저장 단위는 무엇인가?
  3. 어떤 데이터가 항상 함께 읽히는가?
  4. 어떤 데이터가 항상 함께 변경되어야 하는가?
  5. 데이터 하나가 끝없이 커질 가능성이 있는가?
  6. 파티션 키가 특정 고객이나 시점에 몰리지 않는가?
  7. 역방향 조회가 얼마나 많은가?
  8. 새 조회를 위해 몇 개의 사본이 필요한가?
  9. 사본은 얼마나 늦어도 되는가?
  10. 같은 이벤트가 중복 전달되어도 안전한가?
  11. 이벤트 순서가 바뀌어도 복구 가능한가?
  12. 현재값과 역사적 스냅샷을 구분했는가?
  13. 원본 저장소가 무엇인지 명확한가?
  14. 파생 조회 모델을 재생성할 수 있는가?
  15. 제품이 제공하는 실제 트랜잭션 범위는 어디까지인가?

62장. “NoSQL이 더 빠르다”보다 “어떤 질문에 적합한가”를 물어야 한다#

관계형 데이터베이스가 느리고 NoSQL이 빠르다고 일반화할 수 없습니다.

예를 들어 주문ID 하나를 조회하는 키값 모델은 매우 효율적일 수 있습니다.

하지만 복잡한 임의 조건 분석을 요구하면 추가 구조가 필요합니다.

반대로 관계형 데이터베이스는 조인과 다양한 인덱스를 이용해 새로운 질의에 유연하게 대응할 수 있습니다.

따라서 비교 기준은:

DB 종류

가 아니라:

업무 질문

데이터 크기

읽기·쓰기 패턴

일관성 요구

운영 복잡도

여야 합니다.


63장. 핵심 정리#

NoSQL은 하나의 저장 방식이 아닙니다.

키값

문서

와이드 컬럼

그래프

는 서로 다른 질문에 최적화하기 위한 데이터 모델입니다.

키값 모델은:

키를 알고
한 값을 빠르게 찾는 문제

에 자연스럽습니다.

문서 모델은:

주문
+
품목
+
배송 정보

처럼 함께 읽고 변경하는 데이터를 하나의 경계로 묶는 데 유리할 수 있습니다.

와이드 컬럼 모델에서는:

고객ID
→ 파티션

주문 시각
→ 정렬

처럼 질문에서 거꾸로 파티션 키를 선택하는 사고방식이 중요합니다.

그래프 모델은:

고객
→ 주문
→ 상품
← 다른 주문
← 다른 고객

처럼 여러 단계의 관계 경로가 핵심인 질문에 자연스럽습니다.

하지만 어떤 모델을 선택해도 새로운 조회를 위해 데이터를 중복 저장하면 변경 책임도 늘어납니다.

주문 원본 변경
↓
고객별 사본
↓
상품별 사본
↓
검색 인덱스
↓
캐시

가 모두 맞아야 할 수 있습니다.

따라서 중복 데이터에서 가장 중요한 질문은:

중복이 있는가?

가 아니라:

어느 값이 원본이고, 사본이 틀렸을 때 어떻게 다시 맞출 것인가?

입니다.

또한 주문 문서에 복사한 값이 모두 동기화 대상인 것도 아닙니다.

상품 현재 가격

과:

주문 당시 판매 단가

는 다른 사실입니다.

과거 거래 사실을 저장한 값은 상품 가격이 바뀌어도 그대로 유지해야 할 수 있습니다.

NoSQL 모델을 선택할 때 가장 중요한 기준은 제품 이름이나 유행이 아닙니다.

가장 중요한 조회가 어떤 데이터를 함께 읽는가?

그리고:

그 조회를 빠르게 만들기 위해 복사한 데이터를 변경·취소·재처리할 때 얼마나 많은 곳을 다시 맞춰야 하는가?

이 두 질문에 답할 수 있어야 저장 모델의 장점뿐 아니라 실제 운영 비용까지 설명할 수 있습니다.

이 페이지의 목차