관계형 데이터베이스만 쓰다가 처음 NoSQL을 접했을 때는 솔직히 막막했다. "테이블 조인 없이 어떻게 데이터를 설계하지?" 싶었다. 그런데 실무에서 MongoDB를 몇 년 쓰면서 깨달은 건, NoSQL 데이터 모델링이 결국 '다르게 생각하는 법'을 배우는 과정이라는 점이다. 이 글에서는 실제 프로젝트에서 써먹은 NoSQL 데이터 모델링 패턴 6가지를, MongoDB 8 기준으로 정리한다.
NoSQL 데이터 모델링, 왜 RDBMS와 다른가
사고방식의 전환: 쿼리 중심 설계
RDBMS에서는 데이터의 구조(엔티티, 관계)를 먼저 정의하고, 그다음에 쿼리를 작성한다. 정규화를 열심히 하고, 필요하면 JOIN을 걸면 되기 때문이다. 하지만 NoSQL, 특히 MongoDB에서는 정반대다. "이 데이터를 어떻게 읽을 것인가"를 먼저 결정하고, 그에 맞춰 데이터 구조를 설계한다. 이걸 Query-Driven Design이라고 부른다.
예를 들어 전자상거래 서비스에서 주문 상세 페이지를 보여줘야 한다면, 주문 정보·상품 정보·배송 정보를 한 문서에 다 넣는 게 NoSQL에서는 자연스럽다. RDBMS라면 orders, products, shipping 세 테이블을 조인했을 것이다.
정규화 vs 비정규화의 트레이드오프
NoSQL 데이터 모델링에서 가장 핵심적인 판단은 "임베딩(내장)할 것이냐, 레퍼런스(참조)할 것이냐"다. 임베딩은 관련 데이터를 한 문서 안에 넣는 것이고, 레퍼런스는 별도 컬렉션에 두고 ID로 연결하는 것이다. 정답은 없다. 읽기 성능이 중요하면 임베딩, 데이터 일관성과 갱신 빈도가 중요하면 레퍼런스를 택한다.
스키마리스의 함정
MongoDB가 스키마리스(Schemaless)라고 해서 아무렇게나 데이터를 넣어도 된다는 뜻은 절대 아니다. 오히려 스키마 설계를 더 신중하게 해야 한다. $jsonSchema 기반 스키마 검증(Schema Validation)을 걸어두고, 애플리케이션 레벨에서도 Mongoose 같은 ODM으로 스키마를 잡아주는 게 좋다. 이걸 안 하면 나중에 데이터 정합성 문제로 밤새 디버깅하게 된다.
실무에서 자주 쓰는 핵심 모델링 패턴 6가지
1. 임베디드 문서 패턴과 2. 참조 패턴
임베디드 문서 패턴(Embedded Document Pattern)은 가장 기본이 되는 패턴이다. 1:1 또는 1:소수 관계에서 자식 데이터를 부모 문서 안에 내장한다. 블로그 포스트와 댓글 관계가 대표적이다. 댓글이 수십 개 수준이라면 포스트 문서 안에 comments 배열로 넣는 게 효율적이다. 한 번의 읽기로 포스트와 댓글을 모두 가져올 수 있기 때문이다.
반면 참조 패턴(Reference Pattern)은 1:다수 또는 다:다 관계에서 사용한다. 댓글이 수천, 수만 개가 될 수 있는 서비스라면 별도 컬렉션으로 분리하고 postId로 참조해야 한다. MongoDB 문서 크기 제한이 16MB이기 때문에 무한정 임베딩할 수는 없다.
3. 버킷 패턴과 4. 서브셋 패턴
버킷 패턴(Bucket Pattern)은 시계열 데이터나 IoT 센서 데이터처럼 연속적으로 쌓이는 데이터에 최적화된 패턴이다. 센서에서 1초마다 데이터가 들어온다면, 매 초마다 새 문서를 만드는 대신 1시간 단위로 하나의 문서에 묶어 저장한다. 이렇게 하면 문서 수가 크게 줄고 인덱스 크기도 극적으로 감소한다.
요즘은 버킷을 수동으로 만드는 대신 네이티브 시계열 컬렉션(Time Series Collection, 5.0+)을 쓰는 경우가 많다. timeField·metaField만 지정하면 내부적으로 버킷팅·압축을 자동 처리한다. 버킷 패턴의 개념을 이해해두면, 이 기능이 왜 그렇게 동작하는지가 바로 보인다.
서브셋 패턴(Subset Pattern)은 자주 접근하는 데이터만 메인 문서에 두고 나머지는 별도 컬렉션에 보관하는 방식이다. 상품 페이지에서 리뷰를 보여줄 때, 최신 리뷰 5개만 상품 문서에 임베딩하고 전체 리뷰는 reviews 컬렉션에 따로 두는 식이다. Working Set(자주 접근하는 데이터)을 메모리에 유지할 수 있어 캐시 효율이 좋아진다.
5. 다형성 패턴과 6. 속성 패턴
다형성 패턴(Polymorphic Pattern)은 비슷하지만 구조가 조금씩 다른 데이터를 하나의 컬렉션에 저장하는 패턴이다. 콘텐츠 서비스에서 텍스트 글·이미지·동영상이 각각 다른 필드를 갖더라도 하나의 posts 컬렉션에 type 필드로 구분해 저장한다. RDBMS였다면 테이블을 세 개 만들거나 복잡한 상속 구조를 잡아야 했을 작업이다.
속성 패턴(Attribute Pattern)은 필드가 유동적으로 변하는 경우에 유용하다. 전자제품 쇼핑몰에서 노트북은 CPU·RAM·저장공간 스펙이 있고, 냉장고는 용량·에너지 등급 스펙이 있다. 이런 경우 specs라는 배열 안에 { k: "cpu", v: "M4" } 같은 {키, 값} 쌍으로 저장하면, 모든 속성을 하나의 인덱스로 검색할 수 있다.
패턴별 비교 - 언제 무엇을 쓰나
여섯 패턴을 한 표로 정리하면 선택 기준이 한눈에 들어온다.
| 패턴 | 적합한 상황 | 핵심 효과 |
|---|---|---|
| 임베디드 | 1:1 ~ 1:소수, 함께 읽는 데이터 | 단일 읽기로 조회, 조인 제거 |
| 참조 | 1:다수, 다:다, 독립적 갱신 | 중복 제거, 일관성 확보 |
| 버킷 | 시계열·IoT 등 연속 데이터 | 문서 수·인덱스 축소 |
| 서브셋 | 일부만 자주 조회되는 대용량 | Working Set 메모리 유지 |
| 다형성 | 유형이 다른 유사 데이터 | 단일 컬렉션 통합 조회 |
| 속성 | 가변·다양한 속성 필드 | 인덱스 하나로 전 속성 검색 |
실전 모델링 팁
읽기 패턴을 먼저 적어라
설계를 시작하기 전에 "이 서비스에서 가장 자주 실행되는 쿼리 3~5개"를 먼저 글로 적는다. 그 쿼리가 한 번의 조회로 끝나도록 문서 구조를 맞추는 게 NoSQL 설계의 출발점이다. 화면(접근 패턴)에서 거꾸로 스키마를 끌어내는 셈이다.
임베딩의 한계선을 정해두라
배열을 임베딩할 때는 "이 배열이 무한히 커질 수 있는가"를 항상 자문한다. 끝없이 늘어나는 배열(좋아요, 로그, 댓글)은 16MB 한계와 갱신 비용 문제를 일으킨다. 상한이 보이지 않으면 참조 또는 서브셋으로 전환한다.
"임베딩 우선, 필요할 때 참조로 분리"가 안전한 기본 전략이다. 처음부터 과하게 정규화하면 NoSQL의 장점인 단일 읽기 성능을 잃는다. 반대로 무작정 다 임베딩하면 문서가 비대해진다. 접근 패턴이 그 경계를 정해준다.
인덱스를 패턴과 함께 설계하라
모델링과 인덱스는 한 묶음이다. 자주 쓰는 쿼리의 필터·정렬 필드에 복합 인덱스를 잡고, explain()으로 실제 실행 계획을 확인한다. 아무리 좋은 패턴도 인덱스가 없으면 컬렉션 풀스캔으로 무너진다.
마무리
NoSQL 데이터 모델링의 본질은 "정답 스키마"를 찾는 게 아니라, 서비스의 읽기 패턴에 맞는 트레이드오프를 고르는 일이다. 여섯 패턴은 각각 어떤 상황을 위해 존재하는지가 분명하므로, 외우기보다 "지금 내 데이터의 접근 패턴이 어디에 해당하는가"를 떠올리면 자연스럽게 선택된다.
MongoDB 8 시대에는 시계열 컬렉션처럼 패턴을 내장한 기능도 늘었지만, 그 기능이 왜 그렇게 동작하는지는 결국 이 패턴들을 이해해야 보인다. 작은 프로젝트라도 한 번 의식적으로 적용해보면, 다음 설계부터는 접근 패턴이 먼저 보이기 시작한다.