1. 문제의 시작: 저장 비용 증가
서비스 운영 기간이 길어질수록 MongoDB 저장 용량 역시 지속적으로 증가하였습니다.
비즈니스 요구사항상 이미 생성된 데이터를 삭제할 수는 없었지만, 모든 데이터를 일반 스토리지에 계속 유지하는 것은 비용 측면에서 부담이 커지고 있었습니다.
특히 Online Archive 적용 대상이 된 특정 컬렉션은 서비스 핵심 데이터를 포함하고 있었으며, 데이터가 지속적으로 누적되면서 약 1.2TB 수준까지 증가한 상태였습니다.
이로 인해 단순한 저장 비용 문제가 아니라, 장기적인 데이터 운영 전략 자체를 재검토해야 하는 상황이었습니다.
이에 따라 일정 기간이 지난 데이터를 별도 저장소로 이관하는 방안을 검토하게 되었고, MongoDB Online Archive를 도입하기로 결정하였습니다.
- 생성 후 365일이 지난 데이터는 Online Archive로 이관한다. -
이 기준은 조회 패턴 분석을 기반으로 정교하게 설계된 값이라기보다는, 저장 비용 최적화를 위한 운영 정책에 가까웠습니다.
다만 Online Archive는 저장 비용 절감이라는 장점이 있는 반면, 조회 시에는 별도의 비용 구조를 고려해야 하는 특징이 있었습니다.
즉,
무엇을 Archive할 것인가?
뿐만 아니라,
Archive된 데이터를 어떻게 조회할 것인가?
역시 함께 고민해야 하는 상황이었습니다.
2. Online Archive 설계
Archive 정책은 다음과 같이 구성하였습니다.
Archive 기준: time 필드 기준 365일 경과 데이터
Partition 구성: dataId → time
실제 서비스의 주요 조회 패턴은 다음과 같았습니다.
특정 dataId에 대한 전체 데이터 조회
이에 따라 동일한 dataId의 데이터를 우선적으로 그룹화할 수 있도록 dataId를 선행 키로 설정하였고, Archive 기준이 되는 time을 후행 키로 구성하였습니다.
이를 통해 실제 조회 패턴과 Archive 정책의 정합성을 맞추고자 하였습니다.
또한 Online Archive 환경에서는 Partition 기준을 고려한 조회가 Data Scanned를 줄이는 데 유리할 수 있었기 때문에, 비용 최적화 측면에서도 중요한 설계 요소로 판단하였습니다.
3. Archive 운영을 위한 인덱스 전략
Archive 도입 과정에서는 단순히 Archive 정책만 정의한 것이 아니라, Archive 수행 과정과 실제 조회 패턴까지 함께 고려하여 인덱스를 구성하였습니다.
3-1. Archive 대상 식별을 위한 인덱스
{ time: 1, dataId: 1 }
Archive 정책상 생성 후 365일이 지난 데이터를 지속적으로 식별해야 했기 때문에,
Archive 대상 데이터를 효율적으로 식별하기 위해 별도의 인덱스를 생성하였습니다.
3-2. Archive 이후 조회를 위한 인덱스
서비스의 주요 조회 패턴은 dataId 기반 전체 조회였으며,
Online Archive의 Partition 역시 다음과 같이 구성하였습니다.
{ dataId: 1, time: 1 }
이에 따라 통합 Endpoint를 통해 Archive 데이터까지 함께 조회하는 경우를 고려하여, Partition 구성과 동일한 순서의 인덱스를 생성하였습니다.
이를 통해 실제 조회 패턴과 Archive 구조 간의 정합성을 유지하고자 하였습니다.
정리하면 다음과 같습니다.
|
목적 |
인덱스 |
|---|---|
|
Archive 대상 식별 |
{ time: 1, dataId: 1 } |
|
Archive 이후 통합 조회 |
{ dataId: 1, time: 1 } |
4. 기존 대용량 조회 API
우리 서비스에는 특정 식별자에 대한 전체 데이터를 반환하는 API가 존재했습니다.
GET /data/{dataId}
이 API는 단순 조회 API가 아니었습니다.
조회된 데이터를 그대로 반환하는 것이 아니라, 응답 규격에 맞춘 데이터 가공 작업까지 수행해야 했습니다.
MongoDB 조회 → 응답 데이터 가공 → 클라이언트에 반환
또한 데이터 분포 역시 균일하지 않았습니다.
|
dataId별 데이터 규모 |
건수 |
|---|---|
|
일반적인 경우 |
수십 ~ 수천 건 |
|
대용량 케이스 |
최대 수십만 건 |
즉, 동일한 API라도 특정 dataId에 대해서는 대량 데이터 처리가 필요한 구조였습니다.
5. 기존 조회 방식
기존에는 OFFSET 기반의 반복 조회 방식을 사용하고 있었습니다.
예를 들면 다음과 같은 구조였습니다.
LIMIT 1000 OFFSET 0
LIMIT 1000 OFFSET 1000
LIMIT 1000 OFFSET 2000 ...
일반적인 MongoDB 환경에서는 큰 문제가 없었습니다.
그러나 Online Archive 도입 이후에는 기존 조회 방식이 Archive 환경에서도 적절한지 다시 검토할 필요가 있었습니다.
6. OFFSET 기반 조회를 재검토하게 된 이유
Online Archive 환경에서는 Data Scanned 양이 비용에 영향을 줄 수 있습니다.
즉,
Data Scanned 증가 → 조회 비용 증가 가능성
이라는 관계를 고려해야 했습니다.
기존 OFFSET 기반 조회는 원하는 위치까지 도달하기 위해 앞선 데이터를 반복적으로 탐색해야 합니다.
예를 들어,
LIMIT 1000 OFFSET 90000
과 같은 조회는 결과를 반환하기 위해 이전 90,000건을 건너뛰는 과정이 필요합니다.
대량 데이터를 여러 번 나누어 조회하는 구조에서는 이러한 탐색 비용이 반복적으로 발생하게 됩니다.
Archive 환경에서는 이러한 반복 탐색이 Data Scanned 증가로 이어질 가능성이 있다고 판단하였습니다.
OFFSET → 반복 조회 → 반복 탐색 발생 → Data Scanned 증가 → 조회 비용 증가 가능성
결국 기존 OFFSET 기반 조회 전략을 재검토하게 되었습니다.
7. Cursor 기반 조회로 전환
새로운 조회 방식으로 Cursor 기반 조회를 선택하였습니다.
Spring Data MongoDB의 Stream 기능을 활용하여 데이터를 순차적으로 소비하도록 구성하였습니다.
@Meta(cursorBatchSize = 3000)
Stream<DataDoc> streamByDataId(String dataId);
또한 cursorBatchSize를 함께 적용하였습니다.
이번 사례에서 batchSize를 사용한 이유는 대량 데이터를 한 번에 메모리에 적재하지 않고, 일정 크기 단위로 소비하기 위함이었습니다.
Cursor → batch 단위 조회 → 순차 소비 → 메모리 사용량 제어
기존 구조와 비교하면 다음과 같습니다.
OFFSET → 반복 조회 → 반복 탐색 Cursor → 순차 조회 → 단일 조회 흐름 유지
Archive 환경에서는 이러한 구조가 비용 최적화 측면에서도 더 적합하다고 판단하였습니다.
8. 회고
이번 경험을 통해 Online Archive 도입은 단순한 저장 비용 절감 작업이 아니라는 점을 확인할 수 있었습니다.
저장 전략이 바뀌면 조회 전략 역시 함께 재검토되어야 합니다.
특히 비용 구조가 달라지는 환경에서는 기존 방식이 항상 최적이라고 볼 수는 없습니다.
이번 사례에서는 Archive 정책, 인덱스 구성, 조회 패턴, 조회 전략까지 함께 고려해야 했습니다.
저장 비용 최적화
→ Online Archive 도입
→ Partition 설계
→ 인덱스 구성
→ 조회 비용 고려
→ OFFSET 재검토
→ Cursor 기반 조회 전환
결국 이번 사례는 단순히 Cursor를 적용한 이야기가 아니었습니다.
저장 비용 최적화를 위해 시작한 작업이 조회 비용에 대한 고민으로 이어졌고, 그 과정에서 기존 조회 전략을 다시 바라보게 되었습니다.
인프라의 변화는 애플리케이션의 조회 전략 변화를 만들었고, 이를 통해 저장 전략과 조회 전략은 긴밀하게 연결되어 있다는 점을 다시 한번 체감할 수 있었습니다.
참조
https://www.mongodb.com/docs/atlas/online-archive/
https://www.mongodb.com/docs/atlas/data-federation/billing/
https://www.mongodb.com/docs/manual/reference/method/cursor.skip/
yeop