Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | Summary |
|---|---|
FCM 포트 및 모델 backend/src/main/java/moadong/fcm/port/PushNotificationPort.java, backend/src/main/java/moadong/fcm/model/PushPayload.java |
푸시 전송 추상화 인터페이스(PushNotificationPort) 및 전송 페이로드 레코드(PushPayload) 추가 |
FCM 어댑터 backend/src/main/java/moadong/fcm/adapter/FirebasePushNotificationAdapter.java |
PushNotificationPort 구현체 추가 — PushPayload를 Firebase Message로 변환해 전송하고 성공/실패를 boolean으로 반환 |
엔티티/DTO 변경 backend/src/main/java/moadong/club/entity/Club.java, backend/src/main/java/moadong/club/payload/request/ClubRecruitmentInfoUpdateRequest.java |
Club.sendPushNotification(Message) 제거; ClubRecruitmentInfoUpdateRequest에 sendNotification 필드 및 shouldSendNotification() 추가 |
알림 팩토리/빌더 backend/src/main/java/moadong/club/util/ClubNotificationPayloadFactory.java, backend/src/main/java/moadong/club/util/RecruitmentStateNotificationBuilder.java, backend/src/main/java/moadong/club/util/RecruitmentDdayNotificationBuilder.java |
Message 기반 생성 로직을 PushPayload + payloadFactory로 리팩토링; 알림 본문 생성 책임 분리 |
상태 계산기 변경 backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java |
calculate 반환형 void→boolean(상태 변경 여부 반환)으로 변경; 기존의 알림 생성/전송 관련 메서드(및 FCM 의존성) 제거 |
서비스/노티파이어 변경 backend/src/main/java/moadong/club/service/ClubProfileService.java, .../RecruitmentDdayNotifier.java, .../RecruitmentStateChecker.java |
PushNotificationPort 주입으로 club.sendPushNotification(...) 호출 제거; 상태 변경 시 RecruitmentStateNotificationBuilder로 PushPayload 생성 후 포트로 전송 |
테스트 및 픽스처 backend/src/test/.../ClubProfileServiceDateTest.java, .../RecruitmentDdayNotifierTest.java, .../RecruitmentStateCheckerTest.java, .../ClubNotificationPayloadFactoryTest.java, .../RecruitmentStateCalculatorTest.java, .../RecruitmentStateNotificationBuilderTest.java, .../FirebasePushNotificationAdapterTest.java, .../fixture/ClubRequestFixture.java |
포트/빌더 모킹 추가, sendNotification 플래그 관련 테스트 추가/수정, 새 컴포넌트(Firebase adapter, payload factory, builders)에 대한 단위 테스트 추가 |
Sequence Diagram
sequenceDiagram
participant Client
participant Service as ClubProfileService
participant Calculator as RecruitmentStateCalculator
participant Builder as RecruitmentStateNotificationBuilder
participant Port as PushNotificationPort
participant Adapter as FirebasePushNotificationAdapter
participant Firebase as Firebase Cloud Messaging
Client->>Service: updateClubRecruitmentInfo(request)
Service->>Calculator: calculate(club, start, end)
Calculator-->>Service: boolean changed
alt changed && request.shouldSendNotification()
Service->>Builder: build(club, status)
Builder-->>Service: PushPayload
Service->>Port: send(PushPayload)
Port->>Adapter: send(PushPayload)
Adapter->>Firebase: send(message)
Firebase-->>Adapter: messageId
Adapter-->>Port: true
Port-->>Service: true
else no change or no notification
Service-->>Client: no notification
end
Service->>Service: save club & commit
Service-->>Client: updated club
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
- [feat] 네비게이션용 데이터 추가 #1177 — recruitment 알림의 네비게이션 데이터 구성 관련 로직(기존 buildNotificationData)과 연관.
- [release] v1.0.9 배포 #847 — Club 엔티티의
sendPushNotification(Message)책임을 다룬 PR로, 본 PR의 해당 메서드 제거와 직접적으로 연관. - [feature] 동아리 마감전 dday 알림표시 #1188 — D-day 알림 빌더/노티파이어 변경과 연관된 PR.
Suggested labels
✅ Test
Suggested reviewers
- Zepelown
- seongwon030
🚥 Pre-merge checks | ✅ 4 | ❌ 2
❌ Failed checks (2 warnings)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | 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. | |
| Merge Conflict Detection |
✅ 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 "[refactor] fcm 리팩토링" directly reflects the main objective of refactoring FCM-related code structure and dependencies. |
| Linked Issues check | ✅ Passed | All code changes align with MOA-640 objectives: PushNotificationPort 및 FirebasePushNotificationAdapter 도입으로 Firebase 캡슐화 [✓], PushPayload 표준화 [✓], 알림 빌더 및 팩토리 분리 [✓], sendNotification 정책 명시화 [✓], 단위 테스트 추가 [✓]. |
| Out of Scope Changes check | ✅ Passed | All changes are directly related to FCM 리팩토링: 책임 분리(Club 엔티티에서 pushNotification 메서드 제거), 포트/어댑터 도입, 빌더/팩토리 추가, 테스트 작성 모두 리팩토링 범위 내. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing touches
- 📝 Generate docstrings
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Post copyable unit tests in a comment
- Commit unit tests in branch
refactor/#1190-fcm-code-refactoring-MOA-640
⚔️ Resolve merge conflicts (beta)
- Auto-commit resolved conflicts to branch
refactor/#1190-fcm-code-refactoring-MOA-640 - Create stacked PR with resolved conflicts
- Post resolved changes as copyable diffs in a comment
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.
Comment @coderabbitai help to get the list of available commands and usage tips.
Test Results86 tests 86 ✅ 16s ⏱️ Results for commit 37b6765. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Fix all issues with AI agents
In `@backend/src/main/java/moadong/club/service/ClubProfileService.java`:
- Around line 60-67: The notification is being sent before the entity is
persisted: move the pushNotificationPort.send(...) call to after the successful
persistence (after clubRepository.save(...) completes) so notifications reflect
committed state; locate the code using pushNotificationPort.send and
recruitmentStateNotificationBuilder.build and ensure clubRepository.save (or the
method that persists the Club) runs and returns without exception before
invoking send, or alternatively emit a domain event after save and handle
notification dispatch in an event listener to guarantee eventual consistency.
In `@backend/src/main/java/moadong/club/service/RecruitmentDdayNotifier.java`:
- Around line 57-61: The code in RecruitmentDdayNotifier is ignoring the boolean
return from pushNotificationPort.send(), causing sentCount to increment even on
failures; update the block that calls
pushNotificationPort.send(notificationBuilder.build(club, daysLeft)) to capture
its boolean result, only increment sentCount when the result is true, and add a
log.warn or log.error when it returns false (include club.getId(),
club.getName(), and daysLeft in the failure log) so failures are visible for the
scheduler.
In `@backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java`:
- Around line 14-19: The method calculate in RecruitmentStateCalculator should
treat a null previous status as a special case: change the final return to
explicitly check for null so it only reports a change when oldStatus is non-null
and different (i.e., return oldStatus != null && oldStatus != newStatus); locate
the previous status via
club.getClubRecruitmentInformation().getClubRecruitmentStatus() and ensure
club.updateRecruitmentStatus(newStatus) behavior remains, and add/adjust the
RecruitmentStateCalculatorTest to cover the null-oldStatus scenario if needed.
In
`@backend/src/main/java/moadong/club/util/RecruitmentStateNotificationBuilder.java`:
- Around line 24-33: The build method in RecruitmentStateNotificationBuilder can
NPE when ClubRecruitmentInformation.getRecruitmentEnd() or getRecruitmentStart()
is null for OPEN/UPCOMING states; update build(Club, ClubRecruitmentStatus) to
defensively check the relevant dates retrieved via
club.getClubRecruitmentInformation().getRecruitmentEnd() and
getRecruitmentStart(), and if null use a safe fallback message (e.g., a generic
prompt like "모집 기간이 곧 공지됩니다. 확인해주세요!") instead of formatting the date, then call
payloadFactory.create(club, body) as before; ensure you only format when the
date is non-null to avoid NullPointerException.
In
`@backend/src/main/java/moadong/fcm/adapter/FirebasePushNotificationAdapter.java`:
- Around line 20-37: In FirebasePushNotificationAdapter.send, the
Message.builder() chain (creating the Message from payload) is outside the
try-catch so NPEs during message construction are not caught; move the entire
message creation into the try block (i.e., wrap Message.builder() ... .build()
together with firebaseMessaging.send(message) inside the same try) so any
exceptions from Message.builder() or payload fields are handled by the existing
catch in send; retain the existing log/error behavior in the catch.
🧹 Nitpick comments (7)
backend/src/test/java/moadong/fixture/ClubRequestFixture.java (1)
40-66: 주석 처리된 코드 정리를 고려해 주세요.TODO 주석과 함께 주석 처리된
createValidRequest()및createCustomRequest()메서드가 남아 있습니다. 현재 PR의 범위 밖이긴 하지만, 장기적으로 사용하지 않는 코드는 제거하고 필요 시 git history에서 복원하는 것이 좋습니다.backend/src/main/java/moadong/club/service/RecruitmentStateChecker.java (1)
42-52: 알림 전송과 저장 순서에 따른 중복 전송 가능성.
pushNotificationPort.send()가clubRepository.save()이전에 호출됩니다. save 실패 시 상태가 미반영되어 다음 스케줄링에서 동일 알림이 재전송될 수 있습니다. 또한send()의boolean반환값이 여기서도 무시되고 있습니다.현재 규모에서는 심각한 문제는 아니지만, 향후 고려할 사항으로 남겨둡니다.
backend/src/test/java/moadong/club/service/RecruitmentStateCheckerTest.java (2)
140-160:build()호출 시 상태값 검증이 누락되어 있습니다.Line 154에서
any()를 사용하여build의 status 인자를 검증하지 않고 있습니다. 실제RecruitmentStateChecker는calculate()이후 변경된 상태를build()에 전달하므로, 정확한 상태값(예:ClubRecruitmentStatus.OPEN)을 검증해야 테스트의 의미가 명확해집니다.🛠️ 제안
- when(recruitmentStateNotificationBuilder.build(eq(club), any())).thenReturn(payload); + when(info.getClubRecruitmentStatus()) + .thenReturn(ClubRecruitmentStatus.CLOSED) // before calculate + .thenReturn(ClubRecruitmentStatus.OPEN); // after calculate + when(recruitmentStateNotificationBuilder.build(eq(club), eq(ClubRecruitmentStatus.OPEN))).thenReturn(payload);
63-101: 기존 테스트에서 알림 미전송 검증 추가를 권장합니다.
calculate()가 기본적으로false를 반환하므로 알림이 전송되지 않아야 하지만, 이를 명시적으로 검증하지 않고 있습니다.verify(pushNotificationPort, never()).send(any())를 추가하면 의도가 더 명확해집니다.backend/src/test/java/moadong/club/service/ClubProfileServiceDateTest.java (1)
87-103: 테스트 간 중복 setup을 헬퍼로 추출하면 가독성이 좋아집니다.3개의 새 테스트에서
ClubRecruitmentInfoUpdateRequest생성,UserFixture.createUserDetails,Club생성,clubRepositorystubbing이 반복됩니다. 테스트 가독성과 유지보수를 위해 공통 부분을 private 헬퍼 메서드로 추출하는 것을 고려해 보세요.backend/src/main/java/moadong/club/service/ClubProfileService.java (1)
55-67: 알림 전송 로직이 두 메서드에서 중복됩니다.
updateClubRecruitmentInfo(Line 55-67)와updateClubRecruitmentInfoByClubId(Line 113-125)의 알림 전송 블록이 동일합니다. private 메서드로 추출하면 향후 알림 로직 변경 시 한 곳만 수정하면 됩니다.♻️ 알림 전송 로직 추출 제안
+ private void sendNotificationIfNeeded(Club club, ClubRecruitmentInfoUpdateRequest request) { + boolean changed = recruitmentStateCalculator.calculate( + club, + club.getClubRecruitmentInformation().getRecruitmentStart(), + club.getClubRecruitmentInformation().getRecruitmentEnd() + ); + if (changed && request.shouldSendNotification()) { + pushNotificationPort.send( + recruitmentStateNotificationBuilder.build( + club, + club.getClubRecruitmentInformation().getClubRecruitmentStatus() + ) + ); + } + }두 메서드에서 중복 블록을
sendNotificationIfNeeded(club, request)로 대체할 수 있습니다.Also applies to: 113-125
backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java (1)
36-40: Line 36의 조건이 중복됩니다 (기존 코드).Line 29에서
now.isBefore(recruitmentStartDate)가true인 경우를 이미 처리하고 반환하므로, Line 36에 도달한 시점에서!now.isBefore(recruitmentStartDate)는 항상true입니다. 간소화하면 가독성이 향상됩니다.♻️ 조건 간소화 제안
- if (!now.isBefore(recruitmentStartDate) && now.isBefore(recruitmentEndDate)) { + if (now.isBefore(recruitmentEndDate)) {
| if (NOTIFICATION_DAYS.contains(daysLeft)) { | ||
| log.info("D-Day 알림 전송 - clubId: {}, clubName: {}, D-{}", club.getId(), club.getName(), daysLeft); | ||
| club.sendPushNotification(notificationBuilder.build(club, daysLeft)); | ||
| pushNotificationPort.send(notificationBuilder.build(club, daysLeft)); | ||
| sentCount++; | ||
| } |
There was a problem hiding this comment.
send() 반환값이 무시되고 있습니다.
pushNotificationPort.send()는 boolean을 반환하지만 결과를 확인하지 않습니다. 전송 실패 시에도 sentCount가 증가하여 로그의 전송 건수가 부정확해집니다. 스케줄러 특성상 재시도가 없으므로 최소한 실패 로깅이 필요합니다.
🛠️ 제안
if (NOTIFICATION_DAYS.contains(daysLeft)) {
log.info("D-Day 알림 전송 - clubId: {}, clubName: {}, D-{}", club.getId(), club.getName(), daysLeft);
- pushNotificationPort.send(notificationBuilder.build(club, daysLeft));
- sentCount++;
+ boolean sent = pushNotificationPort.send(notificationBuilder.build(club, daysLeft));
+ if (sent) {
+ sentCount++;
+ } else {
+ log.warn("D-Day 알림 전송 실패 - clubId: {}, clubName: {}", club.getId(), club.getName());
+ }
}🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/service/RecruitmentDdayNotifier.java`
around lines 57 - 61, The code in RecruitmentDdayNotifier is ignoring the
boolean return from pushNotificationPort.send(), causing sentCount to increment
even on failures; update the block that calls
pushNotificationPort.send(notificationBuilder.build(club, daysLeft)) to capture
its boolean result, only increment sentCount when the result is true, and add a
log.warn or log.error when it returns false (include club.getId(),
club.getName(), and daysLeft in the failure log) so failures are visible for the
scheduler.
| public boolean calculate(Club club, ZonedDateTime recruitmentStartDate, ZonedDateTime recruitmentEndDate) { | ||
| ClubRecruitmentStatus oldStatus = club.getClubRecruitmentInformation().getClubRecruitmentStatus(); | ||
| ClubRecruitmentStatus newStatus = calculateRecruitmentStatus(recruitmentStartDate, recruitmentEndDate); | ||
| club.updateRecruitmentStatus(newStatus); | ||
|
|
||
| if (oldStatus == newStatus) | ||
| return; | ||
|
|
||
| Message message = buildRecruitmentMessage(club, newStatus); | ||
| club.sendPushNotification(message); | ||
| return oldStatus != newStatus; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find ClubRecruitmentInformation entity
echo "=== Searching for ClubRecruitmentInformation entity ==="
fd "ClubRecruitmentInformation.java" -type f --exec cat {} \;
echo ""
echo "=== Searching for Club entity ==="
fd "Club.java" -type f | head -5 | xargs -I {} sh -c 'echo "File: {}"; cat "{}"'Repository: Moadong/moadong
Length of output: 508
🏁 Script executed:
#!/bin/bash
# Find ClubRecruitmentInformation entity
echo "=== Searching for ClubRecruitmentInformation.java ==="
find backend -name "ClubRecruitmentInformation.java" -exec cat {} \;
echo ""
echo "=== Searching for Club.java ==="
find backend -name "Club.java" -exec cat {} \;
echo ""
echo "=== Searching for clubRecruitmentStatus field references ==="
rg -n "clubRecruitmentStatus" backend --type java -C2Repository: Moadong/moadong
Length of output: 14004
🏁 Script executed:
#!/bin/bash
echo "=== RecruitmentStateCalculatorTest content ==="
cat backend/src/test/java/moadong/club/util/RecruitmentStateCalculatorTest.javaRepository: Moadong/moadong
Length of output: 2501
oldStatus가 null일 수 있으므로 엣지 케이스 처리 필요
ClubRecruitmentInformation의 clubRecruitmentStatus 필드는 @NotNull 검증 어노테이션만 있고, 생성자에서 기본값이 설정되지 않습니다. Club의 생성자들이 ClubRecruitmentInformation.builder().build()로 초기화하므로, clubRecruitmentStatus는 null로 남을 수 있습니다.
현재 테스트(RecruitmentStateCalculatorTest)는 명시적으로 상태를 설정한 경우만 검증하므로, oldStatus가 null인 시나리오를 놓치고 있습니다. 이 경우 oldStatus != newStatus는 항상 true를 반환하여 최초 상태 설정 시에도 "변경됨"으로 간주됩니다.
다음 중 하나를 수행하세요:
- 명시적 null 확인 추가:
oldStatus != null && oldStatus != newStatus(의도가 초기 상태만 제외하려는 경우) - null 케이스에 대한 단위 테스트 추가 (의도가 null도 변경으로 처리하려는 경우)
- 동작 의도를 문서화
🤖 Prompt for AI Agents
In `@backend/src/main/java/moadong/club/util/RecruitmentStateCalculator.java`
around lines 14 - 19, The method calculate in RecruitmentStateCalculator should
treat a null previous status as a special case: change the final return to
explicitly check for null so it only reports a change when oldStatus is non-null
and different (i.e., return oldStatus != null && oldStatus != newStatus); locate
the previous status via
club.getClubRecruitmentInformation().getClubRecruitmentStatus() and ensure
club.updateRecruitmentStatus(newStatus) behavior remains, and add/adjust the
RecruitmentStateCalculatorTest to cover the null-oldStatus scenario if needed.
backend/src/main/java/moadong/club/util/RecruitmentStateNotificationBuilder.java
Show resolved
Hide resolved
| public boolean send(PushPayload payload) { | ||
| Message message = Message.builder() | ||
| .setNotification(Notification.builder() | ||
| .setTitle(payload.title()) | ||
| .setBody(payload.body()) | ||
| .build()) | ||
| .putAllData(payload.data()) | ||
| .setTopic(payload.topic()) | ||
| .build(); | ||
|
|
||
| try { | ||
| String messageId = firebaseMessaging.send(message); | ||
| log.info("FCM send success - topic: {}, messageId: {}", payload.topic(), messageId); | ||
| return true; | ||
| } catch (Exception e) { | ||
| log.error("FCM send failed - topic: {}, error: {}", payload.topic(), e.getMessage()); | ||
| return false; | ||
| } |
There was a problem hiding this comment.
Message 생성이 try-catch 블록 밖에 있어 예외가 누락될 수 있습니다.
Message.builder() 체인(Line 21-28)에서 payload의 필드가 null이면 NullPointerException이 발생하지만, try-catch는 firebaseMessaging.send() 호출만 감싸고 있습니다. Message 생성도 try 블록 안으로 이동하는 것이 안전합니다.
🐛 try-catch 범위 확장 제안
`@Override`
public boolean send(PushPayload payload) {
+ try {
Message message = Message.builder()
.setNotification(Notification.builder()
.setTitle(payload.title())
.setBody(payload.body())
.build())
.putAllData(payload.data())
.setTopic(payload.topic())
.build();
- try {
String messageId = firebaseMessaging.send(message);
log.info("FCM send success - topic: {}, messageId: {}", payload.topic(), messageId);
return true;
} catch (Exception e) {
log.error("FCM send failed - topic: {}, error: {}", payload.topic(), e.getMessage());
return false;
}
}🤖 Prompt for AI Agents
In
`@backend/src/main/java/moadong/fcm/adapter/FirebasePushNotificationAdapter.java`
around lines 20 - 37, In FirebasePushNotificationAdapter.send, the
Message.builder() chain (creating the Message from payload) is outside the
try-catch so NPEs during message construction are not caught; move the entire
message creation into the try block (i.e., wrap Message.builder() ... .build()
together with firebaseMessaging.send(message) inside the same try) so any
exceptions from Message.builder() or payload fields are handled by the existing
catch in send; retain the existing log/error behavior in the catch.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
backend/src/main/java/moadong/club/service/ClubProfileService.java (1)
50-72: 두 메서드 간 알림 전송 로직 중복
updateClubRecruitmentInfo와updateClubRecruitmentInfoByClubId의 상태 계산 → 저장 → 조건부 알림 전송 흐름이 거의 동일합니다. 공통 private 메서드로 추출하면 향후 유지보수가 수월해집니다.Also applies to: 107-130
backend/src/test/java/moadong/club/service/ClubProfileServiceDateTest.java (1)
128-150:changed=false일 때 알림 미전송 케이스 테스트 누락현재
changed=true && sendNotification=true만 검증하고 있습니다.changed=false && sendNotification=true일 때 알림이 전송되지 않는지 명시적으로 검증하는 테스트를 추가하면 상태 변경 조건 분기의 커버리지가 완성됩니다.💡 테스트 추가 예시
`@DisplayName`("모집 상태 변경이 없으면 sendNotification=true여도 알림을 보내지 않는다") `@Test` void 모집글_수정시_상태변경_없으면_알림을_보내지_않는다() { ClubRecruitmentInfoUpdateRequest request = new ClubRecruitmentInfoUpdateRequest( Instant.now(), Instant.now().plusSeconds(3600), "테스트 대상", "https://fake-url.com", true ); CustomUserDetails customUserDetails = UserFixture.createUserDetails("test"); Club club = new Club(); when(clubRepository.findClubByUserId(any())).thenReturn(Optional.of(club)); when(recruitmentStateCalculator.calculate(any(), any(), any())).thenReturn(false); clubProfileService.updateClubRecruitmentInfo(request, customUserDetails); verify(pushNotificationPort, never()).send(any()); }
#️⃣연관된 이슈
📝작업 내용
기존 FCM 전송 구조는 도메인/엔티티 계층과 Firebase SDK가 직접 결합되어 있어 테스트가 어려웠고, 알림 전송 정책 제어도 불명확했습니다.
이번 PR은 결합도 축소 + 테스트 가능성 강화 + 전송 정책 명시화를 목표로 리팩토링했습니다.
기존 코드의 한계
Club엔티티에서 Firebase SDK를 직접 호출해 도메인 모델이 외부 인프라에 의존주요 변경 사항
PushNotificationPort도입FirebasePushNotificationAdapter로 Firebase SDK 호출 캡슐화PushPayload기반으로 전송 데이터 표준화RecruitmentStateCalculator는 상태 계산 책임만 수행RecruitmentStateNotificationBuilder로 분리ClubNotificationPayloadFactory도입으로 topic/data/path/action 조립 로직 공통화ClubRecruitmentInfoUpdateRequest에sendNotification필드 추가sendNotification미지정(null) 시 기본값false적용sendNotification=false면 전송하지 않도록 처리sendNotification=true/false/null분기 테스트 추가PushPayload는 record 동등성(equals) 중심으로 검증기존 대비 개선점
sendNotification으로 명시적 제어중점적으로 리뷰받고 싶은 부분
논의하고 싶은 부분
🫡 참고사항
./gradlew unitTestSummary by CodeRabbit
New Features
Tests