티스토리 뷰

 

 

 

 빅데이터라는 시대의 요구에 맞추어 NoSQL이 등장한 지 십 년이 넘는 세월이 흘렀습니다. 하지만, 아직 RDB에 비해서 스키마 설계를 위한 참고 자료가 부족하다고 생각되는데요. 저 또한 MySQL만 사용해오던 백엔드 개발자로서 신규 프로젝트에서 갑작스럽게 MongoDB 스키마를 설계하게 되어 꽤 당혹스러웠던 경험이 있습니다.

 

뭘... 하라고요?

 저처럼 NoSQL, 그중에서도 MongoDB를 처음 사용하게 된 개발자들을 위해서 빠르게 기본 개념을 훑고 바로 스키마 설계가 가능하도록 도움을 줄 수 있는 글을 써보려 합니다.

 

그래서 MongoDB를 도대체 왜 쓰는데?

 NoSQL/MongoDB 이름만 들어본 분들을 위해 특징 및 사용목적을 간단하게만 짚고 넘어가는 게 좋을 것 같습니다.

 인터넷 서비스가 점점 많은 곳에 보급되고 데이터를 전송하는 device의 수가 증가하게 되면서, 전통적인 RDB로는 취급하기 어려운 방대한 양의 비정형 데이터들을 적재하고 처리하기 위해 새로운 data storage가 필요했습니다. 즉, RDBMS의 한계를 극복하고자 함이 NoSQL의 등장 이유입니다. 여기서 말하는 RDB의 한계란 1) 비정형 데이터를 처리하기가 힘들며, 2) 확장성이 떨어지고, 3) 상대적으로 속도가 느린 것을 들 수 있습니다.

 

데이터가 감당할 수 없이 쏟아진다면...?

 여기저기서 데이터들이 마구 쏟아져 들어오는 상황에서 1) 데이터 포맷은 크게 신경 쓰지 않고 일단 때려 담을 수 있으며, DB 서버 용량이 넘칠 것 같으면 2) 쉽게 새로운 서버를 옆에 팍팍 붙여서 확장할 수 있고, 3) 엄청난 빈도로 읽기/쓰기 연산을 해도 performance에 큰 문제가 없어야 하는 경우에 MongoDB를 사용합니다.

 분당 수만 건씩 쌓이는 사용자 로그를 RDB에 실시간으로 적재할 수 있을까요? 사용자가 클릭하고, 주문하고, 결제하는 복잡한 과정을 모두 RDB 스키마에 맞추어 실시간으로 정형화하여 유실 없이 처리할 수 있을까요? 아마 매우 어려울 것입니다. 이런 경우는 일단 데이터 형태는 크게 고민 않고 MongoDB에 실시간 적재하고 차후에 Batch Process 등을 통해 주기적으로 전처리하여 Hadoop에 올려 관리하는 것이 합리적입니다.

 

MongoDB 스키마 설계를 위한 고려사항

The best approach to design is to represent the data the way your application sees it.
"당신의 어플리케이션이 바라보는 관점에서 설계하는 것이 가장 좋은 접근(설계) 방법이다."
-
Kristina Chodorow, (2019) MongoDB: the Definitive Guide: O'Reily

 

RDB에서의 스키마 설계는 (application이나 query와는 무관하게) entity를 정의하고 정규화를 통해 중복을 없애는 비교적 정형화된 프로세스를 따릅니다. 이에 비해, MongoDB는 application 관점에서 수행되는 query의 성능을 고려하여 유연한 설계를 필요로 합니다. 본격적인 설계에 앞서 우리가 고려해야 할 사항을 살펴보겠습니다.

 Access Pattern

 Application이 데이터에 접근하는 패턴을 파악하여 collection을 정의할 수 있습니다. 우리는 이 과정에서 아래와 같은 질문들을 던질 수 있습니다.

 

  • Application이 어떤 query들을 수행하는가?
  • 어떤 query를 가장 빈번하게 수행하는가?
  • Application은 주로 DB에서 데이터를 읽는가? 아니면 쓰는가?

 

 위의 질문들을 통해 우리는 아래와 같은 과정을 통해 collection을 정의합니다.

 

  • 함께 조회되는 경우가 빈번한 데이터들은 같은 collection에 담아, query의 횟수를 줄일 수 있습니다.
  • 주로 읽기만 하는 데이터와 자주 업데이트하는 데이터는 별개의 collection에 담습니다.

 

 Relation

 Access Pattern을 분석하여 collection들이 정의되면 이제 collection 간의 관계를 파악합니다. 이커머스 플랫폼을 운영하는 입장에서 Product와 Category라는 두 collection이 DB에 존재한다고 해봅시다. RDB에서는 Product Table에 category_id라는 칼럼을 두어 Category Table과 Join 하여 카테고리 정보를 가져오도록 설계할 것입니다.

