Skip to content

feat: Expo 푸시 알림 발송 연동#73

Merged
deli-minju merged 2 commits into
developfrom
feat/#72-push-notification-integration
May 26, 2026
Merged

feat: Expo 푸시 알림 발송 연동#73
deli-minju merged 2 commits into
developfrom
feat/#72-push-notification-integration

Conversation

@deli-minju
Copy link
Copy Markdown
Contributor

@deli-minju deli-minju commented May 26, 2026

📌 작업 요약

  • 요약:
    • Expo Push API 기반 실제 푸시 알림 발송 연동 구현
    • 알림 생성 커밋 이후 푸시 발송 dispatcher가 실행되도록 구성
    • 사용자 알림 수신 설정이 꺼진 경우 푸시 발송을 건너뛰고 SKIPPED delivery log를 남기도록 처리
    • 푸시 설정 비활성화, 활성 토큰 없음, 지원하지 않는 provider/platform 상황도 알림 보관함은 유지하고 delivery log에 추적되도록 처리
    • Expo 발송 성공/실패 결과를 notification_delivery_logsSENT/FAILED로 기록하도록 구현
    • 무효 토큰 응답 수신 시 해당 푸시 토큰을 비활성화하도록 처리
    • delivery log에 provider를 추적할 수 있도록 provider 컬럼 추가
    • 푸시 발송 관련 환경변수, Docker Compose 설정, 환경변수 문서 업데이트
    • 푸시 dispatcher 통합 테스트 추가
  • 관련 이슈: closes [FEAT] 푸시 알림 발송 연동 및 알림 API 완성 #72

🌿 브랜치 정보

  • Source: feat/#72-push-notification-integration
  • Target: develop (기본)

✅ 체크리스트

  • 브랜치 컨벤션 준수 (feat/refac/hotfix/chore/design/bugfix)
  • 커밋 컨벤션 준수 (feat/fix/refactor/docs/style/chore)
  • self-review 완료
  • 테스트 및 로컬 실행 확인 완료

🧪 테스트 결과

.\scripts\run-tests-with-postgres.ps1

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 푸시 알림 발송 기능 추가
    • 환경변수를 통한 푸시 알림 활성화/비활성화 설정 지원
    • 발송 기록 및 상태 추적 가능
  • Documentation

    • 푸시 알림 환경변수 설정 가이드 추가

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Warning

Review limit reached

@deli-minju, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 57 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a35dcd78-6333-49ba-982d-6fb10549e4e5

📥 Commits

Reviewing files that changed from the base of the PR and between 9ca3497 and acfede9.

📒 Files selected for processing (4)
  • src/main/java/com/gachi/be/domain/notification/service/NotificationPushDispatcher.java
  • src/main/java/com/gachi/be/domain/notification/service/PushNotificationClient.java
  • src/main/java/com/gachi/be/global/config/external/NotificationPushProperties.java
  • src/test/java/com/gachi/be/domain/notification/service/NotificationPushDispatcherIntegrationTest.java
📝 Walkthrough

변경 요약

Expo Push 알림 서비스와의 연동을 완성하여 서버에서 생성된 알림을 사용자 디바이스로 실제 푸시 발송하는 기능을 구현했습니다. 설정 관리, 도메인 모델, Expo API 클라이언트, 이벤트 기반 디스패처, 토큰 관리, 배달 로그 기록을 포함합니다.

변경 내용

Expo Push 알림 발송 연동

