LY Corporation Tech Blog

LY Corporation과 LY Corporation Group(LINE Plus, LINE Taiwan and LINE Vietnam)의 기술과 개발 문화를 알립니다.

AI 피처 스토어를 MongoDB와 Spring Cloud Stream으로 새롭게 구축한 이야기

이번 글에서는 AI 피처 스토어를 MongoDB와 Spring Cloud Stream으로 새롭게 구축한 이야기를 공유하려고 합니다. 간단히 요약하자면, 기존의 레거시 AI 피처 스토어 시스템에서 발생한 여러 문제를 해결하기 위해 MongoDB와 Spring Cloud Stream을 도입해 새로운 시스템을 개발해 레거시를 대체하는 과정을 다루려고 합니다.

우리는 새로운 목적으로 처음부터 새로운 시스템을 구축하기도 하지만, 기존 시스템을 인계받아 문제를 개선하거나 때로는 기존 시스템의 목적을 이어받는 완전히 새로운 시스템을 구축하기도 합니다. 즉, 종종 복잡하고 불완전한 레거시 시스템을 받아 이를 개선하고 대체해야 하는 상황과 마주칩니다. SNS에 올라오는 멋진 프로젝트들처럼 모든 개발 여정이 화려한 것은 아니죠. 이 글은 이와 같이 레거시를 대체하는 새로운 시스템을 개발하는 경험을 공유해 비슷한 상황에 처한 많은 개발자분들께 작은 도움이 되기를 바라며 작성했습니다.

이번 글은 특별히 저희 팀이 아닌 Mongo DB의 DBA이신 이세웅 님도 함께 작성해 주셨습니다. 이번 프로젝트의 핵심 과제 중 하나가 그 자체로 또 하나의 도전이라고 할 수 있을 정도로 전례 없는 규모의 대용량 MongoDB 샤딩된 클러스터(sharded cluster)를 구축하는 것이었기 때문입니다. 평소 MongoDB에 관심이 있었던 분들, 특히 대규모 MongoDB 운영 및 구축과 관련된 실제 업무 경험담을 찾고 계신 분들께 도움이 되기를 바라며 본격적으로 이야기를 시작해 보겠습니다.

프로젝트 소개

이번 프로젝트는 여러 문제를 안고 있던 레거시 피처 스토어(이하 레거시)를 대체할 수 있는 완전히 새로운 피처 스토어를 개발하는 프로젝트였습니다. 피처 스토어란 AI에서 사용하는 실시간 및 배치 데이터를 저장하고 처리해 피처를 재사용하고 일관성을 유지할 수 있게 만드는 시스템입니다. AI 모델 개발과 운영을 위한 피처 변환(feature transformation), 서빙, 스토리지 등의 기능을 제공합니다(피처 스토어의 역할이나 효용에 대한 보다 자세한 사항은 MACHINE LEARNING FEATURE STORES: A COMPREHENSIVE OVERVIEW를 참고하시기 바랍니다).

이전에 대용량 AI 실시간 임베딩 데이터를 효율적으로 다루기라는 글로 AI에 사용하는 실시간 임베딩을 제공하는 고성능 고효율 서버를 구축한 과정과 결과를 공유한 적이 있는데요. 이번에 소개하는 프로젝트는 이전에 소개한 프로젝트와 결합해 더 다양한 종류의 AI 데이터를 제공하는 역할을 담당하는 시스템을 개발하는 프로젝트입니다. 

두 프로젝트가 다루는 AI 데이터의 성질이 다르고 그로 인해 시스템 구성도 완전히 다르기 때문에 이전 글을 보지 않으신 분들도 이 글을 이해하시는 데 전혀 문제는 없습니다. 다만 두 글을 모두 읽어보시면 데이터의 성질에 따라 시스템 구조가 얼마나 많이 달라지는지, 또한 각각 어떤 DB를 사용했고 그 이유가 무엇인지 비교해 보실 수 있기 때문에 아직 이전 글을 보지 않으셨다면 이전 글도 함께 읽어보시기를 권해 드립니다.

레거시 시스템 분석

개발자들은 간혹 기존에 존재하던 레거시를 인계받아 더 나은 시스템을 만드는 일을 맡게 됩니다. 이때 레거시 개발에 본인이 참여하지 않았다면 기존 시스템을 분석하는 일부터 시작해야 하며, 얼마나 정확히 분석하느냐가 이후 작업에 큰 영향을 미칩니다.

레거시 시스템을 분석할 때는 레거시의 기획 의도와 목적, 주변 시스템과의 관계, 인프라 환경과 같은 시스템 환경을 파악해야 하고, 만일 시스템에 문제가 있을 경우 문제의 원인을 정확히 진단해야 합니다. 그래야 기존의 역할을 충실히 수행하면서 문제를 해결할 수 있는 새로운 시스템을 만들 수 있습니다.

이번 프로젝트에서도 가장 첫 번째 단계가 레거시 시스템 분석이었으며, 레거시 시스템의 환경과 문제는 아래와 같았습니다.

레거시 시스템의 본래 목표

기존 레거시 AI 피처 스토어 시스템의 주요 목표 중 하나는 대용량 파케이(parquet) 파일을 적재하고 관리하는 것이었습니다. 조금 더 구체적으로 말하자면, 매일 또는 특정 주기에 맞춰 AI 서비스용 대용량 파케이 파일을 다운로드하고 이를 검증한 후 정해진 시간 내에 DB에 적재해 AI 모델의 훈련과 실제 서비스에 활용되도록 만드는 것이었습니다.

겉으로 보기에는 비교적 단순한 데이터 파이프라인처럼 보일 수 있지만 실제로는 다음과 같은 이유로 높은 성능을 요구하는 까다로운 과제였습니다.

  • 시스템이 주기적으로 처리해야 하는 파케이 파일은 하나당 수 GB에서 수백 GB에 이르는 대용량 파일이었으며, 이런 파일을 동시에 혹은 순차적으로 수십 개에서 수백 개씩 처리해야 했습니다.
  • 하루 동안 처리해야 하는 총 데이터량은 수 테라바이트를 초과했으며, 지속적으로 보관해야 할 데이터 규모도 수십 테라바이트에 달했습니다.
  • 전체 프로세스(파케이 파일 준비 → 다운로드 → 압축 해제 → 검증 → DB 적재 → 상태 변경 및 TTL 만료 데이터 삭제)를 반드시 정해진 시간 안에 완료해야 했습니다.
    • 만약 처리 지연이 발생할 경우 연관된 후속 작업과 관련 시스템에 연쇄적인 장애가 발생할 수 있어 안정적인 처리가 필수였습니다.
  • 신규 데이터 적재 작업 중에도 서비스는 중단 없이 실시간 요청을 처리해야 했으며, 이때 응답 시간을 반드시 일정 수준 이내로 유지해야 했습니다.

결과적으로 대용량 데이터 처리와 엄격한 시간 제약, 실시간 응답이라는 세 가지 요구 사항을 모두 만족하는 아키텍처 설계가 프로젝트의 핵심 과제였습니다.

레거시 시스템 문제 분석

레거시 시스템은 앞서 말씀드린 목표를 충족하며 작동하고 있었지만 다소의 문제를 지니고 있었으며, 문제는 서버 및 애플리케이션 구성의 문제와, 메인 DB로 사용된 TiDB의 제약으로 나눌 수 있었습니다.

서버와 애플리케이션 구성의 문제

