Skip to content

Conversation

@NCookies
Copy link
Collaborator

@NCookies NCookies commented Oct 9, 2025

개요

  • 비동기 알림 처리 시스템의 안정성과 데이터 정합성을 보장하기 위한 장애 대응 아키텍처를 구축함
  • 일시적인 장애는 시스템이 스스로 복구하고, 지속적인 장애로부터는 시스템을 보호하며, 모든 처리 과정을 추적할 수 있도록 설계했음

배경

  • 기존 알림 시스템은 외부 시스템(DB, MQ)의 장애에 취약하여, 메시지 유실이나 시스템 전체 과부하의 위험이 있었음
  • 장애 발생 시 원인 추적이 어려웠고, 실패한 메시지를 복구하기 위한 자동화된 프로세스가 부재
  • 안정적인 알림 서비스를 제공하기 위해, 프로덕션 수준의 장애 대응 및 재처리 워크플로우를 도입할 필요가 있었음

변경 사항

이번 PR에서는 다음과 같은 안정성 패턴들을 적용하여 시스템을 개선함.

1. 상태 기반 재처리 (State-Based Retry)

  • 모든 알림 메시지의 처리 상태(PENDING, SUCCESS, FAILED)를 MongoDB process_log 컬렉션에 기록함.
  • @Scheduled 스케줄러를 구현하여, FAILED 상태의 작업을 주기적으로 자동 재시도하도록 구성.
  • 이를 통해 대부분의 일시적인 장애는 시스템이 스스로 복구 가능.

2. 서킷 브레이커 (Circuit Breaker)

  • Resilience4j를 사용하여 DB 호출을 담당하는 UseCase 계층에 서킷 브레이커를 적용.
  • 반복적인 DB 장애 발생 시 서킷을 열어 시스템 과부하를 방지하고, Fallback 로직을 통해 실패를 빠르게 인지함.

3. 멱등성 (Idempotency) 보장

  • 메시지 발행 시 custom_message_id라는 고유 헤더를 사용.
  • 소비자는 이 ID를 process_log의 기본 키로 사용하여, 이미 성공적으로 처리된 메시지의 중복 실행을 방지함.

4. 트랜잭션과 이벤트의 분리

  • @TransactionalEventListener를 사용하여, DB 트랜잭션이 성공적으로 commit된 후에만 WebSocket 알림 발송 이벤트가 처리되도록 보장.
  • 이를 통해 '유령 알림'(DB 저장은 롤백됐는데 알림은 발송되는 현상)을 방지함.

5. 최후의 안전망 (DLQ)

  • 상태 기록조차 불가능한 심각한 장애 발생 시, 메시지가 유실되지 않고 ActiveMQ의 DLQ(Dead Letter Queue)로 이동하도록 구성.
  • 별도의 DLQ 소비자를 통해 해당 메시지를 영구 저장하여 추후 분석이 가능하도록 함.

6. 통합 테스트

  • 위 모든 시나리오(정상, 일시적 장애, 자동 복구, 멱등성)가 의도대로 동작하는지 검증하는 통합 테스트 코드를 작성함.

스크린샷

image

해결해야 할 문제

  • MongoDB에서 트랜잭션을 사용하기 위해서는 기존의 Standalone 모드가 아니라 replica set으로 전환해야 함
    • 기존의 데이터를 백업하고, DB 설정 이후 마이그레이션하는 과정이 필요함

Summary by CodeRabbit

  • 새로운 기능

    • 알림 저장 후 트랜잭션 이벤트 발행 및 이벤트 기반 처리 흐름 추가
    • JMS 발행 시 UUID 기반 커스텀 메시지 ID 헤더 적용 및 멱등성 지원
    • 실패 재처리용 프로세스 로그, 재시도 스케줄러 및 큐 소비자 도입
    • Mongo 트랜잭션 지원 및 DB 보호용 회로 차단기 설정 추가
    • Mongo 초기화·유저 생성 및 재시작 스크립트 추가, .gitignore 항목 추가
  • 리팩터

    • 이벤트 리스너를 트랜잭션 이벤트 기반으로 단순화
  • 작업

    • 빌드 의존성 조정(회로차단기 라이브러리 추가 및 커넥터 범위 변경)
  • 테스트

    • 트랜잭션·회로차단기·통합 흐름 관련 테스트 추가

