• 포스트
  • 도구
  • 작업
  • 포스트
  • 도구
  • 작업
로그인회원가입

© 2005 kaisa.co.kr. All Rights Reserved.

소개개인정보처리방침7083620@hanmail.net
← 목록으로
2024-07-04

MongoDB: 문서형 DB로 유연한 스키마 다루기

MongoDB: 문서형 DB로 유연한 스키마 다루기

요약: 문서 단위로 읽고 쓰는 워크로드에 맞는 문서형 DB로, 임베드·인덱스·레플리카의 실무 경계를 정리한다.


1. 언제 MongoDB인가

MongoDB는 문서형(NoSQL) 데이터베이스다. 행이 아니라 JSON과 비슷한 문서 한 장을 기본 단위로 저장한다. 내부 저장은 BSON이다. BSON은 Binary JSON, JSON을 이진으로 확장한 형식이다.

“SQL이 싫어서”만으로 고르면 나중에 조인·일관성에서 비용이 커진다. 함께 읽는 데이터가 한 문서에 모이는지가 선택 기준이다.

맞는 경우 덜 맞는 경우
상품·게시글처럼 문서 구조가 자주 변할 때 주문·재고처럼 강한 조인·정규화가 핵심일 때
한 화면이 문서 하나면 충분할 때 여러 컬렉션을 매번 조인해야 할 때
수평 확장·샤딩을 전제로 키를 설계할 때 단순 키-값·TTL 캐시면 충분할 때 (Redis)
이벤트·로그성 문서를 대량 적재할 때 복잡한 다테이블 트랜잭션이 업무의 중심일 때
프로토타입에서 스키마를 빠르게 바꿀 때 리포팅 SQL·제약 엔진이 필요할 때 (PostgreSQL/MySQL)

유연함은 무기이자 부채다. 필드 이름이 문서마다 달라지면 조회·인덱스가 무너진다.


2. 핵심 개념

데이터베이스 → 컬렉션 → 문서. 컬렉션은 테이블에 가깝다. 문서에는 _id가 있다. 기본값은 ObjectId다.

임베드 vs 참조. 자주 함께 읽는 작은 객체는 문서 안에 넣는다(임베드). 커지거나 수명주기가 다르면 ObjectId로 가리킨다(참조). 배열이 끝없이 늘어나면 문서 16MB 상한과 갱신 비용이 문제가 된다.

단일 문서 갱신은 원자적이다. 한 문서 안의 여러 필드는 같이 커밋된다. 여러 문서에 걸친 트랜잭션은 가능하다. 다만 비용·제약이 있다. 관계형처럼 쓰려고 멀티 도큐먼트 트랜잭션을 남발하면 이득이 사라진다.

레플리카 셋이 운영 기본이다. Primary에 쓰고 Secondary가 복제한다. 노드 하나짜리 단독 프로세스는 개발용으로만 둔다.

스키마 검증(Schema Validation) 으로 “아무 필드나”를 완화한다. 완전 자유는 운영에서 디버깅 비용이 된다.

TTL 인덱스는 Time To Live, 만료 후 삭제다. 세션·임시 문서에 쓴다. 캐시 전용이면 Redis가 더 단순한 경우가 많다.

샤딩은 데이터를 여러 샤드에 나누는 기법이다. 샤드 키를 잘못 고르면 한 샤드에만 쓰기가 몰린다. 트래픽이 한 노드로 충분하면 샤딩을 미룬다. 레플리카로 읽기·가용성을 먼저 확보한다.


3. 최소 예제 — 게시글 문서

블로그 글과 작성자 스냅샷을 한 문서에 둔다. 작성자 마스터가 자주 바뀌면 참조로 나눈다. 목록 화면이 글+이름이면 임베드가 맞다.

// posts 컬렉션 문서 예시
{
  _id: ObjectId("64f0a1b2c3d4e5f678901234"),
  title: "문서 설계 메모",
  tags: ["db", "mongodb"],
  author: { id: "u1", name: "Ada" },
  published: true,
  createdAt: ISODate("2026-08-26T00:00:00Z")
}
db.posts.createIndex({ tags: 1, createdAt: -1 })
db.posts.createIndex({ "author.id": 1 })

db.posts.insertOne({
  title: "문서 설계 메모",
  tags: ["db", "mongodb"],
  author: { id: "u1", name: "Ada" },
  published: true,
  createdAt: new Date()
})