레거시 시스템의 첫 번째 문제는 서버와 애플리케이션 구성 때문에 서버 자원을 비효율적으로 활용하고 있다는 것이었습니다. 레거시 시스템 개발 시 선택된 서버들은 아래 설명할 파케이 파일에 대한 잘못된 접근법 때문에 CPU 대비 메모리 용량이 비정상적으로 큰 고가 장비들이었고, 단일 노드풀에 집중 배치돼 있었습니다. 그런데 이와 같이 고사양 서버를 다수 사용하고 있었지만 실제 애플리케이션 서버 리소스를 충분히 활용하지는 못하고 있었는데요. 이로 인해 쿠버네티스 환경에서 여러 종류의 파드를 세분화해서 배치하는 데 한계가 있었고, 메모리는 남는 반면 CPU는 부족해지는 비효율이 발생했습니다.

이 문제를 해결하기 위해서는 근본적인 인프라 구조를 개선할 필요가 있었습니다. 일부 장비를 반납하고 새로운 서버를 추가한 후 노드 풀을 나누는 식으로 조치한다면 부분적인 개선은 가능하겠지만 이는 근본적인 해결책이 아니라 그 한계가 분명했습니다. 결국 인프라 비용을 획기적으로 절감하고 운영 유연성을 확보하기 위해 고가 장비를 모두 반납하고 CPU/메모리 비율이 표준인 작은 VM 기반으로 전환하는 전략이 필요했습니다. 또한 서비스 무중단이라는 조건을 충족하기 위해 새로운 노드 풀을 별도로 구축하고 이전하는 방식이 현실적인 선택이었습니다.

TiDB 성능 한계와 애플리케이션 병목

레거시 시스템은 TiDB를 메인 DB로 사용했는데 TiDB의 쓰기 성능과 확장성 한계로 인해 애플리케이션 코드 곳곳에 이를 우회하거나 버티기 위한 비효율적인 구현이 있었습니다. 이런 방식은 시스템 전체의 복잡도를 높이고 프로젝트 전반의 품질을 저하시키는 원인이 되었습니다.

예외 상황 대응의 어려움

레거시 시스템은 대규모 파일 처리 작업에 취약점이 있어 예외 상황에 대응하기 어렵다는 문제도 있었습니다. 레거시 시스템은 매일 수십에서 수백 개의 대용량 파케이 파일을 다운로드해 압축 해제 후 검증하고 DB에 입력하는 과정을 반복했는데요. 파일 처리 도중 오류가 발생하면 정확한 상태를 추적하거나 자동 재시도하는 기능이 없어 사람이 직접 개입해야 했습니다. 이런 문제는 운영의 부담을 가중시켰고, 시스템 정합성에도 잠재적인 위험 요소가 되었습니다.

사용하는 DB(TiDB)의 문제

아래에 기술하는 레거시 시스템의 TiDB 관련 문제들은 TiDB의 특성과 제약에서 비롯된 것일 수도 있지만 다른 한편으로는 구성 방식에 기인한 문제일 가능성도 있습니다. 따라서 본 글에서 언급하는 문제들을 TiDB 전반의 문제로 일반화하는 것은 적절하지 않습니다.

다만 유사한 환경에서 비슷한 어려움을 겪고 있는 분들에게는 저희의 경험이 실질적인 참고 자료가 될 수 있을 것이라고 생각합니다. 특히 시스템 설계나 DB 선택 과정에서 고려해야 할 포인트를 고민하고 계신다면 끝까지 읽어보시길 추천드립니다.

첫 번째 문제는 부하를 분산할 필요가 있었다는 것입니다.

매일, 매시간 대량의 신규 데이터가 레거시 시스템에 입력되면 TiDB의 CPU와 I/O 사용량은 한계에 근접했고 이 때문에 서비스 요청 처리에 어려움이 발생했습니다. TiDB는 래프트 합의 알고리즘을 기반으로 리더 노드가 쓰기(write) 요청을 처리한 후 로그를 팔로워(follower) 노드에 전파하고 이를 통해 데이터 일관성(consistency)을 보장합니다. 기본적으로 읽기와 쓰기 요청 모두 리더 노드가 처리하는 구조이기 때문에 대량 쓰기 작업이 집중되는 시간대에는 리더 노드의 부하가 급격히 증가해 서비스 응답 속도가 현저히 저하될 수 있으며, 과도한 작업 부하를 감당하지 못해 다운될 수도 있습니다. 만약 다운된다면 후보 노드 간 리더 선출 과정이 추가로 발생해 서비스 안정성에 부정적인 영향을 미칠 가능성도 존재합니다.

이 문제를 해결하려면 읽기와 쓰기 작업을 분리해 각각 다른 노드에서 처리하는 방식이 필요합니다. 일반적으로 복제(replication)를 지원하는 DB에서는 쓰기 요청은 리더 노드가 처리하고 읽기 요청은 레플리카 노드로 분산해 시스템 부하를 효과적으로 분산시킬 수 있습니다. TiDB도 팔로워 읽기 기능(참고)을 지원하는 버전부터는 해당 옵션을 사용 시 읽기 요청을 팔로워 노드에서 처리함으로써 리더 노드의 부하를 일부 분산할 수 있게 되었습니다. 다만 레거시 시스템의 경우 구축 시 부하 분산 관련 고려가 없었던 것으로 보였으며, 인계받은 후에는 조치를 취할 수 있는 상황이 아니었습니다.

TiDB에서 집중적인 쓰기 작업이 I/O 사용량 증가를 유발하는 상황의 예시
TiDB에서 집중적인 쓰기 작업이 I/O 사용량 증가를 유발하는 상황의 예시

두 번째 문제는 레거시 시스템의 TiDB는 HA(high availability) 구성이 완벽하다고 할 수 없었다는 점입니다.

아무리 인프라 환경이 좋고 안정적이라고 하더라도 운영하다 보면 하드웨어 문제를 비롯한 여러 문제가 발생할 수밖에 없습니다. 따라서 DB와 서버 등 인프라 환경의 HA 구성은 필수입니다. DB 종류에 따라 약간의 차이는 있지만 HA로 구성해 놓았을 경우 장애 감지부터 대응까지 모든 과정이 자동으로 순식간에 이뤄지기 때문에 사용자가 장애를 체감하지 못하거나 아주 짧은 순간 동안만 접속이 불가능해질 뿐입니다. 만약 HA 구성을 하지 않거나 구성에 문제가 있다면 장애 감지와 대응의 일정 부분 이상을 사람이 대응해야 하는데요. 수동으로 대응하면 속도나 정확도 등 여러 가지 면에서 불리할 수밖에 없습니다.

또한 매우 드문 경우이긴 하지만 HA 구성을 해놓았다고 해도 세컨더리 장비까지 동시에 문제가 발생하거나 천재지변 등으로 장비가 파손될 수도 있습니다. 따라서 데이터를 안전하게 백업해 놓거나 다른 복구 수단 및 시나리오도 준비해야 합니다. 발생할 수 있는 다양한 장애 시나리오에 얼마나 만전을 기해 대비했느냐에 따라 비상 상황 시 복구에 걸리는 시간이 수초에서 수일까지 늘어날 수도 있고 아예 복구에 실패할 수도 있습니다. 레거시 시스템은 자체 진단 결과 이런 면에서 저희가 생각하는 완벽한 지점에 이르기 힘들다고 판단했으며, 이는 다른 DB로 대체하기로 결정한 주요 원인 중 하나였습니다.

세 번째 문제는 TiDB 전문 담당 조직과 DBA의 부재였습니다. 

TiDB는 전통적인 메이저 DBMS와 비교할 때 상대적으로 인력 풀이 작은 DBMS입니다. 따라서 TiDB의 전담 DBA나 전문 조직이 마련되어 있는 경우가 많지 않습니다. 이는 레거시 시스템이 개발된 당시 상황은 모르겠지만 인계받은 시점에서는 큰 부담으로 다가오는 문제였으며, 이 또한 TiDB를 대체할 다른 DB를 도입하기로 결정한 이유 중 하나였습니다. 

