Skip to content

[REFACTOR] IO Bound 작업 비동기 처리를 통한 병목 개선#59

Merged
KoungQ merged 7 commits into
productionfrom
53-refactor-io-bound-작업-비동기-처리를-통한-병목-개선
Mar 15, 2026

Hidden character warning

The head ref may contain hidden characters: "53-refactor-io-bound-\uc791\uc5c5-\ube44\ub3d9\uae30-\ucc98\ub9ac\ub97c-\ud1b5\ud55c-\ubcd1\ubaa9-\uac1c\uc120"
Merged

[REFACTOR] IO Bound 작업 비동기 처리를 통한 병목 개선#59
KoungQ merged 7 commits into
productionfrom
53-refactor-io-bound-작업-비동기-처리를-통한-병목-개선

Conversation

@KoungQ

@KoungQ KoungQ commented Mar 15, 2026

Copy link
Copy Markdown
Member

📝 Pull Request Template

📌 제목

  • [REFACTOR] IO Bound 작업 비동기 처리를 통한 병목 개선

📢 요약

  • FCM 전송 분기를 탈 경우 요청 시간 대부분이 외부 IO(FCM API 호출)에 소요되어 병목이 발생함.

  • 요청 지연을 줄이기 위해 알림 전송을 비동기로 분리했고, 이후 정합성 보장을 위해 아웃박스 패턴을 도입함.

  • 현재는 아웃박스 기반 재처리 + 채널별 전송(SSE/FCM Multicast) 구조로 정리되어 있음.

  • 추가로 SSE 연결 실패를 유발하던 인증 에러 응답 직렬화 문제를 수정함.

  • 무엇을 바꿨는지

    • 리스너/스케줄러의 중복 로직(save -> deliver -> token 정리 -> outbox 상태)을 공통 처리 컴포넌트로 통합함.
    • 채널 분기(SSE/FCM)와 전송 실행을 오케스트레이터 내부 단계로 분리함.
    • FCM 전송 로직은 멀티캐스트 중심으로 정리하고, 실패 토큰 처리 규칙을 명확히 함.
    • 레이어드 구조에 맞지 않는 delivery 인터페이스/팩토리 계층을 제거함.
    • JwtAuthenticationFilterObjectMapper를 스프링 빈으로 주입받게 변경해 LocalDateTime 직렬화 오류를 해결함.
    • dev 환경에서 SSE 인증 쿠키 전달 이슈를 줄이기 위해 jwt.cookie.secure=false를 명시함.
  • 왜 그렇게 했는지

    • FCM IO 병목 해소
      • FCM 호출은 외부 네트워크 IO라 요청-응답 경로에서 수행하면 응답 지연이 커짐.
      • 사용자 요청 경로와 전송 경로를 분리해 API 응답 시간을 안정화하려는 목적.
    • 비동기 처리의 정합성 보완
      • 단순 비동기만 쓰면 중간 실패 시 알림 저장/전송 상태 추적이 어려움.
      • 아웃박스로 상태를 남겨 실패를 재처리 가능한 구조로 바꿔 유실 가능성을 낮춤.
    • 기능 단위 트랜잭션 정렬
      • 처리 단위를 NotificationOutboxDeliveryProcessor로 모아, 성공/실패 상태 전이를 한 흐름으로 보장하려는 목적.
      • 리스너/스케줄러는 “진입점” 역할만 갖고, 핵심 비즈니스/상태 전이는 공통 처리기로 일원화함.
    • 중복 제거 + 유지보수성 개선
      • 동일 로직이 여러 클래스에 퍼져 있으면 변경 시 누락/불일치가 발생하기 쉬움.
      • 공통 처리기로 모아 정책 변경(재시도, 실패 처리)을 한 곳에서 반영 가능하게 함.
    • 장애 복원력/운영 안정성
      • FCM 부분 실패를 토큰 단위로 판정하고, invalid 토큰 정리 + retryable 실패만 재시도하도록 분리함.
      • 인증 실패 응답 직렬화가 깨져 SSE까지 연쇄 실패하던 문제를 수정해 실시간 연결 안정성을 확보함.
  • 어떻게 구현되어 있는지 (아웃박스 흐름)

    • NotificationRequestPublisher
      • 알림 요청 이벤트 발행 시 outboxNo를 함께 생성.
    • NotificationOutboxRecordListener (BEFORE_COMMIT)
      • notification_outbox에 INIT 상태 레코드 저장.
    • NotificationRequestListener (AFTER_COMMIT + @Async)
      • 실제 전송 처리 트리거(요청 트랜잭션과 분리).
    • NotificationOutboxDeliveryProcessor
      • save -> channel 분류/전송 -> invalid token 정리 -> outbox success/fail을 단일 처리 단위로 수행.
    • NotificationOutboxRetryScheduler
      • 실패(backoff) 대상 outbox를 배치로 재처리.
    • NotificationDeliveryOrchestrator
      • 디바이스별 채널 결정 후 SSE는 즉시 전송, FCM은 토큰 수집 후 Multicast 전송.

