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 대비 구현/운영이 어렵다.