본문 바로가기
TYLER SONGBlog
← Blog index
CS학습5 min read

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에서 문서 모델을 구현하는 예제다.

sql
-- 문서 컬렉션 역할을 하는 테이블 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 안에서도 문서 모델을 쓸 수 있고, 핵심 필드는 컬럼으로 분리하는 하이브리드 설계가 현실적이다.
  • 접근 패턴(무엇을 함께 읽고, 무엇이 자주 바뀌는가)이 모델 선택의 기준이다.