원격지원 FCM 발송·해제 버그 수정과 블랙리스트 자동 차단 구조 도입
목차
원격지원 기능에서 FCM 발송과 해제 쪽에 버그가 있었다. 재현 경로를 잡기까지 시간이 좀 걸렸고, 수정 범위는 내부 클래스 세 곳이었음. 수정량 자체는 작은데 맥락이 없으면 놓치기 쉬운 케이스라 따로 정리해둔다.
FCM 버그 - 수정 흐름과 교훈
원격지원 플로우에서 FCM 발송이 특정 조건에서 누락되거나, 반대로 해제 처리가 됐는데 발송이 나가는 케이스가 있었음. 로그를 보면 요청 자체는 들어오는데 FCM 호출부에서 조용히 빠져나가는 형태였다.
내부 클래스 세 곳을 수정하면서 공통적으로 보인 패턴은 아래 표의 첫 두 케이스에 해당했음. null 체크 순서가 뒤집혀 있어서 이후 코드가 실행되지 않거나, 경로 예외 처리가 없어서 필터가 정상 요청을 막는 식이었다.
| 패턴 | 증상 |
|---|---|
| null 체크 순서 오류 | NPE, 또는 조건 분기 미실행 |
| 경로 예외 처리 누락 | 필터에서 정상 요청 차단 |
| YAML 공백 혼용 | 설정값 파싱 실패 |
| import 누락 | 컴파일 에러 |
이런 버그는 테스트 환경에서 잘 안 잡힌다. 원격지원이라는 기능 특성상 연결 상태, 세션, FCM 토큰이 맞물리는 타이밍이 있는데 단위 테스트로는 그 조합을 전부 커버하기 어려움. 결국 운영 로그를 뜯어서 재현 경로를 추적하는 게 제일 빠른 방법이었음.
수정 원칙은 단순하게 유지했다. 재현, 원인 파악, 최소 범위 수정, 배포. 이 루틴이 반복되면서 점점 손에 익어가는 게 느껴진다. 총괄 팀장 포지션에서 직접 코드를 잡는 경우가 줄어들수록, 오히려 이 루틴을 팀에 전파하고 체화시키는 게 더 중요하다는 걸 실감함.
코드 레벨에서 보면 FCM 발송 로직을 내부 클래스 여러 곳에 흩뿌려두는 구조 자체가 문제다. 단일 진입점으로 꺼내면 한 곳만 고쳐도 된다.
// 발송 조건 판단을 한 곳에서만 하도록 래핑
public void sendSupportNotification(SupportSession session) {
if (session == null || !session.isActive()) return;
if (session.getFcmToken() == null) return;
fcmClient.send(session.getFcmToken(), buildPayload(session));
}
이 정도 래핑만 해도 null 체크 순서 버그나 중복 발송 문제는 상당히 줄어든다. 이번 핫픽스는 최소 범위에서 끊었고, 내부 클래스 정리는 다음 스프린트로 미룬 상태임. 급한 불부터 끄고 구조는 따로 잡는 게 맞다고 판단했다.
블랙리스트 자동 등록 구조
공격성 요청은 수동으로 막으면 이미 늦다. 감지 시점과 차단 시점 사이에 갭이 생기고 그 사이에 요청이 계속 들어온다. 자동 등록 후 다음 요청부터 즉시 차단하는 구조를 붙인 이유가 이거임.
흐름 자체는 단순하다. 필터에서 공격 패턴 감지, DB 또는 Redis에 IP/식별자 등록, 다음 요청부터 필터 앞단에서 바로 차단. Rate Limit 초과나 명시적 공격 시그니처가 감지되면 등록 트리거가 걸린다.
자동화라 신경 써야 할 부분이 있음.
- 오탐 범위: 로드 테스트나 배치 작업이 봇으로 오해받을 수 있다. 내부 IP 대역이나 특정 User-Agent는 화이트리스트로 미리 빼야 함.
- TTL 필수: 영구 차단은 오탐 복구가 안 된다. 처음엔 짧게 잡고, 반복 감지 시 TTL을 늘리는 식으로 단계를 두는 게 낫다.
- 알림 연동: 자동으로 등록되더라도 사람이 모르면 의미가 없음. 슬랙이나 디스코드로 "어떤 식별자가 왜 차단됐는지"를 바로 보내야 나중에 오탐 여부를 판단할 수 있다.
public void autoBlock(String identifier, String reason, long ttlSeconds) {
if (whitelist.contains(identifier)) return;
redisTemplate.opsForValue()
.set("blacklist:" + identifier, reason, ttlSeconds, TimeUnit.SECONDS);
notificationService.send("[자동 차단] " + identifier + " - " + reason);
}
알림 없이 자동 차단만 돌리면 어느 순간 블랙리스트가 쌓여 있는데 왜 들어갔는지 아무도 모르는 상태가 된다. 그 자체가 더 큰 리스크임. 차단 내역을 모니터링 가능하게 만드는 게 자동화보다 선행돼야 할 작업이었다고 봄.
Rate Limit 설계에서 놓치기 쉬운 것들
Rate Limit 잡을 때 흔히 빠지는 함정은 "얼마나 막을 것인가"에만 집중하는 것. 정작 중요한 건 정상 사용자는 안 건드리면서 공격만 막을 수 있는가다. 이 둘은 동시에 최적화하기 어려운 트레이드오프임.
고정 윈도우 방식은 구현이 단순한 대신 경계 구간 버스팅에 취약하다. 윈도우 리셋 직전과 직후에 집중 요청을 보내면 실질적으로 제한의 2배를 허용하게 됨. Redis 슬라이딩 윈도우는 그 문제를 해결하지만 Redis 의존도가 올라간다. Redis가 내려가면 Rate Limit 자체가 동작하지 않으니 연결 풀과 타임아웃 설정은 도입 시 반드시 같이 챙겨야 한다.
| 방식 | 장점 | 단점 |
|---|---|---|
| 고정 윈도우 | 구현 단순, 부담 낮음 | 경계 구간 버스팅에 취약 |
| 슬라이딩 윈도우 | 정밀한 제한, 버스팅 방지 | Redis 의존, 구현 복잡도 증가 |
| 토큰 버킷 | 순간 버스트 허용 가능 | 매개변수 튜닝 필요 |
어떤 방식이든 처음 한 달은 로그만 남기고 실제 차단은 하지 않는 드라이런 기간을 두길 권장한다. 임계값을 얼마로 잡아야 정상 사용자를 안 건드리는지 데이터 없이 감으로 잡으면 반드시 오탐이 난다. 그 오탐 한 번이 생각보다 큰 소음을 만들어냄.
원격지원처럼 실시간 응답이 중요한 기능에서 FCM 버그는 유저 경험에 바로 직결된다. 규모가 작은 수정이라도 운영 로그 기반으로 재현하고 최소 범위에서 끊는 습관이 쌓이면, 나중에 대형 장애를 예방하는 데도 실질적인 차이를 만든다.
댓글 0
첫 댓글 달아줘.