✏️ Tip: You can customize this high-level summary in your review settings.

- `runtimeOnly` 사용 시 실행할 때에만 classpath에 올라가므로, IDE/빌드 환경에 따라 드라이버를 찾지 못하는 경우가 발생함
- mongo db에 트랜잭션을 도입하기 위해서 replica set 설정을 수행하고, 실제로 적용되었는지 확인하기 위해 테스트 수행
- 정상적으로 동작하는 것 확인
- 실제 배포 환경에 있는 mongo db 백업 후 replica set 적용해야 함
- 기존 코드에서는 @transactional 메서드 안에서 DB 저장과 알림 발송이 순차적으로 일어난다.
  - 알림이 발송된 직후, 커밋 단계에서 데이터베이스 문제 등 예측하지 못한 이유로 트랜잭션이 롤백된다면?
  - 사용자는 알림을 받았지만, 정작 DB에는 해당 알림 데이터가 존재하지 않는 유령 데이터가 발생한다.
- 스프링에서 제공하는 @TransactionalEventListener를 사용하여 트랜잭션 성공 이후 알림이 발송되는 것을 보장하고, 코드의 관심사를 분리했다.
- 메시지를 처리하는 과정에서 DB에 장애가 발생했을 시, 처리되지 않은 메시지가 누적되어 시스템이 과부하되는 것을 방지하기 위해 서킷 브레이커 패턴을 적용했다.
- Resilience4j 라이브러리 사용
- 테스트 코드를 통해 실제로 서킷 브레이커가 동작하는지 확인함
- 서킷 브레이커에 대해 잘못 이해하고 있는 부분이 있어서 테스트 코드에 오류가 있었음
- 실제로 발생할만한 예외가 던져지도록 하고, 해당 예외가 fallback 메서드를 통해 NotificationException으로 던져지는 것을 확인하도록 함
- 새로 구현된 NotificationProcessLog document에 데이터가 있으면 서킷 브레이커 테스트 중 에러가 발생함. 추후 이 점 유의해야 함
- consumer에 메시지가 들어오면 로그를 db에 적재 (PENDING)
- 이후 메시지 처리 성공 여부에 따라 상태를 업데이트함 (SUCCESS, FAILD)
- 실패했던 메시지들은 별도의 스케쥴러에서 다시 재시도 처리함
  - 만약 특정 횟수의 시도 이후에도 실패한다면 PERMANENTLY_FAILED로 상태 변경
  - 해당 메시지들은 추후 개발자가 직접 처리할 수 있도록 함 (Grafana 등 연동)
@NCookies NCookies self-assigned this Oct 9, 2025
@NCookies NCookies added the enhancement New feature or request label Oct 9, 2025
@coderabbitai
Copy link

coderabbitai bot commented Oct 9, 2025

Walkthrough

MongoDB 레플리카셋 재시작/초기화 및 사용자 생성 스크립트가 추가되었고, Mongo 트랜잭션 빈·알림 파이프라인(퍼블리셔 messageId, JMS 소비자, 프로세스 로그, 재시도 스케줄러, 서킷브레이커, 이벤트 발행) 리팩터링과 관련 의존성·프로퍼티·테스트가 추가/수정되었습니다. .gitignore/replicaset 항목이 추가되었습니다.

Changes