이와 같이 레거시 시스템의 문제는 애플리케이션과 DB 부분으로 나눌 수 있었으며, 이에 따라 해결 방안도 두 갈래로 나눌 수 있었습니다. 먼저 애플리케이션 문제를 어떻게 해결했는지 말씀드리고, 이후 TiDB에서 비롯된 문제를 해결하기 위해 대용량 MongoDB 샤딩된 클러스터를 구축하고 최적화한 과정을 설명하겠습니다.

레거시 시스템의 문제 해결하기 1: 서버와 애플리케이션 문제 해결하기

서버와 애플리케이션 문제를 해결하기 전에 앞서 소개한 문제들의 근본적인 원인을 먼저 살펴보겠습니다. 저희는 이를 통해 문제의 핵심을 이해하고 해결 방안을 도출할 수 있었습니다.

문제의 근본 원인: 파케이 파일에 대한 잘못된 접근법

수백 기가바이트에 달하는 대용량 파케이 파일 데이터는 전략적으로 접근해 적절히 다뤄야 하는데 레거시 시스템 설계는 그렇지 못했습니다. 데이터를 효율적인 방식으로 처리해야 하는데 단순히 고사양 서버를 배정하는 방식으로 문제를 해결하려 했던 점이 근본적인 원인이었습니다. 실제 레거시 시스템의 히스토리를 아는 분께 "왜 고사양 서버를 신청해서 배정하셨나요?"라고 질문하니 "파케이 파일이 워낙 큰데 이를 메모리에 적재해 처리하는 것을 전제하다 보니 메모리가 큰 고사양 서버를 투입하게 됐습니다"라는 대답이 돌아왔습니다.

왜 이런 접근 방식이 적절하지 못한 것인지 알기 위해서는 먼저 '파케이 파일'이 무엇인지 구체적으로 살펴봐야 합니다. 

파케이 파일이란 대용량 데이터를 효율적으로 저장하고 빠르게 조회할 수 있도록 설계된 칼럼 기반 파일 형식입니다. 각 칼럼을 개별적으로 저장하고 압축하기 때문에 필요한 칼럼만 선택적으로 읽을 수 있어 I/O 성능이 뛰어나다는 특성이 있습니다. 또한 데이터를 행 그룹(row group) 단위로 저장하며, 여러 프로세스가 병렬로 데이터를 읽을 수 있도록 지원하고, 쓰기 작업도 분산 환경에서 여러 파일로 나눠 저장할 수 있습니다.

이와 같은 특성을 통해 알 수 있듯이 파케이 파일의 본래 설계 목적은 병렬 처리를 최적화하는 것입니다. 이를 위해 파일을 여러 조각으로 나눠 병렬로 읽고 쓸 수 있도록 설계돼 있는 것인데요. 레거시 시스템은 이런 특성을 고려하지 않고 파케이 파일의 크기만을 고려해 서버의 메모리 사양을 높이는 방식으로 접근했습니다. 이런 접근 방식은 대용량 파케이 파일을 한 번에 메모리에 적재해 처리하려는 비효율적인 방법이었습니다.

파케이 파일을 효율적으로 처리하는 방법은 고사양 서버에 의존하는 것이 아니라 파일을 병렬로 분할해 읽고 쓰는 것입니다. 이렇게 접근해야 그 특성을 십분 활용해 효율적으로 처리할 수 있습니다. 

문제 해결의 열쇠 : 분할 정복으로 파케이 파일의 특성 활용하기

분할 정복(divide and conquer)은 큰 문제를 작고 독립적인 하위 문제로 나눈 다음 각 부분을 해결하고 결과를 결합해 전체 문제를 해결하는 고전적인 알고리즘 전략입니다. 대규모 데이터 처리, 특히 지금 저희와 같은 경우 이 개념이 매우 유용한데요. 파케이가 이런 분할 정복 개념에 자연스럽게 부합하는 저장 형식이기 때문입니다. 다음은 파케이 파일을 다루는 과정을 분할, 정복, 병합으로 나눠 살펴본 것입니다.

분할: 파케이는 데이터를 행 그룹 단위로 내부적으로 분할해서 저장합니다. 이 구조 덕분에 데이터를 물리적으로 쪼개서 저장하면서도 논리적으로는 하나의 데이터셋처럼 관리할 수 있습니다.
정복: 분할된 각 행 그룹 또는 파일은 독립적으로 처리 가능하기 때문에 여러 프로세서가 병렬로 데이터를 읽고 분석할 수 있습니다.
병합(combine): 분석 결과를 다시 합쳐 전체 결과를 구성합니다. 이는 분할 정복의 마지막 단계와 같습니다. 

이 과정은 마치 분할 정복 알고리즘에서 문제를 하위 문제를 나눠 각 하위 문제를 동시에 풀고 그 결과를 합쳐 원래 문제의 해답을 구하는 과정과 유사한데요. 파케이 파일은 필요한 칼럼만 선택적으로 읽을 수 있는 읽기 최적화와  칼럼 기반 저장, 내부 분할 구조(행 그룹)라는 특성 덕분에 분할 정복 전략에 매우 잘 어울리는 데이터 형식입니다. 즉 파케이 파일의 특성을 활용하면 '데이터는 나누고, 처리도 나누고, 성능은 높이고'가 자연스럽게 가능하며, 이번 경우처럼 데이터의 크기가 큰 경우 읽기, 쓰기, 처리 성능의 향상 효과가 극대화됩니다.

분할 정복 미들웨어로서의 Kafka

위에 설명드린 것처럼 분할 정복은 큰 문제를 작게 나누고 각 부분을 독립적으로 해결한 뒤 결과를 합쳐 전체 문제를 해결하는 전략입니다. 이 전략은 시스템 설계에도 그대로 적용할 수 있는데요. 이를 기술적으로 구현하기에 적합한 도구로 Kafka가 있습니다.

Kafka의 핵심 개념 중 하나인 토픽(topic)과 파티션(partition) 구조는 데이터를 물리적으로 분할하는 역할을 합니다. 하나의 토픽을 여러 파티션으로 나누면 각 파티션은 메시지를 독립적으로 처리할 수 있으며, 메시지를 병렬 소비(parallel consumption)하는 게 가능해집니다. 이는 곧 하나의 큰 데이터 흐름을 여러 작은 유닛으로 나눠 동시에 정복하는 구조와 같습니다.

Kafka는 다음과 같은 기능을 통해 분할 정복 전략을 유연하게 구현할 수 있습니다.

  • 재시도: 메시지 소비 중 오류가 발생하더라도 실패한 메시지를 다시 처리할 수 있는 재시도 메커니즘을 제공합니다. 따라서 실패 회복이 용이하고 시스템 전체 장애 확산을 막을 수 있습니다.
  • 반복 읽기(reprocessing): Kafka는 메시지를 디스크에 저장한 뒤 오프셋을 기준으로 다시 읽을 수 있습니다. 이를 통해 컨슈머 로직이 변경됐을 때나 신규 컨슈머를 투입할 때 등의 경우에도 기존 데이터를 소급해서 처리할 수 있습니다.
  • 동시 처리 수 조절: 파티션 수를 조정하고 각 파티션을 담당해 처리하는 컨슈머 수를 유연하게 조절할 수 있습니다. 예를 들어 처리량이 많은 파이프라인에는 컨슈머의 인스턴스를 늘려 처리 속도를 높이고, 그렇지 않은 파이프라인에는 자원 투입을 줄일 수 있습니다. 다만 파티션 수는 증가시키는 것만 가능하며, 컨슈머 인스턴스보다 파티션의 수가 적으면 파티션 수보다 많은 컨슈머 숫자만큼은 유휴 인스턴스가 됩니다.

