Skip to content

feat: FCM 404 에러 핸들링 추가#2283

Open
Soundbar91 wants to merge 2 commits into
developfrom
feat/2282-add-fcm-404-error-handling
Open

feat: FCM 404 에러 핸들링 추가#2283
Soundbar91 wants to merge 2 commits into
developfrom
feat/2282-add-fcm-404-error-handling

Conversation

@Soundbar91
Copy link
Copy Markdown
Collaborator

@Soundbar91 Soundbar91 commented May 30, 2026

🔍 개요


🚀 주요 변경 내용

  • FCM 배치 전송 결과를 FcmSendResponse로 반환하도록 변경했습니다.
  • 알림 이력에 FCM 전송 성공 여부, FCM 에러 코드, Messaging 에러 코드를 저장하도록 추가했습니다.
  • 어드민 알림 발송도 공통 NotificationService 경로를 사용하도록 변경해 전송 결과가 저장되도록 했습니다.
  • 매일 0시에 전날 UNREGISTERED 실패 알림을 기준으로 유저 device_token을 정리하는 스케줄러를 추가했습니다.
  • notification 테이블에 FCM 전송 결과 저장 컬럼을 추가했습니다.

💬 참고 사항


✅ Checklist (완료 조건)

  • 코드 스타일 가이드 준수
  • 테스트 코드 포함됨
  • Reviewers / Assignees / Labels 지정 완료
  • 보안 및 민감 정보 검증 (API 키, 환경 변수, 개인정보 등)

Summary by CodeRabbit

  • New Features

    • Automated daily cleanup of unregistered device tokens from failed push notifications.
    • Push notification system now tracks delivery success status and captures FCM error codes.
  • Improvements

    • Enhanced notification service with improved error handling and status tracking.
    • Optimized FCM integration for detailed failure diagnostics.
  • Infrastructure

    • Database schema updated to store push delivery results and error information.

Review Change Stack

@Soundbar91 Soundbar91 self-assigned this May 30, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR refactors FCM notification delivery to capture per-message send results and persist them on notification records, then introduces a scheduled cleanup job to remove device tokens from users with repeated unregistered delivery failures.

Changes

FCM Push Result Persistence

Layer / File(s) Summary
FcmSendResponse result contract
src/main/java/in/koreatech/koin/infrastructure/fcm/FcmSendResponse.java
New public record with success, errorCode, and messagingErrorCode fields; includes succeeded() and failed(...) factory methods.
Notification entity and persistence schema
src/main/java/in/koreatech/koin/domain/notification/model/Notification.java, src/main/resources/db/migration/V236__add_fcm_result_to_notification.sql
Notification gains pushSuccess, fcmErrorCode, and fcmMessagingErrorCode fields, plus markPushSuccess() and markPushFailure(...) methods; migration adds corresponding nullable columns to the notification table.
FcmClient result reporting
src/main/java/in/koreatech/koin/infrastructure/fcm/FcmClient.java
sendMessages(...) signature changes from void to return List<FcmSendResponse>; implementation batches-sends via FirebaseMessaging.sendEach(...) and maps each SendResponse to FcmSendResponse, with exception handling that appends failed responses per-message.
Push result persistence and status marking
src/main/java/in/koreatech/koin/domain/notification/repository/NotificationJdbcRepository.java, src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java
NotificationJdbcRepository.batchInsert persists push success and error code fields; NotificationService captures the result list from fcmClient.sendMessages(...) and marks each notification's success or failure state via new markPushResults(...) method.
AdminNotificationService delegation
src/main/java/in/koreatech/koin/admin/notification/service/AdminNotificationService.java
Refactored to build notification list and delegate delivery to NotificationService.pushNotifications(...), removing direct FcmClient and AdminNotificationRepository dependencies.

Device Token Cleanup for Unregistered Push Failures