Cohort / File(s) Summary
Git & Scripts
\.gitignore, restart-mongodb-replica.sh, scripts/rs-init.sh, scripts/mongo-create-user.sh
/replicaset을 .gitignore에 추가; MongoDB 레플리카셋 재시작 스크립트 및 rs 초기화·사용자 생성 스크립트 추가.
Build & Dependencies
build.gradle
MySQL 커넥터를 implementation으로 변경하고 resilience4j-spring-boot3, resilience4j-circuitbreaker 의존성 추가.
Mongo Transaction Config
src/main/java/.../infrastructure/mongo/config/MongoTransactionConfig.java
MongoTransactionManager 빈 등록 추가(트랜잭션 지원).
Notification Exceptions / Codes
src/main/java/.../notification/exception/NotificationException.java, .../NotificationExceptionCode.java
NotificationException에 원인(cause) 생성자 추가; enum에 NOTIFICATION_DB_ERROR 상수 추가.
Events & Constants
src/main/java/.../infrastructure/notification/event/NotificationSavedEvent.java, .../model/NotificationQueueConstants.java
NotificationSavedEvent 레코드 추가 및 JMS 커스텀 헤더 키 CUSTOM_HEADER_MESSAGE_ID 추가.
Publisher
src/main/java/.../infrastructure/notification/publisher/NotificationEventPublisher.java
메시지 전송 시 UUID 기반 커스텀 messageId 생성 및 헤더 설정; JSON 변환 실패시 예외로 전파.
Process Log Domain & Repo
src/main/java/.../infrastructure/notification/model/NotificationProcessLog.java, .../repository/NotificationProcessLogRepository.java
처리 로그 도큐먼트(상태·재시도 로직) 및 retryable 조회 메서드 추가.
Process Log Service
src/main/java/.../infrastructure/notification/service/ProcessLogService.java
중복/재시도 관리 구현: startProcessing, finishProcessing, failProcessing, findRetryableJobs(트랜잭션 경계 포함).
Queue Consumer
src/main/java/.../infrastructure/notification/service/NotificationQueueConsumer.java
JMS 리스너 추가(생성/목록/읽음), 프로세스 로그 연동, 페이로드 역직렬화 및 서비스 위임, WebSocket 전송 연계.
Retry Scheduler
src/main/java/.../infrastructure/notification/service/NotificationRetryScheduler.java
5분 주기 재시도 스케줄러: FAILED 로그를 JMS로 재전송(기존 messageId 유지).
Event Listener (WebSocket push)
src/main/java/.../infrastructure/notification/service/NotificationEventListener.java
기존 JMS 기반 일부 로직 제거, @TransactionalEventListener( AFTER_COMMIT )NotificationSavedEvent 처리하여 WebSocket 푸시 수행.
Notification Service
src/main/java/.../infrastructure/notification/service/NotificationService.java
ApplicationEventPublisher 주입, createNewNotification@CircuitBreaker·@Transactional 적용으로 저장 후 NotificationSavedEvent 발행 및 fallback 추가; markAsRead@Transactional 적용.
Configuration Properties
src/main/resources/application.properties, src/test/resources/application-test.properties
Mongo 디버그 로깅 제거, db-circuit resilience4j 설정 추가(슬라이딩 윈도우, 임계값, 예외 목록 등).
Tests
src/test/java/.../MongoTransactionTest.java, .../NotificationCircuitBreakTest.java, .../NotificationIntegrationTest.java
Mongo 트랜잭션 테스트, Resilience4j 서킷브레이커 테스트(비활성화), JMS 통합/재시도/멱등성 통합 테스트(비활성화) 추가.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Producer as Producer
  participant Pub as NotificationEventPublisher
  participant JMS as JMS Broker
  participant Cons as NotificationQueueConsumer
  participant PLog as ProcessLogService
  participant Svc as NotificationService
  participant Repo as NotificationMongoRepository
  participant EVT as ApplicationEventPublisher
  participant EL as NotificationEventListener
  participant WS as SimpMessagingTemplate

  Producer->>Pub: sendMessage(event)
  Pub->>JMS: convertAndSend(payload, header: CUSTOM_HEADER_MESSAGE_ID=UUID)

  JMS-->>Cons: onMessage(payload, headers)
  Cons->>PLog: startProcessing(messageId, payload)
  alt allowed to process
    Cons->>Svc: createNewNotification(event)
    rect rgba(230,245,255,0.6)
      note right of Svc: `@CircuitBreaker` + `@Transactional`
      Svc->>Repo: save(NotificationDocument)
      Repo-->>Svc: saved
    end
    Svc->>EVT: publish(NotificationSavedEvent)
    EVT-->>EL: AFTER_COMMIT
    EL->>WS: send to user (NotificationResponse)
    Cons->>PLog: finishProcessing(messageId)
  else skipped
    Cons-->>Cons: skip processing (dedupe)
  end
  note over Cons: 오류시 PLog.failProcessing(messageId, reason)
