기술 스택: Typescript, NodeJS 20, NestJS, MySQL, Redis, Kafka
인원: 4인 (본인 + 내부서비스 담당 + 인프라 담당 + 팀원)
성과: 25년 3월 2주부터 매주 최대 발송량 갱신 중 (2025.04.09 기준)
상세 설명
프로젝트 설명
커스텀 템플릿 기능 개발 후 해당 템플릿을 활용해 알림톡을 발송할 수 있는 발송부 개발이 필요했습니다. 이에 따라 빠르게 요구사항을 분석 및 협의하고, 필요한 코드 부분을 파악한 뒤, 개발 및 배포하였습니다.
아키텍처 설명
알림톡은 발송 대상을 추출하고, 메시지를 발송한 뒤, 결과를 수신하는 흐름으로 동작합니다.
아키텍처 역시 이러한 발송 프로세스에 맞춰 설계했습니다.
설계 시에는 특히 두 가지를 고려했습니다.
첫째, 대상 생성에 시간이 오래 걸릴 수 있다는 점,
둘째, 발송 시간은 회원마다 달라질 수 있다는 점입니다.
이러한 특성을 반영하여, 메시지 큐를 활용해 각 과정을 분리하고,
프로세스 간 상태를 기반으로 한 Transaction Outbox 패턴을 적용하여 전체 플로우를 일관성 있게 관리할 수 있도록 구성했습니다.
문제
100만 단위 생성을 할 수 있는 구조 필요
초기 요구사항은 기존 친구톡 발송 기능과 동일하게 10만 건 단위 발송이었습니다. 하지만 알림톡의 특성상 전체 회원에게 발송할 수 있다는 점을 고려해, 최종적으로 100만 건 단위 발송으로 요구사항을 협의했습니다.
이에 따라 설계 단계에서 대량의 데이터를 어떻게 효율적으로 처리할지 고민이 필요했습니다. 동기적으로 데이터를 메모리에 모두 올리는 방식은, 대량 처리 시 메모리 부족이 명확하게 예상되었기 때문입니다.
저는 이 문제를 데이터를 분할하여 처리하는 전략으로 접근했습니다. 방법으로는 chunking 과 streaming 을 검토했으며, 최종적으로는 chunking 방식을 선택했습니다. streaming 은 처리 과정에서 DB connection 을 지속적으로 점유하는 특성 때문에, 다른 작업과 병행할 경우 connection 부족 현상이 발생할 위험이 있었기 때문입니다.
결과적으로, chunking 방식을 통해 메모리와 connection 사용을 효율적으로 관리하면서 대량의 데이터를 안정적으로 처리할 수 있었습니다.
발송대상은 유일해야 함.
이 이슈는 대상 생성 과정에서 발생한 문제였습니다.
알림톡 발송 대상은 전화번호 기준으로 유일해야 하며, 동일한 번호로는 중복 발송이 되지 않도록 처리해야 합니다. 이를 위해서는 회원 정보를 추출하는 과정과 이를 처리하는 과정이 일관되게 동작해야 했습니다.
저는 이 문제를 해결하기 위해, Node.js 에서 함수가 일급 객체(First-Class Citizen)로 동작하는 특성을 활용했습니다. 지연 평가(Lazy Evaluation) 방식을 적용하여, 데이터를 chunking 하고 전화번호를 기준으로 중복을 제거하는 unique 처리를 구현했습니다.
결과적으로, 회원 정보 소스가 추가되더라도 유연하게 대응할 수 있는 구조를 갖추게 되었고, 데이터가 커져도 안정적으로 처리할 수 있게 되었습니다.
OOM 발생!!!
이 이슈는 대상 생성 로직을 확정하고, 운영 환경을 따라 자원 제한을 적용했을 때 발생했습니다.
초기에는 docker stats 명령어를 사용하여, Docker Compose 로 띄운 컨테이너들의 자원 사용 현황을 확인했습니다. 이를 통해 메모리가 지속적으로 증가하고 있다는 점은 확인할 수 있었지만, 정확한 원인까지 파악하기에는 정보가 부족했습니다.
더 깊이 있는 분석을 위해, Prometheus와 Grafana를 Docker Compose 환경으로 구성하여 디버깅 환경을 마련했습니다. 이를 통해 특정 오브젝트가 메모리를 지속적으로 점유하며 사용량이 증가하는 것을 발견했고, heap snapshot을 분석한 끝에 문제의 원인이 특정 라이브러리라는 것을 확인할 수 있었습니다.
최종적으로 해당 라이브러리의 옵션을 조정하여 메모리 누수 문제를 해결했습니다. 또한, 이 과정에서 만든 디버깅용 Grafana 설정을 별도로 PR 하여, 동료 개발자들이 본인의 결과물을 테스트할 때 쉽게 활용할 수 있도록 공유했습니다.
발송 실패 상황
알림톡 발송은 외부 발송을 담당하는 내부 서비스를 통해 이루어졌습니다. 하지만 제가 간과했던 점은, 이 서비스가 해당 내부 서비스의 첫 번째 고객이었다는 사실이었습니다.
문제는 10만 건 발송 테스트 과정에서 발생했습니다. Kafka 로부터 쏟아지는 메시지를 내부 서비스가 감당하지 못하고, 결국 서비스가 다운되는 상황이 벌어진 것입니다. 이후 인프라팀과 내부서비스 담당자 분, 팀원들과 협의하여 배포 일정을 조정하고, 함께 원인 분석 및 테스트를 진행했습니다. 이 과정에서 AWS 경고 알림까지 받는 해프닝도 있었지만, 차분히 대응하면서 문제를 해결해 나갔습니다.
결과적으로 문제는 잘 해결되었고, 100만 건 규모의 테스트도 성공적으로 통과했습니다.
덕분에 계획된 배포 일정도 차질 없이 지킬 수 있었습니다.
수신부 처리 누락
알림톡 결과를 수신하는 과정에서 수신부에서 지속적인 누락이 발생하는 문제가 있었습니다.
처음에는 사용 중인 라이브러리인 KafkaJS의 사용 미숙이 원인이라 판단하여 공식 문서를 꼼꼼히 다시 읽고, 내부 구현도 깊게 분석했습니다. 이 과정에 약 하루를 투자했지만, 안타깝게도 원인을 찾지 못했습니다. 다음으로는 로직의 철저한 예외 처리를 시도했습니다. 문제는 예상하지 못한 부분에서 발생했습니다.
초기에 로직을 설계할 때, DB 이슈나 Kafka의 rebalancing 등으로 발생하는 예외는 NestJS의 글로벌 예외 처리기에 의해 처리될 것으로 가정하고 별도로 처리하지 않았습니다. 그러나 KafkaJS의 특성상, 메시지 핸들러 내부에서 예외가 발생하면 더 이상 메시지를 수신하지 않고, 일정 시간이 지나면 connection 자체를 끊어버린다는 것을 확인했습니다. 이 상황에서 KafkaJS는 주기적으로 offset을 commit 하게 되고, 이로 인해 실제로 메시지가 처리되지 않았음에도 불구하고 메시지 누락이 발생하는 것이었습니다.
결국, 모든 핸들러 내부에서 철저하게 예외를 처리하는 방식으로 로직을 수정하여 문제를 해결할 수 있었습니다.
성과
주요 고객사가 사용하기 시작한 3월 2주차 부터 매주 알림톡 발송량 최대치를 갱신하고 있습니다. (2025.04.09 기준)
배운 점
새로운 기술에 대해서는 좀 더 깊이 파악해야 한다는 점
느낀점
KafkaJS의 동작을 넘겨 짚어 누락이 발생한 경험에서 로직의 가정이 완벽해 보여도 한번 더 검토해야 한다는 점을 느꼈습니다.
특히 팀에서도 사용한 경험이 없는 도구를 도입할 때 더 주의해야 겠다고 생각했습니다.
action item
작업 문서에 로직을 좀 더 상세하게 담고, 각 로직의 가정을 함께 기록
일정의 산정을 위해서는 좀 더 면밀하게 검토가 필요하다는 점
느낀점
내부 서비스 이슈를 처음부터 고려하지 못해 고객이 더 많은 이익을 낼 수 있는 도구의 배포일정이 미뤄지는 결정을 해야만 한게 아쉬웠습니다.
action item
설계 문서에 전체 과정에서 이슈가 발생할 수 있는 부분을 기록 및 확인, 이때 테스트 문서 혹은 확답 받기