Skip to content

[FEAT] 비동기 알림 재처리 및 멱등성 보장 로직 구현#77

Merged
KoungQ merged 3 commits intodevelopfrom
feat/#72/비동기-알림-재처리-및-멱등성-보장-로직-구현
Mar 26, 2026

Hidden character warning

The head ref may contain hidden characters: "feat/#72/\ube44\ub3d9\uae30-\uc54c\ub9bc-\uc7ac\ucc98\ub9ac-\ubc0f-\uba71\ub4f1\uc131-\ubcf4\uc7a5-\ub85c\uc9c1-\uad6c\ud604"
Merged

[FEAT] 비동기 알림 재처리 및 멱등성 보장 로직 구현#77
KoungQ merged 3 commits intodevelopfrom
feat/#72/비동기-알림-재처리-및-멱등성-보장-로직-구현

Conversation

@KoungQ
Copy link
Copy Markdown
Member

@KoungQ KoungQ commented Mar 26, 2026

📝 Pull Request Template

📌 제목

[FEAT] 비동기 알림 재시도 구조 정리 및 FCM 멱등 재전송 개선


📢 요약

변경 사항 및 관련 이슈를 간단하게 설명해주세요.

  • 알림 전송 흐름을 outbox 기반 재처리에서 AFTER_COMMIT + @Async + @Retryable + @Recover 구조로 단순화했습니다.
  • 비즈니스 로직 성공 이후에만 알림이 실행되도록 유지하면서, 알림 실패가 비즈니스 트랜잭션에 영향을 주지 않도록 분리했습니다.
  • FCM 재시도 시 전체 디바이스를 다시 전송하지 않고, retryable 실패한 토큰만 다시 전송하도록 개선해 중복 전송을 줄였습니다.
  • FCM 레이트리밋은 글로벌 공용 슬라이딩 윈도우 AOP 구조로 정리했고, 최종 재시도 실패 시 디스코드 시스템 알림이 발행되도록 추가했습니다.

🔗 연관 이슈: #72


🚀 PR 유형

해당하는 항목에 체크해주세요.

  • ✨ 새로운 기능 추가
  • 🐛 버그 수정
  • 🎨 CSS/UI 디자인 변경
  • 🔧 코드에 영향 없는 변경(오타 수정, 탭 사이즈 변경, 변수명 변경 등)
  • 🔨 코드 리팩토링
  • 📝 주석 추가 및 수정
  • 📄 문서 수정
  • 🧪 테스트 추가 또는 리팩토링
  • 🏗️ 빌드 및 패키지 매니저 수정
  • 📂 파일 또는 폴더명 수정
  • 🗑️ 파일 또는 폴더 삭제

✅ PR 체크리스트

PR이 다음 요구 사항을 충족하는지 확인해주세요.

  • 🔹 커밋 메시지 컨벤션을 준수했습니다. (Commit message convention 참고)
  • 🔹 변경 사항에 대한 테스트를 수행했습니다. (버그 수정/기능 테스트)
  • 🔹 관련 문서를 업데이트했습니다. (필요한 경우)

📜 기타

리뷰어가 알면 좋을 추가 사항을 적어주세요.

  • 현재 알림 전송은 이벤트 발행 후 @TransactionalEventListener(phase = AFTER_COMMIT, fallbackExecution = true) + @Async("notificationExecutor")로 동작합니다.
  • 별도 재처리 스케줄러는 제거됐고, 재시도는 인메모리 @Retryable로만 수행합니다.
  • 현재 재시도 백오프는 3초 -> 6초 -> 12초, 최대 4회 시도입니다.
  • FCM 재시도는 retryable 실패 토큰만 대상으로 하며, invalid 토큰은 후처리로 제거합니다.
  • FCM 레이트리밋은 Redis Sorted Set + Lua 기반 슬라이딩 윈도우이며, 현재 기본 정책은 10초 / 20회 / TTL 20초입니다.
  • 최종 재시도 실패 시 디스코드 시스템 알림을 발행합니다.
  • 참고로 현재 구조는 알림 유실을 일부 허용하는 대신 단순성을 우선한 설계입니다.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added real-time group and direct chat rooms with WebSocket support
    • Implemented chat message history with cursor-based pagination
    • Added chat room member management (join, leave, kick functionality)
    • Integrated rate limiting for notification delivery
    • Added unread message tracking and read status marking
  • Chores

    • Removed MongoDB integration and related configurations
    • Simplified notification delivery system
  • Tests

    • Added comprehensive unit tests for chat operations and new services