Loading
sequenceDiagram
  autonumber
  participant Sch as NotificationRetryScheduler
  participant PLog as ProcessLogService
  participant JMS as JMS Broker

  loop every 5 minutes
    Sch->>PLog: findRetryableJobs()
    PLog-->>Sch: FAILED logs with retryCount < maxRetries
    Sch->>JMS: convertAndSend(payload, header: CUSTOM_HEADER_MESSAGE_ID=existingId)
  end
  note over Sch: 재시도 시 기존 messageId 재사용(멱등성)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

주의 깊게 볼 지점:

  • MongoTransactionManager 빈, @Transactional 적용 범위 및 테스트 (MongoTransactionConfig, ProcessLogService, MongoTransactionTest).
  • CircuitBreaker 설정과 fallback 흐름(NotificationService, application(-test).properties, NotificationCircuitBreakTest).
  • 메시지 ID 헤더 흐름과 멱등성(NotificationEventPublisher, NotificationQueueConsumer, NotificationRetryScheduler, ProcessLogService).
  • NotificationProcessLog 상태 전이 및 동시성 경계(증가 로직, PERMANENTLY_FAILED 변환).
  • 통합 테스트의 비동기 대기/모킹(Awaitility, MockitoSpyBean)과 Disabled 상태 확인.

Possibly related PRs

Suggested reviewers

  • Kimminu7
  • minjee2758
  • chat26666
  • pokerbearkr

Poem

깡총깡총 코드밭을 뛰는 토끼가 말하네 🐇
UUID 한 알 톡, 메시지 길을 밝히고
실패면 로그를 쌓아 다시 껑충 뛰어나와요
서킷이 지키고 재시도가 돌아오니
레플리카셋도 새로워져 모두 안심하네 ✨

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main changes: implementing a high-availability notification system with failure response and retry logic, which aligns with the circuit breaker, retry scheduler, and transaction handling changes across the codebase.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
✨ 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 feature/notification-failure-recover

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 90cc69a and 6f780f3.

📒 Files selected for processing (1)
  • src/test/java/org/ezcode/codetest/infrastructure/notification/MongoTransactionTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/org/ezcode/codetest/infrastructure/notification/MongoTransactionTest.java
⏰ 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

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.

- AfterEach 추가해서 어떤 시점에 테스트를 실행해도 전부 통과할 수 있도록 함
- 그러나 DB를 매번 초기화하므로 개발, 배포 환경에 영향이 가지 않도록 disabled 해뒀음
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (4)
build.gradle (1)

124-126: Resilience4j 의존성 버전 업데이트 권장

build.gradle (124-126)의 resilience4j-spring-boot3 및 resilience4j-circuitbreaker 버전을 최신 안정 버전인 2.3.0으로 업그레이드하세요.

scripts/mongo-create-user.sh (1)