RDB식 설계

 이런 entity간의 relation을 MongoDB에서는 collection 간에 reference 할지, embed 할지 결정해야 합니다. 여기서, reference란 collection 간 참조할 수 있도록 id를 저장하는 것이고, embed는 관계된 document를 통째로 저장하는 것입니다.

reference 방식
embed 방식

 우리는 어떤 기준을 가지고 두 방식을 고민해야 할까요? 이는 application의 성격에 따라 달라집니다. 예를 들어, 상품 페이지에 카테고리 정보가 함께 보인다면 두 정보는 대부분 함께 조회된다고 봐야 할 것입니다. 따라서 query 한 번에 모두 가져올 수 있도록 embed 하는 것이 바람직한 선택입니다. 반면에, 카테고리 정보가 끊임없이 변경되는 상황이라면 어떨까요? Embed 방식의 경우, 해당 카테고리의 모든 상품 document를 찾아서 일일이 embed 된 카테고리 정보를 수정해야 합니다. 반면, reference 방식의 경우 별도로 관리되는 카테고리 collection에서 하나의 document만 찾아 수정하면 됩니다. 이렇게 잦은 수정이 예상되는 경우, reference 방식이 더 바람직하다고 볼 수 있습니다.

 

 Reference는 데이터를 정규화하고, embed는 데이터를 비정규화합니다. 일반적으로 최대한 정규화하여 중복을 제거하는 것이 바람직하다고 여겨지는 RDM와 달리, MongoDB는 적절한 수준의 비정규화가 필요한 경우가 많습니다. NoSQL의 경우 RDB처럼 복잡한 Join 연산이 불가능하다는 것을 염두에 두어야 합니다. 만약 가능한 수준까지 정규화하여 entity별 collection으로 모두 쪼개어 놓았더니, join 연산을 통해 application에서 필요한 복잡한 데이터로 재구성하는 것이 어려울 수도 있습니다.

 

 일반적으로 reference(정규화)는 쓰기를 빠르게 하고, embed(비정규화)는 읽기를 빠르게 합니다. 분당 수천~수만 번씩 access 되는 웹페이지에서 이용되는 정보라면 가능한 하나의 document에 모아두는 것이 필요하겠죠. 하지만, 이런 설계에 정답은 없습니다. 사용자들이 인내심을 갖고 이용하는 사내 시스템이라면 읽기 performance에 크게 신경 쓰지 않고 정규화하는 것도 나쁘지 않아 보입니다.

 Cardinality

 서로 관계된 collection 간에 공유 필드가 여러 document에 걸쳐 반복적으로 존재할 수 있습니다. 온라인 북스토어를 운영한다고 가정해봅시다. 책마다 제목은 하나, ISBN도 하나씩이니 One-to-One 관계입니다. 책은 여러 리뷰를 가질 수 있습니다. 책과 리뷰는 One-to-Many관계입니다. 하나의 책은 여러 태그를 가질 수 있고, 하나의 태그는 여러 책을 포함합니다. 책과 태그는 Many-to-Many 관계입니다. Cardinality는 이렇듯 One-to-One, One-to-Many, Many-to-Many가 존재합니다.

 