@KoungQ KoungQ requested a review from ydking0911 March 26, 2026 08:19
@KoungQ KoungQ self-assigned this Mar 26, 2026
@KoungQ KoungQ added the feature label Mar 26, 2026
@KoungQ KoungQ linked an issue Mar 26, 2026 that may be closed by this pull request
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive chat system overhaul by removing MongoDB, implementing WebSocket-based group chat with rooms and members, establishing event-driven architecture for room/roommate flows, adding rate limiting infrastructure, and refactoring the notification delivery pipeline from outbox-based to immediate dispatch with Spring Retry.

Changes

Cohort / File(s) Summary
MongoDB Removal
build.gradle, docker-compose.yml, .github/workflows/cicd.yml, src/main/java/com/project/dorumdorum/DorumdorumApplication.java, src/main/resources/application-dev.yml, src/main/resources/application-prod.yml, .gitignore
Removed MongoDB Spring Boot starter, service definition from Docker Compose, environment variables from CI/CD workflow, @EnableMongoAuditing annotation, and MongoDB URI configuration from all profiles.
Chat Domain Entities
src/main/java/com/project/dorumdorum/domain/chat/domain/entity/ChatRoom.java, ChatRoomMember.java, ChatMessage.java, ChatRoomType.java, MessageType.java
Introduced new JPA entities for room-based chat architecture: ChatRoom with type and last-message tracking, ChatRoomMember for membership with join/read timestamps, updated ChatMessage with room relationship and message type, and enums for room type (GROUP/DIRECT) and message type (TEXT/SYSTEM).
Chat Request/Response DTOs
src/main/java/com/project/dorumdorum/domain/chat/application/dto/request/SendChatMessageRequest.java, src/main/java/com/project/dorumdorum/domain/chat/application/dto/response/ChatMessageResponse.java, ChatMessageSummary.java, ChatRoomMemberResponse.java, ChatRoomSummary.java
Simplified SendChatMessageRequest to contain only content, added response records for message and room summaries including unread counts and member details with host flag.
Chat Service Layer
src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatRoomService.java, ChatRoomMemberService.java, ChatMessageService.java
Added new services for chat room lifecycle (create, find, update), membership management (join, leave, validate, count), and message operations (save, find with cursor, decrease unread count, bulk delete).
Chat Use Cases
src/main/java/com/project/dorumdorum/domain/chat/application/usecase/CreateChatRoomUseCase.java, CreateDirectChatRoomUseCase.java, GetOrCreateDirectChatRoomUseCase.java, GetChatRoomMembersUseCase.java, JoinChatRoomUseCase.java, LeaveChatRoomUseCase.java, LoadChatMessagesUseCase.java, LoadMyChatRoomsUseCase.java, MarkChatRoomReadUseCase.java, SendGroupChatMessageUseCase.java, RoommateKickedEventListener.java
Added transactional event listeners and use cases for chat room creation, member joining/leaving, message loading with pagination, and chat room discovery; replaced direct 1:1 messaging with group chat broadcast to STOMP destinations.
Chat Repositories
src/main/java/com/project/dorumdorum/domain/chat/domain/repository/ChatRoomRepository.java, ChatRoomQueryRepository.java, ChatMessageRepository.java, ChatMessageQueryRepository.java, ChatRoomMemberRepository.java, src/main/java/com/project/dorumdorum/domain/chat/infra/repository/ChatRoomRepositoryImpl.java, ChatMessageRepositoryImpl.java
Added repositories and custom query implementations for room lookup by type/applicant, member existence/counting, and cursor-based message pagination with unread count computation.
Chat API Endpoints
src/main/java/com/project/dorumdorum/domain/chat/ui/ChatMessageController.java, GetChatRoomMembersController.java, GetOrCreateDirectChatRoomController.java, LeaveChatRoomController.java, LoadChatMessagesController.java, LoadMyChatRoomsController.java, MarkChatRoomReadController.java
Added REST and STOMP controllers for chat operations; replaced REST SendChatMessageController with STOMP ChatMessageController for WebSocket message handling via /app/chat-room/{chatRoomNo}/send.
Chat API Specs
src/main/java/com/project/dorumdorum/domain/chat/ui/spec/ChatMessageApiSpec.java, GetChatRoomMembersApiSpec.java, GetOrCreateDirectChatRoomApiSpec.java, LeaveChatRoomApiSpec.java, LoadChatMessagesApiSpec.java, LoadMyChatRoomsApiSpec.java, MarkChatRoomReadApiSpec.java
Added OpenAPI-documented interface contracts for all chat endpoints; removed SendChatMessageApiSpec endpoint in favor of STOMP messaging.
Room Domain Events & Use Cases
src/main/java/com/project/dorumdorum/domain/room/application/event/RoomApplicationSubmittedEvent.java, RoomConfirmedEvent.java, RoommateAcceptedEvent.java, RoommateKickedEvent.java, src/main/java/com/project/dorumdorum/domain/room/application/usecase/ApplyRoomUseCase.java, ConfirmRoomAssignmentUseCase.java, DecideApplicationRequestUseCase.java, KickRoommateUseCase.java
Introduced event-driven architecture with room lifecycle events triggering chat room creation and member operations; added KickRoommateUseCase to remove members and emit system messages.
Room API & Roommate Updates
src/main/java/com/project/dorumdorum/domain/room/ui/KickRoommateController.java, src/main/java/com/project/dorumdorum/domain/room/ui/spec/KickRoommateApiSpec.java, src/main/java/com/project/dorumdorum/domain/roommate/domain/entity/Roommate.java, src/main/java/com/project/dorumdorum/domain/roommate/domain/repository/RoommateRepository.java, src/main/java/com/project/dorumdorum/domain/roommate/domain/service/RoommateService.java, src/main/java/com/project/dorumdorum/domain/roommate/infra/repository/RoommateRepositoryImpl.java, src/main/java/com/project/dorumdorum/domain/room/domain/entity/Room.java
Added roommate kicking endpoint, repository methods for user/room lookup, service methods for host verification and room leaving, and guard against negative mate count decrement.
Notification System Refactoring
src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestEvent.java, NotificationRequestListener.java, NotificationRequestPublisher.java, src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationOutbox.java, NotificationOutboxStatus.java, NotificationType.java, src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationOutboxService.java, NotificationOutboxDeliveryProcessor.java, NotificationOutboxRecordListener.java, src/main/java/com/project/dorumdorum/domain/notification/infra/outbox/NotificationOutboxRetryScheduler.java, NotificationOutboxRepositoryImpl.java, NotificationOutboxQueryRepository.java, NotificationOutboxRepository.java
Removed outbox-based retry pattern, deleted NotificationOutbox entity/services/scheduled retry, and eliminated pre-computed outboxNo from events; chat notification path templates updated from /chat/{messageRoomNo} to /chats/{chatRoomNo}.
Notification Delivery with Retry
src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationDispatchService.java, NotificationDeliveryService.java, NotificationDeviceService.java, src/main/java/com/project/dorumdorum/domain/notification/infra/fcm/FcmNotificationDelivery.java, src/main/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDeliveryOrchestrator.java, src/main/java/com/project/dorumdorum/domain/notification/infra/sse/SseEmitterRegistry.java
Added new dispatch service for immediate notification sending; implemented Spring Retry–enabled NotificationDeliveryService with per-request caching of retryable tokens; updated DeliveryResult and MulticastSendResult to track retryable tokens as lists instead of failure counts; refactored SSE emitter cleanup to use removeIfCurrent for safer concurrent access.
WebSocket Configuration & Security
src/main/java/com/project/dorumdorum/global/config/WebSocketConfig.java, src/main/java/com/project/dorumdorum/global/security/JwtHandshakeInterceptor.java, src/main/java/com/project/dorumdorum/global/security/ChatRoomAuthorizationInterceptor.java
Added STOMP WebSocket endpoint at /ws with SockJS, JWT authentication during handshake, STOMP message validation interceptor to enforce chat room sender membership, and message broker configuration for /topic and /queue destinations.
Rate Limiting Infrastructure
src/main/java/com/project/dorumdorum/global/ratelimit/RateLimited.java, RateLimitAspect.java, RateLimitPolicyRegistry.java, RateLimitRule.java, RateLimitProperties.java, RateLimitRuleProperties.java, SlidingWindowRateLimiter.java, src/main/resources/application.yml
Introduced rate-limit annotation, AOP aspect, policy registry, and sliding-window limiter using Redis Lua scripts with configurable permit windows and TTLs; added FCM tag configuration with environment variable support.
Exception Handling & Response Types
src/main/java/com/project/dorumdorum/global/exception/StompErrorResponse.java, StompExceptionAdvice.java, src/main/java/com/project/dorumdorum/global/exception/code/status/ChatErrorStatus.java, RoomErrorStatus.java, src/main/java/com/project/dorumdorum/global/config/SecurityConfig.java, src/main/java/com/project/dorumdorum/global/alert/DiscordAlertEventListener.java
Added STOMP-specific error handling with StompExceptionAdvice, new chat error status codes, updated room error mappings, added /error endpoint bypass in security config, and fallbackExecution=true for Discord alert listener.
Configuration Updates
src/main/resources/application-dev.yml, src/main/resources/application-prod.yml, src/main/resources/application.yml
Removed MongoDB URI from dev/prod profiles, added UTF-8 connection options to MySQL datasource, changed JPA ddl-auto from update to validate, and added rate-limit rule configuration block.
Chat Unit Tests
src/test/java/com/project/dorumdorum/domain/chat/unit/entity/ChatRoomMemberTest.java, ChatRoomTest.java, src/test/java/com/project/dorumdorum/domain/chat/unit/infra/repository/ChatMessageRepositoryImplTest.java, ChatRoomRepositoryImplTest.java, src/test/java/com/project/dorumdorum/domain/chat/unit/service/ChatMessageServiceTest.java, ChatRoomMemberServiceTest.java, ChatRoomServiceTest.java, src/test/java/com/project/dorumdorum/domain/chat/unit/ui/ChatMessageControllerTest.java, LeaveChatRoomControllerTest.java, LoadChatMessagesControllerTest.java, LoadMyChatRoomsControllerTest.java, MarkChatRoomReadControllerTest.java, SendChatMessageControllerTest.java, src/test/java/com/project/dorumdorum/domain/chat/unit/usecase/CreateChatRoomUseCaseTest.java, CreateDirectChatRoomUseCaseTest.java, JoinChatRoomUseCaseTest.java, LeaveChatRoomUseCaseTest.java, LoadChatMessagesUseCaseTest.java, LoadMyChatRoomsUseCaseTest.java, MarkChatRoomReadUseCaseTest.java, RoommateKickedEventListenerTest.java, SendGroupChatMessageUseCaseTest.java
Comprehensive unit test coverage for chat entities, repositories, services, use cases, and controllers validating entity lifecycle, repository queries, service delegation, use case event handling, and controller endpoint behavior.
Room & Roommate Tests
src/test/java/com/project/dorumdorum/domain/room/unit/ui/KickRoommateControllerTest.java, src/test/java/com/project/dorumdorum/domain/room/unit/usecase/ApplyRoomUseCaseTest.java, ConfirmRoomAssignmentUseCaseTest.java, DecideApplicationRequestUseCaseTest.java, KickRoommateUseCaseTest.java, src/test/java/com/project/dorumdorum/domain/roommate/unit/service/RoommateServiceTest.java
Added tests for room event publication, roommate kicking workflow, and host verification; updated existing tests to mock ApplicationEventPublisher for event assertions.
Notification & Global Tests
src/test/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestListenerTest.java, NotificationRequestPublisherTest.java, src/test/java/com/project/dorumdorum/domain/notification/domain/service/NotificationDeliveryServiceTest.java, NotificationDispatchServiceTest.java, NotificationOutboxDeliveryProcessorTest.java, src/test/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDeliveryOrchestratorTest.java, src/test/java/com/project/dorumdorum/domain/notification/infra/fcm/FcmNotificationDeliveryTest.java, src/test/java/com/project/dorumdorum/domain/notification/mapper/NotificationMapperTest.java, src/test/java/com/project/dorumdorum/global/ratelimit/RateLimitAspectTest.java, RateLimitPolicyRegistryTest.java, SlidingWindowRateLimiterTest.java
Updated notification event listener and publisher tests for new dispatch service; added tests for delivery retry, dispatch flow, Redis rate limiting, and policy registry; updated path assertion in mapper test; removed outbox processor tests.

