Skip to content

푸시 알림 Payload를 변경한다.#52

Merged
strongmhk merged 4 commits intodevelopfrom
fix/#51-modify-fcm-payload
Aug 12, 2025
Merged

푸시 알림 Payload를 변경한다.#52
strongmhk merged 4 commits intodevelopfrom
fix/#51-modify-fcm-payload

Conversation

@strongmhk
Copy link
Copy Markdown
Member

@strongmhk strongmhk commented Aug 11, 2025

📄 PR 요약

푸시 알림 Payload를 변경한다.

✍🏻 PR 상세

  1. 푸시 알림 Payload를 변경
  • 기존 notification 제거, data.title, data.body 추가

👀 참고사항

✅ 체크리스트

  • PR 양식에 맞게 작성했습니다.
  • 모든 테스트가 통과했습니다.
  • 프로그램이 정상적으로 작동합니다.
  • 적절한 라벨을 설정했습니다.
  • 불필요한 코드를 제거했습니다.

🚪 연관된 이슈 번호

Closes #51

Summary by CodeRabbit

  • New Features
    • 푸시 전송 개선: 본문별 묶음·토큰 중복제거로 대량 배치 전송(최대 500개) 지원, 데이터 페이로드(title/body)로 전환, Android/iOS 플랫폼 전달 우선순위·실시간성 최적화, 전송 결과 집계 및 무효 토큰 식별 강화, 무효 토큰 제거를 회원별로 정밀 적용
  • Chores
    • 알림 예약 작업에 시작/종료 로그 추가로 모니터링 가시성 향상

- notification을 제거, title과 body를 notification이 아닌 data.title, data.body에 담기
@strongmhk strongmhk self-assigned this Aug 11, 2025
@strongmhk strongmhk added the 🔨 fix fix feature label Aug 11, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 11, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

스케줄러의 sendPreAlarmNotifications에 시작/종료 로그가 추가되고, FcmService에 데이터 전용 멀티캐스트 전송 경로와 sendBulkNotification 메서드가 도입되어 토큰 중복 제거, 본문 기준 그룹화, 플랫폼별 설정, 배치(<=500) 전송 및 결과 집계 로직이 추가되었습니다.

Changes

Cohort / File(s) Change Summary
Scheduler Logging
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
sendPreAlarmNotifications 시작 log.info 추가; 기존 스케줄링 로직을 try { ... } finally { ... }로 감싸 종료 로그 보장. 기존 시간 윈도 계산, DB 필터링, Redis 토큰 조회, 타깃 구성, 조기 반환, FCM 전송, 성공 플래그 업데이트, 무효 토큰 정리 흐름 유지.
FCM Data-only Bulk Send
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
새 공개 메서드 sendBulkNotification(List<PushTargetDto>) 추가. Notification 대신 data payload(title, body) 사용. 본문 기준 그룹화, 토큰 중복 제거(dedupByToken), 최대 500개 토큰 배치로 Multicast 전송, AndroidConfig/ApnsConfig 플랫폼별 설정, 배치별 결과 집계(successOccurrenceIds, invalidTokens, memberToTokens), 무효 토큰 판별 로직 확장 및 내부 헬퍼 추가.

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler as AlarmReminderScheduler
  participant FCMService as FcmService
  participant Firebase as Firebase Messaging

  Scheduler->>FCMService: sendBulkNotification(targets)
  activate FCMService
  FCMService->>FCMService: 그룹화(by body) / 토큰 중복 제거 / 배치 생성(<=500)
  loop 각 배치
    FCMService->>Firebase: sendEachForMulticast(data:{title,body}, AndroidConfig, ApnsConfig)
    Firebase-->>FCMService: BatchResponse(개별 SendResponse[])
    FCMService->>FCMService: handleSendResult -> 성공 occurrenceId 수집, 무효 토큰 식별, member↔token 매핑
  end
  FCMService-->>Scheduler: FcmSendResult(성공 IDs, 무효 토큰, memberToTokens)
  deactivate FCMService
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~35 minutes

Assessment against linked issues