Layer / File(s) Summary
Failed notification lookup
src/main/java/in/koreatech/koin/domain/notification/repository/NotificationRepository.java
New findUnregisteredPushFailureUserIds(start, end) method queries user IDs for notifications with pushSuccess = false and fcmMessagingErrorCode = 'UNREGISTERED' within a time window.
Cleanup service and daily scheduler
src/main/java/in/koreatech/koin/domain/notification/service/NotificationDeviceTokenCleanupService.java, src/main/java/in/koreatech/koin/domain/notification/scheduler/NotificationDeviceTokenCleanupScheduler.java, src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java
NotificationDeviceTokenCleanupService.cleanupYesterdayUnregisteredDeviceTokens() queries yesterday's unregistered failures and clears matching device tokens via new UserRepository.clearDeviceTokensByIdIn(...); NotificationDeviceTokenCleanupScheduler runs daily on schedule with error handling and logging.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • BCSDLab/KOIN_API_V2#2278: Refactors FcmClient to support batch sending; this PR extends it to return per-message results and wire them into Notification persistence.
  • BCSDLab/KOIN_API_V2#2263: Adds FcmClient.sendMessages(List<FcmSendRequest>) batch API; this PR changes it to return List<FcmSendResponse> and captures results.
  • BCSDLab/KOIN_API_V2#2165: Modifies the notification delivery pipeline; this PR's result-tracking work enables per-user delivery monitoring and failure handling.

Suggested labels

공통

Suggested reviewers

  • BaeJinho4028
  • dh2906
  • ImTotem

Poem

🐰 A rabbit hops through FCM logs,
Catching results that once were lost,
Each message tracked from send to fate,
And troubled tokens cleaned up straight,
Now push delivery's tale is told! 📬

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: FCM 404 에러 핸들링 추가' clearly indicates the main change: adding FCM 404 error handling, which aligns with the changeset's focus on FCM error tracking and device token cleanup.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #2282 objectives: FCM error handling is implemented through result tracking, error codes are persisted, and automated device token cleanup via scheduler is included.
Out of Scope Changes check ✅ Passed All changes are directly scoped to FCM 404 error handling: FcmSendResponse tracking, Notification persistence fields, device token cleanup scheduler, and supporting repository methods are all aligned with the issue objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/2282-add-fcm-404-error-handling

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the 공통 백엔드 공통으로 작업할 이슈입니다. label May 30, 2026
@github-actions github-actions Bot requested review from dh2906 and kih1015 May 30, 2026 04:47
@github-actions
Copy link
Copy Markdown

Unit Test Results

669 tests   666 ✔️  1m 17s ⏱️
167 suites      3 💤
167 files        0

Results for commit 97382a4.

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

🧹 Nitpick comments (2)
src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java (1)

70-80: 💤 Low value

Consider adding a defensive size check for list alignment.

The method assumes notifications and responses have identical sizes. While this is true given the current FcmClient.sendMessages implementation, a defensive check would prevent IndexOutOfBoundsException if the contract is accidentally violated.

