refactor: 좋아요 알림 재발송 문제 해결 - #1440
Conversation
Walkthrough게시글 좋아요 처리를 마일스톤 도달 이벤트 방식으로 변경했습니다. 마일스톤 달성 이력과 기존 게시글 기준선 적재를 추가했습니다. 알림 조건을 검증한 뒤 상태를 기록하고, 커밋 후 푸시를 비동기로 전송합니다. Changes좋아요 마일스톤 알림
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant LikePostService
participant PostLikeMilestoneReachedListener
participant PostLikeMilestoneAchievementRecorder
participant PostLikeMilestoneNotificationProcessor
participant PostLikeMilestonePushListener
LikePostService->>PostLikeMilestoneReachedListener: PostLikeMilestoneReachedEvent 발행
PostLikeMilestoneReachedListener->>PostLikeMilestoneAchievementRecorder: 마일스톤 달성 기록 저장
PostLikeMilestoneAchievementRecorder->>PostLikeMilestoneNotificationProcessor: 신규 기록 처리
PostLikeMilestoneNotificationProcessor->>PostLikeMilestonePushListener: 커밋 후 PostLikeMilestonePushEvent 발행
PostLikeMilestonePushListener->>PostLikeMilestonePushListener: 수신자 조회 및 푸시 전송
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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 |
Test Results Summary181 files 181 suites 18s ⏱️ Results for commit a4c18a7. ♻️ This comment has been updated with latest results. |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (2)
app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessor.java (1)
67-92: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value선택: 메시지 생성 로직을 분리하면 가독성이 좋아집니다.
process는 검증, 메시지 조립, 영속화, 이벤트 발행을 모두 수행합니다. 메시지 조립을 작은 private 메서드나 별도 값 객체로 분리하면 흐름이 명확해집니다. 문구 변경 시 수정 지점도 한 곳으로 줄어듭니다.현재 동작에는 문제가 없습니다. 후속 작업으로 검토해 주세요.
🤖 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 `@app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessor.java` around lines 67 - 92, Update the process flow in PostLikeMilestoneNotificationProcessor by extracting the serviceTitle, serviceBody, and pushTitle construction into a focused private helper or value object, then reuse its results for persistence and event publication. Keep the existing Korean messages and notification behavior unchanged while making process primarily coordinate validation, persistence, and event publishing.app-main/src/main/resources/db/migration/V20260801133019__CreatePostLikeMilestoneAchievementTable.sql (1)
27-84: 🚀 Performance & Scalability | 🔵 Trivial운영 조언: 대량 백필의 실행 시간과 락 영향을 점검해 주세요.
이
INSERT ... SELECT는tb_like_post전체를 스캔합니다. 좋아요 데이터가 많으면 마이그레이션 시간이 길어지고 FK 대상 테이블에 잠금이 발생합니다. 배포 전에 운영 데이터 규모로 실행 시간을 측정해 주세요. 필요하면 백필을 별도 배치로 분리하고 마이그레이션은 DDL만 수행하는 방식을 검토해 주세요.참고로
WITH RECURSIVE thousand_milestones는 MySQL 기본cte_max_recursion_depth(1000)에 묶입니다. 좋아요 수가 1,000,000을 넘는 게시글이 생기면 재귀가 중단되고 마이그레이션이 실패합니다. 현재 데이터에서는 문제가 없어도 제한을 인지해 주세요.🤖 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 `@app-main/src/main/resources/db/migration/V20260801133019__CreatePostLikeMilestoneAchievementTable.sql` around lines 27 - 84, The migration performs an unbounded full-table backfill and its thousand_milestones CTE can exceed MySQL’s default recursion depth. Measure this INSERT backfill against production-sized data and, if runtime or locking is unacceptable, move it to a separate batch job while keeping the migration DDL-only; also ensure thousand_milestones handles post like counts above the cte_max_recursion_depth limit without migration failure.
🤖 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
`@app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementReader.java`:
- Around line 18-22: Update findById in PostLikeMilestoneAchievementReader to
replace IllegalStateException with the project’s BaseException and
notification-domain XxxErrorCode implementing BaseResponseCode. Add or reuse an
error-code entry for the missing milestone history and throw it with
achievementId, matching existing exception and ErrorCode naming conventions.
In
`@app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementRecorder.java`:
- Around line 25-32: Update PostLikeMilestoneAchievementRecorder.record to catch
post or liker lookup failures from PostReader.findById and
UserReader.findUserById, and create a suppressed achievement via
PostLikeMilestoneAchievement.suppress with the unavailable-target outcome
instead of propagating the exception. Preserve the existing savePendingIfAbsent
flow when both entities are found, and return the resulting achievement ID
consistently.
In
`@app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementWriter.java`:
- Around line 24-37: Update savePendingIfAbsent to absorb the unique-constraint
DataIntegrityViolationException raised by concurrent saveAndFlush calls,
returning Optional.empty() when another request has already recorded the same
post and milestone. Ensure the exception is handled at an appropriate separate
transaction boundary, accounting for the existing Propagation.REQUIRES_NEW
behavior so the caller’s transaction is not marked rollback-only.
In
`@app-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessorTest.java`:
- Around line 147-232: PostLikeMilestoneNotificationProcessorTest에 누락된
TARGET_UNAVAILABLE 경계 테스트를 추가하세요. liker가 null인 이력은 TARGET_UNAVAILABLE로 억제되고
notificationWriter, notificationSettingReader, blockReader, eventPublisher와
상호작용하지 않는지 검증하며, 기존
givenInactiveWriter_whenProcess_thenSuppressAsTargetUnavailable를 isInactive()와
isDropped() 조건을 모두 검증하도록 파라미터화하세요.
In
`@app-main/src/test/java/net/causw/app/main/domain/notification/notification/service/listener/PostLikeMilestoneReachedListenerTest.java`:
- Around line 46-55: Update
givenConsumedAchievement_whenHandle_thenSkipNotificationProcessing to verify
that notificationProcessor has no interactions at all, replacing the
argument-specific never().process("achievementId") check with Mockito
verifyNoInteractions and adding the required static import.
---
Nitpick comments:
In
`@app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessor.java`:
- Around line 67-92: Update the process flow in
PostLikeMilestoneNotificationProcessor by extracting the serviceTitle,
serviceBody, and pushTitle construction into a focused private helper or value
object, then reuse its results for persistence and event publication. Keep the
existing Korean messages and notification behavior unchanged while making
process primarily coordinate validation, persistence, and event publishing.
In
`@app-main/src/main/resources/db/migration/V20260801133019__CreatePostLikeMilestoneAchievementTable.sql`:
- Around line 27-84: The migration performs an unbounded full-table backfill and
its thousand_milestones CTE can exceed MySQL’s default recursion depth. Measure
this INSERT backfill against production-sized data and, if runtime or locking is
unacceptable, move it to a separate batch job while keeping the migration
DDL-only; also ensure thousand_milestones handles post like counts above the
cte_max_recursion_depth limit without migration failure.
🪄 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: CHILL
Plan: Pro Plus
Run ID: 724ab3f7-838d-4136-9970-f161df4f5adb
📒 Files selected for processing (27)
app-main/src/main/java/net/causw/app/main/domain/community/post/service/LikePostService.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/entity/PostLikeMilestoneAchievement.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/enums/PostLikeMilestoneAchievementStatus.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/enums/PostLikeMilestoneSuppressionReason.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/event/PostLikeMilestonePushEvent.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/event/PostLikeMilestoneReachedEvent.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/event/PostLikedEvent.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/repository/PostLikeMilestoneAchievementRepository.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementReader.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementRecorder.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementWriter.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessor.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/listener/LikePostNotificationListener.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/listener/PostLikeMilestonePushListener.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/listener/PostLikeMilestoneReachedListener.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/policy/LikePostNotificationPolicy.javaapp-main/src/main/resources/db/migration/V20260801133019__CreatePostLikeMilestoneAchievementTable.sqlapp-main/src/test/java/net/causw/app/main/SchemaValidationTest.javaapp-main/src/test/java/net/causw/app/main/domain/community/post/service/LikePostServiceTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/entity/PostLikeMilestoneAchievementTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementRecorderTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementWriterTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessorTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/listener/LikePostNotificationListenerTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/listener/PostLikeMilestonePushListenerTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/listener/PostLikeMilestoneReachedListenerTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/policy/LikePostNotificationPolicyTest.java
💤 Files with no reviewable changes (3)
- app-main/src/main/java/net/causw/app/main/domain/notification/notification/event/PostLikedEvent.java
- app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/listener/LikePostNotificationListener.java
- app-main/src/test/java/net/causw/app/main/domain/notification/notification/service/listener/LikePostNotificationListenerTest.java
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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
`@app-main/src/main/resources/db/migration/V20260801170000__BaselineEarlyPostLikeMilestones.sql`:
- Around line 21-39: Update the early_milestones CTE and baseline INSERT to
cover every fixed milestone defined by LikePostNotificationPolicy, not only 1–4,
and add each 1,000-unit milestone less than or equal to
post_like_counts.like_count. Reuse the policy’s milestone definitions, preserve
the existing BASELINED records and filtering by current like count, and ensure
each applicable milestone is inserted once per post.
🪄 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: CHILL
Plan: Pro Plus
Run ID: b85f0d82-767e-4c22-9978-c9eb182a5d27
📒 Files selected for processing (9)
app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementReader.javaapp-main/src/main/java/net/causw/app/main/domain/notification/notification/service/policy/LikePostNotificationPolicy.javaapp-main/src/main/java/net/causw/app/main/shared/exception/errorcode/PostLikeMilestoneAchievementErrorCode.javaapp-main/src/main/resources/db/migration/V20260801170000__BaselineEarlyPostLikeMilestones.sqlapp-main/src/test/java/net/causw/app/main/domain/community/post/service/LikePostServiceTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementReaderTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessorTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/listener/PostLikeMilestoneReachedListenerTest.javaapp-main/src/test/java/net/causw/app/main/domain/notification/notification/service/policy/LikePostNotificationPolicyTest.java
🚧 Files skipped from review as they are similar to previous changes (5)
- app-main/src/test/java/net/causw/app/main/domain/notification/notification/service/listener/PostLikeMilestoneReachedListenerTest.java
- app-main/src/test/java/net/causw/app/main/domain/notification/notification/service/policy/LikePostNotificationPolicyTest.java
- app-main/src/main/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneAchievementReader.java
- app-main/src/test/java/net/causw/app/main/domain/community/post/service/LikePostServiceTest.java
- app-main/src/test/java/net/causw/app/main/domain/notification/notification/service/implementation/PostLikeMilestoneNotificationProcessorTest.java
| early_milestones AS ( | ||
| SELECT 1 AS milestone_count | ||
| UNION ALL SELECT 2 | ||
| UNION ALL SELECT 3 | ||
| UNION ALL SELECT 4 | ||
| ) | ||
| SELECT | ||
| UUID(), | ||
| NOW(6), | ||
| NOW(6), | ||
| post_like_counts.post_id, | ||
| NULL, | ||
| early_milestones.milestone_count, | ||
| 'BASELINED', | ||
| NULL, | ||
| NULL | ||
| FROM post_like_counts | ||
| INNER JOIN early_milestones | ||
| ON early_milestones.milestone_count <= post_like_counts.like_count; |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
모든 기존 마일스톤을 기준선으로 기록해 주세요.
Line 21-26은 1~4만 BASELINED로 저장합니다. 따라서 이미 좋아요 5개, 10개, 50개 이상에 도달한 기존 게시글에는 해당 이력이 없습니다.
이 게시글이 좋아요 취소 후 기존 마일스톤에 다시 도달하면 새 이력이 생성됩니다. 이후 알림 처리도 다시 실행될 수 있습니다. 이는 중복 알림 방지 목표와 맞지 않습니다.
LikePostNotificationPolicy의 모든 고정 마일스톤과 현재 좋아요 수 이하의 1,000 단위 마일스톤을 기준선으로 삽입해 주세요.
수정 방향 예시
-early_milestones AS (
+policy_milestones AS (
SELECT 1 AS milestone_count
UNION ALL SELECT 2
UNION ALL SELECT 3
UNION ALL SELECT 4
+ UNION ALL SELECT 5
+ UNION ALL SELECT 10
+ UNION ALL SELECT 50
+ UNION ALL SELECT 100
+ UNION ALL SELECT 500
+ -- 현재 최대 좋아요 수 이하의 1,000 단위 마일스톤도 생성
)
...
-INNER JOIN early_milestones
- ON early_milestones.milestone_count <= post_like_counts.like_count;
+INNER JOIN policy_milestones
+ ON policy_milestones.milestone_count <= post_like_counts.like_count;🤖 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
`@app-main/src/main/resources/db/migration/V20260801170000__BaselineEarlyPostLikeMilestones.sql`
around lines 21 - 39, Update the early_milestones CTE and baseline INSERT to
cover every fixed milestone defined by LikePostNotificationPolicy, not only 1–4,
and add each 1,000-unit milestone less than or equal to
post_like_counts.like_count. Reuse the policy’s milestone definitions, preserve
the existing BASELINED records and filtering by current like count, and ensure
each applicable milestone is inserted once per post.
🚩 관련사항
Closes #1439
📢 전달사항
좋아요 알림이 발송된 뒤 좋아요를 취소하고 다시 눌러 같은 마일스톤에 재도달하면, 동일한 알림이 반복해서 발송되는 문제가 있었습니다.
이를 해결하기 위해 게시글과 마일스톤 조합을 영구적으로 기록하는
PostLikeMilestoneAchievement를 추가했습니다.(post_id, milestone_count)조합에 유니크 제약조건을 이용해 방지합니다.기존 마일스톤 정책은 그대로 유지했습니다.
기존 게시글은 마이그레이션 시점의 좋아요 수를 기준으로 이미 도달한 마일스톤을
BASELINED상태로 기록합니다. 이 과정에서는 과거 알림을 발송하지 않습니다.좋아요 저장을 우선하기 위해 알림 처리는 좋아요 트랜잭션 커밋 후 시작합니다. 이벤트 흐름은 아래 두 종류로 단순화했습니다.
PostLikeMilestoneReachedEvent→ 마일스톤 이력 기록
→ 알림·알림 로그·처리 상태 저장
→
PostLikeMilestonePushEvent→ DB 커밋 후 푸시 발송
본인 좋아요, 알림 설정 OFF, 차단 관계, 삭제된 게시글·게시판 또는 탈퇴한 작성자는 알림을 발송하지 않되 각각의 사유와 함께 저장합니다.
정상 알림은
Notification,NotificationLog, 마일스톤 처리 상태를 하나의 트랜잭션에서 저장합니다. 이 트랜잭션이 커밋된 뒤에만 푸시를 요청합니다.📸 스크린샷
📃 진행사항
⚙️ 기타사항
서비스 규모에 맞춰 동시성은 일단 고려하지 않았습니다.
개발기간: 2026.08.01
Summary by CodeRabbit
새로운 기능
개선 사항