🔗 연관 이슈: Resolves #이슈번호


🚀 PR 유형

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

✅ PR 체크리스트

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

📜 기타

  • 주요 변경 포인트

    • NotificationRequestListener, NotificationOutboxRetryScheduler
      • 진입점 역할로 단순화하고 공통 처리기로 위임.
    • NotificationOutboxDeliveryProcessor
      • 알림 저장/전송/토큰 정리/outbox 상태 갱신을 한 처리 단위로 통합.
    • NotificationDeliveryOrchestrator
      • 채널 분류 -> SSE 전송 -> FCM 멀티캐스트 전송 흐름으로 단계화.
    • FcmNotificationDelivery, SseNotificationDelivery
      • 역할을 분명히 하고 불필요한 추상화 의존 제거.
    • JwtAuthenticationFilter, SecurityConfig
      • 에러 응답 직렬화 안정성 확보(ObjectMapper 빈 주입).
    • application-dev.yml
      • 로컬 쿠키 전송 안정화를 위한 secure 설정 보완.
  • 삭제/정리

    • NotificationDelivery, NotificationDeliveryFactory 및 관련 테스트 제거.
    • 사용되지 않던 delivery outbox 잔여 구현 제거.
  • 검증

    • ./gradlew compileJava 통과.
    • 알림 관련 단위 테스트(리스너/프로세서/오케스트레이터/SSE/FCM/토큰 등록) 통과.

Summary by CodeRabbit

  • New Features

    • Enhanced notification delivery with automatic retry capability and exponential backoff for improved reliability.
    • Improved device token management for push notifications.
  • Improvements

    • Asynchronous notification processing for better app responsiveness.
    • Refactored calendar, chat, and notice functionality for improved maintainability.

@KoungQ KoungQ requested a review from ydking0911 March 15, 2026 10:00
@KoungQ KoungQ self-assigned this Mar 15, 2026
@KoungQ KoungQ added the feature label Mar 15, 2026
@KoungQ KoungQ merged commit e7a3ea9 into production Mar 15, 2026
@coderabbitai

coderabbitai Bot commented Mar 15, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b90ded8a-46ce-40af-8e7c-d414fa52113e

📥 Commits

Reviewing files that changed from the base of the PR and between c1059a5 and ebde4d7.

📒 Files selected for processing (37)
  • src/main/java/com/project/dorumdorum/domain/calendar/application/usecase/LoadCalendarEventsUseCase.java
  • src/main/java/com/project/dorumdorum/domain/calendar/domain/service/CalendarEventService.java
  • src/main/java/com/project/dorumdorum/domain/chat/application/usecase/SendChatMessageUseCase.java
  • src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatMessageService.java
  • src/main/java/com/project/dorumdorum/domain/notice/application/usecase/LoadNoticesUseCase.java
  • src/main/java/com/project/dorumdorum/domain/notice/domain/service/NoticeService.java
  • src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationOutboxRecordListener.java
  • src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestEvent.java
  • src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestListener.java
  • src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestPublisher.java
  • src/main/java/com/project/dorumdorum/domain/notification/application/usecase/RegisterDeviceTokenUseCase.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/entity/Device.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationOutbox.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationOutboxStatus.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationDeviceRepository.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationOutboxQueryRepository.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationOutboxRepository.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationDeviceService.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationOutboxDeliveryProcessor.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationOutboxService.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDelivery.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDeliveryFactory.java
  • src/main/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDeliveryOrchestrator.java
  • src/main/java/com/project/dorumdorum/domain/notification/infra/fcm/FcmNotificationDelivery.java
  • src/main/java/com/project/dorumdorum/domain/notification/infra/outbox/NotificationOutboxRetryScheduler.java
  • src/main/java/com/project/dorumdorum/domain/notification/infra/repository/NotificationOutboxRepositoryImpl.java
  • src/main/java/com/project/dorumdorum/domain/notification/infra/sse/SseNotificationDelivery.java
  • src/main/java/com/project/dorumdorum/global/config/AsyncConfig.java
  • src/main/java/com/project/dorumdorum/global/config/SecurityConfig.java
  • src/main/resources/application-dev.yml
  • src/test/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestListenerTest.java
  • src/test/java/com/project/dorumdorum/domain/notification/application/usecase/RegisterDeviceTokenUseCaseTest.java
  • src/test/java/com/project/dorumdorum/domain/notification/domain/service/NotificationOutboxDeliveryProcessorTest.java
  • src/test/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDeliveryFactoryTest.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/infra/sse/SseNotificationDeliveryTest.java