4-5: scripts/mongo-create-user.sh: 하드코딩된 자격 증명 제거 권장
현재 이 스크립트는 로컬 개발 환경에서만 호출되며(.github/workflows에 포함되지 않음), 프로덕션/스테이징 환경에서도 사용될 가능성이 있다면 MONGO_USER/MONGO_PASSWORD 환경 변수로 주입하도록 변경하세요.

restart-mongodb-replica.sh (1)

31-33: rm 대상 경로는 반드시 따옴표로 감싸주세요.

변수에 공백이나 글로빙 문자가 섞이면 의도치 않은 경로가 삭제될 수 있습니다. 안전을 위해 인자를 감싸는 것이 좋습니다.

-rm -rf ${DATA_FILE_PATH}
+rm -rf "${DATA_FILE_PATH}"
src/main/java/org/ezcode/codetest/infrastructure/notification/publisher/NotificationEventPublisher.java (1)

51-58: 로그에 커스텀 메시지 ID도 함께 남겨주세요.

재처리·멱등성 추적 시 큐 헤더와 로그를 매칭해야 합니다. 성공 로그에 customMessageId를 포함하면 추후 사고 분석이 훨씬 수월합니다.

-			log.info("알림 메시지 전송 성공 ({}) : {}", destination, jsonMessage);
+			log.info("알림 메시지 전송 성공 (destination={}, messageId={}) : {}", destination, customMessageId, jsonMessage);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1819bb and e0faa16.

