안녕하세요. 백엔드 개발자 최혜미입니다.
네 제목처럼 FCM 토픽 구독 제한 있는거 몰랐던 사람이 바로 접니다...🫠
최근 무슨 바람이 불었는지
이미 운영 중인 서비스의 마케팅 푸시 발송 로직을 다시 한번 뜯어봐야 한다는 강렬한 예감이 들었습니다.
평소 잘 돌아가던 로직이었지만 혹시나 하는 마음으로 검토를 시작했는데,
아니나 다를까 그대로 내버려 뒀다가는 조만간 큰 사고로 이어질 뻔한 로직이 숨어 있었습니다.
오늘은 이를 기술적으로 리팩토링한 과정을 공유해 보려 합니다.
결론부터 말씀드리면, FCM 토픽의 구독 제한을 고려하지 않은 무분별한 토픽 생성이 서비스 전체의 푸시 수신 불가 장애로 이어질 뻔한 사례였습니다.
기존의 마케팅 푸시 일괄 발송 프로세스는 다음과 같았습니다.
발송 시점의 타임스탬프를 이용해 임시 토픽을 생성합니다. (예: marketing_agree_<timestamp>)
전체 대상자의 FCM 토큰을 해당 토픽에 구독(subscribeToTopic)시킵니다.
생성된 토픽을 대상으로 메시지를 발송합니다.
이 방식은 구현이 간단하고 Firebase 서버가 발송 부하를 처리해준다는 장점이 있지만, 장점만 생각하고 치명적인 단점을 생각치도 못하고 있었습니다...🫠
"하나의 앱 인스턴스(디바이스)당 구독할 수 있는 최대 토픽 수는 2,000개로 제한된다."
발송 시마다 새로운 토픽을 생성하면 유저는 푸시를 받을 때마다 구독 슬롯을 하나씩 소진하게 됩니다.
결국 2,000회 발송 이후에는 해당 디바이스에서 신규 토픽 구독이 불가능해지며, 결과적으로 모든 마케팅 푸시를 수신할 수 없는 '영구적 수신 불가' 상태에 빠지게 됩니다.
문제를 파악하며 코드를 분석해보니 두 가지 결함이 더 존재했습니다.
수신 거부 필터링 미비: pushEnabled = false로 설정한 유저의 토큰까지 발송 대상에 포함되어 있었습니다.
이는 유저의 설정을 무시할 뿐만 아니라 불필요한 네트워크 비용을 발생시킵니다.
예외 처리 방식: 구독 처리 중 발생하는 예외를 catch한 뒤 로그만 남기고 다음 로직을 진행하고 있었습니다.
일부 토큰이 구독에 실패하더라도 발송은 강행되는 구조라 발송 누락의 원인이 되었습니다.
임시 토픽을 생성하는 방식 대신
애플리케이션 레벨에서 대상자를 관리하고 직접 발송하는 방식으로 아키텍처를 변경했습니다.
sendEachForMulticast 도입FCM 토픽 방식 대신 멀티캐스트 직접 발송 방식을 선택했습니다.
FCM 토큰을 조회하는 쿼리에 pushEnabled = true 조건을 추가하여 발송 대상을 명확히 분리했습니다.
배치 발송 과정에서 예외가 발생할 경우 RuntimeException을 던져 전체 프로세스를 중단하고 롤백하도록 수정했습니다.
불완전한 상태에서의 발송보다는 시스템의 정합성을 우선순위에 두었습니다.
이미 생성되어 유저 디바이스에 남아있는 수많은 임시 토픽 구독 정보는 차후 별도의 스크립트를 통해 점진적으로 정리할 계획입니다.
기술적 제약 사항이 얼마나 중요한지 알면서도 여전히 놓치는 부분이 많은 것 같습니다...
항상 느끼는 거지만 편리한 추상화 뒤에는 항상 시스템의 한계치가 존재하는 것 같네요.
완벽한 시스템은 없지만
발생 가능한 장애 포인트를 인지하고 이를 기술적으로 방어해 나가는 과정에서 시스템은 더 견고해진다고 생각합니다.
혹시 비슷한 방식으로 토픽을 활용하고 계신다면,
현재 우리 서비스의 '구독 카운트'가 얼마나 쌓였는지 한 번쯤 체크해보시길 권장합니다.
혹시 담당자가 저같이 아무 생각없이 개발하고 방치해뒀을 수 있으니까요...🥲