책과 태그 관계 예시, 교보문고의 소설 <파친코> 상세 페이지

 

 단순히 many에서 끝나지 않고, how many를 고려하는 것도 설계에 필요합니다. 책 한 권이 갖는 태그는 기껏해야 10개 미만이지만, 태그 하나에 포함되는 도서는 수백~수천 권이 될 것입니다. 즉, 책과 태그는 Many-to-Few 관계라고 볼 수 있습니다. 책과 리뷰, 책과 주문기록의 관계는 둘 다 One-to-Many이지만, 리뷰보다는 주문기록의 훨씬 많을 것으로 예상할 수 있습니다. 일반적으로 적은 many는 embed, 많은 many는 개별 collection을 두어 reference 하는 것이 바람직합니다.

 

MongoDB 스키마 설계 Toy Example

 온라인 서점을 운영하는 입장에서 간단한 스키마 설계 예제를 진행하도록 하겠습니다. 워낙 간단한 예제이기에, 실제 project의 설계 전반을 모두 커버할 수는 없지만, 스키마 설계의 과정을 일부분 따라가 보고 설계상 고민할 포인트를 함께 짚어보는 의미로는 충분할 것입니다.

하나의 책에 여러 저자가 참여할 수 있고, 한 저자가 여러 책을 집필할 수 있기에 책(Book)과 저자(Author)는 Many-to-Many의 관계를 갖습니다. RDB의 경우, Book과 Author 사이에 BookAuthor라는 mapping table을 두어, 책과 저자의 다대다 관계를 두 개의 일대다 관계로 설계하였을 것입니다. 이런 RDB 식 설계를 MongoDB 스키마로 옮겨 예제를 시작해보도록 하겠습니다.

RDB식 설계
MongoDB schema

 RDB의 설계 철학을 그대로 MongoDB에 적용하였습니다. 각각의 속성(필드)은 하나의 엔티티(컬렉션)에 속해있고, 정규화를 통해 중복되는 데이터는 없습니다. 사실 스키마 자체는 크게 문제 될 요소는 없습니다. 예를 들어, 데이터의 일관성이 중요하고 속도에 크게 신경 쓸 필요가 없다면 이 스키마 그대로 사용해도 상관없습니다. 오히려, 책과 저자 정보가 실시간으로 계속 업데이트되고 있다면 이렇게 정규화한 RDB 스러운 스키마가 더 바람직하다고 할 수 있습니다.

 하지만, 끊임없이 책과 저자 정보가 프론트에서 요청되고 있다면 어떨까요? 장사가 너무 잘되어 매분마다 수만 번의 DB 조회가 발생한다면, 위와 같은 모델은 1) Book Collection 조회, 2) Book ID로 BookAuthor Collection 조회, 3) Author ID로 Author Collection 조회, 총 3번의 query를 수행해야 하는 치명적인 단점이 존재합니다. 만약 author 정보를 Book document에 embed 시켜놓았으면, query 한 번으로 끝날 작업을 3 단계로 나누어서 하게 됨으로써, DB 서버에 과부하를 줄 수도 있고 페이지 로딩 속도가 느려져 사용자 경험에 악영향을 줄 수도 있습니다.

 

embedded model

 그런 이유로, BookAuthor라는 중간 collection을 없애고, author 정보를 Book document에 embed 하였습니다. 이제 책과 저자 정보를 query 한 번에 가져올 수 있어서 읽기 성능이 재고될 것으로 기대됩니다. 그럼 모든 문제가 해결된 것일까요? 조금 더 현실에 가까운 예제를 위해, 사용자들이 관심 작가를 follow 할 수 있는 기능을 추가해보겠습니다.

 

한강 작가의 교보문고 프로필 페이지

 