📒 Files selected for processing (23)
  • .gitignore (1 hunks)
  • build.gradle (2 hunks)
  • restart-mongodb-replica.sh (1 hunks)
  • scripts/mongo-create-user.sh (1 hunks)
  • scripts/rs-init.sh (1 hunks)
  • src/main/java/org/ezcode/codetest/application/notification/exception/NotificationException.java (1 hunks)
  • src/main/java/org/ezcode/codetest/application/notification/exception/NotificationExceptionCode.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/mongo/config/MongoTransactionConfig.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/event/NotificationSavedEvent.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/model/NotificationProcessLog.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/model/NotificationQueueConstants.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/publisher/NotificationEventPublisher.java (2 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/repository/NotificationProcessLogRepository.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationEventListener.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationQueueConsumer.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationRetryScheduler.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationService.java (3 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1 hunks)
  • src/main/resources/application.properties (1 hunks)
  • src/test/java/org/ezcode/codetest/infrastructure/notification/MongoTransactionTest.java (1 hunks)
  • src/test/java/org/ezcode/codetest/infrastructure/notification/NotificationCircuitBreakTest.java (1 hunks)
  • src/test/java/org/ezcode/codetest/infrastructure/notification/NotificationIntegrationTest.java (1 hunks)
  • src/test/resources/application-test.properties (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationRetryScheduler.java (3)
src/main/java/org/ezcode/codetest/infrastructure/notification/model/NotificationQueueConstants.java (1)
  • NotificationQueueConstants (3-13)
src/main/java/org/ezcode/codetest/infrastructure/notification/publisher/NotificationEventPublisher.java (1)
  • Slf4j (20-63)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationQueueConsumer.java (1)
  • Slf4j (23-91)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationQueueConsumer.java (3)
src/main/java/org/ezcode/codetest/infrastructure/notification/model/NotificationQueueConstants.java (1)
  • NotificationQueueConstants (3-13)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1)
  • Service (16-85)
src/main/java/org/ezcode/codetest/infrastructure/notification/publisher/NotificationEventPublisher.java (1)
  • Slf4j (20-63)
src/test/java/org/ezcode/codetest/infrastructure/notification/NotificationIntegrationTest.java (1)
src/main/java/org/ezcode/codetest/infrastructure/notification/model/NotificationQueueConstants.java (1)
  • NotificationQueueConstants (3-13)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (2)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationQueueConsumer.java (1)
  • Slf4j (23-91)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationRetryScheduler.java (1)
  • Slf4j (15-38)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationService.java (3)
src/main/java/org/ezcode/codetest/application/notification/service/NotificationUseCase.java (1)
  • Service (11-30)
src/main/java/org/ezcode/codetest/infrastructure/notification/publisher/NotificationEventPublisher.java (1)
  • Slf4j (20-63)
src/main/java/org/ezcode/codetest/infrastructure/event/publisher/StompMessageService.java (1)
  • Slf4j (19-135)
src/test/java/org/ezcode/codetest/infrastructure/notification/NotificationCircuitBreakTest.java (2)
src/test/java/org/ezcode/codetest/infrastructure/notification/NotificationIntegrationTest.java (1)
  • Disabled (35-174)
src/test/java/org/ezcode/codetest/infrastructure/notification/MongoTransactionTest.java (1)
  • SpringBootTest (13-31)
🪛 Shellcheck (0.11.0)
scripts/rs-init.sh

[error] 1-1: Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.

(SC2148)

scripts/mongo-create-user.sh

[error] 1-1: Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.

(SC2148)

restart-mongodb-replica.sh

[error] 1-1: Tips depend on target shell and yours is unknown. Add a shebang or a 'shell' directive.

(SC2148)

🔇 Additional comments (5)
.gitignore (1)

46-46: 변경 사항 확인 완료.

MongoDB 레플리카 셋 관련 로컬 데이터 디렉토리를 무시 목록에 추가한 것은 적절합니다. 버전 관리에서 제외되어야 할 디렉토리가 올바르게 설정되었습니다.

src/test/resources/application-test.properties (1)

107-117: Circuit Breaker 테스트 설정 확인 완료.

DB 호출에 대한 Circuit Breaker 설정이 테스트 환경에 적절하게 구성되었습니다:

  • 슬라이딩 윈도우 크기(10)와 최소 호출 횟수(5)가 적절합니다
  • 실패율 임계값(50%)과 대기 시간(10초)이 테스트에 적합합니다
  • MongoDB 및 네트워크 관련 예외들이 포괄적으로 등록되었습니다

이 설정은 PR의 장애 대응 아키텍처 목표와 잘 부합합니다.

src/main/java/org/ezcode/codetest/infrastructure/notification/model/NotificationQueueConstants.java (1)

7-8: 상수 추가 확인 완료.

멱등성 보장을 위한 커스텀 메시지 ID 헤더 상수가 깔끔하게 추가되었습니다. 기존 코드 패턴을 따르고 있으며 PR의 멱등성 보장 요구사항을 지원합니다.

src/main/java/org/ezcode/codetest/infrastructure/notification/event/NotificationSavedEvent.java (1)

5-11: 이벤트 레코드 정의 확인 완료.

NotificationSavedEvent가 Java record로 깔끔하게 정의되었습니다. 불변성이 보장되며 PR의 트랜잭션 분리 아키텍처(@TransactionalEventListener 활용)를 지원하는 적절한 구조입니다.

scripts/rs-init.sh (1)

26-27: 이 스크립트는 로컬 개발 전용으로, 프로파일링 레벨 2 유지해도 무방합니다.

- MongoDB 트랜잭션 매니저를 transactionManager라는 기본 이름으로 등록하면, JPA 등 다른 데이터소스용 기본 트랜잭션 매니저와 충돌하거나 덮어써서 애플리케이션 부팅 실패 혹은 잘못된 매니저 선택으로 이어질 수 있음
- null 예외 처리 추가해서 커스텀 message id 헤더가 존재하지 않는 메시지가 무한히 재시도 되는 상황을 방지함
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0faa16 and 1712ec9.

📒 Files selected for processing (7)
  • restart-mongodb-replica.sh (1 hunks)
  • scripts/mongo-create-user.sh (1 hunks)
  • scripts/rs-init.sh (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/mongo/config/MongoTransactionConfig.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationQueueConsumer.java (1 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationService.java (3 hunks)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • scripts/mongo-create-user.sh
  • src/main/java/org/ezcode/codetest/infrastructure/mongo/config/MongoTransactionConfig.java
🧰 Additional context used
🧬 Code graph analysis (3)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (2)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationQueueConsumer.java (1)
  • Slf4j (23-95)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationRetryScheduler.java (1)
  • Slf4j (15-38)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationQueueConsumer.java (4)
src/main/java/org/ezcode/codetest/infrastructure/notification/model/NotificationQueueConstants.java (1)
  • NotificationQueueConstants (3-13)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1)
  • Service (16-85)
src/main/java/org/ezcode/codetest/infrastructure/notification/publisher/NotificationEventPublisher.java (1)
  • Slf4j (20-63)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationRetryScheduler.java (1)
  • Slf4j (15-38)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationService.java (2)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1)
  • Service (16-85)
src/main/java/org/ezcode/codetest/application/notification/service/NotificationExecutor.java (1)
  • Slf4j (15-38)
🔇 Additional comments (4)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationService.java (2)

86-96: Resilience4j 서킷 오픈 대기시간 설정 확인

프로덕션 환경에 resilience4j.circuitbreaker.instances.db-circuit.wait-duration-in-open-state 설정이 없어 기본값(60초)이 적용됩니다. 이 기간 동안 JMS 예외가 계속 재시도되어 불필요한 반복이 발생할 수 있으므로 application.yml 혹은 application.properties에 적절한 대기시간을 설정하거나 fallback 동작을 조정하세요.


63-66: 이벤트 리스너는 이미 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)로 설정되어 있으므로 변경이 필요 없습니다.