Sequence Diagram(s)

sequenceDiagram
    actor User as User (Host)
    participant Frontend
    participant RoomAPI as Room API
    participant RoomSvc as Room Service
    participant EventBus as Event Publisher
    participant ChatRoomUC as CreateChatRoomUseCase
    participant ChatRoomSvc as ChatRoomService
    participant ChatMemberSvc as ChatRoomMemberService

    User->>Frontend: Confirm room assignment
    Frontend->>RoomAPI: POST /api/rooms/{id}/confirm
    RoomAPI->>RoomSvc: execute(roomNo)
    RoomSvc->>RoomSvc: Update status to COMPLETED
    RoomSvc->>RoomSvc: Collect all roommate userNos
    RoomSvc->>EventBus: publishEvent(RoomConfirmedEvent)
    EventBus->>ChatRoomUC: handle(RoomConfirmedEvent)
    activate ChatRoomUC
    ChatRoomUC->>ChatRoomSvc: findByRoomNo(roomNo)
    alt Chat room not exists
        ChatRoomUC->>ChatRoomSvc: create(roomNo)
    end
    ChatRoomUC->>ChatMemberSvc: join(chatRoom, each userNo)
    ChatRoomUC->>ChatRoomSvc: (save updates)
    deactivate ChatRoomUC
    RoomSvc-->>RoomAPI: success
    RoomAPI-->>Frontend: 200 OK