이와 같은 특성을 이용하면 Kafka는 단순한 메시지 큐를 넘어서 복잡한 시스템을 작은 단위로 나눠 안정적으로 처리하는 분할 정복의 실질적 구현체가 됩니다. 대용량 데이터 흐름과 실시간 처리, 장애 복구 등 복합적인 문제를 분산 구조로 정리하고 해결하는 데 있어 Kafka는 가장 적합한 미들웨어 중 하나입니다.

Kafka 기반 시스템 설계와 추상화 수준 고민

Kafka를 기반으로 애플리케이션을 구성할 때 추상화 수준 관점에서 크게 세 가지 선택지가 있었습니다.

첫 번째는 Spring Kafka로, Kafka의 프로듀서와 컨슈머를 직접 코드로 구현하는 방식입니다. 메시지 처리 시나리오를 세밀하게 제어할 수 있고 내부 메커니즘에 대한 설정을 가장 상세히 구성할 수 있어 고성능 혹은 고가용성(high availability, HA)이 요구되는 상황에 적합한데요. 개발자가 직접 개발하고 구성해야 되는 부분이 많아질수록 개발과 운영에 많은 수고가 따르게 된다는 단점이 있습니다.

두 번째는 Spring Cloud Stream입니다. Spring Cloud Stream은 Spring 기반의 메시징 추상화 프레임워크로, Kafka나 RabbitMQ 같은 메시징 시스템 위에 애플리케이션 로직을 보다 간결하게 구성할 수 있도록 도와줍니다. 복잡한 바인딩 설정 없이 어노테이션 기반으로 손쉽게 스트림 처리를 구현할 수 있으며, Kafka Streams와 통합해 상태 저장이나 재시도 및 윈도 처리도 가능한데요. 내부 동작을 완전히 제어하기는 어렵다는 단점이 있습니다.

세 번째는 Spring Cloud Data Flow(SCDF)입니다. Spring Cloud Stream을 기반으로 구성된 스트림 오케스트레이션 플랫폼으로 시각적으로 파이프라인을 구성하고 운영 도구를 통해 손쉽게 관리할 수 있다는 장점이 있습니다.

개발 초기 단계에서는 SCDF를 선택했습니다. 파이프라인을 시각적으로 구성할 수 있다는 점과 운영 편의성이 눈에 띄었고, 빠르게 시작할 수 있었기 때문입니다. 하지만 장애 상황을 가정한 반복 테스트를 진행하는 과정에서 간헐적으로 처리 단계에서 프로세스가 멈춘 뒤 UI가 정상적으로 복구되지 않고 해당 작업이 중단된 채로 남는 문제가 발생했습니다. 이처럼 멈춰버린 상태는 수동으로 일일이 확인하고 해결해야 했으며, 장애 상황에서 재처리 로직이나 상태 복구 부분에서의 세밀한 제어가 어렵다는 점도 뚜렷한 한계로 드러났습니다. 도입 단계에서 파일럿 테스트를 통해 조기에 문제를 발견할 수 있어서 다행이었고, 시스템을 설계할 때에는 단순히 사양이나 기능뿐 아니라 장애 대응과 운영 환경까지 고려한 적절한 도구를 선택하는 것이 중요하다는 점을 다시 한 번 깨닫게 되었습니다.

결국 시스템의 안정성과 신뢰성을 강화하기 위해 Spring Cloud Stream을 활용해 직접 구성하는 방향으로 전환했습니다. 이 방식은 SCDF보다 초기 개발은 다소 복잡하지만 장애 대응 로직을 정교하게 다룰 수 있어 결과적으로 시스템의 안정성과 신뢰성 확보에 더 효과적이라는 판단이었습니다. 물론 개인적으로 개발 경험이 가장 많은 Spring Kafka를 선택할 수도 있었지만, 이번에는 해결해야 할 문제가 많았기에 한 단계 더 추상화된 Spring Cloud Stream을 통해 빠른 개발과 안정성 확보라는 두 가지 목표를 함께 달성하고자 했습니다.

파케이 파일 적재 스트림 파이프라인 구성하기

실제 파이프라인은 아래와 같은 모듈로 구성했습니다.

모듈 이름역할설명
API
  • 시작 신호 수신 및 조건 검증
  • 처리 상태 외부 제공
시작 신호를 수신하면 조건을 검증한 후 프로세스를 시작하며, 처리 상태를 외부에 제공합니다. 
ParquetFileProcessor
  • 파케이 파일 다운로드
  • 데이터 이상 유무 확인
  • 데이터 가공 처리
  • 파일이 이 단계에서 나뉘어 Kafka 파티셔닝이 적용되므로 분산 처리 프로세스의 시작점이 됨
파케이 파일을 분산해서 병렬로 다운로드하고 데이터의 이상 유무를 감지합니다. 병렬로 다운로드해 확인 후 가공한 파일을 노드의 스토리지가 아닌 별도의 대용량 블록 스토리지에 저장합니다. 또한 파티셔닝의 시작점이 되고 이후 추적에 필요한 정보를 추가합니다.
MongoSinker
  • 데이터 파싱 후 MongoDB에 저장
블록 스토리지에서 파케이 파일을 병렬로 읽고 이를 파싱해 MongoDB에 저장합니다.
ProcessingStatusManager
  • 각 단계의 처리 상황 수집
  • 이상 유무 및 정합성 확인
전체 파이프라인의 진행 상태를 수집하고 이상 유무와 정합성을 확인합니다.
Support
  • 부가 작업 처리
  • 지표 수집 및 제공
블록 스토리지에 임시로 저장된 파일을 관리하며, 각종 지표를 수집해 제공합니다.

Spring Cloud Stream 관점에서 이 구성의 장점은 아래와 같습니다.

  1. 모듈 간 느슨한 결합
    • 각 모듈이 Kafka를 통해 메시지를 주고받는 느슨한 결합 형태이기 때문에 각 모듈은 독립적으로 개발, 배포, 확장이 가능합니다.
  2. 쉽고 빠른 스케일아웃
    • 각 모듈은 별도 인스턴스로 병렬 실행이 가능합니다.
    • 확장이 필요할 경우 모듈의 현재 상태를 고려해 부분적으로 확장을 시작하는 것이 가능하기 때문에 무중단 서비스가 전제되는 상황에서도 부담이 적습니다.
  3. 인프라 비용 절감 효과
    • 어떤 모듈은 CPU를 많이 사용하고 어떤 모듈은 메모리를 많이 사용하는데요. 쿠버네티스 환경을 사용하고 있으므로 각 모듈의 성능 요구치를 감안해 기능별로 나뉜 애플리케이션을 적절히 혼합해서 배치하면 전체 노드의 효율을 높일 수 있습니다.
    • 확장이나 축소 시 각 모듈의 성능을 고려해서 실행할 수 있으므로 낭비가 없습니다.
  4. 장애 격리 및 복원력
    • 커밋 시점을 잘 조정하면 장애 발생 시에도 데이터 손실 없이 재처리가 가능합니다.
    • 파티션 기반으로 분할한 데이터를 단계별로 격리해 처리하기 때문에 부분적인 장애는 전파되지 않고, 최악의 경우에도 전체 데이터를 못 쓰게 되는 상황은 발생하지 않습니다.
  5. 모니터링과 추적 용이
    • 스트림의 각 지점에서 쉽고 편하게 모니터링하고 로그를 추적할 수 있습니다. 전체를 뭉뚱그려서 구성했을 때보다 기능별로 구성했을 때 원인 파악이 쉬운 것도 당연합니다. 

 아래는 파케이 파일 적재 스트림 파이프라인의 모듈 구성을 간략히 나타낸 것입니다.