Likely an incorrect or invalid review comment.

src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1)

37-56: messageId 필드는 @id로 _id에 매핑되어 기본 고유 인덱스가 이미 적용됩니다
별도 @indexed(unique=true) 설정 없이도 중복 삽입 시 DuplicateKeyException이 발생하므로 추가 조치 불필요합니다.

scripts/rs-init.sh (1)

3-27: 레플리카셋 포트 매핑 수동 확인 필요
scripts/rs-init.sh가 --port 10021로 접속을 시도하지만, 리포지토리 내 Docker Compose나 mongod 설정에서 컨테이너 내부 포트(10021/10022/10023)가 매핑됐는지 확인할 수 없습니다. 관련 파일 경로 및 포트 설정을 수동으로 검증하거나, 스크립트에서 기본 포트(27017) 사용으로 조정할지 검토해주세요.

- finishProcessing, finishProcessing 메서드의 findById()가 비어있다면 `ifPresent` 블록은 아무 일도 하지 않고 넘어감
- 개발자는 에러가 발생했다는 사실조차 모르고 넘어감 -> 조용한 실패(Silent Failure)
- finishProcessing에서 로그가 없다는 것은 비즈니스적인 문제(사용자 잘못 등)가 아니라, 시스템 내부의 심각한 상태 불일치를 의미
  - 따라서 NotificationException보다는 IllegalStateException이나 SystemException 같은 런타임 예외를 사용하는 것이 의미상 더 적합함
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (3)

26-57: 멱등성 및 재시도 로직이 잘 구현되었습니다.

중복 메시지 검사와 실패한 작업의 재시도를 적절히 처리하고 있으며, 트랜잭션으로 데이터 정합성을 보장합니다.