레이어 / 파일 요약
푸시 알림 설정 및 환경 구성
src/main/java/com/gachi/be/global/config/external/NotificationPushProperties.java, src/main/java/com/gachi/be/global/config/external/ExternalApiConfig.java, src/main/resources/application.yml, .env.example, deploy/.env.example, deploy/docker-compose.yml, docs/env.md
NotificationPushProperties로 Expo 제공자 설정(활성화 여부, API URL, 액세스 토큰, 타임아웃)을 Spring 설정 프로퍼티로 관리합니다. 환경변수와 YAML을 통해 배포 환경별 설정을 제어하고, 배포 및 운영 문서를 함께 업데이트합니다.
푸시 발송 도메인 계약
src/main/java/com/gachi/be/domain/notification/service/PushNotificationClient.java, src/main/java/com/gachi/be/domain/notification/service/PushSendResult.java, src/main/java/com/gachi/be/domain/notification/service/NotificationCreatedEvent.java
PushNotificationClient 인터페이스로 제공자 구현체가 준수할 계약(providerName, send)을 정의합니다. PushSendResult 레코드로 성공/실패/무효토큰 상태를 표현하고, NotificationCreatedEvent로 알림 생성 후 발송을 트리거합니다.
Expo Push API 클라이언트
src/main/java/com/gachi/be/domain/notification/service/ExpoPushNotificationClient.java
Expo API HTTP 호출로 푸시를 발송합니다. 요청 본문 직렬화, 응답 상태/메시지 파싱, 토큰 무효화 감지, 타임아웃 및 예외 처리(interrupt, I/O 오류)를 포함합니다.
데이터 모델 확장 (provider 추적)
src/main/java/com/gachi/be/domain/notification/entity/NotificationDeliveryLog.java, src/main/java/com/gachi/be/domain/notification/repository/NotificationDeliveryLogRepository.java, src/main/resources/db/migration/V15__notification_delivery_provider.sql
NotificationDeliveryLogprovider 필드를 추가하여 어느 서비스로 발송했는지 기록합니다. 저장소에 findAllByNotificationIdOrderByAttemptedAtAsc 메서드를 추가하고, DB 마이그레이션으로 컬럼과 인덱스를 생성합니다.
푸시 발송 디스패처
src/main/java/com/gachi/be/domain/notification/service/NotificationPushDispatcher.java
NotificationCreatedEvent 수신 시 사용자 상태, 알림 수신 설정, 전역 푸시 활성화를 검증하고, 활성 EXPO 토큰으로 발송합니다. 결과에 따라 SENT/FAILED/SKIPPED를 로깅하고, 무효 토큰을 자동으로 비활성화합니다.
NotificationService 이벤트 발행 통합
src/main/java/com/gachi/be/domain/notification/service/NotificationService.java
알림 저장 후 ApplicationEventPublisherNotificationCreatedEvent를 발행하여 디스패처를 트리거합니다. 수동 배달 결과 기록 시 제공자를 "MANUAL"로 설정합니다.
통합 테스트
src/test/java/com/gachi/be/domain/notification/service/NotificationPushDispatcherIntegrationTest.java
알림 생성 후 푸시 발송 기록, 사용자 알림 설정 비활성화 시 발송 스킵, 무효 토큰 자동 비활성화를 검증합니다. 테스트 전용 캡처 클라이언트로 실제 API 호출 없이 발송 흐름을 테스트합니다.

시퀀스 다이어그램

sequenceDiagram
  participant App as React Native 앱
  participant Server as NotificationService
  participant EventBus as EventPublisher
  participant Dispatcher as NotificationPushDispatcher
  participant Client as ExpoPushNotificationClient
  participant ExpoAPI as Expo Push API
  participant DB as Database
  
  App->>Server: 알림 생성 요청
  Server->>DB: notification 저장
  Server->>EventBus: NotificationCreatedEvent 발행
  EventBus->>Dispatcher: 이벤트 수신 (트랜잭션 커밋 이후)
  Dispatcher->>DB: 사용자/토큰 조회
  Dispatcher->>Dispatcher: 사용자 활성화 확인
  Dispatcher->>Dispatcher: 알림 수신 설정 확인
  Dispatcher->>DB: 활성 EXPO 토큰 조회
  Dispatcher->>Client: send(notification, token, payload)
  Client->>ExpoAPI: POST /api/v2/push/send
  ExpoAPI-->>Client: 응답 (status, messageId, error 등)
  Client->>Dispatcher: PushSendResult (sent/failed/invalidToken)
  Dispatcher->>DB: NotificationDeliveryLog 저장 (SENT/FAILED/SKIPPED)
  alt invalidToken인 경우
    Dispatcher->>DB: PushDeviceToken 비활성화 (enabled=false)
  end
Loading

예상 코드 리뷰 노력

🎯 4 (복잡도) | ⏱️ ~50분

관련 PR

  • GACHI-Project/GACHI-BE#71: 알림 도메인 초기 구현(컨트롤러/서비스/엔티티/저장소/토큰 관리)과 푸시 발송 연동 구현의 의존 관계가 있으며, PR #71에서 추가된 NotificationService, NotificationDeliveryLog, PushDeviceToken 등을 본 PR에서 확장합니다.

제안 라벨

feat

제안 검토자

  • Hminkyung
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목 '푸시 알림 발송 연동'은 Expo Push API 기반 푸시 알림 발송 연동 구현이라는 핵심 변경 사항을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 작업 요약, 브랜치 정보, 체크리스트, 테스트 결과를 모두 포함하며 템플릿 요구사항을 충족하고 있습니다.
Linked Issues check ✅ Passed PR의 모든 주요 변경사항이 #72의 요구사항을 충족하고 있습니다. Expo 푸시 클라이언트 구현, 알림 생성 후 자동 발송, 사용자 설정 반영, 발송 결과 기록, 무효 토큰 처리, 환경변수 설정, 통합 테스트 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #72의 푸시 알림 발송 연동 범위 내에 있습니다. 설정, 클라이언트, 이벤트 발행, 디스패처, 테스트 등 모두 푸시 발송 기능 구현과 직결되어 있습니다.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#72-push-notification-integration

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.

