Document: 스키마 유연성과 계층 구조를 한 덩어리로 저장하는 데이터 모델
문서(Document) 데이터베이스는 JSON 같은 계층 구조의 데이터를 하나의 단위로 저장하고 조회하는 모델이다. 관련 데이터를 한 문서에 모아 조인(JOIN) 없이 읽을 수 있고, 레코드마다 구조가 달라도 된다. PostgreSQL의 JSONB 타입으로 문서 모델을 관계형 DB 안에서 구현하는 방법과 한계를 함께 다룬다.
By 송민성
1. 개념
문서(Document)는 키-값 쌍, 배열, 중첩 객체로 이루어진 자기 서술적(self-describing) 데이터 단위다. 보통 JSON 또는 이진 인코딩 형식(BSON 등)으로 표현한다. 문서 데이터베이스(Document Database)는 이 문서를 저장의 기본 단위로 삼으며, 문서 내부의 필드를 기준으로 질의하고 인덱스를 만들 수 있다.
관계형 모델과 비교하면 다음과 같다.
| 구분 | 관계형 (Relational) | 문서 (Document) |
|---|---|---|
| 저장 단위 | 행(row) | 문서 |
| 구조 | 테이블 스키마로 고정 | 문서마다 달라질 수 있음 |
| 관계 표현 | 외래 키(foreign key) + JOIN | 중첩(embedding) 또는 참조(reference) |
| 정규화 | 일반적으로 높음 | 비정규화(denormalization) 경향 |
2. 왜 사용하는가
- 지역성(locality): 주문과 주문 항목처럼 함께 읽는 데이터를 한 문서에 두면 한 번의 조회로 가져온다.
- 객체-관계 불일치(impedance mismatch) 완화: 애플리케이션의 객체/JSON 구조와 저장 구조가 비슷해 매핑 코드가 줄어든다.
- 스키마 유연성: 필드가 제품마다 다른 카탈로그, 이벤트 로그, 설정 데이터처럼 구조가 자주 바뀌는 데이터에 적합하다.
- 수평 확장(sharding) 친화: 문서 하나가 독립된 단위라 키 기준으로 분산하기 쉽다.
3. 동작 원리
임베딩(embedding)과 참조(reference)
- 임베딩: 하위 데이터를 문서 안에 중첩한다. 읽기는 빠르지만 중복이 생기고 문서가 커진다.
- 참조: 다른 문서의 ID만 저장한다. 중복은 줄지만 추가 조회(애플리케이션 조인)가 필요하다.
스키마 온 리드(schema-on-read) 저장 시점에 구조를 강제하지 않고, 읽는 쪽 코드가 구조를 해석한다. 구조가 암묵적으로 존재하므로 변경 시 코드와 데이터의 호환성은 개발자가 관리해야 한다.
내부 필드 인덱싱 문서 DB는 중첩 필드나 배열 요소에도 인덱스를 만들 수 있다. PostgreSQL에서는 jsonb 타입에 GIN 인덱스를 만들어 포함(containment) 연산 @>, 키 존재 연산 ? 등을 가속한다. jsonb는 텍스트를 파싱한 분해된 이진 형식으로 저장하므로 입력 시 약간 느리지만 처리는 빠르고, 중복 키는 마지막 값만 남기며 키 순서는 보존하지 않는다.
4. 코드 예제
PostgreSQL에서 문서 모델을 구현하는 예제다.
-- 문서 컬렉션 역할을 하는 테이블
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
doc JSONB NOT NULL
);
-- 구조가 서로 다른 문서 삽입
INSERT INTO products (doc) VALUES
('{"name": "노트북", "price": 1500000, "tags": ["전자", "컴퓨터"],
"specs": {"cpu": "8코어", "ram_gb": 16}}'),
('{"name": "티셔츠", "price": 25000, "tags": ["의류"],
"sizes": ["S", "M", "L"]}');
-- GIN 인덱스: 포함(@>), 키 존재(?) 연산 가속
CREATE INDEX idx_products_doc ON products USING GIN (doc);
-- 포함 연산: tags 배열에 "전자"가 있는 문서
SELECT id, doc->>'name' AS name
FROM products
WHERE doc @> '{"tags": ["전자"]}';
-- 중첩 필드 접근과 형 변환
SELECT doc->>'name' AS name,
(doc->'specs'->>'ram_gb')::int AS ram_gb
FROM products
WHERE doc ? 'specs';
-- 특정 필드 값에 대한 B-tree 표현식 인덱스 (범위 조회용)
CREATE INDEX idx_products_price
ON products (((doc->>'price')::numeric));
SELECT doc->>'name'
FROM products
WHERE (doc->>'price')::numeric < 100000;
-- 문서 일부만 수정 (jsonb_set)
UPDATE products
SET doc = jsonb_set(doc, '{price}', '1400000')
WHERE doc->>'name' = '노트북';5. 시간 복잡도 또는 성능 특성
- 키 기반 단건 조회: 기본 키 또는
_id인덱스가 B-tree라면 O(log n)이다. 해시 기반 인덱스는 평균 O(1)이지만 범위 조회는 못 한다. - 인덱스 없는 필드 조건 조회: 전체 문서를 훑는 전체 스캔(full scan)이므로 O(n)이다.
- GIN 인덱스: 읽기(포함 질의)는 빠르지만, 쓰기 시 인덱스 갱신 비용이 크다. PostgreSQL GIN은
fastupdate로 쓰기를 모아 처리해 이 비용을 완화한다. - 갱신 비용: PostgreSQL에서
jsonb의 일부 필드만 바꿔도 행 전체가 새 버전으로 기록된다. 큰 문서를 자주 부분 수정하면 쓰기 증폭이 커진다. 큰 값은 TOAST로 별도 저장되며 압축될 수 있다. - 지역성 이점: 임베딩된 데이터는 한 번의 I/O로 읽히지만, 문서가 클수록 필요 없는 부분까지 읽는 비용이 늘어난다.
6. 실무 사용 사례
- 상품 카탈로그: 카테고리별로 속성이 다른 상품 정보.
- 콘텐츠 관리/블로그: 게시글, 댓글, 메타데이터를 묶어서 저장.
- 사용자 프로필 및 설정: 확장될 가능성이 큰 선택적 필드.
- 이벤트/로그 수집: 스키마가 자주 바뀌는 이벤트 페이로드.
- 하이브리드 설계: 핵심 필드(ID, 생성일, 상태)는 일반 컬럼으로, 가변 속성은
jsonb컬럼에 두는 방식이 흔하다.
대표적인 전용 문서 DB로는 MongoDB, Couchbase, Amazon DocumentDB 등이 있다.
7. 주의할 점
- 무제한 임베딩 금지: 댓글처럼 계속 늘어나는 배열을 한 문서에 넣으면 문서가 비대해진다. MongoDB는 문서 하나의 최대 크기가 16MB다. 증가하는 데이터는 별도 문서로 분리하고 참조한다.
- 중복 데이터 불일치: 비정규화한 값이 원본과 어긋나지 않도록 갱신 경로를 설계해야 한다.
- 다대다(many-to-many) 관계와 복잡한 조인: 관계가 복잡할수록 관계형 모델이 더 적합하다.
- 스키마 부재의 비용: 필드 타입이 문서마다 다르면 질의와 형 변환에서 오류가 난다. 애플리케이션 검증이나
CHECK제약, DB의 스키마 검증 기능으로 최소한의 구조를 강제하는 편이 좋다. - 트랜잭션 범위: 단일 문서 연산은 원자적(atomic)으로 보장되는 DB가 많지만, 여러 문서에 걸친 일관성은 제품별 지원 범위를 확인해야 한다.
- 인덱스 없는 JSON 질의: 운영 환경에서
EXPLAIN으로 실제 인덱스 사용 여부를 확인한다.
8. 핵심 정리
- 문서는 중첩 구조를 가진 자기 서술적 데이터 단위이며, 문서 DB는 이를 저장·질의의 기본 단위로 쓴다.
- 함께 읽는 데이터는 임베딩, 독립적으로 커지거나 공유되는 데이터는 참조로 설계한다.
- 스키마 유연성의 대가로 구조 일관성은 애플리케이션과 검증 규칙이 책임진다.
- PostgreSQL의
jsonb+ GIN 인덱스로 관계형 DB 안에서도 문서 모델을 쓸 수 있고, 핵심 필드는 컬럼으로 분리하는 하이브리드 설계가 현실적이다. - 접근 패턴(무엇을 함께 읽고, 무엇이 자주 바뀌는가)이 모델 선택의 기준이다.