Loading
sequenceDiagram
    actor WebSocketClient as Client (STOMP)
    participant WebSocket as STOMP Endpoint
    participant JwtInterceptor as JWT Interceptor
    participant AuthInterceptor as Chat Auth Interceptor
    participant ChatMsgCtrl as ChatMessageController
    participant SendGroupMsgUC as SendGroupChatMessageUseCase
    participant ChatMsgSvc as ChatMessageService
    participant SimpMessaging as STOMP Broker
    participant NotificationPub as NotificationPublisher

    WebSocketClient->>WebSocket: CONNECT /ws
    WebSocket->>JwtInterceptor: validateToken from cookie
    JwtInterceptor->>WebSocket: Extract userNo, allow handshake
    WebSocket-->>WebSocketClient: CONNECTED (userNo in session)

    WebSocketClient->>WebSocket: SEND /app/chat-room/{roomNo}/send
    WebSocket->>AuthInterceptor: preSend (STOMP frame)
    AuthInterceptor->>AuthInterceptor: Extract userNo from session
    AuthInterceptor-->>WebSocket: Validate userNo exists
    WebSocket->>ChatMsgCtrl: send(roomNo, request, headerAccessor)
    ChatMsgCtrl->>SendGroupMsgUC: send(roomNo, senderNo, content)
    activate SendGroupMsgUC
    SendGroupMsgUC->>ChatMsgSvc: save(chatRoom, senderNo, content, TEXT, unreadCount)
    ChatMsgSvc-->>SendGroupMsgUC: ChatMessage
    SendGroupMsgUC->>SimpMessaging: convertAndSend(/topic/chat-room/{roomNo}, ChatMessageResponse)
    SendGroupMsgUC->>NotificationPub: publish(recipientNo) for each member except sender
    deactivate SendGroupMsgUC
    SimpMessaging->>WebSocketClient: Broadcast to all room subscribers
    NotificationPub-->>NotificationPub: Send FCM notification asynchronously