@deli-minju deli-minju self-assigned this May 26, 2026
@deli-minju deli-minju added the feat 새로운 기능 추가 작업 label May 26, 2026
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: 8

🤖 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/com/gachi/be/domain/notification/service/NotificationPushDispatcher.java`:
- Around line 55-57: Don't trust event.userId() directly; in
NotificationPushDispatcher replace lookups that use event.userId() (e.g.,
userRepository.findById(event.userId()) and similar token lookups around the
73-75 area) with the Notification's owner id and/or validate the event id
matches the notification owner: fetch the user by notification.ownerId() (or
notification.owner() accessor), if missing call saveSkipped(notification, null,
"...") and if event.userId() is present but does not equal
notification.ownerId() treat as mismatch and skip/log, and similarly ensure any
push token looked up (tokenRepository/findByUserId or similar method) belongs to
that notification owner before sending.
- Around line 107-123: saveSkipped currently calls saveDeliveryLog which
unconditionally writes pushNotificationClient.providerName() into
NotificationDeliveryLog, causing logs to show the wrong provider; change
saveSkipped and saveDeliveryLog signatures to accept the actual provider string
(or derive it from the PushDeviceToken via token.getProvider()/token.getType())
and use that provider value in the NotificationDeliveryLog.builder() instead of
pushNotificationClient.providerName(); update all callers of saveDeliveryLog
(including saveSkipped) to pass the correct provider for each branch
(disabled/unsupported/provider-specific) so the recorded provider matches the
real one.

In
`@src/main/java/com/gachi/be/domain/notification/service/PushNotificationClient.java`:
- Around line 12-13: PushNotificationClient의 send(Notification, PushDeviceToken,
Map<String, Object>)에서 사용되는 payload의 컴파일 타임 타입 안전성을 개선하세요: 현재 Map<String,
Object> 대신 향후 확장성을 고려해 공통 베이스 레코드나 sealed interface(예: PushPayload,
ExpoPushPayload 등)를 도입하고 send 메서드 시그니처를 send(Notification, PushDeviceToken,
PushPayload)로 변경하여 제공자별 페이로드 구조를 명확히 하세요; 당장은 Expo 전용이라면 ExpoPayload 레코드를 만들고
구현체를 통해 점진적으로 리팩토링하는 방안을 적용하세요.
- Around line 7-14: Add JavaDoc to the PushNotificationClient interface:
document providerName() explaining the returned provider identifier format
(e.g., constant name or unique id) and its intended use; document
send(Notification, PushDeviceToken, Map<String,Object>) describing when it may
throw exceptions (e.g., network, invalid token, validation), the expected keys
and value types in the payload map (required vs optional fields and any nested
structures), and the semantics of the returned PushSendResult (success,
transient failure, permanent failure, retry hint) so implementers know how to
populate it; also reference the Notification and PushDeviceToken inputs and any
thread-safety or blocking behavior expectations.

In
`@src/main/java/com/gachi/be/global/config/external/NotificationPushProperties.java`:
- Around line 20-23: NotificationPushProperties.Expo의 accessToken 필드가 빈 문자열로
초기화되어 있어 "미설정" 상태를 명확히 표현하지 못하므로 accessToken의 기본값을 null로 변경하고, 이 필드에 의존하는 코드(예:
토큰 존재 여부를 검사하는 로직)들이 null-safe 하도록 검사(or Optional 사용)으로 업데이트하세요; 구체적으로
NotificationPushProperties.Expo 클래스의 private String accessToken 초기화를 제거하거나 null로
설정하고, getAccessToken/usage 지점에서 빈 문자열 체크 대신 null 체크 또는
Objects.nonNull/Optional.ofNullable로 처리하도록 수정하세요.
- Around line 12-16: Add validation to the timeout fields in
NotificationPushProperties: annotate connectTimeoutSeconds and
readTimeoutSeconds with a constraint such as `@Min`(1) or `@Positive` to prevent
zero/negative (or unreasonably small) values, import the corresponding
javax.validation constraint, and ensure the class is validated at binding by
adding `@Validated` on NotificationPushProperties (or enabling validation on the
ConfigurationProperties consumer) so invalid values fail fast.

In
`@src/test/java/com/gachi/be/domain/notification/service/NotificationPushDispatcherIntegrationTest.java`:
- Around line 98-106: The helper method registerToken currently returns the
first element of
pushDeviceTokenRepository.findAllByUserIdAndEnabledTrueAndDeletedAtIsNull(user.getId()).get(0),
which is fragile; change registerToken to explicitly verify the repository
result (e.g., assert or throw if size != 1) or filter/select the expected token
before returning so the test fails clearly when multiple tokens exist; update
references in the test to use registerToken(User, String) that returns the
verified PushDeviceToken and keep symbols pushDeviceTokenRepository,
registerToken, and PushDeviceToken to locate the change.
- Around line 46-57: In dispatchSendsPushAndRecordsDeliveryLog add an assertion
that the push client was invoked exactly once: after registering the token and
before/after checking delivery logs assert the PushNotificationClient invocation
count (either assert sendCount == 1 on your test helper/mock or use
Mockito.verify(pushNotificationClient, times(1))). Reference the test method
dispatchSendsPushAndRecordsDeliveryLog and the PushNotificationClient (or the
mock instance used in NotificationPushDispatcherIntegrationTest) so the test
explicitly fails if the client isn't called once.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2f9bf940-fe5f-42ae-b77b-73d0fa16691f

📥 Commits

Reviewing files that changed from the base of the PR and between b7575ef and 9ca3497.

📒 Files selected for processing (17)
  • .env.example
  • deploy/.env.example
  • deploy/docker-compose.yml
  • docs/env.md
  • src/main/java/com/gachi/be/domain/notification/entity/NotificationDeliveryLog.java
  • src/main/java/com/gachi/be/domain/notification/repository/NotificationDeliveryLogRepository.java
  • src/main/java/com/gachi/be/domain/notification/service/ExpoPushNotificationClient.java
  • src/main/java/com/gachi/be/domain/notification/service/NotificationCreatedEvent.java
  • src/main/java/com/gachi/be/domain/notification/service/NotificationPushDispatcher.java
  • src/main/java/com/gachi/be/domain/notification/service/NotificationService.java
  • src/main/java/com/gachi/be/domain/notification/service/PushNotificationClient.java
  • src/main/java/com/gachi/be/domain/notification/service/PushSendResult.java
  • src/main/java/com/gachi/be/global/config/external/ExternalApiConfig.java
  • src/main/java/com/gachi/be/global/config/external/NotificationPushProperties.java
  • src/main/resources/application.yml
  • src/main/resources/db/migration/V15__notification_delivery_provider.sql
  • src/test/java/com/gachi/be/domain/notification/service/NotificationPushDispatcherIntegrationTest.java

Comment on lines +12 to +13
PushSendResult send(
Notification notification, PushDeviceToken pushDeviceToken, Map<String, Object> payload);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚖️ Poor tradeoff

payload 파라미터의 타입 안정성 개선을 고려하세요.

Map<String, Object> 타입은 유연하지만 컴파일 타임 타입 안정성이 없습니다. 제공자별로 필요한 payload 구조가 명확하다면, sealed interface나 공통 베이스 레코드를 도입하여 타입 안전성을 높이는 것을 향후 고려해볼 수 있습니다.

현재 단일 제공자(Expo)만 지원하는 상황에서는 Map으로 충분하지만, 제공자가 늘어날 경우 리팩토링을 검토하세요.

🤖 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/com/gachi/be/domain/notification/service/PushNotificationClient.java`
around lines 12 - 13, PushNotificationClient의 send(Notification,
PushDeviceToken, Map<String, Object>)에서 사용되는 payload의 컴파일 타임 타입 안전성을 개선하세요: 현재
Map<String, Object> 대신 향후 확장성을 고려해 공통 베이스 레코드나 sealed interface(예: PushPayload,
ExpoPushPayload 등)를 도입하고 send 메서드 시그니처를 send(Notification, PushDeviceToken,
PushPayload)로 변경하여 제공자별 페이로드 구조를 명확히 하세요; 당장은 Expo 전용이라면 ExpoPayload 레코드를 만들고
구현체를 통해 점진적으로 리팩토링하는 방안을 적용하세요.

@deli-minju deli-minju requested a review from Hminkyung May 26, 2026 16:21
Copy link
Copy Markdown
Collaborator

@Hminkyung Hminkyung left a comment

Choose a reason for hiding this comment

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

확인했씁니다!!! 고생하셨씁니당~~~~~

@deli-minju deli-minju merged commit 7f81258 into develop May 26, 2026
3 checks passed
@deli-minju deli-minju deleted the feat/#72-push-notification-integration branch May 26, 2026 16:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat 새로운 기능 추가 작업

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEAT] 푸시 알림 발송 연동 및 알림 API 완성

2 participants