followers array 추가

 Embedded 모델에 follower 정보를 추가하면 Author 아래에 follower라는 array가 추가되는 형태일 것입니다. 그럼 이제 한 가지 문제가 생깁니다. 이제 한강 작가에게 follower가 추가될 때마다 작가의 모든 저서의 document를 순회하면서 authors array에서 한강 작가에 해당하는 document를 찾고, 이 document의 followers array에 신규 follower 정보를 추가해야 합니다. Follower가 following을 취소할 때 역시 마찬가지로 이런 비효율적인 과정을 통해 follower 정보를 삭제해야 합니다. 이는 Book document에 Author의 모든 속성을 embed 하는 것이 비효율적임을 보여줍니다.

 

hybrid model

 이는 Author document 필드 각각의 성격을 고려하지 않고 일괄적으로 embed 하여 발생하는 문제입니다. 어떤 필드는 거의 읽기만 수행하는 정적인 값이고, 어떤 필드는 자주 수정이 발생하는 동적인 성격을 갖는다면 이 두 필드는 별개로 처리되어야 합니다. 지금 예제에서 작가의 이름은 정적인 값이고, follower는 끊임없이 수정이 발생하는 동적인 정보입니다. 이런 경우 정적 필드는 embed 시켜 함께 조회되도록 하고, 동적인 필드는 개별 collection에 저장하여 효율적으로 수정될 수 있도록 하는 hybrid model이 권장됩니다.

 

 Embed와 Reference의 장단점을 정리하면 아래와 같습니다.

  장점 단점
Embed 읽기가 빠르고, 효율적(query 한 번에 모두 조회) 수정이 비효율적(여러 document 중복 작업 필요)
Reference 쓰기가 빠르고, 효율적(document 하나만 수정) 읽기가 비효율적(query를 여러번 실행)

 

 이미 많이 최적화하였지만, follower 정보를 추가한 김에 하나의 상황을 더 다루어보도록 하겠습니다. 예시로 들던 교보문고 홈페이지를 보면 한강, 김영하 작가처럼 소수의 인기 작가들은 수천 명의 follower가 있지만, 대다수의 경우 없거나 한 두 명인 것을 확인할 수 있습니다. 그렇다고 이런 소수의 인기 작가의 followers array에 무한정 데이터를 추가할 수는 없습니다. MongoDB의 물리적인 한계(doc size < 16Mb)도 있고, 한 번의 조회에 너무 많은 정보를 읽어오면서 application에 무리가 생길 수도 있습니다.

인기 작가의 follower 정보 저장 예시

 이런 경우, 해당 document가 outlier임을 나타내는 flag를 저장하고(위 예시의 "tbc"), Id를 기준으로 여러 document에 나누어 저장하는 방식으로 설계합니다. 이제 application은 flag가 존재하는 경우에만 추가 query를 수행하여 follower 정보를 가져오게 됩니다. 이로써, 우리의 북스토어 서버는 필요한 상황에 필요한 만큼만 데이터를 가져오도록 최적의 스키마를 갖추었습니다.😎

 

글을 마무리하며...

 긴 글 읽어주셔서 감사합니다. 모쪼록 이 글을 읽고 첫 몽고 DB 프로젝트에 도움이 되길 바라겠습니다. MongoDB 스키마 설계의 핵심인 Embed vs Reference를 정리한 표와 함께 마치도록 하겠습니다.🙇🏻‍♂️

 

Embed 권장 Reference 권장
변경이 (거의)없는 정적인 데이터 변경이 잦은 데이터
함께 조회되는 경우가 빈번한 데이터 조회되는 경우가 많지 않은 데이터 
빠른 읽기가 필요한 경우 빠른 쓰기가 필요한 경우
결과적인 일관성이 허용될 때 즉각적으로 일관성이 충족되어야 할 때

 

References

니시다 케이스케. 『빅데이터를 지탱하는 기술』. 제이펍, 2018.

크리스티나 초도로, 섀년 브래드쇼 外 1명 저. 김인범 역.  『몽고DB 완벽 가이드』. 한빛미디어. 2021.

카일 뱅커, 피터 배컴 外 3명 저. 김인범 역. 『몽고DB 완벽 가이드』. 제이펍. 2018

Building with Patterns, MongoDB 공식홈페이지, https://www.mongodb.com/blog/post/building-with-patterns-a-summary 

댓글