파케이 파일 적재 스트림 파이프라인의 모듈 구성

더 작고 평범한 VM 위에 효율적으로 파드 배치하기

우선 VM을 CPU와 메모리 비율이 일반적인 낮은 사양의 VM으로 전량 구성한 뒤 각 VM에 여러 기능별 파드가 낭비 없이 배치될 수 있도록 혼합 배치했습니다.

ParquetFileProcessor의 리소스 설정은 아래와 같습니다.

resources:
  limits:
    cpu: 3500m
    memory: 7400Mi
  requests:
    cpu: 3500m
    memory: 7400Mi

MongoSinker의 리소스 설정은 아래와 같습니다.

resources:
  limits:
    cpu: 1650m
    memory: 3600Mi
  requests:
    cpu: 1650m
    memory: 3600Mi

위와 같이 requestslimits가 동일한 전략은 파케이 파일 처리처럼 워크로드의 I/O와 CPU 사용량이 높을 때 이를 고려해 리소스를 안정적으로 격리하고 예측 가능한 성능을 확보하기 위해 사용합니다. 파케이 파일 처리는 칼럼 기반 압축 해제, 디코딩, 필터링 등의 과정에서 CPU를 집중적으로 사용하며, 특히 분산 처리 시 각 컨테이너에 일정 수준 이상의 리소스가 보장돼야 처리 효율이 극대화됩니다. 이 전략을 적용하면 쿠버네티스에서 해당 파드에 Guaranteed QoS가 부여돼 리소스 경쟁 상황에서도 안정적으로 운영되며, 스케줄러가 정확하게 자원을 할당할 수 있어 파케이 파일 처리 중 성능 저하나 리소스 스로틀링 없이 일관된 처리 성능을 유지할 수 있습니다.

각 스트림 처리 단계에 재시도 로직 넣기

Spring Cloud Stream에서는 메시지 처리 중 예외가 발생하면 재시도를 수행하고, 재시도 후에도 실패하면 DLQ(dead letter queue)로 보내도록 설정할 수 있습니다. 재시도 관련 설정은 다음과 같습니다. 

  • maxAttempts: 최대 시도 횟수
  • backOffInitialInterval: 최초 재시도 대기 시간(ms)
  • backOffMultiplier: 재시도 간 대기 시간 증가 배율
  • backOffMaxInterval: 최대 대기 시간
  • defaultRetryable: 기본 예외에 대한 재시도 여부

아래 설정은 메시지 처리 중 오류가 발생했을 때 최대 10회까지 재시도하도록 구성한 것입니다. 첫 번째 재시도는 1.5초 뒤에 진행되며, 이후 매번 1.8배씩 대기 시간이 증가하지만,  최대 12초까지만 증가하도록 제한해 과도한 지연이 발생하지 않도록 설정했습니다. 

    stream:
      bindings:
        downloadParquet-in-0:
          consumer:
            max-attempts: 10
            back-off-initial-interval: 1500   # 시작 대기 시간 1.5초
            back-off-multiplier: 1.8          # 1.8배씩 증가
            back-off-max-interval: 12000      # 최대 대기 시간 12초

실제 재시도 간격을 나열해 보면 [1.5초, 2.7초, 4.86초, 8.75초, 12초, ... 이후 계속 12초]입니다. 이와 같은 점진적 백오프(backoff) 전략은 시스템에 불필요한 부하를 주지 않으면서 장애 발생 시 안정적으로 메시지를 복구하는 데 목적이 있습니다. 재시도 로직을 설계할 때 특히 중요한 점은 단순히 예외 상황에서 재시도가 정확하게 작동하는 것뿐 아니라 여러 번 재처리하더라도 최종 데이터가 중복 생성되거나 오염되지 않도록 하는 것입니다. 이를 보장하기 위해 멱등성(idempotency)을 철저히 준수하는 것이 핵심입니다.

레거시 시스템의 문제 해결하기 2: 대규모 MongoDB 샤드 클러스터 구축하기

지금까지 레거시 시스템의 서버와 애플리케이션 측면에서의 문제가 무엇이었고 이를 어떻게 해결했는지 말씀드렸습니다. 이제 TiDB를 대체한 대규모 MongoDB 샤딩된 클러스터를 구축하고 애플리케이션에 적용할 때 고려했던 부분을 소개하겠습니다(참고로 이 글은 MongoDB 6.0.14 버전을 기준으로 작성했습니다).  

MongoDB 아키텍처 선택하기

MongoDB 도입 시 가장 먼저 고민해야 하는 부분은 MongoDB의 아키텍처를 선택하는 것입니다. 선택지로는 MongoDB 복제본 세트(replica set)와 샤딩된 클러스터(sharded cluster), 두 가지가 있습니다. 각 구성은 목적과 특성이 다르기 때문에 서비스의 규모와 데이터 처리 특성에 따라 신중하게 선택해야 합니다. 두 종류를 모두 이용해 본 사용자 입장에서 말씀드리자면, 이 둘은 완전히 다른 DB가 아닐까 생각될 정도로 아키텍처가 다르고, 그에 따라 특징도 다릅니다. 하나씩 간략히 살펴보겠습니다.

먼저 복제본 세트는 MongoDB의 고가용성과 데이터 무결성을 보장하기 위한 기본 구성입니다. 하나의 프라이머리 노드와 두 개 이상의 세컨더리 노드로 구성되며, 프라이머리에 장애가 발생할 경우 세컨더리 중 하나가 자동으로 프라이머리로 승격됩니다. 모든 노드에 동일한 데이터가 복제되기 때문에 데이터 일관성이 보장되며, 비교적 구조가 단순해 운영하기 수월한 편입니다. 다만 모든 쓰기 연산이 프라이머리 노드에 집중되기 때문에 대규모 트래픽을 처리하는 데에는 한계가 있을 수 있습니다.

다음으로 샤딩된 클러스터는 대규모 데이터를 분산 저장하고 고성능으로 처리할 수 있는 구조입니다. 데이터는 샤드 키(shard key)를 기준으로 여러 샤드에 분산 저장되며 각 샤드는 독립적인 복제본 세트로 구성됩니다. 이를 통해 읽기와 쓰기 트래픽이 여러 노드에 걸쳐 분산되며, 수평 확장이 가능해 대량의 데이터를 빠르게 처리할 수 있습니다. 다만 초기 설계 단계에서 샤드 키를 신중하게 선택해야 하며, 운영 복잡도 또한 복제본 세트에 비해 훨씬 높습니다. 대규모 시스템에 적합하고 특히 데이터 확장이 필수인 시스템에서는 유일한 선택지가 됩니다.

다음은 MongoDB 복제본 세트와 샤딩된 클러스터를 비교한 표입니다.

항목복제본 세트샤딩된 클러스터
주요 목적데이터 무결성과 안정성을 위한 복제 구성수평 확장과 대규모 데이터 처리를 위한 구조
데이터 저장 방식모든 노드에 동일한 데이터 복제샤드 키 기준으로 데이터를 분산 저장
고가용성제공(프라이머리 장애 시 자동 전환)제공(샤드/설정 서버 각각 복제본 세트 구성)
읽기 부하 분산세컨더리 사용 가능복수 샤드의 세컨더리 사용 가능
쓰기 부하 분산프라이머리 노드에 집중됨샤드 키를 기준으로 샤드 간 쓰기 부하 분산 가능
데이터 분산없음(모든 데이터 복제)샤드 키를 기준으로 각 샤드에 데이터 분할
확장성수직 확장(스케일 업) 중심수평 확장(스케일 아웃) 지원
운영 복잡도낮음(구조 단순)상대적으로 높음(복잡한 구성 관리 필요)
장애 복구간단하고 빠름상대적으로 복잡하지만 장애 범위를 국소화할 수 있다는 장점이 있음
비용상대적으로 적음상대적 구조 및 운용 복잡성으로 수반되는 비용이 큼
적합 사례중소 규모, 읽기 부하 대비 안정성이 중요한 시스템대규모 데이터 처리 및 고속 쓰기/읽기가 필요한 시스템에 적합하며, 특정 규모 이상의 시스템에서는 유일한 선택지

저희는 이번 프로젝트에서 다음과 같은 이유로 샤딩된 클러스터를 선택했습니다.

  • 복제본 세트로는 프로젝트에서 다뤄야 할 크기의 데이터를 요구되는 성능 수준으로 처리할 수 없었습니다.
  • 전문적인 MongoDB 팀에서 구축과 운용을 모두 담당해 주기 때문에 구축과 운영 측면의 어려움과 복잡도에 대한 부담이 없었습니다.
  • AI용 데이터는 수시로 확장이 필요합니다. 따라서 비용이 많이 들고 한계가 있는 수직 확장보다는 상대적으로 저렴한 비용으로 확장할 수 있는 수평 확장을 지원하는 샤딩된 클러스터가 적합했습니다. 수직 확장의 경우 어느 지점 이상에서는 아무리 비용을 들여도 더 좋은 CPU와 메모리를 얻을 수 없기 때문에 한계가 명확합니다.
  • HA와 장애 복구는 양쪽 모두 가능합니다. 

MongoDB 샤딩된 클러스터를 선택했다면 샤드 키 선정, 초기 청크 개수 설정, 대용량 데이터 삭제 전략, 부하 분산 전략 수립, 애플리케이션 클라이언트 설정, Oplog file 최적화 등 도입할 때 검토해야 할 것들이 있습니다. 하나씩 살펴보겠습니다.

샤드 키 선택하기

샤딩된 클러스터의 성능과 안정성은 대부분 샤드 키 선택에 달려 있습니다. 한 번 설정한 샤드 키는 쉽게 변경하기 어렵습니다. MongoDB 5.0 이전 버전에서는 아예 온라인 중 변경이 불가능했고, 5.0 버전부터는 ReshardCollection 명령으로 온라인 중 변경할 수 있게 됐지만 여전히 신중한 설계가 필수입니다.

샤드 키를 선택할 때 고려해야 할 요소들은 아래 표와 같습니다.

항목설명
분산도(cardinality)데이터가 고르게 분산될 수 있는 키를 선택합니다.
불변성샤드 키 변경은 매우 어렵기 때문에 자주 수정되지 않는 필드를 선택해야 합니다.
쿼리 활용도쿼리 조건으로 자주 사용하는 필드를 선택하면 성능이 향상됩니다.
복합 키 여부샤드 키로 선택할 만한 단일 필드가 없을 때에는 두 개 이상의 필드를 조합해 복합 키를 사용할 수 있습니다.
청크 이동샤드 키가 자주 변경되면 청크 이동(chunk migration)이 많이 발생할 수 있고, 이는 시스템 부하와 성능 저하를 유발할 수 있습니다.

샤딩된 컬렉션의 초기 청크 개수 정하기

MongoDB 샤딩된 클러스터에서 데이터는 청크(chunk) 단위로 나뉘어 저장됩니다. 각 청크는 일정 범위의 데이터를 포함하며, 데이터를 고르게 분배하기 위해 청크 개수 혹은 청크 데이터 크기를 기준으로 분할되는데요. 만약  대량 데이터 적재 시 생성돼 있는 청크가 없다면, DML 쿼리 비용에 청크 분할 및 청크 이동 비용이 추가됩니다. 따라서 초기 데이터를 예상할 수 있다면 numInitialChunks 설정으로 초기 청크 개수를 정의해 청크 분할과 청크 이동 비용을 줄일 수 있습니다.

이번 프로젝트는 특히 적재되는 컬렉션들의 크기 차이가 매우 컸기 때문에 초기 청크 개수가 중요했고, 컬렉션을 수시로 생성하거나 삭제할 필요가 있었습니다. 이에 따라 개발 팀과 DBA 팀이 여러 번 논의를 반복하며 컬렉션 생성 시 데이터 유형별로 초기 청크 개수를 결정하는 기능을 개발해서 탑재했습니다. 먼저 데이터 적재 테스트를 진행한 뒤 개발 팀과 DBA 팀이 모여 증가한 청크 수를 기반으로 적절한 청크 수에 대해 여러 번 논의를 거쳤고, 이후 제가 데이터 유형별 초기 청크 수를 정하는 계산식을 만들어 개발 팀에 전달했고 이 계산식이 초기 청크 개수를 지정하는 개발 로직에 적용됐습니다. 

다음은 저희가 개발해 탑재한 기능을 실행하는 명령어입니다.

  • numInitialChunks를 지정하는 MongoDB 명령어(데이터가 없는 상태에서만 실행 가능)
sh.shardCollection( "{DATABASE-NAME}.{COLLECTIONNAME}", { _id: "hashed" } , false, { numInitialChunks: 10000  })  
  • 애플리케이션 서버에서 데이터 유형별 numInitialChunks 개수를 지정하는 설정 
mongo-sink:
  default-initial-chunks: 828
  units:
    - unit-name-prefix: type1_countryA_subtype1
      initial-chunks: 120
    - unit-name-prefix: type1_countryA_subtype2
      initial-chunks: 12
    - unit-name-prefix: type1_countryB_subtype3
      initial-chunks: 156
    - unit-name-prefix: type2_countryC_subtype1
      initial-chunks: 3540
    - unit-name-prefix: type2_countryC_subtype2
      initial-chunks: 798

대용량 데이터 삭제 전략: TTL Index vs Drop Collection

이번 프로젝트에서 MongoDB 데이터를 삭제하는 방식으로 TTL Index(time to live index)와 Drop Collection 중에서 고민했습니다. 두 방법 모두 데이터를 실시간 또는 주기적으로 삭제할 수 있지만 특징과 적합한 사용처가 다릅니다. 다음은 두 방식의 특징을 비교 정리한 표입니다. 

구분TTL IndexDrop Collection
설명
  • 컬렉션의 도큐먼트가 언제까지 유효한지 판단해 더 이상 유효하지 않은 도큐먼트는 자동으로 삭제
  • 검색 성능 향상을 위한 인덱스가 아닌 TTL 모니터 스레드가 삭제할 도큐먼트를 찾기 위한 인덱스
  • TTL Index 조건에 부합할 경우 내부에서 DELETE 쿼리 실행
  • 컬렉션 스키마 및 데이터 전체를 삭제
  • 실행 시 컬렉션 전체에 베타락(exclusive lock)을 걸며, 락이 해제될 때까지 읽기 및 쓰기를 포함한 모든 작업은 대기
장점
  • 만료된 데이터를 자동으로 삭제 
  • 매우 큰 데이터도 빠르게 삭제
단점
  • 대량 삭제 시 서버 성능 문제 발생
  • 일부 데이터만 삭제하는 것은 불가

이번 프로젝트에서는 매일 테라바이트 단위의 데이터를 쓰고 삭제해야 했는데요. TTL Index는 수행 시간이 데이터 크기와 비례하기 때문에 TTL Index를 사용할 경우 삭제 작업이 오랜 시간 지속되면서 MongoDB 클러스터 전체 성능에 악영향을 줄 수 있습니다. 반면 Drop Collection은 수행 속도는 매우 빠르지만 실행이 완료될 때까지는 베타락(exclusive lock)이 걸리기 때문에 다른 명령어가 대기해야 한다는 치명적인 단점이 있습니다.

