MongoDB 확장 JSON (Extended JSON)
- 내부적으로 BSON의 일부 타입을 지원하기 때문에 mongo에서 생성된 JSON 도큐먼트를 다른 도구들이 인식하지 못하는 경우 발생
- mongo의 JSON을 STRICT 모드와 mongo shell 모드로 구분하여 사용할 수 있다
- STRICT 모드는 mongo 도구뿐만 아니라 외부의 모든 JSON 도구들이 JSON 도큐먼트를 파싱할 수 있다
- 하지만 Binary, ObjectId와 같은 타입은 인식하지 못한다.
- mongo shell 모드의 JSON 표기법을 사용하면 예외 발생
모델링 고려 사항
- 도큐먼트 크기
- 일반적으로 도큐먼트는 RDB의 레코드의 크기보다 큰 경우가 많다.
- Document DB 특성 상 하나의 도큐먼트의 여러 데이터를 모아 저장하는 경우가 있는 것으로 보임
- 최대 크기 제한
- 단일 document의 최대 크기는 16MB로 고정되어 있다.
- 16MB를 초과하는 파일, 이미지, 대용량 로그 등의 경우 GridFS를 활용해 여러 청크로 분산 저장해야 한다.
- 성능 및 리소스
- 큰 document는 조회, 수정, 저장 시 더 많은 RAM과 디스크 I/O가 필요하므로 성능 저하 문제가 발생할 수 있다.
- read/write 시 불필요한 large field까지 모두 불러오게 되어 네트워크 대역폭과 전체 시스템의 부담으로 이어진다.
- frequently accessed document 크기가 크면 working set이 RAM을 쉽게 초과하여, 디스크 접근 빈도가 늘어나 DB 성능에 직접적인 영향을 준다.
- 스키마 설계 Best Practice
- 필요한 정보만 document에 포함시키고, 불필요한 필드를 최소화한다.
- 깊은 중첩(nesting)을 피하며, embedded document(역정규화)와 referenced document(정규화) 전략을 적절하게 섞는다.
- 대형 배열, unbounded array는 도큐먼트 크기를 빠르게 증가시키므로, 배열 내 개수를 제어하거나 분할 설계를 적용해야 한다.
- 인덱스 대상 필드를 신중히 선정하며, 모든 필드를 인덱싱하면 오버헤드가 커진다.
- 성장 가능성이 높은 document 구조는 사전에 분리(분할) 설계를 고려한다.
- 기타 참고사항
- Object.bsonsize() 등 shell 함수로 BSON 크기를 직접 체크할 수 있다.
- 도큐먼트 크기 제한은 디버깅이나 ETL, batch 작업에서도 자주 문제가 되므로, 예상 성장 패턴까지 감안한 모델링이 필요하다.
- 정규화와 역정규화
- 정규화 (Document Referencing)
- 관계형 DB의 “정규화”와 유사하게, 관련 데이터를 별도의 컬렉션(테이블)로 분리하고 필요할 때 참조(reference)하는 방식
- 학생과 수강과목 데이터를 각각 독립된 컬렉션에 저장하고, 학생 도큐먼트에는 각 과목의 id만 리스트 형태로 저장
- 장점
- 데이터 중복 최소화(변경 발생 시 하나만 수정)
- 데이터 일관성, 무결성 보장에 유리
- 컬렉션(테이블)의 크기를 작게 유지 가능
- 단점
- 데이터 조회 시 여러 컬렉션을 조합해야 하므로 join(lookup) 비용 증가
- MongoDB는 복잡한 join이나 조인 성능이 RDB에 비해 떨어지는 편이다.
- 권장 사례
- 큰 서브 도큐먼트, 자주 갱신되는 데이터, 즉각적 일관성 필요, 증가량 많은 데이터, 빠른 쓰기 필요
- 쓰기나 수정이 잦고, 일관성·데이터 크기가 큰 경우 referencing이 더 적합합니다.
- 역정규화 (Embedding)
- 관련 데이터를 한 도큐먼트 안에 중첩(embedded) 형태로 직접 포함시키는 방식
- 학생 도큐먼트 내에 수강과목 정보를 배열로 직접 포함(예시: { classes: [{ class: “이산수학” }, …] }).
- 장점
- 조회 시 필요한 데이터가 한 번에 로드되어 읽기 성능(쿼리 효율)이 좋음
- 데이터 구조가 간단해져 복합 쿼리를 줄일 수 있음
- join이 필요없는 구조이므로 빠른 read에 최적화
- 단점
- 데이터 중복 증가(같은 정보가 여러 문서에 있음)
- 일관성 관리가 어렵고, 데이터 갱신 시 여러 문서를 동시 수정 필요
- 단일 도큐먼트 최대 크기(16MB) 제한에 주의 필요
- 권장 사례
- 작은 서브 도큐먼트, 자주 변하지 않는 데이터, 결과적 일관성 허용, 증가량 적은 데이터, 빠른 읽기 필요
- 읽기 빈도가 높고, 데이터 변경이 적거나 크기가 작으면 embedding을 추천합니다.
- 서브 도큐먼트
- 갱신 패턴과 단편화 문제
- 도큐먼트 갱신은 항상 전체 도큐먼트 단위(atomic)로 이뤄진다.
- 서브 도큐먼트가 빈번히 추가/삭제/변경되면, 내부적으로 단편화(fragmentation)가 심화되어 성능 저하 발생
- 주기적 갱신이 많은 데이터는 Reference(정규화) 방식이 더 적합하다.
- 데이터 액세스 패턴
- 서브 도큐먼트가 항상 부모 도큐먼트와 함께 조회된다면 embedding이 이상적이다.
- 반면, 독립 조회나 별도 join이 빈번하다면 referencing이 효율적일 수 있다.
- 일관성과 중복
- embedded 구조는 데이터 일관성 유지가 쉽지 않다. 부모 도큐먼트가 여러 곳에 중복 저장될 수 있으므로, 일괄 갱신이 어렵다.
- 자주 변하지 않는 데이터, 작은 정적 데이터(코드, 속성 등)에 적합하다.
- 배열의 크기와 관리
- 큰 배열이나 크기가 계속 증가하는 embedded array는 도큐먼트 크기 한계에 가까워질 수 있다.
- 배치가 필요하거나 배열이 무제한적으로 늘어날 수 있다면, 별도 컬렉션에 저장 후 referencing을 권장한다.
- 인덱싱과 쿼리 성능
- 서브 도큐먼트 내 필드도 인덱스 생성 가능하지만, 지나치게 깊은 중첩은 인덱스 성능에 영향을 줄 수 있다.
- 주로 사용하는 쿼리 조건과 인덱스 구성을 미리 설계해야 한다.
- 정리하면, 서브 도큐먼트 구조는 데이터의 라이프사이클, 크기, 변경 빈도, 접근 패턴, 일관성 요구, 쿼리의 효율성을 모두 종합적으로 고려하여 설계해야 한다.
- 불필요한 대형 도큐먼트 생성을 피하고 유지보수와 성능 모두를 균형 있게 신경 쓰는 것이 중요
- 배열
- RDB에서는 다른 형태의 데이터를 저장할 수 없고 배열같은 타입은 정규화를 통해 해결한다.
- Mongo는 배열 타입을 가질 수 있으며 멀티키를 통해 인덱스도 가능하다.
- 또한 단일 도큐먼트 내에서만 원자성을 보장하기 때문에 배열 타입은 트랜잭션이 지원하지 않는 단점을 보완해준다
- 도큐먼트 크기 증가
- 만약 하나의 게시물 도큐먼트의 댓글을 배열로 저장하면 도큐먼트의 크기는 계속 증가하게 되며 이는 디스크나 메모리, CPU 자원 낭비 발생
- 배열 관련 연산자 선택
- $push, $pop
- 배열 타입에 저장된 모든 아이템을 비교하지 않아도 되어 빠른 처리 가능
- $addToSet, $pull($pullAll)
- 기존 배열에 있는지/없는지 확인해야 하므로 배열의 모든 아이템과 비교 작업 수행
- 배열과 복제
- 가용성을 높이기 위해 레플리카셋을 활용하여 동일한 데이터 복제본을 갖게 되며 복제를 위해 oplog 사용
- 복제 방식
- 모든 데이터 변경은 Primary 노드에서만 이루어지며, 세부적 오퍼레이션 단위로 OpLog에 기록
- 배열 내 원소의 추가, 삭제, 수정 같은 배열 연산도 OpLog에 개별 오퍼레이션 형태로 남는다.
- Secondary 멤버는 OpLog의 각 엔트리를 읽어 원본과 동일하게 배열 연산도 순서대로 적용한다.
- OpLog에는 “update” 오퍼레이션 발생하면,
- 업데이트 명령(예: 배열 추가 시 $push, $addToSet, $pull 등)이 실제 쿼리 조건 및 변경된 데이터와 함께 저장
- 일관성과 최종 상태
- 모든 도큐먼트 복제는 최종 일관성 모델을 보장
- 네트워크 이상 없이 충분히 시간이 지나면 모든 레플리카 노드의 배열 필드 내용도 Primary와 정확히 일치
- 배열 크기나 배열의 경우에도 단일 document 크기(16MB) 제한은 그대로 적용된다.
- 배열 필드 복제 관련 유의점
- document-level 복제이므로, 배열 필드가 크거나 변경이 잦을수록 OpLog 사이즈와 Secondary 동기화 부하가 커진다.
- 대규모 배열 변경(배열 교체, 전체 삭제 등)은 diff가 아닌 전체 값 교체로 기록될 수 있다.
- 복제 실패나 재동기화 시에는 전체 document가 다시 복제된다.
- 필드 이름
- mongo는 스키마를 갖지 않기 때문에 필드명을 정의할 필요 없이 필드명-필드값을 key-value 쌍으로 데이터 저장
- 즉, 필드명도 데이터의 일부가 되고, 필드의 이름이 차지하는 공간이 크면 데이터의 크기 또한 커진다
- 프레그멘테이션과 패딩
- 도큐먼트 단편화(Fragmentation)
- 도큐먼트가 반복적으로 갱신·확장되어 크기가 커질 경우, 기존 저장 공간에 다 들어가지 못하면 WiredTiger(storage engine)는 새로운 위치에 저장하고 원래 공간은 미사용(unallocated) 처리
- 이런 미사용 공간이 누적되면, 디스크에 큰 단편화가 발생하며, 읽기 및 저장 효율이 계속 떨어진다.
- 잦은 배열/서브도큐먼트 추가, 값 확장, remove & insert 등이 심한 데이터에서 단편화 문제가 주로 발생한다.
- 패딩(Padding) 정책
- 패딩은 도큐먼트가 늘어날 것을 예상해, 실제 저장 시 추가 공간을 ‘버퍼’로 남겨두는 정책이다.
- MMAPv1 엔진에서 자동 패딩을 적용했지만, WiredTiger는 기본적으로 패딩 없이 도큐먼트를 효율적으로 압축/저장한다.
- 패딩이 없으면 도큐먼트 크기가 늘 때마다 공간이 부족해져 빈번한 재배치가 일어나고, 이는 곧 단편화로 연결된다.
- 실전에서 고려할 점
- 도큐먼트가 자주 커지는 데이터 구조(배열, 서브도큐먼트의 반복적 추가)는 최대 크기 예측과 더불어, 단편화 영향까지 설계 단계에서 신경 써야 한다.
- 대형 배열, 주기적 구조 변동이 예상되면 정규화(Referencing)로 설계를 분산하는 것이 디스크 효율과 성능 관리에 유리하다.
- 저장 공간 reclaim(회수)은 기본적으로 자동화되지 않으므로, 심각한 단편화가 누적된 경우 컬렉션 compact, reIndex, 데이터 마이그레이션 등의 운영 작업이 필요
- WiredTiger는 단편화를 줄이기 위한 내부 압축과 공간 관리 기능을 제공
- 그러나 구조적 단편화(모델링에서 오는 문제)는 사전에 설계로 예방하는 것이 가장 효율적이다.
- 도큐먼트 유효성 체크
- 스키마 유효성 규칙 정의
- MongoDB는 JSON Schema 기반 validation을 컬렉션 별로 적용할 수 있다.
- 필수 필드 존재 확인, 타입 제약, 값 범위, 패턴(정규표현식), 중첩 구조 등 복잡한 조건을 스키마에 명시할 수 있다.
- 변경 요구가 많은 환경은 너무 세밀한 규칙을 피하는 게 유리하다.
- 성능 영향
- 유효성 검사는 document insert/update 시마다 수행되므로, 복잡한 rule이 많으면 쓰기 성능이 저하될 수 있다.
- 대량 데이터 입력·마이그레이션 시, validation 옵션을 일시적으로 해제할 필요도 있다.
- 유연성 vs. 무결성의 균형
- NoSQL의 장점(유연함)을 살리되, 비즈니스 중요 정보는 꼭 validation rule을 적용해야 한다.
- 버전 관리(스키마 진화) 상황에서는 규칙 변경·이관이 쉬운 구조로 설계하는 것이 편하다.
- 운영 및 관리
- 유효성 오류 발생 시 명확한 에러 메시지로 디버깅이 가능해야 하며, 개발/운영 환경별로 rule을 다르게 적용할 수도 있다.
- 잘못된 데이터가 누적되지 않도록, validation 수준(서버의 strict, moderate, off 옵션), application 레이어에서의 복합 체크도 함께 고려한다.
- 조인
- $lookup 사용 시 성능 이슈
- MongoDB는 document 기반이며 native join은 없다.
- 하지만 Aggregation Pipeline의 $lookup 스테이지를 사용해 컬렉션 간 조인을 구현할 수 있다.
- $lookup은 별도의 컬렉션 전체 스캔(특히 인덱스가 없거나 join 키가 분산적일 경우)으로 인해, 대량 데이터/높은 QPS 환경에서 성능 저하가 발생할 수 있다.
- join 대상이 커질 경우, 작업 시간이 길어지고 서버 리소스 소비가 매우 늘어난다.
- 데이터 구조와 read/write 효율
- 조인이 자주 발생하는 구조라면, 한 컬렉션에 embedding(역정규화)으로 데이터를 포함시키는 모델이 성능상 더 유리할 수 있다.
- referencing(정규화)로 분할한 경우, join·lookup이 불가피하지만 데이터 중복은 최소화된다.
- 인덱스 설계 필수
- $lookup의 join 키(외래키 역할)는 반드시 인덱스를 생성해야 컬렉션 스캔을 막고 속도를 최대한 높일 수 있다.
- join 빈도가 높은 필드는 단일 인덱스 혹은 복합 인덱스로 관리한다.
- 확장성과 분산 환경
- Sharding 환경에서는 $lookup 사용 시 제한과 구조적 제약이 있다. sharded collection 간의 lookup은 shard key, 데이터 분포에 따라 불가능하거나 성능 저하가 심할 수 있다.
- 대형 서비스에서는 lookup을 최소화하고, embedding, application-side join(애플리케이션단에서 두 번 쿼리 후 병합) 등의 구조도 고려해야 한다.
- 유지보수와 구조 변경
- 데이터 모델 변경, 구조 진화 시 embedding과 referencing 전략의 전환이 요구될 수 있다.
- 조인이 많아질수록 스키마 관리, 마이그레이션의 난이도가 높아진다.
- 복잡한 multi-way join, 집계 join 등은 RDBMS 대비 구현/운영이 어렵다.