선택사항: 방어적 코딩 강화를 위해 messageIdpayload에 대한 null/empty 검증을 고려할 수 있습니다.

 @Transactional(transactionManager = "mongoTransactionManager")
 public boolean startProcessing(String messageId, String payload) {
-	
+	if (messageId == null || messageId.isBlank()) {
+		throw new IllegalArgumentException("messageId는 null이거나 비어있을 수 없습니다.");
+	}
+	if (payload == null || payload.isBlank()) {
+		throw new IllegalArgumentException("payload는 null이거나 비어있을 수 없습니다.");
+	}
+	
 	Optional<NotificationProcessLog> existingLogOpt = processLogRepository.findById(messageId);

59-72: 과거 리뷰 피드백이 반영되어 개선되었습니다.

로그 미존재 시 명시적으로 예외를 발생시켜 조용한 실패 문제가 해결되었습니다.

참고: Line 65에서 ProcessLogService.log 대신 log를 직접 사용하면 더 간결합니다.

 		NotificationProcessLog log = processLogRepository.findById(messageId)
 			.orElseThrow(() -> {
-				ProcessLogService.log.error("처리 완료할 로그를 찾을 수 없습니다. messageId={}", messageId);
+				log.error("처리 완료할 로그를 찾을 수 없습니다. messageId={}", messageId);
 				return new IllegalStateException("Process log not found: " + messageId);
 			});

74-85: 과거 리뷰 피드백이 반영되어 개선되었습니다.

로그 미존재 시 명시적으로 예외를 발생시켜 실패 상태를 놓치는 문제가 해결되었습니다.

참고: Line 80에서도 ProcessLogService.log 대신 log를 직접 사용하면 더 간결합니다.

 		NotificationProcessLog log = processLogRepository.findById(messageId)
 			.orElseThrow(() -> {
-				ProcessLogService.log.error("실패 기록할 로그를 찾을 수 없습니다. messageId={}", messageId);
+				log.error("실패 기록할 로그를 찾을 수 없습니다. messageId={}", messageId);
 				return new IllegalStateException("Process log not found: " + messageId);
 			});
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1712ec9 and ad90a91.

📒 Files selected for processing (1)
  • src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (1)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/NotificationService.java (1)
  • Service (25-97)
🔇 Additional comments (2)
src/main/java/org/ezcode/codetest/infrastructure/notification/service/ProcessLogService.java (2)

1-24: 클래스 구조 및 의존성 설정이 적절합니다.

서비스 선언, 의존성 주입, 설정값 관리가 Spring 모범 사례를 따르고 있으며 깔끔하게 구현되어 있습니다.


87-92: 재시도 대상 조회 로직이 적절합니다.

읽기 전용 트랜잭션을 사용하여 성능을 고려했으며, 재시도 조건이 명확하게 구현되어 있습니다.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/main/resources/application.properties (1)

162-173: Circuit Breaker 임계값 재검토 필요 - 데이터베이스 장애 복구 시간 고려

현재 설정에서 minimum-number-of-calls=5 + failure-rate-threshold=50의 조합은 단 5개의 호출 중 3개 실패 시 즉시 회로를 개방하게 됩니다. 데이터베이스의 일시적 네트워크 지연이나 연결 풀 고갈 상황에서는 정상 요청까지 차단될 수 있습니다.

추천 사항:

  1. minimum-number-of-calls 증대: 최소 10~20 으로 상향하여 더 많은 샘플링 후 판단
  2. wait-duration-in-open-state 재검토: MongoDB 레플리카셋 복구 시간을 고려하여 30s~60s 로 연장 검토 필요 (현재 10s는 DB 재시작 완료 시간에 비해 짧을 수 있음)
  3. failure-rate-threshold 조정: 50%에서 70% 이상으로 상향하여 진정한 장애만 감지하도록 검토

PR의 @Scheduled 재시도 로직, ActiveMQ DLQ, 그리고 @TransactionalEventListener의 트랜잭션 기반 처리와 함께 고려하여, 회로 차단 전략과 재시도 전략의 상호작용을 검증해주시기 바랍니다.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ad90a91 and 90cc69a.

📒 Files selected for processing (1)
  • src/main/resources/application.properties (1 hunks)
⏰ 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

@NCookies NCookies merged commit 79aa8ea into dev Nov 25, 2025
2 checks passed
@NCookies NCookies deleted the feature/notification-failure-recover branch November 25, 2025 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants