이번 글에서는 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는 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 |
| 파케이 파일을 분산해서 병렬로 다운로드하고 데이터의 이상 유무를 감지합니다. 병렬로 다운로드해 확인 후 가공한 파일을 노드의 스토리지가 아닌 별도의 대용량 블록 스토리지에 저장합니다. 또한 파티셔닝의 시작점이 되고 이후 추적에 필요한 정보를 추가합니다. |
MongoSinker |
| 블록 스토리지에서 파케이 파일을 병렬로 읽고 이를 파싱해 MongoDB에 저장합니다. |
ProcessingStatusManager |
| 전체 파이프라인의 진행 상태를 수집하고 이상 유무와 정합성을 확인합니다. |
Support |
| 블록 스토리지에 임시로 저장된 파일을 관리하며, 각종 지표를 수집해 제공합니다. |
Spring Cloud Stream 관점에서 이 구성의 장점은 아래와 같습니다.
- 모듈 간 느슨한 결합
- 각 모듈이 Kafka를 통해 메시지를 주고받는 느슨한 결합 형태이기 때문에 각 모듈은 독립적으로 개발, 배포, 확장이 가능합니다.
- 쉽고 빠른 스케일아웃
- 각 모듈은 별도 인스턴스로 병렬 실행이 가능합니다.
- 확장이 필요할 경우 모듈의 현재 상태를 고려해 부분적으로 확장을 시작하는 것이 가능하기 때문에 무중단 서비스가 전제되는 상황에서도 부담이 적습니다.
- 인프라 비용 절감 효과
- 어떤 모듈은 CPU를 많이 사용하고 어떤 모듈은 메모리를 많이 사용하는데요. 쿠버네티스 환경을 사용하고 있으므로 각 모듈의 성능 요구치를 감안해 기능별로 나뉜 애플리케이션을 적절히 혼합해서 배치하면 전체 노드의 효율을 높일 수 있습니다.
- 확장이나 축소 시 각 모듈의 성능을 고려해서 실행할 수 있으므로 낭비가 없습니다.
- 장애 격리 및 복원력
- 커밋 시점을 잘 조정하면 장애 발생 시에도 데이터 손실 없이 재처리가 가능합니다.
- 파티션 기반으로 분할한 데이터를 단계별로 격리해 처리하기 때문에 부분적인 장애는 전파되지 않고, 최악의 경우에도 전체 데이터를 못 쓰게 되는 상황은 발생하지 않습니다.
- 모니터링과 추적 용이
- 스트림의 각 지점에서 쉽고 편하게 모니터링하고 로그를 추적할 수 있습니다. 전체를 뭉뚱그려서 구성했을 때보다 기능별로 구성했을 때 원인 파악이 쉬운 것도 당연합니다.
아래는 파케이 파일 적재 스트림 파이프라인의 모듈 구성을 간략히 나타낸 것입니다.