Loading
sequenceDiagram
    participant FcmService as FCM Service
    participant NotificationDeliveryService as NotificationDeliveryService
    participant NotificationDeliveryOrchestrator as DeliveryOrchestrator
    participant DeviceService as DeviceService
    participant FcmNotificationDelivery as FcmNotificationDelivery
    participant SystemAlertPublisher as SystemAlertPublisher

    FcmService->>NotificationDeliveryService: deliver(Notification)
    activate NotificationDeliveryService
    NotificationDeliveryService->>DeviceService: findByUserNo(recipientNo)
    DeviceService-->>NotificationDeliveryService: Device list
    NotificationDeliveryService->>NotificationDeliveryOrchestrator: deliver(payload, deviceList)
    NotificationDeliveryOrchestrator->>FcmNotificationDelivery: sendMulticast(...)
    FcmNotificationDelivery-->>NotificationDeliveryOrchestrator: DeliveryResult {retryableTokens, invalidTokens}
    alt Has Invalid Tokens
        NotificationDeliveryOrchestrator->>DeviceService: clearInvalidFcmTokens(invalidTokens)
    end
    alt Has Retryable Tokens
        NotificationDeliveryOrchestrator-->>NotificationDeliveryService: throw RestApiException (trigger retry)
        NotificationDeliveryService->>NotificationDeliveryService: Store retryableTokens in RetryContext
        NotificationDeliveryService->>NotificationDeliveryService: retry attempt
    else Delivery Success
        deactivate NotificationDeliveryService
    end
    alt Max Retries Exceeded
        NotificationDeliveryService->>SystemAlertPublisher: publish(SystemAlertEvent with error)
        SystemAlertPublisher-->>NotificationDeliveryService: Alert logged/sent
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • ydking0911

Poem

🐰 Whiskers twitching with delight...

Room chat blooms from seeds once sown,
STOMP WebSockets carry messages home,
Rate limits guard each user's load,
Events dance down the delivery road,
Hops away with tail held high 🚀

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#72/비동기-알림-재처리-및-멱등성-보장-로직-구현

@KoungQ KoungQ changed the base branch from production to develop March 26, 2026 08:20
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.

[FEAT] 비동기 알림 재처리 및 멱등성 보장 로직 구현

2 participants