원래 Drop Collection 전에 해당 컬렉션 참조 여부를 확인하는 작업은 매우 신중히 진행해야 하는 작업인데요. 이번 프로젝트는 대부분의 서비스와는 달리 매일 컬렉션을 새로 생성해 사용했기 때문에 참조 여부를 확인하기가 매우 용이했습니다. 이에 따라 삭제 대상 컬렉션이 다른 연결(connection)에서 참조하지 않는 컬렉션이라는 것을 쉽게 확인할 수 있었고, 그 덕분에 Drop Collection은 대기하는 작업 없이 서버에 부하를 주지 않고 빠르게 수행할 수 있는 방법이 될 수 있었고, 이런 이유로 이번 프로젝트에서는 Drop Collection을 선택했습니다.

부하 분산 전략 수립

기존에 사용하던 TiDB 환경에서는 읽기와 쓰기 부하를 효과적으로 분산하는 데 한계가 있었습니다. 특히 쓰기 트래픽이 집중되거나 대규모 읽기 요청이 동시에 발생할 때 성능 저하가 빈번하게 발생했습니다.

이 문제를 해결하기 위해 MongoDB 샤딩된 클러스터 도입 시 읽기와 쓰기 부하를 모두 수평적으로 분산하는 구조를 구축했습니다. MongoDB 샤딩된 클러스터에서는 데이터를 여러 샤드로 나눠 저장하고 각 샤드를 복제본 세트로 구성해 고가용성과 읽기 부하 분산을 함께 달성할 수 있습니다.

부하 분산 관점에서 특히 중요하게 고려한 부분은 다음과 같습니다.

  • 읽기 부하 분산 전략(세컨더리 서버 활용)
    • 각 샤드의 세컨더리 노드는 읽기 전용으로 활용해 읽기 트래픽을 세컨더리로 분산할 수 있도록 설정했습니다. 특히 세컨더리 서버를 하나 더 추가해 두 개의 세컨더리 서버로 대규모 읽기 부하를 효과적으로 분산했으며, 이는 MongoDB의 Read Preference: secondaryPreferred 설정을 통해 가능했습니다.
  • 쓰기 부하 분산 전략(샤드 키 설계)
    • 데이터가 특정 샤드에 몰리지 않도록 균등하게 분포될 수 있는 샤드 키를 신중하게 선정해서 샤드 간 트래픽과 저장 용량이 고르게 나뉘도록 했습니다. MongoDB 샤딩된 클러스터는 컬렉션별로 독립적인 샤드 키를 설정할 수 있습니다.

이와 같은 구조를 통해 단순히 수평 확장이 가능해진 것뿐 아니라 쓰기와 읽기 트래픽 모두를 효율적으로 분산할 수 있게 되었습니다. 결과적으로 TiDB 환경에서 겪었던 트래픽 병목 문제를 근본적으로 해결할 수 있었으며, 데이터량과 트래픽이 급격히 증가하는 상황에서도 안정적인 서비스를 유지할 수 있었습니다.

성능 향상과 부하 분산을 위해 고려해야 하는 클라이언트 설정

MongoDB를 안정적으로 운영하기 위해서는 읽기/쓰기 관련 설정을 세심하게 조정할 필요가 있습니다. 특히 데이터 규모가 크거나 트래픽이 많은 환경이라면 클라이언트 설정만으로도 체감 성능이 크게 달라질 수 있습니다. MongoDB의 읽기/쓰기 관련 설정 항목과 각 항목의 대표적인 설정값을 간략히 살펴보겠습니다. 

  • 읽기 고려(Read Concern): 데이터를 읽을 때 전체 노드에서 어느 정도의 일관성을 확보한 데이터를 읽을지 설정합니다. 다음은 readConcern 항목의 설정값과 설명입니다.
설정값설명
available가장 낮은 일관성 수준으로 복제본의 동기화 여부와 관계없이 데이터를 반환합니다. 지연 시간이 가장 짧지만 데이터가 롤백될 수 있으며, 샤딩 환경에서는 고아 도큐먼트가 반환될 수 있습니다.
local가장 빠른 읽기 성능을 제공하며, 프라이머리 노드의 메모리에만 존재하는 데이터를 바로 읽기 때문에 지연이 거의 없지만, 복제본에 따라 아직 읽은 데이터가 전파되지 않은 복제본이 존재할 가능성이 있습니다.
majority복제본 과반수로부터 승인된 데이터만 읽습니다. 데이터의 일관성은 높지만 그만큼 읽기 지연이 따를 수 있습니다.
linearizable가장 높은 일관성 수준으로 읽기 시점에 프라이머리 노드가 여전히 프라이머리인지 확인하고 해당 시점의 최신 데이터를 반환합니다.
snapshot특정 시점에 샤드 전체의 과반수에 커밋된 데이터를 반환합니다. 주로 다중 문서 트랜잭션에서 사용되며 트랜잭션의 writeConcernmajority로 커밋되는 경우에만 보장합니다.
  • 쓰기 고려(Write Concern): 데이터를 쓸 때 어떤 기준으로 성공을 판단할지 설정합니다. w, j, wtimeout 등의 항목이 있으며, 아래는 w 항목의 대표적인 설정값에 대한 설명입니다.
설정값설명
1프라이머리 노드에만 쓰기 성공이 확인되면 완료로 간주합니다. 속도는 빠르지만 프라이머리 노드에 장애가 발생할 경우 데이터 손실이 발생할 가능성도 있습니다.
majority절반 이상의 다수 노드에 쓰기가 완료돼야 성공으로 인정합니다. 안정성은 높아지지만 쓰기 성능은 1 옵션에 비해 저하될 수 있습니다.
설정값설명
primary가장 낮은 일관성 수준으로 복제본의 동기화 여부와 관계없이 데이터를 반환합니다. 지연 시간이 가장 짧지만 데이터가 롤백될 수 있으며, 샤딩 환경에서는 고아 도큐먼트가 반환될 수 있습니다.
secondary가장 빠른 읽기 성능을 제공하며, 프라이머리 노드의 메모리에만 존재하는 데이터를 바로 읽기 때문에 지연이 거의 없지만, 복제본에 따라 아직 읽은 데이터가 전파되지 않은 복제본이 존재할 가능성이 있습니다.
primaryPreferred복제본 과반수로부터 승인된 데이터만 읽습니다. 데이터의 일관성은 높지만 그만큼 읽기 지연이 따를 수 있습니다.
secondaryPreferred가장 높은 일관성 수준으로 읽기 시점에 프라이머리 노드가 여전히 프라이머리인지 확인하고 해당 시점의 최신 데이터를 반환합니다.
nearest특정 시점에 샤드 전체의 과반수에 커밋된 데이터를 반환합니다. 주로 다중 문서 트랜잭션에서 사용되며 트랜잭션의 writeConcernmajority로 커밋되는 경우에만 보장합니다.

아래는 샤딩된 클러스터의 3-노드(복제본 세트를 의미하며, 저희의 경우 프라이머리, 세컨더리, 세컨더리로 구성)에서 쓰기 고려 설정을 각각 w:1w:majority로 설정한 뒤 YCSB 벤치마크 툴로 테스트한 결과입니다. w:majority로 설정하면 w:1로 설정했을 때보다 QPS가 약 46%에서 48%까지 감소하는 것으로 나타났습니다. 이와 같이 성능에 큰 영향을 줄 수 있으므로 선택할 때 여러 자료를 참고해 신중히 설정해야 합니다. 

쓰기 고려 설정w:1w:majority
MongoDB 버전5.06.05.06.0
QPS(query per second)6.8k7.6k3.7k4.0k

