Conversation
Walkthrough알림 기능을 구현하고 기존 서비스에 통합합니다. 금전 거래, 팔로우, 게시글 구매 및 리뷰 등의 이벤트에서 알림을 생성하고, 만료된 알림을 정리하는 스케줄러를 추가합니다. 여러 인프라 클래스를 공통 패키지에서 인프라 패키지로 이동시킵니다. Changes
Sequence DiagramsequenceDiagram
participant User
participant Service as 서비스<br/>(Follow/Coin/Review 등)
participant NS as NotificationService
participant NR as NotificationRepository
participant Scheduler as DailyScheduler
User->>Service: 이벤트 발생<br/>(팔로우/코인이체/리뷰)
activate Service
Service->>Service: 핵심 비즈니스 로직 실행
Service->>NS: createNotification<br/>(recipientId, type, payload)
deactivate Service
activate NS
NS->>NS: generateTitle/Body<br/>(NotificationType 기반)
NS->>NS: 페이로드 JSON 직렬화
NS->>NR: save(NotificationEntity)
NR-->>NS: 저장 완료
deactivate NS
User->>NS: GET /api/notifications?page=0
activate NS
NS->>NR: findActiveNotifications<br/>(userId, now, pageable)
NR-->>NS: 활성 알림 페이지 반환
NS->>NS: 페이로드 역직렬화<br/>& 상태 READ로 변경<br/>(readAt + retention)
NS-->>User: GetNotificationsResponse 페이지 반환
deactivate NS
User->>NS: GET /api/notifications/unread-count
activate NS
NS->>NR: countActiveByRecipientIdAndStatus<br/>(userId, UNREAD, now)
NR-->>NS: 미읽음 개수
NS-->>User: GetUnreadCountResponse 반환
deactivate NS
Note over Scheduler: 매일 자정 0시 0분 0초
activate Scheduler
Scheduler->>NS: cleanupExpiredNotifications()
NS->>NR: deleteByStatusAndExpiresAtBefore<br/>(READ, now)
NR-->>NS: 만료 알림 삭제 완료
deactivate Scheduler
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 상세 검토 영역
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (8)
src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java (1)
6-6: 불필요한 주석 제거 필요프로덕션 코드에서는
// 추가됨과 같은 임시 주석을 제거해야 합니다.- String senderEmail, // 추가됨 + String senderEmail,src/main/java/hanium/modic/backend/common/scheduler/DailyScheduler.java (2)
16-19: 타임존 설정을 명시하는 것을 권장합니다.현재 cron 표현식은 서버의 기본 타임존을 사용합니다. 배포 환경에 따라 예상치 못한 시간에 실행될 수 있으므로,
zone속성으로 명시적인 타임존 지정을 고려해보세요.다음과 같이 타임존을 명시할 수 있습니다:
- @Scheduled(cron = "0 0 0 * * *") + @Scheduled(cron = "0 0 0 * * *", zone = "Asia/Seoul") public void runEveryMidnight() { notificationService.cleanupExpiredNotifications(); }
17-19: 메서드 접근 제어자를 제한하는 것을 고려하세요.스케줄러 메서드는 Spring에서만 호출되므로
public대신 package-private으로 변경할 수 있습니다. 단, 외부에서 수동 호출이 필요한 경우 현재 상태를 유지하세요.- public void runEveryMidnight() { + void runEveryMidnight() { notificationService.cleanupExpiredNotifications(); }src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java (1)
18-20: 팔로우 시 알림 생성 연동은 자연스럽지만, 중복 알림 여부를 한 번 더 점검하면 좋겠습니다.
FOLLOW분기에서followRepository.insertFollowIfExist(me.getId(), target.getId())호출 결과와 무관하게 항상notificationService.createNotification(...)이 호출되고 있습니다.
만약insertFollowIfExist가 “이미 팔로우 중이면 아무 것도 하지 않는” 형태라면, 같은 사용자가 반복해서 팔로우 요청을 보낼 때 대상 유저에게 중복 알림이 쌓일 수 있습니다.
이 경우, 저장 여부(또는 영향 row 수)를 반환하도록 리포지토리 메서드를 확장한 뒤 “실제로 새 팔로우가 생성된 경우에만” 알림을 발송하는 쪽이 UX 상 더 자연해 보입니다.그 외에는 팔로우 대상(
targetId)를 수신자,me를 액터로 두고NotificationType.FOLLOWED와NotificationPayload를 구성한 부분이 도메인 관점에서 직관적입니다.Also applies to: 33-41, 62-68
src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java (1)
54-64: 추가 쿼리 최적화를 고려해보세요.현재 구현은 알림 생성을 위해
UserEntity를 다시 조회하고 있습니다(Lines 54-55).NotificationPayload.builder가 userId, name, email을 모두 요구하는 것으로 보이는데, 만약 알림 생성 시 userId만으로도 충분하다면 불필요한 DB 조회를 줄일 수 있습니다.다만, 현재 방식이 트랜잭션 격리나 데이터 일관성을 위한 의도적인 설계일 수 있으므로 선택적으로 고려해주세요.
src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java (1)
14-15: 문자열 생성 방식 개선을 고려해보세요.현재 문자열 연결 연산자(
+)를 사용하고 있는데, 가독성과 유지보수성 향상을 위해String.format()이나 텍스트 블록 사용을 고려해보세요.예시:
@Override public String generateBody(NotificationPayload payload) { - return sender(payload) + "님이 " + payload.amount() + " 코인을 보내셨습니다."; + return String.format("%s님이 %d 코인을 보내셨습니다.", sender(payload), payload.amount()); }다만 현재 구현도 충분히 읽기 쉬우므로 선택적으로 적용하시면 됩니다.
Also applies to: 26-30, 42-45, 57-57
src/main/java/hanium/modic/backend/web/notification/controller/NotificationController.java (1)
30-30: 생성자 접근 제한자를 검토하세요.
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)를 사용하고 있는데, 컨트롤러의 생성자는 일반적으로public이거나 접근 제한자를 지정하지 않습니다. Spring이 빈을 생성할 때protected생성자도 접근할 수 있지만, 관례상 명시적인 이유가 없다면 기본값을 사용하는 것이 좋습니다.-@RequiredArgsConstructor(access = AccessLevel.PROTECTED) +@RequiredArgsConstructorsrc/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java (1)
118-126: 직렬화 예외 처리를 개선하세요.Line 124에서 일반적인
RuntimeException을 던지고 있습니다. 더 구체적인 예외 타입을 사용하거나 커스텀 예외를 정의하는 것이 좋습니다.private String serializePayload(NotificationPayload payload) { try { return objectMapper.writeValueAsString(payload); } catch (Exception e) { - throw new RuntimeException("Failed to serialize notification payload", e); + throw new AppException(ErrorCode.NOTIFICATION_SERIALIZATION_FAILED, e); } }참고: 이를 위해서는
ErrorCode에 적절한 에러 코드를 추가해야 합니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (45)
src/main/java/hanium/modic/backend/common/property/config/PropertyConfig.java(2 hunks)src/main/java/hanium/modic/backend/common/property/property/NotificationProperties.java(1 hunks)src/main/java/hanium/modic/backend/common/scheduler/DailyScheduler.java(1 hunks)src/main/java/hanium/modic/backend/common/scheduler/config/SchedulingConfig.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java(4 hunks)src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedDlqListener.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/DlqListener.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiResponseSseService.java(1 hunks)src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java(1 hunks)src/main/java/hanium/modic/backend/domain/follow/service/FollowService.java(3 hunks)src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java(1 hunks)src/main/java/hanium/modic/backend/domain/notification/dto/GetUnreadCountResponse.java(1 hunks)src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java(1 hunks)src/main/java/hanium/modic/backend/domain/notification/entity/NotificationEntity.java(1 hunks)src/main/java/hanium/modic/backend/domain/notification/enums/NotificationStatus.java(1 hunks)src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java(1 hunks)src/main/java/hanium/modic/backend/domain/notification/repository/NotificationRepository.java(1 hunks)src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java(1 hunks)src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java(1 hunks)src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java(3 hunks)src/main/java/hanium/modic/backend/domain/ticket/service/TicketService.java(1 hunks)src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java(4 hunks)src/main/java/hanium/modic/backend/domain/user/service/UserVoteStreakService.java(1 hunks)src/main/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListener.java(1 hunks)src/main/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestService.java(1 hunks)src/main/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardService.java(1 hunks)src/main/java/hanium/modic/backend/domain/vote/service/VotingService.java(1 hunks)src/main/java/hanium/modic/backend/infra/amqp/config/RabbitMqConfig.java(1 hunks)src/main/java/hanium/modic/backend/infra/amqp/service/MessageQueueService.java(1 hunks)src/main/java/hanium/modic/backend/infra/redis/config/RedisConfig.java(1 hunks)src/main/java/hanium/modic/backend/infra/redis/config/RedissonConfig.java(1 hunks)src/main/java/hanium/modic/backend/infra/redis/distributedLock/AopForTransaction.java(1 hunks)src/main/java/hanium/modic/backend/infra/redis/distributedLock/DistributionLockExecutor.java(1 hunks)src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java(1 hunks)src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockOptions.java(1 hunks)src/main/java/hanium/modic/backend/infra/sse/service/EmitterService.java(1 hunks)src/main/java/hanium/modic/backend/web/notification/controller/NotificationController.java(1 hunks)src/main/java/hanium/modic/backend/web/notification/dto/response/NotificationUnreadCountResponse.java(1 hunks)src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java(4 hunks)src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java(3 hunks)src/test/java/hanium/modic/backend/domain/user/service/UserVoteStreakServiceTest.java(1 hunks)src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java(1 hunks)src/test/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardServiceTest.java(1 hunks)src/test/java/hanium/modic/backend/domain/vote/service/VotingServiceTest.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/main/java/hanium/modic/backend/common/scheduler/config/SchedulingConfig.java (1)
src/main/java/hanium/modic/backend/common/property/config/PropertyConfig.java (1)
Configuration(20-36)
src/main/java/hanium/modic/backend/domain/notification/entity/NotificationEntity.java (1)
src/main/java/hanium/modic/backend/common/property/property/NotificationProperties.java (1)
Getter(10-31)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (48)
src/main/java/hanium/modic/backend/domain/ticket/service/TicketService.java (1)
10-10: 패키지 리팩토링 변경사항 확인 완료인프라 관련 클래스들을
common패키지에서infra패키지로 이동시킨 변경사항이 적절하게 반영되었습니다.src/main/java/hanium/modic/backend/domain/notification/dto/NotificationPayload.java (1)
3-70: 레코드와 빌더 패턴 구현 확인 완료필수 필드(senderId, senderNickname, senderEmail)와 선택적 필드를 적절히 구분한 빌더 패턴이 잘 구현되어 있습니다.
empty()팩토리 메서드도 기본값 처리에 유용합니다.src/main/java/hanium/modic/backend/infra/sse/service/EmitterService.java (1)
1-1: 패키지 리팩토링 변경사항 확인 완료SSE 서비스를 인프라 패키지로 이동시킨 변경사항이 적절합니다.
src/main/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardService.java (1)
11-11: 패키지 리팩토링 변경사항 확인 완료LockManager 임포트 경로가 새로운 인프라 패키지 구조에 맞게 업데이트되었습니다.
src/test/java/hanium/modic/backend/domain/user/service/UserVoteStreakServiceTest.java (1)
19-19: 테스트 코드 업데이트 확인 완료테스트에서 사용하는 LockManager 임포트 경로가 새로운 패키지 구조에 맞게 업데이트되었습니다.
src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedListener.java (1)
3-3: 패키지 리팩토링 변경사항 확인 완료RabbitMQ 설정 클래스의 임포트 경로가 인프라 패키지로 적절히 업데이트되었습니다.
src/main/java/hanium/modic/backend/domain/vote/service/VotingService.java (1)
14-14: 패키지 리팩토링 변경사항 확인 완료분산 락 매니저의 임포트 경로가 새로운 인프라 패키지 구조에 맞게 업데이트되었습니다.
src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiServerService.java (1)
15-15: 패키지 리팩토링 변경사항 확인 완료메시지 큐 서비스의 임포트 경로가 인프라 패키지로 적절히 업데이트되었습니다.
src/test/java/hanium/modic/backend/domain/vote/service/VoteCompletionRewardServiceTest.java (1)
19-19: 패키지 리팩토링이 올바르게 적용되었습니다.
LockManager의 import 경로가common.redis.distributedLock에서infra.redis.distributedLock으로 정상적으로 변경되었습니다. 테스트 로직은 영향을 받지 않습니다.src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/AiImageCreatedDlqListener.java (1)
3-3: 패키지 리팩토링이 올바르게 적용되었습니다.
RabbitMqConfig의 static import 경로가common.amqp.config에서infra.amqp.config로 정상적으로 변경되었습니다.src/main/java/hanium/modic/backend/domain/postLike/service/PostLikeService.java (1)
15-15: 패키지 리팩토링이 올바르게 적용되었습니다.
LockManager의 import 경로가common.redis.distributedLock에서infra.redis.distributedLock으로 정상적으로 변경되었습니다.src/main/java/hanium/modic/backend/domain/ai/aiServer/listener/DlqListener.java (1)
3-3: 패키지 리팩토링이 올바르게 적용되었습니다.
RabbitMqConfig의 static import 경로가common.amqp.config에서infra.amqp.config로 정상적으로 변경되었습니다.src/test/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestServiceTest.java (1)
3-3: 패키지 리팩토링이 올바르게 적용되었습니다.
RabbitMqConfig의 static import 경로가common.amqp.config에서infra.amqp.config로 정상적으로 변경되었습니다.src/main/java/hanium/modic/backend/domain/vote/listener/SimilarityCheckListener.java (1)
4-4: 패키지 리팩토링이 올바르게 적용되었습니다.
RabbitMqConfig의 static import 경로가common.amqp.config에서infra.amqp.config로 정상적으로 변경되었습니다.src/test/java/hanium/modic/backend/domain/vote/service/VotingServiceTest.java (1)
14-14: 패키지 리팩토링이 올바르게 적용되었습니다.
LockManager의 import 경로가common.redis.distributedLock에서infra.redis.distributedLock으로 정상적으로 변경되었습니다.src/main/java/hanium/modic/backend/domain/user/service/UserVoteStreakService.java (1)
7-7: 패키지 리팩토링이 올바르게 적용되었습니다.인프라 관련 클래스를
infra패키지로 이동하는 것은 적절한 구조 개선입니다.src/main/java/hanium/modic/backend/domain/ai/aiServer/service/AiResponseSseService.java (1)
11-11: SSE 서비스 임포트 경로 업데이트가 적절합니다.인프라 계층 분리에 따른 일관된 패키지 구조 개선입니다.
src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockManager.java (1)
1-1: 인프라 패키지로의 이동이 적절합니다.분산 락 매니저는 인프라 계층에 위치하는 것이 올바른 아키텍처 구조입니다.
src/main/java/hanium/modic/backend/domain/vote/service/AiSimilarityRequestService.java (1)
3-3: RabbitMQ 설정 임포트 경로 변경이 올바릅니다.AMQP 설정의 인프라 패키지 이동에 맞춰 정적 임포트가 적절히 업데이트되었습니다.
src/main/java/hanium/modic/backend/infra/redis/distributedLock/AopForTransaction.java (1)
1-1: 트랜잭션 AOP 컴포넌트의 패키지 이동이 적절합니다.분산 락 관련 유틸리티 클래스들이 일관되게 인프라 패키지로 이동되었습니다.
src/main/java/hanium/modic/backend/infra/redis/distributedLock/LockOptions.java (1)
1-1: 락 옵션 클래스의 패키지 이동이 일관됩니다.분산 락 관련 모든 컴포넌트가 인프라 패키지로 일관되게 재구성되었습니다.
src/main/java/hanium/modic/backend/common/scheduler/config/SchedulingConfig.java (1)
1-9: 스케줄링 설정이 올바르게 추가되었습니다.알림 기능 구현의 일환으로 스케줄링 지원을 활성화하는 것은 적절합니다. 만료된 알림 정리 등의 주기적 작업에 필요한 설정입니다.
src/main/java/hanium/modic/backend/infra/redis/config/RedissonConfig.java (1)
1-1: Redisson 설정의 패키지 이동이 적절합니다.Redis 관련 설정과 인프라 컴포넌트들이 일관되게
infra.redis패키지로 정리되었습니다.src/main/java/hanium/modic/backend/infra/redis/config/RedisConfig.java (1)
1-1: Redis 설정 클래스를 infra 레이어로 이동한 구조 정리가 적절합니다.
common→infra.redis.config로 패키지를 옮겨 인프라 설정 클래스들의 위치가 일관되게 정리된 점이 유지보수 관점에서 좋습니다.src/main/java/hanium/modic/backend/domain/notification/enums/NotificationStatus.java (1)
1-6: 알림 상태 값을 READ/UNREAD 두 가지로 단순하게 분리한 설계가 명확합니다.현재 요구사항(읽음/안 읽음)에는 충분히 대응하고, 추후 상태가 늘어나더라도 상수만 추가하면 되도록 확장 여지도 열려 있어서 괜찮습니다.
src/main/java/hanium/modic/backend/common/property/config/PropertyConfig.java (1)
10-10: Notification 관련 설정을 공통 PropertyConfig 에 통합한 방향이 좋습니다.
NotificationProperties를@EnableConfigurationProperties목록에 추가해서notification.*설정을 타입 세이프하게 주입할 수 있게 한 점이 구성 관리 측면에서 깔끔합니다.Also applies to: 32-33
src/main/java/hanium/modic/backend/infra/amqp/config/RabbitMqConfig.java (1)
1-1: RabbitMQ 설정 클래스를 infra.amqp 패키지로 이동한 것이 구조적으로 자연스럽습니다.Redis 등 다른 인프라 설정 클래스들과 동일한
infra네이밍으로 맞춰서 레이어링이 더 명확해졌습니다.src/main/java/hanium/modic/backend/domain/notification/dto/GetUnreadCountResponse.java (1)
1-6: 읽지 않은 알림 개수를 별도 응답 DTO 로 분리한 구성이 명확합니다.도메인 서비스에서 계산한
unreadCount를 이 DTO를 통해 그대로 노출하므로, 이후 필드가 늘어나더라도 API 스펙 관리가 수월해질 것 같습니다.src/main/java/hanium/modic/backend/infra/redis/distributedLock/DistributionLockExecutor.java (1)
1-1: 패키지 구조 개선이 적절합니다.인프라 관련 코드를
common에서infra패키지로 이동한 것은 적절한 구조 개선입니다.src/main/java/hanium/modic/backend/infra/amqp/service/MessageQueueService.java (1)
1-3: LGTM!패키지 리팩토링이 일관되게 적용되었습니다.
src/test/java/hanium/modic/backend/domain/follow/service/FollowMockingServiceTest.java (1)
49-49: 테스트 모킹이 적절히 업데이트되었습니다.프로덕션 코드의
NotificationService의존성 추가에 맞춰 테스트도 올바르게 수정되었습니다.Also applies to: 78-78
src/main/java/hanium/modic/backend/web/notification/dto/response/NotificationUnreadCountResponse.java (1)
5-11: LGTM!간단한 DTO 구조로 문제없습니다.
src/main/java/hanium/modic/backend/domain/ai/aiChat/service/AiImagePermissionService.java (1)
81-91: 알림 생성 로직이 일관되게 구현되었습니다.티켓 구매 시에도 동일한 패턴으로 알림을 생성하고 있어 좋습니다.
src/main/java/hanium/modic/backend/domain/postReview/service/PostReviewService.java (1)
80-88: 알림 기능이 적절히 통합되었습니다.리뷰 생성 후 게시글 작성자에게 알림을 보내는 로직이 올바르게 구현되었습니다.
user파라미터를 활용하여 추가 DB 조회 없이 알림을 생성하는 점도 효율적입니다.src/main/java/hanium/modic/backend/domain/notification/enums/NotificationType.java (2)
91-96: 널 안전성 처리가 적절합니다.
sender()메서드에서 이메일이 없는 경우를 처리하고 있어 좋습니다.
56-57: 리뷰 내용 출력 시 보안을 고려하세요.Line 57에서
payload.reviewContent()를 직접 메시지에 포함하고 있습니다. 만약 이 알림이 웹에서 렌더링될 경우 XSS 공격 가능성이 있으므로, 프레젠테이션 레이어에서 적절한 이스케이핑 처리가 되는지 확인해주세요.src/main/java/hanium/modic/backend/domain/notification/dto/GetNotificationsResponse.java (1)
19-29: 이 리뷰 코멘트는 제거 가능합니다.
getCreateAt()메서드는 정상적입니다.NotificationEntity는BaseEntity를 상속하며,BaseEntity의@CreatedDate필드명이 의도적으로createAt으로 정의되어 있고 데이터베이스 컬럼명은create_at입니다. 이 네이밍은 코드베이스 전체에서 일관되게 사용되고 있습니다.src/main/java/hanium/modic/backend/domain/notification/repository/NotificationRepository.java (1)
15-44: LGTM! 리포지토리 쿼리 메서드가 잘 구현되었습니다.JPQL 쿼리가 올바르게 작성되었으며, 활성 알림 필터링 로직(expiresAt이 NULL이거나 미래)이 일관되게 적용되었습니다. 벌크 삭제를 위한
@Modifying어노테이션 사용도 적절합니다.src/main/java/hanium/modic/backend/domain/notification/entity/NotificationEntity.java (2)
59-78: LGTM! 엔티티 생성자가 잘 구현되었습니다.
status필드에 대한 null 안전성 처리(Line 72)가 적절하며, 기본값으로UNREAD를 설정하는 것이 합리적입니다.
84-91: LGTM! 읽음 처리 로직이 잘 구현되었습니다.이미 읽은 알림에 대한 중복 처리를 방지하는 가드 조건(Line 85-87)이 적절하며, retention 기간에 따른 만료 시간 설정 로직(Line 90)도 올바릅니다.
src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java (1)
52-59: 수신자 존재 확인 로직이 잘 추가되었습니다.자기 자신에게 송금하는 것을 방지하고(Line 53-55), 수신자가 존재하는지 검증하는 로직(Line 58-59)이 적절합니다.
src/main/java/hanium/modic/backend/web/notification/controller/NotificationController.java (2)
37-49: LGTM! 안 읽은 알림 수 조회 엔드포인트가 잘 구현되었습니다.인증된 사용자 확인, 서비스 호출, 응답 변환이 적절하게 처리되었습니다.
80-87: LGTM! 페이지네이션 파라미터 검증이 잘 구현되었습니다.페이지 번호와 크기에 대한 검증 제약 조건이 적절합니다(page >= 0, size 1-50).
src/main/java/hanium/modic/backend/domain/notification/service/NotificationService.java (3)
128-141: LGTM! 역직렬화 에러 처리가 잘 구현되었습니다.null/빈 문자열 체크(Line 131-133)와 역직렬화 실패 시 빈 페이로드 반환(Line 139)이 적절합니다. 실패 시 로깅도 포함되어 있어 디버깅에 유용합니다.
40-60: LGTM! 알림 조회 및 읽음 처리 로직이 잘 구현되었습니다.페이지네이션, 필터링, 읽음 처리가 트랜잭션 내에서 적절하게 처리되었습니다.
34-34: 리뷰 코멘트는 부정확합니다 - 현재 코드가 올바릅니다.검증 결과, BaseEntity의 실제 필드명은
createAt이며, NotificationService.java의 Line 34에서 사용 중인"createAt"이 정확히 일치합니다. 리뷰 코멘트에서 지적한"createdAt"은 BaseEntity에 존재하지 않습니다.Likely an incorrect or invalid review comment.
src/test/java/hanium/modic/backend/domain/ai/service/AiImagePermissionServiceTest.java (2)
55-72: LGTM! 알림 서비스 통합을 위한 테스트 업데이트가 잘 되었습니다.새로운 의존성인
NotificationService와UserEntityRepository에 대한 Mock 설정(Line 55-59, 71-72)이 적절하게 추가되었습니다. 테스트가 새로운 알림 생성 흐름을 올바르게 반영하고 있습니다.
137-138: LGTM! 티켓 구매 테스트의 Mock 설정이 잘 업데이트되었습니다.코인 구매 테스트와 동일하게 알림 관련 Mock 설정이 추가되어 일관성이 유지되고 있습니다.
| @Getter | ||
| @Setter |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
중복된 getter/setter를 제거하세요.
Lombok의 @Getter와 @Setter 어노테이션을 사용하면서 Line 20-26에서 명시적으로 getter/setter를 다시 정의하고 있어 중복입니다.
다음 중 하나를 선택하세요:
- Lombok 어노테이션을 유지하고 Line 20-26의 명시적 메서드를 제거
- Lombok 어노테이션을 제거하고 명시적 메서드만 유지
권장 방안: Lombok 어노테이션 유지
@Getter
@Setter
@ConfigurationProperties(prefix = "notification")
public class NotificationProperties {
/**
* 읽은 알림을 얼마 동안 유지할지(분 단위)
*/
private long readRetentionMinutes = Duration.ofDays(30).toMinutes(); // 기본값: 30일
- public long getReadRetentionMinutes() {
- return readRetentionMinutes;
- }
-
- public void setReadRetentionMinutes(long readRetentionMinutes) {
- this.readRetentionMinutes = readRetentionMinutes;
- }
-
public Duration getReadRetentionDuration() {
return Duration.ofMinutes(readRetentionMinutes);
}
}📝 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.
| @Getter | |
| @Setter | |
| @Getter | |
| @Setter | |
| @ConfigurationProperties(prefix = "notification") | |
| public class NotificationProperties { | |
| /** | |
| * 읽은 알림을 얼마 동안 유지할지(분 단위) | |
| */ | |
| private long readRetentionMinutes = Duration.ofDays(30).toMinutes(); // 기본값: 30일 | |
| public Duration getReadRetentionDuration() { | |
| return Duration.ofMinutes(readRetentionMinutes); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/hanium/modic/backend/common/property/property/NotificationProperties.java
around lines 10-11 and 20-26, you have duplicated getters/setters: Lombok
@Getter/@Setter are applied at the class level while explicit getter and setter
methods are defined on lines 20-26; remove the explicit methods (lines 20-26)
and keep the Lombok annotations so the compiler-generated accessors are used,
ensuring imports for Lombok remain and no other references rely on the removed
method bodies.
| // 알림 | ||
| notificationService.createNotification( | ||
| toUserId, | ||
| NotificationType.COIN_RECEIVED, | ||
| NotificationPayload.builder(fromUserId, toUser.getName(), toUser.getEmail()) | ||
| .amount(coin) | ||
| .build() | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
알림 페이로드에 이메일 포함 여부를 검토하세요.
Line 76에서 toUser.getEmail()을 알림 페이로드에 포함하고 있습니다. 이메일은 민감한 개인정보(PII)이므로 다음을 검토하세요:
- 알림 페이로드에 이메일이 반드시 필요한지 확인
- 필요하다면, 페이로드가 로그에 기록되지 않도록 보장
- 불필요하다면, 이메일을 페이로드에서 제거
다음 스크립트로 NotificationPayload가 로그에 기록되는지 확인하세요:
🏁 Script executed:
#!/bin/bash
# NotificationPayload가 로그에 기록되는지 확인
rg -n "log\." --type=java -A 2 -B 2 | rg -i "payload|notificationpayload"Length of output: 467
추가 검증이 필요합니다. 알림 페이로드의 로깅 범위와 이메일의 필요성을 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
# 1. 모든 로깅 문 확인 (notification, payload 관련)
echo "=== All logging with notification/payload ==="
rg -n "log\.(debug|info|warn|error)" --type=java -B 2 -A 2 | rg -i "notification|payload" -B 2 -A 2
echo ""
echo "=== NotificationPayload class definition ==="
fd -t f "NotificationPayload.java" --exec cat {}
echo ""
echo "=== Email usage in notification context ==="
rg -n "getEmail\|setEmail" --type=java -B 3 -A 3 | head -50Length of output: 2526
🏁 Script executed:
#!/bin/bash
# NotificationPayload가 로그에 노출되는 다른 경로 확인
echo "=== All places where NotificationPayload is handled ==="
rg -n "NotificationPayload|notification.*Payload" --type=java -B 1 -A 1 | grep -E "(\.builder|\.build|\.toString|log\.|return)" -B 1 -A 1 | head -80Length of output: 6624
이메일의 실제 필요성과 다른 로깅 경로를 확인하겠습니다.
🏁 Script executed:
#!/bin/bash
# sender() 메서드 구현 확인 및 이메일 사용 여부
echo "=== NotificationType sender() method ==="
rg -n "private.*sender\|sender.*payload" --type=java -A 5 -B 1
echo ""
echo "=== Check if senderEmail is actually used anywhere ==="
rg -n "senderEmail\|\.senderEmail" --type=java -B 2 -A 2
echo ""
echo "=== Check for toString() or serialization of payload ==="
rg -n "toString\(\)|@ToString|ObjectMapper|writeValueAsString|asJson" --type=java -B 2 -A 2 | grep -i "payload\|notification" -B 2 -A 2Length of output: 2790
🏁 Script executed:
#!/bin/bash
# NotificationEntity와 payload 저장 방식 확인
echo "=== NotificationEntity structure ==="
fd -t f "NotificationEntity.java" --exec cat {}
echo ""
echo "=== How payload is stored and retrieved ==="
rg -n "payload.*save\|save.*payload\|setPayload" --type=java -B 3 -A 3Length of output: 2620
추가 검증을 통해 문제를 확인했습니다.
🏁 Script executed:
#!/bin/bash
# sender() 메서드가 실제로 NotificationType에 있는지 더 자세히 확인
echo "=== NotificationType full content ==="
fd -t f "NotificationType.java" --exec cat {}Length of output: 2432
검증 완료. 최종 분석 결과를 정리하겠습니다.
확인 결과, sender() 메서드가 NotificationType.java에 존재하며, 이메일이 실제로 알림 메시지에 사용됩니다 ("nickname(email)" 형식으로 표시). 하지만 PII 로깅 문제는 여전히 유효합니다.
이메일 필드 제거 필요 - 알림 페이로드의 이메일이 오류 로그에 노출됩니다.
NotificationService.java 라인 138에서 역직렬화 실패 시 페이로드 전체가 로그에 기록됩니다:
log.warn("Failed to deserialize notification payload: {}", payload, exception);
이는 다음 문제를 야기합니다:
- NotificationPayload는 record이므로 자동으로 toString()이 모든 필드(senderEmail 포함)를 출력
- 예외 발생 시 민감한 이메일 정보가 운영 환경 로그에 노출
- 이메일은 NotificationType의 sender() 메서드에서 알림 메시지에 포함되므로, 데이터베이스에 평문 JSON으로 저장될 때도 노출
필요한 조치:
- UserCoinService.java 라인 76의
.builder(fromUserId, toUser.getName(), toUser.getEmail())호출에서 이메일 제거 또는 - NotificationService.java 라인 138의 로깅 시 페이로드를 마스킹/필터링하여 이메일 제외
🤖 Prompt for AI Agents
In src/main/java/hanium/modic/backend/domain/user/service/UserCoinService.java
around lines 72-79, the NotificationPayload is being built with the recipient's
email which leads to PII (email) being stored and potentially logged; remove the
email argument from NotificationPayload.builder (i.e., stop passing
toUser.getEmail()) so the payload does not include email, and if the
builder/signature does not have an overload, add a builder/constructor variant
or adjust NotificationPayload to omit the email field; after change, run
compilation and update any callers/tests and ensure NotificationService
logging/masking still prevents accidental PII exposure.
개요
작업사항
Summary by CodeRabbit
릴리스 노트