Objective Addressed Explanation
푸시 알림 Payload 변경 (#51)

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Semgrep (1.128.1)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java

{"version":"1.128.1","results":[],"errors":[{"code":4,"level":"error","type":"InvalidRuleSchemaError","long_msg":"{'pattern-not-either': [{'pattern': '@GetMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PostMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PutMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@DeleteMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}, {'pattern': '@PatchMapping(...)\n...\n@CustomErrorCodes(...)\n...\npublic $RET $M(...){ ... }\n'}]} is not valid under any of the given schemas","short_msg":"Invalid rule schema","spans":[{"file":".semgrep/semgrep.yml","start":{"line":89,"col":9,"offset":-1},"end":{"line":120,"col":5,"offset":-1},"source_hash":"b582dea1be576d791d43c1fb4f7261c55ab9e08f176bcb3c071834a52d4d2d6d"}]},{"code":7,"level":"error","type":"SemgrepError","message":"invalid configuration file found (1 configs were invalid)"}],"paths":{"scanned":[]},"skipped_rules":[]}


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4023204 and f68b989.

📒 Files selected for processing (1)
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (2 hunks)
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/#51-modify-fcm-payload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (4)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)

66-72: 무효 토큰 정리 로직 버그: 잘못된 memberId로 remove 호출 + 일부 토큰 누락 가능

  • 현재 구현은 “성공한 멤버 목록(memberToTokens)”을 기준으로 모든 invalidTokens를 각 멤버에 대해 제거 시도합니다. 그 결과:
    • 실패만 발생한 멤버의 무효 토큰은 제거되지 않습니다.
    • 반대로 다른 멤버에게 속한 토큰을 불필요하게 여러 멤버에 대해 remove 시도합니다(O(N_members)×O(N_invalidTokens)).
  • 정확한 매핑(토큰 → 소유 memberId)을 기준으로 제거해야 합니다. targets를 이용해 token→memberId 맵을 만들고, invalidTokens 각각에 대해 해당 소유자에게서만 제거하도록 변경하세요.

아래 diff로 교체하면 정확성과 성능 모두 개선됩니다.

-        // 4. 무효 토큰 정리(멤버 매핑 정보가 있으면 거기서 제거)
-        for (Map.Entry<Long, List<String>> e : result.getMemberToTokens().entrySet()) {
-            Long memberId = e.getKey();
-            for (String bad : result.getInvalidTokens()) {
-                redisService.removeInvalidToken(memberId, bad);
-            }
-        }
+        // 4. 무효 토큰 정리: 토큰 소유자 기준으로 정확히 제거
+        Map<String, Long> tokenToMemberId = targets.stream()
+            .collect(java.util.stream.Collectors.toMap(
+                PushTargetDto::token,
+                PushTargetDto::memberId,
+                (a, b) -> a // 동일 토큰에 대해 첫 매핑 유지
+            ));
+        for (String bad : result.getInvalidTokens()) {
+            Long owner = tokenToMemberId.get(bad);
+            if (owner != null) {
+                redisService.removeInvalidToken(owner, bad);
+            }
+        }
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (3)

139-146: 민감정보/자격증명 노출 위험: 실패 로그에 FCM 토큰 원문이 출력됩니다.

FCM 토큰은 자격증명에 준하는 민감 정보입니다. 원문 대신 마스킹/해시 후 일부만 로그에 남기세요.

권장 수정:

- log.warn("FCM 실패: token={}, error={}", dto.token(), fme.getErrorCode());
+ log.warn("FCM 실패: token={}, error={}", maskToken(dto.token()), fme.getErrorCode());
...
- log.warn("FCM 실패(원인 미상): token={}, ex={}", dto.token(), ex != null ? ex.getClass().getSimpleName() : "null");
+ log.warn("FCM 실패(원인 미상): token={}, ex={}", maskToken(dto.token()), ex != null ? ex.getClass().getSimpleName() : "null");

헬퍼 메서드(파일 내 추가):

private String maskToken(String token) {
    if (token == null) return "null";
    int len = token.length();
    int head = Math.min(8, len);
    int tail = Math.min(4, Math.max(0, len - head));
    String prefix = token.substring(0, head);
    String suffix = tail > 0 ? token.substring(len - tail) : "";
    return prefix + "...(len=" + len + ")..." + suffix;
}

151-157: 오류코드 문자열 비교 대신 SDK enum 사용을 고려하세요.

문자열 비교는 취약합니다. FirebaseMessagingException의 MessagingErrorCode(enum)를 사용하면 안전합니다.

예시:

import com.google.firebase.messaging.MessagingErrorCode;

private boolean isTokenInvalid(FirebaseMessagingException e) {
    MessagingErrorCode code = e.getMessagingErrorCode();
    return code == MessagingErrorCode.UNREGISTERED
        || code == MessagingErrorCode.INVALID_ARGUMENT
        || code == MessagingErrorCode.INVALID_REGISTRATION_TOKEN;
}

33-35: TITLE 하드코딩은 재사용성을 떨어뜨립니다.

향후 다른 타입의 푸시에 재사용하려면 TITLE을 파라미터화(메서드 인자/DTO 필드)하거나 구성으로 외부화하는 것이 좋습니다.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be7f7d3 and 36d3b43.

📒 Files selected for processing (2)
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (2 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (6 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
**/*{Controller,Service,Repository}.java

📄 CodeRabbit Inference Engine (AGENT.md)

Controller/Service는 getUser, createUser, removeUser 등 동사 기반으로 명명하고, Repository는 findByUser, insertUser, deleteUser처럼 명확히 구분합니다.

Files:

  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
🧬 Code Graph Analysis (2)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmOccurrenceBatchScheduler.java (2)
  • Scheduled (22-27)
  • Slf4j (10-47)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (1)
src/main/java/akuma/whiplash/domains/alarm/application/dto/etc/PushTargetDto.java (1)
  • Builder (5-13)
🔇 Additional comments (2)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (2)

74-77: PR 목표 부합: Notification 제거 + data.title/body 사용이 명확합니다.

data-only로 전환하면서 title/body를 data에 넣는 방향이 명시적이고, 클라이언트 구현과의 계약만 일치하면 문제 없습니다. LGTM.


60-65: 그룹핑 기준이 body(주소 문구)입니다. 토큰 중복 전송 의도 확인 필요

동일 멤버가 여러 주소로 다수 알림 대상인 경우, 하나의 토큰이 서로 다른 body 그룹에 중복 포함되어 여러 번 전송됩니다. 이것이 요구사항(주소별 별도 알림)과 일치하는지 확인 부탁드립니다. 또한 address가 null인 케이스 처리도 고려가 필요합니다.

Comment on lines +82 to +96
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(Priority.HIGH) // 즉시 전송
.build()
)
.setApnsConfig( // IOS 설정
ApnsConfig.builder()
.putHeader("apns-priority", "10") // 즉시 전송
.setAps(
Aps.builder()
.setContentAvailable(true) // 백그라운드 데이터 수신 허용
.build()
)
.build()
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

iOS 데이터 전용 푸시: apns-push-type=background 헤더 누락, 우선순위 10은 부적절할 수 있습니다(배경 데이터는 5 권장). Android/iOS TTL도 지정하세요.

  • iOS silent/data-only 알림은 iOS 13+에서 apns-push-type 헤더가 요구됩니다. background 전송은 보통 priority=5가 권장됩니다. 현재 10으로 설정되어 있어 의도(백그라운드 데이터 수신)와 상충할 수 있습니다.
  • 본 알림은 “1시간 전 리마인드” 용도이므로 만료 시간(TTL/expiration)도 설정해야 지연 전송을 방지할 수 있습니다.

제안 패치:

 .setAndroidConfig(
     AndroidConfig.builder()
-        .setPriority(Priority.HIGH) // 즉시 전송
+        .setPriority(Priority.HIGH) // 즉시 전송
+        .setTtl(90 * 60_000)        // TTL 90분 (필요시 조정)
         .build()
 )
 .setApnsConfig( // IOS 설정
     ApnsConfig.builder()
-        .putHeader("apns-priority", "10") // 즉시 전송
+        .putHeader("apns-priority", "5")        // background 권장
+        .putHeader("apns-push-type", "background")
         .setAps(
             Aps.builder()
                 .setContentAvailable(true) // 백그라운드 데이터 수신 허용
                 .build()
         )
+        // .putHeader("apns-expiration", String.valueOf(Instant.now().plus(90, ChronoUnit.MINUTES).getEpochSecond()))
         .build()
 )

최신 가이던스 확인 요청:


🌐 Web query:

APNs silent/data-only remote notification requires which apns-push-type and priority? Is priority=5 recommended for background content-available notifications, and is the apns-push-type=background header mandatory on iOS 13+?

💡 Result:

Short answer

  • For a silent (data-only / content-available) APNs push:

  • Yes — notifications whose payload contains only "content-available":1 should be sent with priority 5 (Apple treats 10 as immediate and “an error” for content-only background pushes). (developer.apple.com, kodeco.com)

  • Yes — the apns-push-type header is required for delivery to devices running iOS 13+ (and watchOS 6+); if it’s missing or mismatched APNs may delay, drop, or return an error. The header is an HTTP/2 request header (not a key in the JSON payload). (developer.apple.com, docs.azure.cn)

Notes / practical tips

  • If your notification also contains alert/sound/badge (user‑visible content), use apns-push-type: "alert" and apns-priority: "10". (developer.apple.com)
  • apns-push-type and apns-priority are HTTP/2 headers you send to APNs (not fields inside the aps JSON). (docs.azure.cn, developer.apple.com)

If you want, I can show an example HTTP/2 POST to api.push.apple.com with the exact headers + JSON payload for a silent notification.

Citations:


iOS 백그라운드 전용 푸시: apns-push-type/priority 수정 및 Android·iOS TTL 설정 필요

아래 항목을 반영해주세요:

  • AndroidConfig에 TTL 설정 추가
  • ApnsConfig에 silent 알림용 HTTP/2 헤더 추가
    • apns-priority: "5" (background 권장)
    • apns-push-type: "background" (iOS 13+ 필수)
  • 필요 시 apns-expiration 헤더로 만료 시간 지정

File: src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
Lines: 82–96

제안 패치:

 .setAndroidConfig(
     AndroidConfig.builder()
-        .setPriority(Priority.HIGH) // 즉시 전송
+        .setPriority(Priority.HIGH)      // 즉시 전송
+        .setTtl(90 * 60_000)             // TTL 90분 (필요 시 조정)
         .build()
 )
 .setApnsConfig(
     ApnsConfig.builder()
-        .putHeader("apns-priority", "10") // 즉시 전송
+        .putHeader("apns-priority", "5")           // background 권장
+        .putHeader("apns-push-type", "background") // iOS 13+ 필수
         .setAps(
             Aps.builder()
                 .setContentAvailable(true) // 백그라운드 데이터 수신 허용
                 .build()
         )
+        // .putHeader("apns-expiration", String.valueOf(
+        //     Instant.now().plus(90, ChronoUnit.MINUTES).getEpochSecond()
+        // ))
         .build()
 )

참고:

  • content-available 전용 푸시는 HTTP/2 헤더(apns-push-type, apns-priority)로만 제어하며, JSON payload 내부 필드가 아닙니다.
  • TTL 또는 apns-expiration 설정을 통해 메시지 만료 시점을 명시적으로 관리하세요.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(Priority.HIGH) // 즉시 전송
.build()
)
.setApnsConfig( // IOS 설정
ApnsConfig.builder()
.putHeader("apns-priority", "10") // 즉시 전송
.setAps(
Aps.builder()
.setContentAvailable(true) // 백그라운드 데이터 수신 허용
.build()
)
.build()
)
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(Priority.HIGH) // 즉시 전송
.setTtl(90 * 60_000) // TTL 90분 (필요 시 조정)
.build()
)
.setApnsConfig( // IOS 설정
ApnsConfig.builder()
.putHeader("apns-priority", "5") // background 권장
.putHeader("apns-push-type", "background") // iOS 13+ 필수
.setAps(
Aps.builder()
.setContentAvailable(true) // 백그라운드 데이터 수신 허용
.build()
)
// .putHeader("apns-expiration", String.valueOf(
// Instant.now().plus(90, ChronoUnit.MINUTES).getEpochSecond()
// ))
.build()
)
🤖 Prompt for AI Agents
In src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java around
lines 82–96, the APNS headers and Android TTL are missing for silent
(background) iOS pushes and Android message expiry; add an AndroidConfig TTL
(e.g., AndroidConfig.builder().setTtl(Duration.ofSeconds(...)) alongside
existing priority), and modify the ApnsConfig to include HTTP/2 headers for
silent pushes by using putHeader("apns-push-type","background") and
putHeader("apns-priority","5") (and optionally putHeader("apns-expiration",
String.valueOf(expirationEpochSeconds)) if you need explicit expiration), while
keeping Aps.builder().setContentAvailable(true); ensure headers are added via
ApnsConfig.putHeader calls rather than message payload fields.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🔭 Outside diff range comments (3)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (2)

60-65: 그룹 키를 문자열 메시지가 아닌 주소로 묶고, 메시지 문자열은 루프 내에서 구성 권장

현재 그룹핑 키가 최종 body 문자열입니다. 주소를 키로 사용하고 메시지 문자열은 루프에서 생성하면 메시지 템플릿 변경 시 불필요한 재그룹핑 오버헤드를 줄일 수 있습니다.

예시(diff):

-        Map<String, List<PushTargetDto>> groupedByBody = targets.stream()
-            .collect(Collectors.groupingBy(
-                dto -> String.format("1시간 뒤 %s에서 알림이 울릴 예정이에요!", dto.address())
-            ));
+        Map<String, List<PushTargetDto>> groupedByAddress = targets.stream()
+            .collect(Collectors.groupingBy(PushTargetDto::address));
@@
-        for (Map.Entry<String, List<PushTargetDto>> entry : groupedByBody.entrySet()) {
-            String body = entry.getKey();
+        for (Map.Entry<String, List<PushTargetDto>> entry : groupedByAddress.entrySet()) {
+            String body = String.format("1시간 뒤 %s에서 알림이 울릴 예정이에요!", entry.getKey());
             List<PushTargetDto> group = dedupByToken(entry.getValue());

150-157: 무효 토큰 에러 코드 보강 제안

환경에 따라 에러 코드 prefix가 붙는 경우가 있으니 아래 변형도 고려해 주세요.

         return List.of(
             "registration-token-not-registered",
             "invalid-argument",
             "unregistered",
-            "messaging/invalid-registration-token"
+            "messaging/invalid-registration-token",
+            "messaging/registration-token-not-registered"
         ).contains(e.getErrorCode());
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)

67-72: 무효 토큰 정리 로직 오류: 일부 토큰 미삭제 및 불필요한 N×M 반복

현재 로직은 memberToTokens(“성공한 멤버”)만 순회하면서 result.invalidTokens 전체를 모든 멤버에서 제거 시도합니다. 문제:

  • 성공 토큰이 없는 멤버(=모든 전송이 실패한 멤버)의 무효 토큰은 제거되지 않습니다.
  • 모든 멤버에 대해 동일 토큰 제거를 반복하는 비효율(O(M×N)).

두 가지 방향 중 하나로 수정하세요.

방안 A) FcmService에서 즉시 제거(권장, 서비스 주석과 일치). 위 FcmService 코멘트의 diff 참고.

방안 B) 스케줄러에서 정확한 매핑으로 제거. targets를 이용해 token→memberId 맵을 만든 뒤 invalidTokens만 단일 루프로 제거:

-        // 4. 무효 토큰 정리(멤버 매핑 정보가 있으면 거기서 제거)
-        for (Map.Entry<Long, List<String>> e : result.getMemberToTokens().entrySet()) {
-            Long memberId = e.getKey();
-            for (String bad : result.getInvalidTokens()) {
-                redisService.removeInvalidToken(memberId, bad);
-            }
-        }
+        // 4. 무효 토큰 정리: 정확한 매핑으로 단일 루프 제거
+        Map<String, Long> tokenToMember = targets.stream()
+            .collect(java.util.stream.Collectors.toMap(
+                PushTargetDto::token,
+                PushTargetDto::memberId,
+                (a, b) -> a // 충돌 시 최초 값 유지
+            ));
+        for (String bad : result.getInvalidTokens()) {
+            Long owner = tokenToMember.get(bad);
+            if (owner != null) {
+                redisService.removeInvalidToken(owner, bad);
+            }
+        }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between be7f7d3 and 36d3b43.

📒 Files selected for processing (2)
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (2 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (6 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Files:

  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
**/*{Controller,Service,Repository}.java

📄 CodeRabbit Inference Engine (AGENT.md)

Controller/Service는 getUser, createUser, removeUser 등 동사 기반으로 명명하고, Repository는 findByUser, insertUser, deleteUser처럼 명확히 구분합니다.

Files:

  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
🧬 Code Graph Analysis (2)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmOccurrenceBatchScheduler.java (2)
  • Slf4j (10-47)
  • Scheduled (22-27)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (1)
src/main/java/akuma/whiplash/infrastructure/firebase/dto/FcmSendResult.java (1)
  • Getter (9-15)
🔇 Additional comments (5)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (3)

6-9: Firebase 플랫폼 설정 import 적절

데이터 전용 멀티캐스트 전송을 위한 Android/APNs 설정 import 추가가 목적에 부합합니다.


72-72: 토큰 기준 중복 제거의 의미적 영향 확인 필요

같은 토큰에 대해 여러 occurrenceId가 존재할 수 있는 시나리오에서 토큰 기준 dedup이 일부 occurrence의 성공 집계를 누락시킬 수 있습니다. 의도대로 “디바이스당 1회 발송”이 맞는지, 혹은 (memberId, token) 단위 dedup이 맞는지 확인 바랍니다.


159-166: 토큰 중복 제거 구현 적절

LinkedHashMap 기반의 최초 등장 우선 보존으로 안정적 dedup이 이뤄집니다.

src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (2)

30-30: 시작 로그 추가 적절

스케줄러 시작 지점의 라이프사이클 로그가 문제 분석에 유용합니다.


74-75: 종료 로그 추가 적절

스케줄러 종료 지점의 라이프사이클 로그 추가가 유용합니다.

Comment on lines +81 to +96
.putAllData(data)
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(Priority.HIGH) // 즉시 전송
.build()
)
.setApnsConfig( // IOS 설정
ApnsConfig.builder()
.putHeader("apns-priority", "10") // 즉시 전송
.setAps(
Aps.builder()
.setContentAvailable(true) // 백그라운드 데이터 수신 허용
.build()
)
.build()
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

iOS(APNs) 데이터 전용 푸시 설정 재검토: background 타입과 우선순위

data-only + content-available=true 케이스는 일반적으로

  • apns-push-type=background
  • apns-priority=5
    을 권장합니다. 현재는 content-available=true와 apns-priority=10 조합으로 설정되어 있어 플랫폼 권장과 상충될 수 있습니다.

제안(diff):

-                    .setApnsConfig( // IOS 설정
+                    .setApnsConfig( // iOS 설정: data-only background 권장 설정
                         ApnsConfig.builder()
-                            .putHeader("apns-priority", "10") // 즉시 전송
+                            .putHeader("apns-push-type", "background")
+                            .putHeader("apns-priority", "5")
                             .setAps(
                                 Aps.builder()
                                     .setContentAvailable(true) // 백그라운드 데이터 수신 허용
                                     .build()
                             )
                             .build()
                     )

추가로, 만약 “표시형(alert)” 동작을 원하시면 data-only 대신 aps.alert을 구성하고 apns-push-type=alert를 사용해야 합니다. 현재 PR 목표(데이터 전용)와 맞는지 재확인 부탁드립니다.


🏁 Script executed:

#!/bin/bash
rg -n ".setApnsConfig" -A5 -B5 src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java

Length of output: 662


🌐 Web query:

What are the recommended APNs headers for data-only background pushes on iOS regarding apns-push-type and apns-priority?

💡 Result:

Use these APNs headers for a data‑only background push on iOS:

  • apns-push-type: background[2].
  • apns-priority: 5[2][5].

Essential details:

  • Apple introduced apns-push-type in iOS 13; for silent/background updates it must be set to background, otherwise delivery may be delayed or dropped[2].
  • For background (silent/content-available) pushes, apns-priority must be 5; priority 10 is not allowed for background-only notifications and may prevent the app from launching in the background[5]. Apple’s WWDC guidance and industry providers document this requirement; vendors note that priority 5 is required when there’s no alert payload[2].
  • When sending via FCM/APNs, pair these headers with an aps payload that contains only content-available: 1 (no alert/sound/badge) to make it truly background-only[1][7].
  • Example (FCM HTTP v1 headers block under apns.headers): apns-push-type: background and apns-priority: 5[1].

Citations:


iOS 무음 백그라운드 푸시: apns-push-type 및 우선순위 수정 필요

Apple 권장사항에 따라 data-only + content-available=true 푸시는
• apns-push-type: background
• apns-priority: 5
를 사용해야 하며, priority=10은 백그라운드 푸시에서 허용되지 않아 푸시 지연 또는 누락을 초래할 수 있습니다.

수정 대상:

  • 파일: src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
  • 위치: .setApnsConfig 빌더 부분 (약 87–96라인)

제안(diff):

-                    .setApnsConfig( // IOS 설정
+                    .setApnsConfig( // iOS 무음(background) 푸시 권장 설정
                         ApnsConfig.builder()
-                            .putHeader("apns-priority", "10") // 즉시 전송
+                            .putHeader("apns-push-type", "background")
+                            .putHeader("apns-priority", "5")
                             .setAps(
                                 Aps.builder()
                                     .setContentAvailable(true) // 백그라운드 데이터 수신 허용
                                     .build()
                             )
                             .build()
                     )

또한, 알림(alert) 표시가 필요하다면 aps.alert 페이로드와 함께
apns-push-type: alertapns-priority: 10 설정을 별도로 적용해야 합니다.
현재 PR 목표가 순수 데이터 전송인지 재확인 바랍니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.putAllData(data)
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(Priority.HIGH) // 즉시 전송
.build()
)
.setApnsConfig( // IOS 설정
ApnsConfig.builder()
.putHeader("apns-priority", "10") // 즉시 전송
.setAps(
Aps.builder()
.setContentAvailable(true) // 백그라운드 데이터 수신 허용
.build()
)
.build()
)
.putAllData(data)
.setAndroidConfig(
AndroidConfig.builder()
.setPriority(Priority.HIGH) // 즉시 전송
.build()
)
.setApnsConfig( // iOS 무음(background) 푸시 권장 설정
ApnsConfig.builder()
.putHeader("apns-push-type", "background")
.putHeader("apns-priority", "5")
.setAps(
Aps.builder()
.setContentAvailable(true) // 백그라운드 데이터 수신 허용
.build()
)
.build()
)

- 백그라운드 알림이 아닌 UI에 표시되는 푸시 알림으로 설정
- TTL은 60분
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (3)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (3)

49-51: 주석과 실제 동작 불일치: "무효 토큰은 즉시 Redis에서 제거" 명시했지만 실제로는 스케줄러에서만 제거

주석(49-51줄)에서는 "무효 토큰은 즉시 Redis에서 제거"라고 명시되어 있지만, 실제로는 handleSendResult 메서드에서 무효 토큰을 수집만 하고 Redis 제거는 스케줄러(AlarmReminderScheduler)에서 수행됩니다. 이는 주석과 동작이 일치하지 않습니다.

다음과 같이 수정하여 서비스 내에서 즉시 제거하도록 구현하시는 것을 권장합니다:

                 if (fme != null) {
                     log.warn("FCM 실패: token={}, error={}", dto.token(), fme.getErrorCode());
                     if (isTokenInvalid(fme)) {
                         invalidTokens.add(dto.token());
+                        // 서비스 내부에서 즉시 정리 (주석과 동작 일치)
+                        try {
+                            redisService.removeInvalidToken(dto.memberId(), dto.token());
+                        } catch (Exception cleanEx) {
+                            log.warn("무효 토큰 정리 중 오류: memberId={}, token={}", dto.memberId(), dto.token(), cleanEx);
+                        }
                     }
                 } else {

76-79: 불변 Map 사용으로 코드 간결화 가능

HashMap을 생성하고 put으로 값을 추가하는 대신 Map.of()를 사용하면 더 간결하고 명확합니다.

-            Map<String, String> data = Map.of(
-                "title", DEFAULT_TITLE,
-                "body", body
-            );
+            Map<String, String> data = Map.of("title", DEFAULT_TITLE, "body", body);

95-95: 로그 레벨 조정 고려: 빈번한 배치 전송 시 debug 레벨 권장

매 배치마다 info 레벨로 성공/실패 건수를 로깅하면 운영 환경에서 로그가 과도하게 생성될 수 있습니다.

-                    log.info("FCM 멀티캐스트 결과: success={}, failure={}", response.getSuccessCount(), response.getFailureCount());
+                    log.debug("FCM 멀티캐스트 결과: success={}, failure={}", response.getSuccessCount(), response.getFailureCount());
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 36d3b43 and 4023204.

📒 Files selected for processing (2)
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1 hunks)
  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (8 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*{Controller,Service,Repository}.java

📄 CodeRabbit Inference Engine (AGENT.md)

Controller/Service는 getUser, createUser, removeUser 등 동사 기반으로 명명하고, Repository는 findByUser, insertUser, deleteUser처럼 명확히 구분합니다.

Files:

  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
**/*.java

📄 CodeRabbit Inference Engine (AGENT.md)

**/*.java: 세미콜론 누락, 들여쓰기 불일치, import 순서 등의 형식 오류를 검토합니다.
사용되지 않는 변수나 주석은 제거를 제안합니다.
한 줄이 너무 길거나 중첩이 과도한 경우 구조 개선을 권장합니다.

Files:

  • src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java
  • src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java
🧬 Code Graph Analysis (2)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (1)
src/main/java/akuma/whiplash/infrastructure/firebase/dto/FcmSendResult.java (1)
  • Getter (9-15)
src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (3)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmQueryServiceImpl.java (1)
  • Slf4j (32-180)
src/main/java/akuma/whiplash/domains/alarm/persistence/entity/AlarmOccurrenceEntity.java (1)
  • updateReminderSent (86-88)
src/main/java/akuma/whiplash/domains/alarm/domain/service/AlarmCommandServiceImpl.java (2)
  • Override (243-248)
  • Slf4j (48-341)
🔇 Additional comments (4)
src/main/java/akuma/whiplash/infrastructure/firebase/FcmService.java (3)

146-152: LGTM! Android TTL 설정이 적절히 구현됨

Duration을 활용한 TTL 설정이 깔끔하게 구현되었습니다. null 체크와 유효성 검증도 적절합니다.


154-172: iOS 알림 설정이 잘 구현됨

UI 표시용 iOS 알림 설정이 적절히 구현되었습니다. apns-push-type="alert"와 priority="10" 설정이 올바르며, TTL 처리도 적절합니다.


88-90: iOS 백그라운드(데이터 전용) 푸시 설정 확인 필요

현재 .setApnsConfig(buildApnsConfigAlert(...))를 사용해 UI 알림을 전송하고 있지만, 상단 주석에서는 “데이터 전용(data-only)” 멀티캐스트라고 명시되어 있어 구현 의도와 불일치합니다. 아래 중 하나를 선택해 의도를 명확히 해 주세요:

  • 데이터 전용 푸시가 목표인 경우

    1. 코드 교체
      - .setApnsConfig( // iOS 설정
      -     buildApnsConfigAlert(DEFAULT_TITLE, body, Duration.ofMinutes(60))
      - )
      + .setApnsConfig( // iOS 데이터 전용 설정
      +     buildApnsConfigBackground(Duration.ofMinutes(60))
      + )
    2. buildApnsConfigBackground 메서드 추가
      private ApnsConfig buildApnsConfigBackground(Duration ttl) {
          ApnsConfig.Builder apns = ApnsConfig.builder()
              .putHeader("apns-push-type", "background")
              .putHeader("apns-priority", "5")
              .setAps(Aps.builder()
                  .setContentAvailable(true)
                  .build());
          
          if (ttl != null && !ttl.isZero() && !ttl.isNegative()) {
              long epochSec = (System.currentTimeMillis() + ttl.toMillis()) / 1000;
              apns.putHeader("apns-expiration", String.valueOf(epochSec));
          }
          return apns.build();
      }
  • UI 알림(alert)이 목표인 경우
    • 현재 buildApnsConfigAlert 호출이 올바르므로 상단 주석을 “UI 알림”으로 수정

src/main/java/akuma/whiplash/domains/alarm/application/scheduler/AlarmReminderScheduler.java (1)

30-77: 시작/종료 로그가 try-finally로 적절히 보장됨

조기 return이 있어도 finally 블록에서 종료 로그가 항상 출력되도록 잘 구현되었습니다. 운영 모니터링에 유용한 개선입니다.

@strongmhk strongmhk merged commit 6fefa89 into develop Aug 12, 2025
1 of 2 checks passed
@strongmhk strongmhk deleted the fix/#51-modify-fcm-payload branch August 12, 2025 15:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🔨 fix fix feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

푸시 알림 Payloa를 변경한다.

1 participant