db.posts.find({ tags: "mongodb", published: true })
  .sort({ createdAt: -1 })
  .limit(20)

느린 쿼리는 실행 계획을 본다.

db.posts.find({ tags: "mongodb" }).sort({ createdAt: -1 }).explain("executionStats")

IXSCAN이 아니라 COLLSCAN이면 인덱스가 안 탄다. 쿼리 패턴이 바뀌면 인덱스도 다시 짠다. 인덱스는 쓰기 비용을 올린다. “나중에 쓸지도”로 만들지 않는다.

연결 문자열은 시크릿이다. 예시는 플레이스홀더만 쓴다.

mongodb://APP_USER:YOUR_DB_PASSWORD@db.internal:27017/app?authSource=admin

4. 운영 시 주의

연결. 드라이버 풀 기본값을 앱 인스턴스 수와 곱해 본다. 유휴 연결이 너무 길면 방화벽이 끊는다. retryWrites와 타임아웃을 드라이버 문서대로 맞춘다.

백업. Atlas 같은 관리형은 스냅샷·PITR을 상품으로 제공한다. PITR은 Point-In-Time Recovery다. 직접 운영이면 레플리카 + 주기 스냅샷을 최소로 둔다. mongodump는 논리 백업이다. 큰 클러스터에서는 시간과 부하를 계산한다. 복원을 스테이징에서 한 번 돌린다.

보안. 인증을 켠다. 앱 사용자는 해당 DB만. 27017을 인터넷에 열지 않는다. VPC 또는 프라이빗 링크 안쪽에서만 붙는다.

스키마 드리프트. 필드명 오타가 조용히 새 필드를 만든다. 검증 규칙이나 앱 DTO로 허용 필드를 고정한다.

핫 도큐먼트. 한 문서의 배열에 댓글을 수만 개 붙이면 갱신이 싸워진다. 커지는 관계는 컬렉션을 나눈다.

비용. Atlas 등 관리형은 클러스터 티어·스토리지·백업이 요금이다. 인덱스와 불필요 필드가 문서를 키운다. 프로젝션으로 큰 필드를 목록 조회에서 뺀다.

관측. 프로파일러 또는 Atlas 슬로우 쿼리로 COLLSCAN을 찾는다. 커넥션 수와 replication lag를 대시보드에 둔다.

스키마 검증은 “완전 자유”를 한 단계 줄인다.

db.createCollection("posts", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["title", "createdAt"],
      properties: {
        title: { bsonType: "string", minLength: 1 },
        published: { bsonType: "bool" }
      }
    }
  }
})

규칙은 느슨하게 시작해도 된다. 필수 필드만이라도 막으면 오타 필드가 줄어든다.

흔한 실수. 댓글 배열을 글 문서에 무한히 붙이기, 인덱스 없이 regex 전체 스캔, 멀티 도큐먼트 트랜잭션으로 관계형을 흉내 내기. 세 가지가 운영 비용을 키운다.


5. 정리

MongoDB는 유연함이 무기다. 읽기 패턴에 맞는 문서 설계와 인덱스 없이 쓰면 관계형보다 더 아프다. 임베드는 작게, 트랜잭션은 드물게, 레플리카와 백업은 기본값으로 둔다.

체크리스트

  • 화면 단위로 임베드/참조를 정한다. 조인을 전제로 시작하면 모델이 무너진다.
  • 실제 find/sort에 인덱스를 맞춘다. explain이 COLLSCAN이면 데이터가 커질수록 죽는다.
  • 레플리카 셋을 운영 기본으로 둔다. 단일 노드는 디스크 장애에 약하다.
  • 스키마 검증 또는 앱 계약으로 필드 난립을 막는다.
  • 인증·네트워크 범위를 최소로 두고 덤프/스냅샷 복원을 연습한다.

연습

  1. posts에 글 다섯 건을 넣고 tags+createdAt 인덱스로 목록을 조회한다.
  2. 같은 쿼리에 explain("executionStats")를 붙여 스캔 방식을 확인한다.
  3. mongodump 후 다른 DB 이름에 복원해 건수가 같은지 본다.

댓글

0

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

이전 글MySQL: 웹 서비스에서 가장 흔한 관계형 DB다음 글Redis: 인메모리 키-값으로 캐시·세션·큐
← 목록으로