-DB 비용 폭탄을 해결한 구원투수: S3 기반 데이터 티어링 도입기-
늘어나는 서비스, 그리고 조용히 다가온 DB 비용 폭탄
최근 진행하는 프로젝트에서는 서비스의 수가 많아지고 많은 개발 내용이 추가되고 있었습니다. 이로 인해, 기하급수적으로 증가하는 데이터와 그에 비례해 치솟는 인프라 비용을 마주해야 했습니다.
특히 마이크로서비스 아키텍처(MSA)를 차용하고 있었기에 로그의 증가 속도는 상상을 초월했습니다. 단 한 번의 사용자 요청(Request)이 발생하더라도, 이를 처리하기 위해 게이트웨이를 거쳐 수많은 마이크로 서비스 간의 내부 분산 호출이 연쇄적으로 일어났기 때문입니다. 시스템 전체의 트랜잭션을 추적하고 모니터링하기 위해 각 독립된 서비스마다 분산 로그를 꼼꼼하게 남겨야만 했고, 이는 모놀리식 구조와 비교할 수 없을 정도로 방대한 양의 로그 데이터가 동시다발적으로 생성되는 결과를 초래했습니다.
초기에는 이러한 분산 로그를 저장할 저장소로 MongoDB를 선택했습니다. MSA 환경에서는 서비스마다 로그 포맷이 조금씩 다를 수 있는데, MongoDB의 유연한 문서(Document) 구조 덕분에 스키마 변경에 대한 부담이 전혀 없었기 때문입니다. 또한, 대량의 로그가 여러 마이크로 서비스로부터 동시다발적으로 쏟아져 들어와도 끄떡없는 빠른 쓰기 성능과 대시보드 연동 편의성은 최고의 매력이었습니다.
하지만 마이크로 서비스의 수가 늘어나고 서비스가 가파르게 성장하면서, 매달 수백 GB 수준이던 로그 데이터는 어느새 수 TB 단위로 불어나며 데이터베이스 용량을 순식간에 잠식하기 시작했습니다. 더 큰 문제는 성능이었습니다. MongoDB는 성능을 유지하기 위해 인덱스와 자주 사용하는 데이터를 메모리(RAM)에 올려두는 WiredTiger 캐시 엔진을 사용합니다. 로그 데이터가 늘어나자 인덱스 크기도 함께 비대해졌고, 제한된 RAM 용량을 초과하면서 다음과 같은 치명적인 연쇄 작용이 일어났습니다.-
-
RAM 압박 및 디스크 I/O 병목: 메모리가 부족해지자 데이터베이스가 디스크에서 페이지를 읽고 쓰는 빈도가 급증했고, 이는 곧바로 프로덕션 서비스의 쿼리 지연으로 이어졌습니다.
-
복제본(Replica Set)으로 인한 비용 3배수 증가: 가용성을 위해 고성능 SSD를 장착한 EC2 인스턴스 3대로 클러스터를 구성하다 보니, 1TB의 로그를 저장하기 위해 실제로는 3TB 분량의 고비용 스토리지 값을 지불해야 했습니다.
운영 현황을 분석해 보니 전체 로그 조회 요청의 대다수는 최근 3일 이내의 데이터에 한정되어 있었으며, 그 이전의 과거 로그는 장애 사후 분석(Post-mortem)이나 규제 감사 등 간헐적인 목적으로만 사용되고 있었습니다. 우리는 비싼 DB 스토리지와 RAM을 점유하고 있는 오래된 데이터를 분리하여 인프라 비용을 획기적으로 낮추기로 결단했습니다. 그리고 그 해결책으로 성능과 비용을 모두 잡을 수 있는 아키텍처인 데이터 티어링(Data Tiering)을 도입했습니다.
데이터 티어링(Data Tiering) 아키텍처 설계
데이터 티어링의 핵심은 모든 데이터를 동일한 저장소에 밀어 넣지 않고, 접근 빈도(Access Frequency)와 비즈니스 요구사항에 따라 데이터의 저장 위치를 최적화하는 것입니다. 관리 공수(Overhead)를 최소화하면서도 비용 절감 효과를 극대화하기 위해, 복잡한 다단계 계층 대신 Hot Tier와 Cold Tier 두 가지로만 다음과 같이 깔끔하게 스토리지를 분리했습니다.
- Hot Tier (최근 30일): 실시간 디버깅을 위한 고성능 구간
-
스토리지: MongoDB
-
목적: 라이브 에러 모니터링, 최근 배포된 기능의 이슈 트래킹, MSA 분산 트레이싱 추적
-
설계 방향: 한 달 이내의 데이터는 개발 및 운영팀이 장애 대응과 디버깅을 위해 가장 빈번하게 조회하는 '뜨거운 데이터'입니다. 따라서 성능 향상을 최우선으로 둡니다.
- Cold Tier (31일 ~ 1년): 규제 준수 및 아카이빙을 위한 비용 절약 구간
-
스토리지: AWS S3 Standard
-
목적: 수개월 전 발생한 드문 버그 추적, 장기적인 데이터 통계 분석, 보안 및 법적 감사 대응
-
설계 방향: 한 달이 지난 로그는 조회 빈도가 급격히 떨어지는 '차가운 데이터'입니다. 이 구간은 인덱스를 모두 걷어내고 데이터를 압축된 파일 형태로 S3 Standard에 저장합니다. S3 Standard는 별도의 데이터 인덱싱 비용이 없고 용량당 비용이 MongoDB 대비 10배에서 25배 이상 저렴합니다.
이 2-Tier 아키텍처 모델이 가진 가장 큰 엔지니어링적 가치는 스토리지(Storage)와 컴퓨팅(Computing)을 완벽하게 분리했다는 점입니다. 기존 구조에서는 로그 용량이 늘어나면 디스크 용량뿐만 아니라, 비대해진 인덱스를 소화하기 위해 MongoDB 서버의 사양까지 강제로 스케일업(Scale-up)해야 했습니다. 즉, 단순히 데이터를 '보관'만 하기 위해 비싼 연산 장치와 메모리 비용까지 세트로 지불했던 셈입니다. 그러나 30일이 지난 차가운 데이터를 S3라는 서버리스 객체 스토리지로 밀어내면서 두 자원이 독립적으로 확장될 수 있는 발판을 마련했습니다. MongoDB는 항상 딱 30일 치의 예측 가능한 범위의 데이터만 유지하므로 최소한의 고정 비용으로 최상의 퍼포먼스를 유지하고, 수 테라바이트에 달하는 과거 데이터는 저렴한 S3 공간을 무한대로 활용하며 인프라의 확장성(Scalability)을 극대화했습니다.
자동화 파이프라인 구축
아무리 이상적인 아키텍처를 설계했더라도, 데이터를 옮기고 비우는 과정에 엔지니어의 매뉴얼한 작업이 들어간다면 지속 가능하지 않습니다. 30일이 지난 로그를 MongoDB에서 안전하게 지우고, S3 Standard로 깔끔하게 아카이빙하는 자동화 데이터 파이프라인을 구축했습니다. 데이터 파이프라인은 크게 다음과 같이 구성됩니다.
-
MongoDB에서 30일 경과한 로그 찾고 데이터 가공
-
S3로 데이터 백업하기 및 백업한 데이터 1년 후 자동 삭제 설정
-
백업한 데이터 MongoDB 삭제
실제 운영 환경에서는 하루에도 수천만 건의 로그가 쏟아지기 때문에, 이 데이터를 한 번에 메모리에 올렸다간 곧바로 OOM(Out Of Memory) 장애로 이어집니다. 따라서 메모리 부하를 줄이기 위해 다음과 같은 전략을 세웠습니다.
-
커서 기반 페이징(Pagination): 데이터를 수천 건 단위(2,000건)로 끊어서 안전하게 가져옵니다.
-
디스크 스트리밍(Streaming Out): DB에서 읽어온 데이터를 메모리에 유지하지 않고, 로컬 임시 파일(Temp File)에 즉시 스트리밍 방식으로 파일 시스템에 써 내려갑니다.
-
배치 삭제(Batch Delete): 이주(Migration)가 끝난 ID들을 모아 디스크에 부담을 주지 않도록 10,000건씩 나누어 안전하게 지웁니다.
이 핵심 흐름을 담은 마이그레이션 파이프라인의 코드는 다음과 같습니다.
private <T> void executeLogMigrationPipeline(...) {
List<String> logIds = new ArrayList<>();
// 1. 메모리 오버헤드를 방지하기 위한 임시(Temp) 파일 생성
Path tempFile = Files.createTempFile(filePrefix, ".json");
// 2. Jackson SequenceWriter를 활용해 디스크로 즉시 스트리밍 (JSON Array 구조)
try (var fileOut = Files.newOutputStream(tempFile);
var seqWriter = objectMapper.writer().writeValuesAsArray(fileOut)) {
int pageCounter = 0;
int pageSize = 2000;
while (true) {
// DB에서 정해진 배치 사이즈만큼 순차 조회
Pageable pageable = PageRequest.of(pageCounter++, pageSize, Sort.by("registrationTime"));
List<T> logs = logFinder.apply(targetEpochMillisToMove, pageable);
if (logs.isEmpty()) break;
// ID 수집 및 파일에 즉시 기록 (메모리 점유 방지)
logIds.addAll(logs.stream().map(idExtractor).toList());
LogHelper.appendLogsToJsonStream(logs, seqWriter);
if (logs.size() < pageSize) break;
}
}
// 3. S3 스토리지(Depot)로 안전하게 파일 업로드
if (!logIds.isEmpty() && Files.exists(tempFile)) {
depotProxy.uploadDTSPFiles(remoteFilePath, new FileSystemResource(tempFile.toFile()), targetEpochMillisToRemoval);
// 4. 대량의 DB 쓰기 부하를 방지하기 위해 10,000건씩 쪼개서 삭제(Bulk Delete)
int deleteBatchSize = 10000;
for (int i = 0; i < logIds.size(); i += deleteBatchSize) {
List<String> subBatchIds = logIds.subList(i, Math.min(i + deleteBatchSize, logIds.size()));
batchRemover.accept(subBatchIds);
}
}
// 5. 작업이 끝나면 로컬 임시 파일 자원 해제 (Retry 로직 포함)
cleanUpTempFile(tempFile);
}
제네릭(T)과 함수형 인터페이스(BiFunction, Consumer)를 활용해 설계한 덕분에, 로그의 타입이나 형태가 다르더라도 동일한 마이그레이션 파이프라인 로직을 공통으로 재사용할 수 있는 확장성까지 챙길 수 있었습니다. MongoDB가 보관하는 로그 데이터가 항상 최근 30일 치로 고정되면서, 비대해지던 인덱스 크기가 RAM 용량 이내로 쏙 들어오게 되었습니다. 이로 인해 디스크 I/O 병목이 완전히 사라졌고, 프로덕션 환경의 쿼리 평균 응답 속도(Latency)가 빨라졌습니다. 또한, 기존에 고비용으로 지출되던 DB 스토리지 비용을 아낄 수 있었습니다. 1TB당 비용을 비교했을 때, MongoDB 클러스터 대비 S3 Standard는 압도적으로 저렴했기 때문에 전체 로그 인프라 비용의 상당수를 아낄 수 있었습니다.
마치며
많은 서비스들이 성장 가도에 오르면 데이터가 늘어남에 따라 무작정 데이터베이스의 스펙을 높이는(Scale-up) 방식으로 임시방편을 마련하곤 합니다. 하지만 데이터의 비즈니스 가치와 접근 빈도는 시간이 흐름에 따라 반드시 변하기 마련입니다. 이번 S3 기반 데이터 티어링 도입은 단순히 비용 몇 푼을 줄인 것을 넘어, 비즈니스 요구사항과 기술적 비용 간의 불일치를 아키텍처적으로 해결한 경험이었습니다. 가장 비싸고 무거운 저장소가 항상 정답은 아닙니다. 우리 시스템이 가진 데이터의 성격과 온도를 파악하고 적재적소에 배치하는 것, 그것이 인프라 비용 폭탄을 해결하는 엔지니어의 진짜 역할이 아닐까 생각합니다. 늘어나는 데이터와 인프라 비용 사이에서 고민하고 계시는 분들께 이 글이 작은 힌트가 되었기를 바랍니다.
Hustle Paul