ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • MongoDB 스키마 디자인 패턴
    MongoDB 2020. 8. 20. 18:40

    데이터 모델링이 중요한 이유

     

    1. Hardware
       하드웨어의 자원은 무한하지 않기 때문입니다. 자원을 최대한 잘 활용할 수 있도록 모델링이 필요합니다.

     

    2. MongoDB 
       몽고디비의 단일 다큐먼트 16MB 이상으로 저장할 수 없으며 WiredTiger 엔진은 다큐먼트의 정보를 읽기 위해서 전체 문서를 메모리에 올린 후 작업을 하기 때문에 다큐먼트에 무턱대고 때려박아(?) 놓는다면 메모리 이슈가 발생합니다.

     

    3. Network

       네트워크 속도에 대한 물리적한계가 존재합니다. 전 세계적으로 배포되는 애플리케이션의 모델링은 서비스 품질을 저하시키지 않고 데이터에 접근할 수 있도록 구성되어야 합니다.

     

     

    디자인 패턴

    (각 패턴별 자세한 설명과 예를 확인하시려면 더보기를 클릭해주세요)

    Attribute Pattern 

    더보기

    다큐먼트의 검색해야 할 필드가 많을수록 인덱스를 많이 잡아야 할 수 있습니다.

    더군다나 몽고 디비는 스키마 리스이므로 다큐먼트의 필드를 예측할 수 없는 상황이 생길 수 있습니다.

     

    때문에 새로운 필드가 추가되거나 검색될 때마다 스키마를 검증하며 유저 인터페이스를 수정해야 할 수 있습니다.

     

    이럴 경우 Attribute 패턴을 사용해서 해결이 가능합니다.

     

    // old schema
    // iPhone Collection
    
    {
    	"model": "X",
      "released_USA": "2017/11/03",
      "released_Mexico": "2017/11/03",
      
      ...
      
      "released_KOR": "2017/11/24"
    }
    

    iPhone의 나라별 출시일을 이용하여 검색을 하고 싶다면 아래와 같이 모델링을 한다면

    매 새로운 아이폰이 출시될 때마다 출고된 국가에 대한 인덱스 작업을 새롭게 해줘야 하는 문제가 생깁니다.

    하지만 아래와 같이 수정하면 인덱스 작업을 해줄 필요가 없이 스키마 관리가 가능해집니다.

    // new schema
    // iPhone Collection
    
    {
    	"model": "X",
        "release": [
        	{"country": "USA", "date": "2017/11/03"},
            {"country": "Mexico", "date": "2017/11/03"},
            ...
            {"country": "KOR", "date": "2017/11/24"},
        ]
    }
    
    db.iPhone.createIndex({"release.country": 1, "release.date": 1})
    db.iPhone.find({"release.date": {"$gt": "2017/11/10"}})

     

     

     Subset Pattern

    더보기

    MongoDB는 disk에서 필요한 다큐먼트만 메모리에 올려 메모리 사용을 최적화합니다. 만약 메모리가 가득하차면 사용하지 않는 다큐먼트를 메모리에서 제거하여 메모리를 확보합니다.

    때문에 작업세트(한번의 쿼리에서 불러오는 다큐먼트)가 적당하다면 좋은 성능을 보여줄것입니다.

     

    하지만 메모리를 초과하거나 메모리를 많이 차지하게 되면 아래와같은 문제가 생길 수 있습니다. 
    자주 발생하는 A 작업, B 작업이 있다고 할때, 

    1. A작업 실행, 다큐먼트들 메모리에 저장

    2. B 작업 실행, 메모리 초과시 A 작업의 문서들을 메모리에서 제거한 후 B작업의 문서들을 메모리에 저장

    3. A작업 실행, B작업 다큐먼트제거 후  다시 디스크에서 읽어와서 메모리에 저장

     

    이렇게 작업이 일어날때마다 매번 디스크에서 읽어와야한다면  메모리에 다큐먼트들을 캐싱하는 이유가 없으며, 성능또한 떨어지게 됩니다

    .

    만약 A, B작업의 다큐먼트들이 모두 메모리에 저장될 수 있으려면 아래와 같은 방법이 있습니다.

     

    1. 하드웨어를 업그레이드한다.

    2. 샤드를 추가한다.

    3. 작업세트의 사이즈를 줄인다.

     

    Subset 패턴을 이용하여 작업세트의 사이즈를 줄이는 방법을 알아보겠습니다.

     

    하나의 다큐먼트를 2개의 다큐먼트로 나누는것입니다.

     

    // old schema
    // Movie Collection
    
    {
    	_id: ObjectId("123")
    	title: "다만 악에서 구하소서",
        releaseDate: "2020/08/05",
        runtime: "108",
        genre: "action",
    	director: "홍원찬",
        cast: [...],
        staff: [...]
        
        ...
        
    }

     

    영화에 대한 정보를 알려주는 작업에서  매번 수십, 수백명의 staff(제작진) 의 값이 같이 메모리에 저장될 필요는 없을것입니다.

     

    // new schema
    // Movie Collection
    
    {
    	_id: ObjectId("123")
    	title: "다만 악에서 구하소서",
        releaseDate: "2020/08/05",
        runtime: "108",
        genre: "action",
    	director: "홍원찬",
        cast: ObjectId("456"),
        
        ...
        
    }
    
    // Staff Collection
    {
    	_id: ObjectId("456"),
        movieId: ObjectId("123"),
       	staff: [...]
    }

     

    위와같이 데이터를 나누어서 필요시에만 staff 정보를 조회하여 사용한다면 메모리를 더 효율적으로 사용이 가능해집니다.

     

     

    Computed Pattern

    더보기

    computed pattern은 CPU의 사용률을 줄이며 읽기작업의 시간을 단축시키기 위한 방법입니다.

    영화 평점을 보여주기 위해서 모든 리뷰의 점수를 계산해야하고 이 작업을 유저가 해당 영화 페이지에 들어올때마다 해야한다면 결코 좋은 설계가 아닙니다.

     

    1. 계산량이 비싼 경우

    2. 빈번하게 결과값이 필요한 경우

    3. 결과값이 자주 변하지 않는 경우

     

    위 3가지가 모두 만족할때가 computed pattern을 적용하기에 가장 이상적이라고 생각합니다.

     

     

    앞서 말씀드린것처럼 매번 전체 리뷰를 종합하여 계산을 통하여 영화 평점을 도출하는것 보다

    // old schema
    
    db.Review.insert({
    	movieId: ObjectId("123"),
        rating: 3.0,
        comment: "~~~"
    })
    
    ...
    
    db.Review.insert({
    	movieId: ObjectId("123"),
        rating: 4.5,
        comment: "~~~"
    })
    
    
    // 매번 모든 리뷰를 읽어와서 계산
    db.Review.aggregate([
      {
        $match: {
          movieId: ObjectId("123"),
        },
      },
      {
        $group: {
          _id: null,
          sum: { $sum: "$rating" },
          count: { $sum: 1 },
        },
      },
      {
        $project: { rating: { $divide: ["$sum", "$count"] } },
      },
    ])
    
    

     

    리뷰가 작성될때 다른 컬렉션의 다큐먼트를 업데이트를 한다면 한번의 읽기작업으로 해당 영화의 평점을 읽어올 수 있게됩니다.

     

    // new schema
    
    // 리뷰 작성과 동시에 다른 컬렉션의 값을 업데이트
    db.Review.insert({
      movieId: ObjectId("123"),
      rating: 3.0,
      comment: "~~~",
    })
    
    db.ComputedMovie.updateOne({
      movieId: ObjectId("123")
    }, {
      $inc: {
        sum: 3.0,
        count: 1,
      }
    })

     

     

     

     

     

    디자인 패턴은 문제 해결을 위한 마스터키가 아닙니다. 단지 문제 해결을 위한 일부분에 불과합니다.
    때문에 하나의 문제해결을 위해 다양한 지식과 시도가 필요합니다.

     

     

    'MongoDB' 카테고리의 다른 글

    MongoDB Oplog 와 Journaling 차이  (0) 2021.04.28
    MongoDB Shard 개수에 따른 퍼포먼스 확인하기  (0) 2021.04.26
    MongoDB 초간단 log 관리하기  (0) 2021.04.26
Designed by Tistory.