🛡️ Optional defensive check
 private void markPushResults(List<Notification> notifications, List<FcmSendResponse> responses) {
+    if (notifications.size() != responses.size()) {
+        log.warn("알림과 FCM 응답 수 불일치. notifications={}, responses={}", notifications.size(), responses.size());
+        return;
+    }
     for (int index = 0; index < notifications.size(); index++) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java`
around lines 70 - 80, The markPushResults in NotificationService assumes
notifications and responses are the same length and can throw
IndexOutOfBoundsException; add a defensive size check and only iterate up to
Math.min(notifications.size(), responses.size()) (or throw a clear
IllegalStateException if sizes differ) so
Notification.markPushSuccess()/markPushFailure(...) are only called with
matching FcmSendResponse entries; reference the method markPushResults and the
types Notification and FcmSendResponse when making the change.
src/main/resources/db/migration/V236__add_fcm_result_to_notification.sql (1)

1-4: 💤 Low value

Consider adding an index for the cleanup scheduler query.

The cleanup scheduler will query notifications by is_push_success, fcm_messaging_error_code, and time range. If the notification table is large, consider adding a composite index to optimize these queries:

CREATE INDEX idx_notification_push_failure ON notification (is_push_success, fcm_messaging_error_code, created_at);

This is not blocking since the scheduler runs daily and the query volume is low.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/resources/db/migration/V236__add_fcm_result_to_notification.sql`
around lines 1 - 4, The migration adds is_push_success, fcm_error_code and
fcm_messaging_error_code to the notification table but doesn’t add an index for
the cleanup scheduler; add a composite index (e.g.
idx_notification_push_failure) on (is_push_success, fcm_messaging_error_code,
created_at) so queries filtering by is_push_success and fcm_messaging_error_code
and a time range are optimized; modify V236__add_fcm_result_to_notification.sql
to create that index after adding the columns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@src/main/java/in/koreatech/koin/domain/notification/repository/NotificationRepository.java`:
- Around line 29-42: The query in
NotificationRepository.findUnregisteredPushFailureUserIds relies on
User.updatedAt which is a coarse entity-wide audit field and can falsely skip
stale device tokens; change the predicate to use a token-specific timestamp or
persisted token on the Notification entity instead (e.g. add
Notification.deviceToken and/or Notification.deviceTokenUpdatedAt and persist
the token/timestamp when sending), then update the JPQL in
findUnregisteredPushFailureUserIds to compare n.deviceToken (or
n.deviceTokenUpdatedAt) against the sent token/timestamp rather than
n.user.updatedAt so only notifications referencing the same stale token are
considered for cleanup.

---

Nitpick comments:
In
`@src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java`:
- Around line 70-80: The markPushResults in NotificationService assumes
notifications and responses are the same length and can throw
IndexOutOfBoundsException; add a defensive size check and only iterate up to
Math.min(notifications.size(), responses.size()) (or throw a clear
IllegalStateException if sizes differ) so
Notification.markPushSuccess()/markPushFailure(...) are only called with
matching FcmSendResponse entries; reference the method markPushResults and the
types Notification and FcmSendResponse when making the change.

In `@src/main/resources/db/migration/V236__add_fcm_result_to_notification.sql`:
- Around line 1-4: The migration adds is_push_success, fcm_error_code and
fcm_messaging_error_code to the notification table but doesn’t add an index for
the cleanup scheduler; add a composite index (e.g.
idx_notification_push_failure) on (is_push_success, fcm_messaging_error_code,
created_at) so queries filtering by is_push_success and fcm_messaging_error_code
and a time range are optimized; modify V236__add_fcm_result_to_notification.sql
to create that index after adding the columns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6e675769-b3bb-4d65-a39f-efeeac94904b

📥 Commits

Reviewing files that changed from the base of the PR and between a5ba8b1 and 97382a4.

📒 Files selected for processing (11)
  • src/main/java/in/koreatech/koin/admin/notification/service/AdminNotificationService.java
  • src/main/java/in/koreatech/koin/domain/notification/model/Notification.java
  • src/main/java/in/koreatech/koin/domain/notification/repository/NotificationJdbcRepository.java
  • src/main/java/in/koreatech/koin/domain/notification/repository/NotificationRepository.java
  • src/main/java/in/koreatech/koin/domain/notification/scheduler/NotificationDeviceTokenCleanupScheduler.java
  • src/main/java/in/koreatech/koin/domain/notification/service/NotificationDeviceTokenCleanupService.java
  • src/main/java/in/koreatech/koin/domain/notification/service/NotificationService.java
  • src/main/java/in/koreatech/koin/domain/user/repository/UserRepository.java
  • src/main/java/in/koreatech/koin/infrastructure/fcm/FcmClient.java
  • src/main/java/in/koreatech/koin/infrastructure/fcm/FcmSendResponse.java
  • src/main/resources/db/migration/V236__add_fcm_result_to_notification.sql

Comment on lines +29 to +42
@Query("""
SELECT DISTINCT n.user.id
FROM Notification n
WHERE n.pushSuccess = false
AND n.fcmMessagingErrorCode = 'UNREGISTERED'
AND n.createdAt >= :start
AND n.createdAt < :end
AND n.user.deviceToken IS NOT NULL
AND n.user.updatedAt <= n.createdAt
""")
List<Integer> findUnregisteredPushFailureUserIds(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

updatedAt is too coarse to guard token freshness.

User.updatedAt is the entity-wide audit field from BaseEntity, so any later user-row update can make this predicate false even when the same invalid deviceToken is still stored. That means the cleanup job can silently skip exactly the stale tokens this PR is trying to remove. Please base this guard on token-specific state instead, e.g. persist the token used for the send on Notification or compare against a dedicated deviceTokenUpdatedAt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/main/java/in/koreatech/koin/domain/notification/repository/NotificationRepository.java`
around lines 29 - 42, The query in
NotificationRepository.findUnregisteredPushFailureUserIds relies on
User.updatedAt which is a coarse entity-wide audit field and can falsely skip
stale device tokens; change the predicate to use a token-specific timestamp or
persisted token on the Notification entity instead (e.g. add
Notification.deviceToken and/or Notification.deviceTokenUpdatedAt and persist
the token/timestamp when sending), then update the JPQL in
findUnregisteredPushFailureUserIds to compare n.deviceToken (or
n.deviceTokenUpdatedAt) against the sent token/timestamp rather than
n.user.updatedAt so only notifications referencing the same stale token are
considered for cleanup.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

공통 백엔드 공통으로 작업할 이슈입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[공통] FCM 404 에러 처리 로직 추가

1 participant