아래는 성능을 중시한 읽기/쓰기 부하 분산을 설정한 예제입니다. 만약 데이터 정합성이 동기 DB 수준으로 필요한 것이 아니라면 아래와 같이 설정하는 것을 추천합니다. 

readConcern: local
writeConcern w: 1
readPreference: secondaryPreferred

대규모 MongoDB 샤딩된 클러스터에서 oplog 최적화의 중요성

MongoDB를 대규모로 운영하다 보면 '백업'이 단순한 보조 작업이 아니라 서비스의 생존을 좌우하는 핵심 작업이라는 것을 체감할 수 있습니다. 특히 샤딩된 클러스터 환경에서는 그 중요성이 훨씬 더 커지는데요. 백업과 관련해서 왜 oplog 최적화가 필요한지, oplog 최적화가 장애 복구 시간에 어떤 영향을 미치는지 살펴보겠습니다.

먼저 oplog(operations log, 작업 로그) 크기가 커지면 문제가 시작됩니다. MongoDB는 백업 시 특정 시점 이후의 변경 사항까지 복구할 수 있도록 oplog를 함께 활용하며, 샤딩된 클러스터에서는 각 샤드마다 oplog가 존재하는데요. 데이터의 규모가 크다 보니 oplog의 크기가 급격히 커질 수 있습니다. oplog 크기가 커지면 백업 파일 크기도 함께 증가하기 때문에 스토리지 사용량이 늘어납니다. 이때 앞서 살펴봤던 대규모 데이터 삭제 전략으로 TTL Index를 선택했다면 전체 성능이 저하될 수 있습니다. 

oplog의 크기가 커지면 복구 시간 또한 길어집니다. 백업은 결국 복구를 위한 것입니다. 그런데 백업 파일이 크고 oplog를 반영해야 할 쿼리가 많으면 복구 시간은 기하급수적으로 늘어납니다. 장애 발생 시 복구가 지연되면 서비스 다운타임이 길어지며, 만약 oplog 보관 기간을 24시간 정도로 밖에 설정하지 않았다면 최악의 경우 복구에 실패할 수도 있습니다. 따라서 oplog를 관리하는 것은 매우 중요합니다. 

DBA가 느낀 MongoDB

저는 MySQL DBA를 거쳐 현재는 MongoDB DBA로 일하고 있습니다. 오픈소스 기반의 대표적인 RDBMS(MySQL)와 NoSQL(MongoDB)을 수년간 운영한 경험이 있는데요. 마지막으로 DBA로서 느낀  MongoDB를 MySQL과 비교해 말씀드리겠습니다.

검증된 안정성의 MySQL, 빠르게 진화하는 후발 주자 MongoDB

먼저 MySQL의 가장 큰 장점 중 하나는 검증돼 있는 매우 견고한 안정성입니다. MySQL을 운영하면서 가장 인상적이었던 점은 CPU 사용률이 100%에 달하는 극한의 부하 상황에서도 인스턴스가 쉽게 다운되지 않는다는 것이었습니다. 이는 오랫동안 검증된 엔진 구조 덕분이라고 생각합니다.

MongoDB는 후발 주자로 처음 등장했을 때(특히 3.x 이하 버전)만 해도 안정성이 다소 부족했습니다. 하지만 MySQL이 InnoDB를 도입하며 다양한 기능을 지원하고 안정화된 것처럼 MongoDB도 WiredTiger 엔진 도입 이후로 기능이 많이 확장되며 안정화됐습니다.

또한 MongoDB는 버전 업그레이드 속도도 매우 빠릅니다. MySQL은 약 4개월마다 마이너 버전이 출시되는 반면 MongoDB는 약 2개월마다 마이너 버전이 출시되고 있습니다. 물론 이처럼 빠른 성장은 MongoDB의 강점이기도 하지만 운영하는 입장에서는 EOL(end of life) 주기가 짧다는 점이 부담스럽기도 합니다. MySQL에 비해 새로운 기능이 빠르게 추가되지만 그만큼 버전 관리 및 업그레이드에 신경을 많이 써야 하기 때문입니다.

MongoDB, 이제는 믿고 추천할 수 있는 DB

운영하는 입장에서 버전 관리 및 업그레이드에 신경을 써야 한다는 단점에도 불구하고 DBA로서 MongoDB를 추천할 수 있는 이유는 MongoDB의 대표적인 장점인 네이티브 HA와 샤딩 기능 덕분입니다. 

  • 네이티브 고가용성 지원: 외부 고가용성 솔루션을 추가로 구성할 필요 없이 기본으로 제공하는 복제본 세트를 통해 고가용성을 보장합니다.
  • 샤딩 기능: MongoDB는 자체적으로 제공하는 샤딩된 클러스터를 통해 수평 확장이 용이합니다.

과거에는 MongoDB가 다소 불안정하다는 이미지가 있었지만 이젠 특정 개발 요구 사항이 MongoDB의 장점으로 잘 해결될 수 있다고 판단한다면 자신 있게 추천할 수 있는 DB입니다. 특히 고가용성과 수평 확장이 중요한 환경이라면 MongoDB를 긍정적으로 검토해 볼 가치가 충분합니다. 관심 있는 분들은 직접 한 번 테스트해 보면서 본인의 프로젝트에 적합한지 고민해 보셔도 좋을 것 같습니다.

프로젝트 도입 효과

이와 같이 AI 피처 스토어를 MongoDB와 Spring Cloud Stream으로 새롭게 구축한 결과 여러 가지 면에서 개선된 효과를 느낄 수 있었는데요. 크게 세 가지로 나눠 살펴보겠습니다.

안정성과 성능 향상

먼저 시스템의 안정성과 성능이 크게 향상됐습니다. 새로운 시스템은 부하량에 관계없이 레거시 시스템과 비교할 수 없는 수준의 일정하고 안정된 응답 속도를 보여주고 있습니다. 새로 도입된 MongoDB는 여유 있게 요청을 처리하면서 TiDB에서 느끼던 불안감을 완전히 해소했습니다. 또한 더욱 탄력적이고 효율적으로 시스템을 확장할 수 있게 됐습니다.

인프라 비용 절감

이번 프로젝트를 시작하면서 인프라 팀과 별도 협의가 필요했습니다. 그 이유는 인프라 팀에서는 레거시에 사용된 서버와 같은 고사양 서버 중 활용률이 낮은 서버를 특별히 별도로 집계해 관리하고 있었기 때문인데요. 신규 서버 요청이 들어오자 자원 관리 방안과 요청 배경을 설명해 달라고 요청을 받았습니다. 이에 저는 이번 프로젝트를 설명하면서 레거시 시스템의 고사양 서버들을 모두 반납할 것을 약속하고 새로운 서버를 할당받았고, 프로젝트가 마무리된 후 고성능 애플리케이션과 새로 도입된 MongoDB 덕분에 기존 서버를 모두 반납하며 인프라 비용을 절감할 수 있었습니다.

운영 피로 경감

시스템 운영 중 일부 외부 시스템이나 인프라의 영향으로 작은 에러가 발생한 경우도 있었지만 알람을 받았을 때 이미 재시도가 작동돼 순식간에 복구됐습니다. 서비스에는 전혀 영향이 없었고, 상세한 이력과 내력을 파악할 수 있는 추적 시스템과 편리한 운영 도구를 이용해 사후 점검만 진행하면 돼 운영 피로가 현저히 줄어들었습니다. 덕분에 새로운 기능 추가에 더 집중할 수 있었습니다.

마치며

이번 글에서 소개한 내용이 레거시 시스템 개선과 MongoDB에 관심이 있는 독자분들께 도움이 되기를 바랍니다. 짧은 시간 안에 프로젝트를 성공적으로 마칠 수 있도록 도와주신 모든 분들께 감사드리며 이만 마치겠습니다.