📝 Walkthrough

Walkthrough

Introduces an outbox-based event sourcing pattern for reliable notification delivery, replaces direct repository access with service layers across calendar, chat, and notice domains, and refactors notification delivery to support batched multicast with exponential backoff retry mechanism.

Changes

Cohort / File(s) Summary
Service Layer Introductions
src/main/java/com/project/dorumdorum/domain/calendar/domain/service/CalendarEventService.java, src/main/java/com/project/dorumdorum/domain/chat/domain/service/ChatMessageService.java, src/main/java/com/project/dorumdorum/domain/notice/domain/service/NoticeService.java, src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationDeviceService.java
New service classes introduced to abstract repository operations; each provides domain-specific read or write methods with transactional boundaries.
Service Layer Integration
src/main/java/com/project/dorumdorum/domain/calendar/application/usecase/LoadCalendarEventsUseCase.java, src/main/java/com/project/dorumdorum/domain/chat/application/usecase/SendChatMessageUseCase.java, src/main/java/com/project/dorumdorum/domain/notice/application/usecase/LoadNoticesUseCase.java, src/main/java/com/project/dorumdorum/domain/notification/application/usecase/RegisterDeviceTokenUseCase.java
Updated use cases to delegate to new service layer instead of direct repository access; simplified internal logic via service methods.
Outbox Infrastructure
src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationOutbox.java, src/main/java/com/project/dorumdorum/domain/notification/domain/entity/NotificationOutboxStatus.java, src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationOutboxRepository.java, src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationOutboxQueryRepository.java, src/main/java/com/project/dorumdorum/domain/notification/infra/repository/NotificationOutboxRepositoryImpl.java
New outbox entity with status tracking and retry logic; repositories support CRUD and custom retry queries with QueryDSL pagination.
Outbox Services
src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationOutboxService.java, src/main/java/com/project/dorumdorum/domain/notification/domain/service/NotificationOutboxDeliveryProcessor.java, src/main/java/com/project/dorumdorum/domain/notification/infra/outbox/NotificationOutboxRetryScheduler.java
Services manage outbox lifecycle, coordinate multi-step delivery with error handling, and schedule periodic retry batches with exponential backoff.
Event Listener Refactoring
src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestEvent.java, src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestPublisher.java, src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationRequestListener.java, src/main/java/com/project/dorumdorum/domain/notification/application/event/NotificationOutboxRecordListener.java
Added outboxNo to event; publisher generates unique IDs; listeners now delegate to outbox-based delivery processor instead of direct notification logic.
Delivery Layer Refactoring
src/main/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDeliveryOrchestrator.java, src/main/java/com/project/dorumdorum/domain/notification/infra/fcm/FcmNotificationDelivery.java, src/main/java/com/project/dorumdorum/domain/notification/infra/sse/SseNotificationDelivery.java, src/main/java/com/project/dorumdorum/domain/notification/domain/entity/Device.java
Orchestrator refactored from single-device to multi-device delivery with per-device channel classification and DeliveryResult reporting; FCM now supports multicast batching; SSE simplified; removed factory pattern.
Removed Delivery Abstractions
src/main/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDelivery.java, src/main/java/com/project/dorumdorum/domain/notification/domain/service/delivery/NotificationDeliveryFactory.java
Deleted interface and factory; direct delegation to delivery implementations replaces contract-based dispatch.
Configuration & Utilities
src/main/java/com/project/dorumdorum/global/config/AsyncConfig.java, src/main/java/com/project/dorumdorum/global/config/SecurityConfig.java, src/main/resources/application-dev.yml
New async executor configuration for notification processing; minor SecurityConfig reformatting; added jwt.cookie.secure property.
Device Repository
src/main/java/com/project/dorumdorum/domain/notification/domain/repository/NotificationDeviceRepository.java
Added findByFcmTokenIn query method to support batch token lookups for cleanup.
Tests
src/test/java/com/project/dorumdorum/domain/notification/.../\*Test.java
Updated test suites to verify outbox-based flow, multicast delivery, async processor delegation, and removal of factory/interface patterns; new comprehensive tests for delivery processor retry scenarios.

