개요
기존 게시물(https://jaimemin.tistory.com/2332)에서도 언급했다시피 MongoDB의 스키마가 자유로운 것은 맞으나 관련 없는 데이터들끼리 하나의 컬렉션에 모을 경우 관리도 힘들고 성능 저하를 야기하기 때문에 어느 정도의 모델링은 필요합니다.
또한, 하나의 document 사이즈가 최대 16MB로 제한되어 있기 때문에 경우에 따라서 MongoDB 철학인 "data access together, stays together" 원칙을 깨고 정규화를 진행해야 하는 케이스도 존재합니다.
이번 게시물에서는 자주 쓰이는 MongoDB 스키마 모델링 기법에 대해 간단히 정리해보겠습니다.
1. Extended Reference 패턴
Extended Reference 패턴은 회원이 많은 OLTP성 서비스에 유용하게 쓰일 수 있습니다.
유튜브 채널 정보를 저장하는 channels 컬렉션이 있고 해당 컬렉션 내 구독자들의 목록을 배열 필드 subscribers에 저장한다고 가정하겠습니다.
위와 같이 스키마를 모델링할 경우 대형 채널은 members 배열이 너무 커져 Document 크기가 MongoDB의 제약사항인 16MB을 도달하는 문제가 발생할 것입니다.
반대로 members 컬렉션에 각 멤버가 구독한 채널들의 목록을 배열 필드인 channels에 저장할 경우 멤버가 채널을 무한정 가입하여 단일 document 크기가 16MB를 넘는 케이스는 희박하겠지만 채널 정보를 업데이트할 때마다 컬렉션 스캔을 진행하므로 부하가 커지는 단점이 존재합니다. (각 채널의 last_updated 필드는 지속적으로 업데이트되는 필드)
두 번째 방법의 해결책으로 MongoDB의 철학에 어긋나지만 channels와 members 컬렉션을 분리한 뒤 조회할 때 $lookup aggregation을 통해 LEFT OUTER JOIN 하는 방법이 있습니다.
이렇게 할 경우 채널 정보와 구독자 정보를 업데이트할 때 부하가 걸리지 않고 단일 document 크기가 과도하게 커질 일은 없습니다.
이제 모든 문제가 해결된 것처럼 보이지만 MongoDB의 Join 성능은 상대적으로 RDBMS보다 열위이기 때문에 channels와 members 컬렉션 사이즈가 커질 경우 $lookup aggregation에 대한 비용이 증가해 쿼리 호출 속도가 느려질 것입니다.
물론 실행 계획을 분석한 뒤 적절히 인덱스를 부여하는 방식으로 효율을 증가시킬 수도 있겠지만 join에 대한 근본적인 한계가 존재하기 때문에 컬렉션 분리 후 Embedding 할 수 있는 필드를 일부 내장시키는 Extended Reference 패턴을 많이 사용합니다.
Extended Reference 패턴을 적용하면 컬렉션은 분리되어 있지만 조회에 필요한 필드가 하나의 컬렉션(members) 내 존재하기 때문에 굳이 $lookup aggregation을 진행하지 않아도 됩니다.
즉, 단일 컬렉션을 조회하므로 실행 속도가 훨씬 빨라지는 것을 확인할 수 있습니다.
* 주의: Extended Reference 패턴을 적용할 때 절대 바뀌지 않거나 자주 바뀌지 않는 필드들만 내장시켜야 합니다.
2. Attribute 패턴
대항해시대라는 게임에서 플레이어는 아래와 같이 다양한 활동을 할 수 있습니다.
- 나라 방문
- 항해
- 거래
- 전투
- 퀘스트 수행
- 낚시
- 도박
- 농사
- etc.
플레이어가 수행한 모든 행위를 로깅을 해야 한다는 요구사항이 들어와 log 컬렉션 스키마를 아래와 같이 모델링했다고 가정하겠습니다.
위와 같이 각 유저의 logInfo 필드에 로그인 후 수행한 활동을 로깅할 경우 인덱스 관리도 어렵고 게임 패치를 할 때마다 신규로 추가되는 활동 관련 배열 필드를 추가해야 하는 오버헤드가 존재합니다.
위와 같은 케이스의 경우 모든 활동을 key-value 형태로 묶어서 하나의 배열에 넣어주고 단일 인덱스를 통해 관리할 수 있도록 Attribute 패턴을 적용할 수 있습니다.
아래 예제에서는 모든 활동을 actions 배열 필드에서 관리하고 모든 활동에 대해 action-value + 알파 형태로 관리하고 있습니다.
위와 같이 관리할 경우 actions 배열 필드 내 action과 value에 대해 복합 인덱스를 생성함에 따라 어떤 형태의 행동이 나오더라도 action에 대한 타입만 정의하고 값과 추가적인 정보를 필드로 추가할 경우 더 단일 복합 인덱스를 통해 관리가 가능하다는 장점이 있습니다.
db.logs.createIndex({"actions.action": 1, "actions.value": 1})
또한 actions 배열 필드가 계속 추가된다고 해서 하나의 document가 16MB를 초과하는 케이스는 거의 없다고 봐도 무방합니다.
이처럼 스키마가 제각각일 경우 위 케이스처럼 Attribute 패턴을 적용하는 것을 권장합니다.
3. Bucket 패턴 / Time Series 패턴
20층 아파트 내 모든 층의 온도를 1분마다 체크하는 IOT 센서가 있다고 가정하겠습니다.
이때 별도로 스키마 모델링을 하지 않고 매분 각층의 온도를 하나의 document로 저장할 경우 한 달만 지나도 864,000개의 document가 저장되어 storage 크기가 너무 커지는 허들이 생길 것입니다.
실제로 한달 치 데이터를 컬렉션에 넣은 결과 무려 60MB를 차지하는 것을 확인할 수 있었습니다.
Bucket 패턴을 적용 시 일정 시간 범위 내 데이터를 하나의 배열 필드로 저장해 storage 문제를 해결할 수 있습니다.
Bucket 패턴을 적용하기 위해서는 아래와 같이 시작 날짜, 종료 날짜 그리고 각 시간 범위 내 저장된 온도들을 하나의 배열 필드에 저장하면 됩니다.
Bucket 패턴 적용 결과 document를 864,000개에서 14,400개로 줄일 수 있었고 이에 따라 확실히 차지하는 메모리를 줄일 수 있었습니다.
하지만 위와 같이 모델링을 진행할 경우 한 시간 단위로 조회할 때는 성능이 잘 나오지만 랜덤한 시간대로 조회를 할 때 실행 속도가 느려질 가능성이 높습니다.
이에 따라 MongoDB 5.0 버전부터는 Bucket 패턴을 사용하지 않고 Time Series 패턴을 사용합니다.
Time Series 패턴은 시간 기반으로 돌아가는 시계열 데이터에 대해서 쉽게 분석하고 매우 효율적으로 저장하는 컬렉션 타입으로서 내부적으로 Clustered 인덱스를 사용하여 메모리를 절약하고 $setWindowsField aggregation이 사용 가능하여 검색 속도가 빠르다는 장점이 있습니다.
Time Series 패턴 적용 결과 Bucket 패턴보다 메모리를 절약한 것을 확인할 수 있습니다.
4. Subset 패턴
Subset 패턴을 이해하기 위해서는 우선 Working Set 개념을 파악할 필요가 있습니다.
Working Set이란 당분간 집중적으로 참조될 데이터의 집합을 의미하며 조회 성능 향상을 위해 캐시에 올립니다.
문제는 Working Set의 크기가 Cache 사이즈보다 클 경우인데 이때 cache eviction이 발생하고 디스크로부터 조회할 데이터를 다시 캐시에 올리는데 시간이 오래 걸립니다.
따라서 Working Set 크기를 줄이는 것이 중요하며 아래와 같은 방법을 통해 줄일 수 있습니다.
- 각 document 크기를 줄임
- 사용자의 패턴을 분석 후 필드의 일부만 저장
- ex) 음식점 조회 시 상위 10개 댓글만 조회, 모든 댓글을 조회할 필요 X
- reviews 컬렉션에는 모든 댓글을 저장
- shops 컬렉션에는 각 document마다 상위 10개 댓글만 일부 저장
이처럼 document 내 필드를 일부만 저장하는 기법을 Subset 패턴이라고 합니다.
5. Tree 패턴
사내 결제선, 쇼핑몰 카테고리와 같이 hierarchy 구조에 사용하기 좋은 구조로 $graphLookup aggregation을 지원합니다.
사실 위와 같은 구조는 Neo4j와 같은 graph db를 사용하는 것을 권장하지만 MongoDB에서도 지원합니다.
예를 들어 팀원 -> 팀장 -> 본부장 -> CEO 순으로 보고하는 조직에서 각 직원의 보고 체계를 조회하고 싶을 경우 아래와 같이 $graphLookup aggregation을 통해 확인할 수 있습니다.
비고
상기에 명시한 패턴 외에도 수많은 스키마 모델링 기법이 존재하며 이들 중 간단하게 아래 세 가지만 부연 설명을 진행하겠습니다.
Schema Versioning 패턴
개발 초기에 계속해서 스키마가 변경되는 경우 버저닝을 통해 스키마를 순차적으로 변경하는 케이스에 유용한 기법입니다.
컬렉션을 바꾸지 않으면서도 명시적으로 스키마 버전 필드를 추가하여 스키마를 변경할 수 있는 패턴입니다.
Outlier 패턴
Schema Versioning 패턴과 유사하며 Flag 필드를 사용합니다.
document가 배열 필드를 가지고 있고 특정 몇 개의 document만 특출 나게 사이즈가 큰 배열을 가지고 있을 경우 추가 데이터가 있다는 의미를 지닌 flag 필드 추가 후 다른 컬렉션과 aggregation을 통해 전체 조회해 오는 기법입니다.
이는 앞서 언급한 Extended Reference 패턴과 같이 사용하면 유용합니다.
참고
백엔드 개발자를 위한 한 번에 끝내는 대용량 데이터 & 트래픽 처리 초격자 패키지 Online - 비즈니스 요구사항에 유연한 MongoDB
'DB > MongoDB' 카테고리의 다른 글
[MongoDB] 인덱스 (0) | 2023.12.01 |
---|---|
[MongoDB] Read/Write 제어 (0) | 2023.11.29 |
[MongoDB] MongoDB 개요 (0) | 2023.11.26 |