Sequence Diagrams

sequenceDiagram
    actor Publisher
    participant RequestEvent as Event System
    participant OutboxListener as OutboxRecord<br/>Listener
    participant OutboxService as OutboxService
    participant RequestListener as RequestListener<br/>(Async)
    participant DeliveryProcessor as Delivery<br/>Processor
    participant DeviceService as DeviceService
    participant Orchestrator as Orchestrator
    participant FCM as FCM
    participant SSE as SSE

    Publisher ->> RequestEvent: publish(outboxNo, data)
    RequestEvent ->> OutboxListener: BEFORE_COMMIT
    OutboxListener ->> OutboxService: saveInit(outboxNo, ...)
    OutboxService ->> OutboxService: persist with status=INIT

    RequestEvent ->> RequestListener: AFTER_COMMIT (async)
    activate RequestListener
    RequestListener ->> DeliveryProcessor: processFromEvent(event)
    DeliveryProcessor ->> OutboxService: find outbox
    DeliveryProcessor ->> DeviceService: findByUserNo(recipientNo)
    DeviceService -->> DeliveryProcessor: List<Device>
    DeliveryProcessor ->> Orchestrator: deliver(notification, devices)
    
    Orchestrator ->> Orchestrator: classify devices by channel
    Orchestrator ->> SSE: send(payload, sseDevices)
    SSE -->> Orchestrator: success
    Orchestrator ->> FCM: sendMulticast(payload, fcmTokens)
    FCM -->> Orchestrator: MulticastSendResult
    Orchestrator -->> DeliveryProcessor: DeliveryResult
    
    alt hasRetryableFailure
        DeliveryProcessor ->> OutboxService: fail(outboxNo)
        OutboxService ->> OutboxService: set status=INIT, nextRetryAt=now+backoff
    else success
        DeliveryProcessor ->> OutboxService: success(outboxNo, notificationNo)
        OutboxService ->> OutboxService: set status=SUCCESS, processedAt=now
    end
    
    alt has invalid tokens
        DeliveryProcessor ->> DeviceService: clearInvalidFcmTokens(invalidTokens)
        DeviceService ->> DeviceService: clear fcmToken on matching devices
    end
    deactivate RequestListener
Loading
sequenceDiagram
    participant Scheduler as RetryScheduler
    participant OutboxService as OutboxService
    participant DeliveryProcessor as DeliveryProcessor
    participant Outbox as NotificationOutbox
    participant Orchestrator as Orchestrator
    participant Notification as NotificationService

    loop Every 5 seconds
        Scheduler ->> OutboxService: loadRetryBatch(100)
        OutboxService ->> OutboxService: query INIT status, nextRetryAt <= now
        OutboxService -->> Scheduler: List<NotificationOutbox>
        
        loop for each outbox
            Scheduler ->> DeliveryProcessor: processFromOutbox(outbox)
            DeliveryProcessor ->> Notification: save(...)
            Notification -->> DeliveryProcessor: Notification
            DeliveryProcessor ->> Orchestrator: deliver(notification, devices)
            Orchestrator -->> DeliveryProcessor: DeliveryResult
            
            alt retry allowed && hasRetryableFailure
                DeliveryProcessor ->> Outbox: failWithBackoff(maxRetries=5, baseBackoff=5s)
                Outbox ->> Outbox: retryCount++, nextRetryAt=now+delay
            else maxRetries exceeded
                Outbox ->> Outbox: status=FAIL, finalizeProcess
            else success
                Outbox ->> Outbox: status=SUCCESS
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ydking0911

🐰 Hops with joy at patterns so grand,
Outbox retries now perfectly planned,
Events and services dance hand-in-hand,
Async delivery across every land,
With exponential backoff, we'll take our stand! 📬✨

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 53-refactor-io-bound-작업-비동기-처리를-통한-병목-개선
📝 Coding Plan
  • Generate coding plan for human review comments

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.

Tip

CodeRabbit can scan for known vulnerabilities in your dependencies using OSV Scanner.

OSV Scanner will automatically detect and report security vulnerabilities in your project's dependencies. No additional configuration is required.

@KoungQ KoungQ deleted the 53-refactor-io-bound-작업-비동기-처리를-통한-병목-개선 branch March 16, 2026 06:18
@KoungQ KoungQ linked an issue Mar 16, 2026 that may be closed by this pull request
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.

[REFACTOR] IO Bound 작업 비동기 처리를 통한 병목 개선

1 participant