Skip to content

[BE][feat]: mq 리스너 관련 코드 수정#979

Open
Jin409 wants to merge 31 commits intodevelop-befrom
feat/977
Open

[BE][feat]: mq 리스너 관련 코드 수정#979
Jin409 wants to merge 31 commits intodevelop-befrom
feat/977

Conversation

@Jin409
Copy link
Contributor

@Jin409 Jin409 commented Oct 19, 2025

🔗 관련 이슈

체크리스트

  • 알림 전송 여부 확인 테스트
  • 패키지 위치 수정

Summary by CodeRabbit

  • Chores
    • 비동기 알림 처리를 위한 메시지 큐 인프라 구축
    • 워커 프로세스 설정 및 스레드 풀 관리 추가
    • 개발, 운영, 테스트 환경 설정 업데이트
    • 불필요한 코드 정리 및 제거

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 19, 2025

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Walkthrough

이 변경사항은 RabbitMQ 기반의 메시지 큐 시스템을 도입하여 FCM 알림 전송을 비동기로 처리하는 이벤트 기반 아키텍처로 전환합니다. 새로운 설정 클래스들(RabbitMQConfig, RabbitWorkerConfig, WebAsyncConfig)과 메시지 처리 컴포넌트들(NotificationConsumer, NotificationEventHandler, NotificationMQProducer)을 추가하며, Worker 프로필을 통해 독립적인 메시지 처리 서비스를 구현합니다. 기존의 AsyncConfig는 제거되고, 환경별 YAML 설정이 확장됩니다.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


상세 검토 의견

1. 아키텍처 전환의 복잡성

새로운 메시지 큐 기반 아키텍처가 상당히 많은 컴포넌트를 한번에 도입하고 있습니다. RabbitMQConfig → RabbitWorkerConfig → NotificationConsumer → NotificationMQProducer → NotificationEventHandler으로 이어지는 의존성 체인이 있는데, 각 계층 간의 관계가 명확히 문서화되지 않아 있습니다.

구체적 한계:

  • 설정 클래스들이 서로 다른 파일에 분산되어 있어, 전체 메시지 흐름을 추적하기 위해 여러 파일을 오가며 봐야 합니다.
  • @Profile("worker")과 @async가 혼재되면서 실행 환경에 따른 동작이 복잡합니다.

개선 방향:
README나 설계 문서에 메시지 흐름도(이벤트 발행 → 프로듀서 → 큐 → 컨슈머 → FCM)를 추가하면, 새로운 팀원이나 향후 유지보수자가 빠르게 이해할 수 있습니다.


2. 에러 처리 전략 부재

가장 주목할 점은 NotificationMQProducer와 NotificationConsumer에서 명시적인 에러 처리가 없다는 것입니다.

문제점:

  • NotificationConsumer의 세 리스너 메서드는 RabbitMQ에서 메시지를 수신해 FCM 요청을 보내는데, 만약 FCM 서비스가 응답하지 않거나 네트워크 오류가 발생하면 어떻게 될까요?
  • FcmClient에서 FirebaseMessagingException을 AlarmException으로 변환하지만, NotificationConsumer는 이를 catch하지 않습니다.
  • RabbitMQ 기본 동작은 예외 발생 시 메시지를 재큐(requeue)하는데, 이것이 의도한 동작인지 명시되지 않았습니다.

권장 개선 방안:

// 선택지 1: NotificationConsumer 내에서 try-catch + 수동 확인(acknowledge)
- 장점: 실패한 메시지를 DLQ(Dead Letter Queue)로 보낼 수 있음
- 단점: 각 메서드마다 에러 처리 코드가 중복될 수 있음

// 선택지 2: RabbitMQConfig에서 ErrorHandler 빈 등록
- 장점: 전역 에러 처리 정책 적용 가능
- 단점: 세밀한 제어가 어려울 수 있음

3. 스레드 풀 설정의 타당성 검토

RabbitWorkerConfig에서 fcmWorkerExecutor의 설정이 합리적인가요?

검토 포인트:

  • Core pool size: 4, Max pool size: 8, Queue capacity: 200
  • 이 설정은 최대 동시성이 8이라는 뜻입니다. FCM API의 Rate Limiting이 몇 req/s인지에 따라 부적절할 수 있습니다.
  • Queue capacity가 200이면, 메시지 처리 속도가 느려질 경우 대기 메시지가 쌓입니다. 만약 Pod이 재시작되면 메모리에 쌓인 200개 메시지가 손실됩니다.

권장 검토 항목:

  • FCM API의 실제 처리량 테스트 후 설정 재조정 필요
  • Queue capacity를 구성 가능하게(properties) 변경하여 환경별로 튜닝 가능하게 할 것을 제안합니다.
# application-worker.yml에 추가 가능
executor:
  core-pool-size: 4
  max-pool-size: 8
  queue-capacity: 200

4. NotificationMQProducer의 배치 로직

publishTokens: req -> Lists.partition(tokens, 500) -> publish each

확인해야 할 사항:

  • 왜 500개인가요? FCM의 Multicast 제한이 500개라서 그렇다면, 이를 상수로 정의하고 주석을 달아야 합니다.
  • Guava의 Lists.partition은 래퍼 리스트를 반환하므로, 대용량 데이터에서 메모리 효율이 우수합니다. 이 선택은 좋습니다.

개선 제안:

private static final int TOKEN_BATCH_SIZE = 500; // FCM Multicast API 제한

이렇게 상수화하면, 향후 정책 변경 시 한 곳만 수정하면 됩니다.


5. 프로필 관리의 복잡성

현재 구조:

  • prod, dev: WebAsyncConfig (producerExecutor) 활성화
  • worker: RabbitMQConfig, RabbitWorkerConfig 활성화
  • local, ci: 테스트용 설정

우려사항:

  • 본 서버와 Worker 서버가 같은 코드베이스에서 구동되는데, 프로필이 잘못 지정되면 의도치 않은 동작이 발생할 수 있습니다.
  • 예: prod 서버에서 Worker 프로필을 실수로 포함하면, NotificationConsumer도 함께 실행되어 메시지를 중복 처리할 수 있습니다.

권장 대응:

  • 시작 시 로그를 남겨 어느 프로필이 활성화되었는지 명시하기
  • 필요시 상호 배제 검증 로직 추가

6. 데이터 정합성 문제

NotificationEventHandler는 @Async@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)을 함께 사용합니다.

확인 사항:

  • 이벤트 발행 후 커밋되기 전에 비동기 스레드가 메시지를 발행할 가능성은 없나요?
  • 스프링의 @TransactionalEventListener가 보장하는 순서는 DB 커밋 이후 이벤트 리스너 호출이므로, @async와 함께 사용해도 안전합니다. 이 부분은 올바른 구현입니다.

7. application-worker.yml의 설정

server.port: 0  # 웹 서버 미실행
spring.main.web-application-type: none
task.scheduling.enabled: false
flyway.enabled: false

좋은 점:

  • Worker 서버가 웹 요청을 받지 않도록 명시적으로 설정했습니다.

검토 포인트:

  • Flyway 마이그레이션을 비활성화했다면, Worker 배포 시 DB 스키마 일관성을 어떻게 보장하나요?
  • 권장: 초기 배포 시만 마이그레이션을 실행하는 별도의 배포 스크립트 또는 Job을 고려하세요.

최종 평가

이 PR은 확장성 있는 메시지 기반 아키텍처로의 전환이라는 긍정적인 방향입니다. 다만 다음을 보완하면 더욱 견고해질 것입니다:

  1. 에러 처리 전략 수립 (DLQ 활용 고려)
  2. 스레드 풀 설정 문서화 및 환경변수화
  3. 배치 크기(500) 상수화 및 주석 추가
  4. 프로필 혼재 방지 검증 로직
  5. Worker 배포 시 DB 마이그레이션 전략 수립

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 2 inconclusive)
Check name Status Explanation Resolution
Description Check ⚠️ Warning PR 설명이 저장소의 필수 템플릿 구조를 충분히 따르지 않고 있습니다. 템플릿에서 요구하는 "📝 작업 내용"(어떤 개발을 했는지에 대한 설명)과 "주요 변경사항"(구체적인 변경 항목 1, 2, 3)이 완전히 누락되어 있으며, 대신 템플릿에 없는 "체크리스트" 섹션이 추가되었습니다. 관련 이슈 정보는 올바르게 제공되었으나, 이 PR이 정확히 어떤 기능을 구현했고 어떤 부분이 주요 변경사항인지 리뷰어가 명확하게 파악하기 어려운 상태입니다. PR 설명을 템플릿에 맞게 수정해주시기 바랍니다. 특히 "📝 작업 내용" 섹션에서 MQ 리스너 구현의 목적과 전체적인 아키텍처 변경을 요약하고, "주요 변경사항" 섹션에서 (1) RabbitMQ 기반 메시지 큐 인프라 구성, (2) 비동기 이벤트 리스너 패턴 구현, (3) Worker 프로필 기반 독립 실행 구조 등 핵심 변경 내용을 구체적으로 나열해주면 좋겠습니다.
Docstring Coverage ⚠️ Warning Docstring coverage is 13.04% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Linked Issues Check ❓ Inconclusive 링크된 이슈 #977에 대한 상세한 요구사항이나 객관적인 수락 기준(acceptance criteria)이 제공되지 않아 규격 준수 여부를 명확하게 검증할 수 없습니다. 이슈 제목만으로는 "mq 리스너 관련 코드 수정"이라는 의도를 알 수 있으나, 구체적으로 어떤 기능이 구현되어야 하는지, 어떤 동작 방식이 기대되는지 알 수 없는 상태입니다. 다만 PR의 코드 변경사항 자체를 보면 MQ 리스너 구현에 필요한 요소들(설정, 컨슈머, 이벤트 핸들러, 프로듀서)이 비교적 체계적으로 구성된 것으로 보입니다. GitHub 이슈 #977에 들어가서 요구사항을 확인해주시고, 이 PR이 실제로 모든 수락 기준을 충족하는지 재검증해주기 바랍니다. 특히 PR 설명의 체크리스트에 있는 "알림 전송 여부 확인 테스트"와 "패키지 위치 수정"이 완료되었는지, 그리고 이들이 이슈 #977의 필수 요구사항인지 확인이 필요합니다.
Out of Scope Changes Check ❓ Inconclusive 대부분의 변경사항은 PR 제목인 "mq 리스너 관련 코드 수정"의 범주 내에 있습니다. 다만 몇 가지 부분이 주의 깊게 검토할 필요가 있습니다. 첫째, backend/src/main/resources/data.sql에서 MEMBER 테이블의 테스트 데이터가 제거되었는데, 이것이 MQ 리스너 기능 구현과 어떤 연관이 있는지 명확하지 않습니다. 둘째, .github/workflows/backend-dev-cd.yml에서 "feat/977" 브랜치를 트리거 대상에 추가하는 변경사항은 코드 기능 구현보다는 CI/CD 인프라 설정에 가깝습니다. 이 두 항목이 이슈 #977의 명시적인 요구사항인지 확인이 필요합니다. 특히 data.sql의 테스트 데이터 제거와 워크플로우 브랜치 추가가 이슈 #977의 필수 요구사항인지, 아니면 우발적으로 포함된 변경인지 명확히 해주시기 바랍니다. 만약 이슈와 무관한 변경이라면 별도의 PR로 분리하는 것을 권장드립니다. 이렇게 하면 각 PR의 목적이 명확해져 리뷰 프로세스가 더욱 효율적이 되고, 나중에 git history를 추적할 때도 변경의 의도를 더 쉽게 파악할 수 있습니다.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed PR 제목 "[BE][feat]: mq 리스너 관련 코드 수정"은 변경 사항의 핵심을 명확하게 반영하고 있습니다. 실제 코드 변경사항을 살펴보면 RabbitMQ 기반 메시지 큐 인프라 구축, NotificationConsumer와 NotificationEventHandler 등 리스너 패턴 구현, 그리고 NotificationMQProducer를 통한 메시지 발행 기능이 추가되었습니다. 이는 제목에서 명시한 "mq 리스너 관련 코드"와 정확하게 일치합니다.

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.

@github-actions github-actions bot changed the title Feat/977 [BE][feat]: mq 리스너 관련 코드 수정 Oct 19, 2025
@github-actions github-actions bot added the BE 백엔드 관련 이슈입니다. label Oct 19, 2025
@Jin409 Jin409 marked this pull request as ready for review October 21, 2025 04:59
Copy link
Contributor

@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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmEventListener.java (1)

28-38: 에러 처리 부재: FCM 전송 실패 시 재시도 메커니즘 필요

세 메서드 모두 fcmClient 호출 시 예외 처리가 없습니다. FCM 전송이 실패하면 이벤트는 소실되고, 사용자는 알림을 받지 못하게 됩니다. 특히 onTokens 메서드의 배치 처리에서는 일부 배치만 실패할 경우 어떤 배치가 실패했는지 추적 불가능합니다.

권장하는 개선 방안:

선택지 1 (재시도 + 로깅, 현재 권장):

@Transactional
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onTokens(SendMessageByFcmTokensRequest request) {
    Lists.partition(request.allTokens(), FCM_BATCH_SIZE)
        .forEach(tokens -> {
            try {
                fcmClient.sendMulticast(request.withTokens(tokens));
            } catch (FcmException e) {
                log.error("FCM 배치 전송 실패. 토큰: {}, 원인: {}", 
                    tokens, e.getMessage());
                // 실패한 토큰을 재시도 큐에 저장하거나
                // 모니터링 대시보드에 기록
            }
        });
}
  • 장점: 에러 격리, 부분 성공 처리 가능
  • 단점: 개별 배치별 실패 추적 필요

선택지 2 (Resilience4j + Circuit Breaker, 더 견고함):

  • RabbitMQ로 이미 전환 중이라면, 불필요한 추가 복잡도
  • 기존 시스템이 오래 유지된다면 고려 가치

현재 RabbitMQ 전환 과정이므로, 최소한 선택지 1 수준의 에러 핸들링을 추가하여 데이터 손실을 방지하시는 걸 강력히 권장합니다.

backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmClient.java (1)

67-67: 로그 레벨을 재검토하세요.

log.warn은 경고나 문제 상황에 사용하는 레벨인데, 정상적인 multicast 배치 응답을 로깅하는 데 사용되고 있습니다. BatchResponse에는 성공/실패 정보가 모두 포함되므로, 로그 레벨을 상황에 맞게 조정해야 합니다.

개선 방안:

  • 성공 케이스는 log.info 또는 log.debug
  • 실패가 포함된 경우만 log.warn 또는 log.error
  • 실패 건에 대해서만 별도 로깅
-            log.warn("multicast batchResponse : {}", batchResponse.toString());
+            if (batchResponse.getFailureCount() > 0) {
+                log.warn("Multicast 전송 중 실패 발생 - 성공: {}, 실패: {}, 응답: {}", 
+                    batchResponse.getSuccessCount(), 
+                    batchResponse.getFailureCount(), 
+                    batchResponse);
+            } else {
+                log.info("Multicast 전송 완료 - 성공: {}", batchResponse.getSuccessCount());
+            }
🧹 Nitpick comments (11)
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmEventListener.java (2)

14-17: @async 실행기 설정 확인 필요: 기본 스레드 풀로 인한 병목 가능성

현재 @Async 어노테이션이 있지만, 전용 Executor가 지정되지 않았습니다. 기본값(SimpleAsyncTaskExecutor)을 사용하면 요청마다 새로운 스레드가 생성되어 다음 문제가 발생할 수 있습니다:

  • 높은 FCM 요청 트래픽 시 스레드 고갈
  • 메모리 누수 위험 (스레드 재사용 안 됨)
  • 컨텍스트 스위칭 오버헤드

PR 설명에서 WebAsyncConfigproducerExecutor 언급이 있으니, 다음과 같이 명시적으로 지정하는 것을 권장합니다:

@Async("fcmWorkerExecutor")  // 또는 워커 전용 executor 지정

또는 별도로 FcmEventListener 전용 executor 생성:

@Bean(name = "fcmListenerExecutor")
public Executor fcmListenerExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(5);
    executor.setMaxPoolSize(20);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("fcm-listener-");
    executor.initialize();
    return executor;
}

현재 설정이 어떻게 되어 있는지 WebAsyncConfig와 함께 검토해주시면, 이 부분이 예상대로 동작하는지 확인할 수 있을 것 같습니다.


33-37: 배치 처리 로직의 시점 검토: 리스너 vs MQ 컨슈머

현재 리스너 내부에서 배치를 나누어 처리하는 방식인데, MQ 기반 아키텍처로 전환할 때 이 로직의 위치를 재검토하는 것이 좋습니다.

현재 방식의 한계:

  • 리스너에서 배치 처리 → 하나의 리스너 스레드에서 순차 처리
  • 높은 토큰 수(예: 10,000개) 시 리스너 블로킹 가능
  • 배치 간 재시도 전략이 불명확함

MQ 전환 후 권장 방식:
배치 처리를 라우팅 로직으로 이동 (MQ Producer 단계에서 처리):

// NotificationMQProducer에서 처리
public void publishTokensEvent(SendMessageByFcmTokensRequest request) {
    Lists.partition(request.allTokens(), FCM_BATCH_SIZE)
        .forEach(tokens -> {
            MQMessage msg = new MQMessage(request.withTokens(tokens));
            rabbitTemplate.convertAndSend(msg);
        });
}
  • 장점: 리스너는 단일 배치만 처리, 확장성 좋음, 병렬 처리 용이
  • 단점: Producer 쪽 로직 증가

현재 이 클래스는 임시 상태이므로, 새로운 NotificationConsumer에서는 이 배치 로직을 어디에 배치할 예정인지 확인해주면 좋을 것 같습니다.

backend/src/main/java/backend/mulkkam/common/config/WebAsyncConfig.java (1)

13-22: ThreadPoolTaskExecutor 설정에 대한 검토가 필요합니다.

현재 설정(core=2, max=4, queue=50)이 상대적으로 작아 보입니다. 특히 RabbitWorkerConfigfcmWorkerExecutor(core=4, max=8, queue=200)와 비교했을 때 차이가 큽니다.

잠재적 문제점:

  1. 큐 용량 부족: 큐 크기 50은 부하가 높을 때 금방 차서 RejectedExecutionException이 발생할 수 있습니다 (기본 정책이 AbortPolicy).
  2. 처리량 제한: 최대 4개 스레드로는 많은 메시지 발행 요청을 처리하기 어려울 수 있습니다.

개선 방안:

  • 큐 용량을 최소 100 이상으로 증가시키거나, 예상 부하에 맞춰 조정하세요.
  • Rejection policy를 명시적으로 설정하세요 (예: CallerRunsPolicy - 호출 스레드에서 실행, 또는 DiscardOldestPolicy - 가장 오래된 작업 제거).
  • 운영 환경에서 스레드 풀 메트릭을 모니터링하고 튜닝하세요.
 @Bean("producerExecutor")
 public ThreadPoolTaskExecutor producerExecutor() {
     var ex = new ThreadPoolTaskExecutor();
     ex.setCorePoolSize(2);
     ex.setMaxPoolSize(4);
-    ex.setQueueCapacity(50);
+    ex.setQueueCapacity(100);
     ex.setThreadNamePrefix("producer-async-");
+    ex.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
     ex.initialize();
     return ex;
 }

추가 import가 필요합니다:

import java.util.concurrent.ThreadPoolExecutor;
backend/src/main/resources/application-prod.yml (1)

33-37: 운영 환경에 RabbitMQ 고급 설정 추가를 검토하세요.

현재는 기본 연결 정보만 설정되어 있습니다. 운영 환경의 안정성과 성능을 위해 다음 설정들을 추가로 고려해보세요:

권장 추가 설정:

  1. Connection pool: 연결 재사용 및 관리
  2. Timeout 설정: 네트워크 장애 시 빠른 실패
  3. Retry 정책: 일시적 장애 대응
  4. TLS/SSL: 보안 통신 (운영 환경 필수)
   rabbitmq:
     host: ${MQ_HOST}
     port: 5672
     username: ${MQ_USER}
     password: ${MQ_PASSWORD}
+    connection-timeout: 30000  # 30초
+    requested-heartbeat: 60    # 60초마다 heartbeat
+    cache:
+      connection:
+        mode: connection       # 연결 풀 모드
+        size: 5                # 연결 풀 크기
+      channel:
+        size: 50               # 채널 풀 크기
+        checkout-timeout: 5000 # 채널 대기 타임아웃
+    template:
+      retry:
+        enabled: true
+        initial-interval: 1000
+        max-attempts: 3
+        multiplier: 2
backend/src/main/resources/application-worker.yml (2)

1-9: Worker 프로필 기본 설정이 적절합니다.

웹 애플리케이션을 띄우지 않고 순수 메시지 컨슈머로 동작하도록 설정되어 있어 의도에 맞습니다. lazy-initialization: true는 시작 시간을 단축시키지만, 몇 가지 주의사항이 있습니다.

Lazy Initialization 주의사항:

  1. 첫 요청 시 지연: 첫 메시지 처리 시 빈 초기화로 인한 지연 발생
  2. 초기화 오류 발견 지연: 애플리케이션 시작 시가 아닌 런타임에 설정 오류 발견
  3. Listener 활성화: @RabbitListener는 eager 초기화가 필요할 수 있음

필요시 특정 빈만 eager 로딩하도록 설정할 수 있습니다:

@Component
@Lazy(false)  // 이 컴포넌트는 즉시 로딩
public class NotificationConsumer {
    // ...
}

18-23: 스케줄러와 Flyway 비활성화가 적절합니다.

Worker는 메시지 소비에만 집중하므로 스케줄링이나 마이그레이션이 불필요합니다. 이는 리소스 절약과 책임 분리 측면에서 올바른 설계입니다.

다만, 로깅 설정이 누락되어 있습니다. Worker의 메시지 처리 과정을 모니터링하려면 로그 레벨과 패턴 설정이 필요합니다.

다음 로깅 설정 추가를 권장합니다:

logging:
  level:
    backend.mulkkam.common.infrastructure.fcm: INFO
    org.springframework.amqp: INFO  # RabbitMQ 관련 로그
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationEventHandler.java (1)

12-15: @async 실행기 지정 + 프로필 범위 축소 제안

현재 클래스 레벨 @async는 기본 실행기(SimpleAsyncTaskExecutor)로 동작할 수 있습니다. prod/dev에서만 비동기 발행이 필요하고, 이미 WebAsyncConfig에 "producerExecutor"가 있으므로 해당 풀을 명시적으로 사용하고, worker 프로필에서는 빈이 생성되지 않도록 제한하는 편이 안전합니다.

-@Async
-@RequiredArgsConstructor
-@Component
+@RequiredArgsConstructor
+@Component
+@Profile({"prod","dev"})
 public class NotificationEventHandler {

그리고 각 메서드에 실행기 지정:

-@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+@Async("producerExecutor")
+@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
 public void onTopic(...)

-@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+@Async("producerExecutor")
+@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
 public void onToken(...)

-@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
+@Async("producerExecutor")
+@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
 public void onTokens(...)

장점: 요청 쓰레드 비차단 + 일관된 풀 사용 + worker 기동 시 불필요한 빈 생성 방지. 단점: prod/dev 외 프로필에서 이벤트 발행이 필요하다면 추가 프로필 고려 필요. (As per coding guidelines)

backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java (1)

17-26: 워커 풀 종료 시 그레이스풀 셧다운 보강

현재 ThreadPoolTaskExecutor는 즉시 종료될 수 있어 처리 중 작업이 중단될 위험이 있습니다. 셧다운 대기 설정을 권장합니다.

 public ThreadPoolTaskExecutor fcmWorkerExecutor() {
   var ex = new ThreadPoolTaskExecutor();
   ex.setCorePoolSize(4);
   ex.setMaxPoolSize(8);
   ex.setQueueCapacity(200);
   ex.setThreadNamePrefix("fcm-worker-");
+  ex.setWaitForTasksToCompleteOnShutdown(true);
+  ex.setAwaitTerminationSeconds(30);
   ex.initialize();
   return ex;
 }

장점: 배포/스케일인 시 메시지 유실 방지. 단점: 종료 시간이 늘 수 있음.

backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java (1)

17-21: 퍼블리셔 확인/리턴 처리(Confirm/Return) 여부 확인

RabbitTemplate 사용 시 라우팅 실패나 브로커 비가용을 조기에 감지하려면 Publisher Confirm/Returns 활성화가 필요합니다. 현 클래스에서는 설정할 수 없고 ConnectionFactory/Template 설정에서 켜야 합니다. RabbitMQConfig 코멘트를 참고해 주세요.

backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationConsumer.java (1)

27-43: 멀티캐스트/토픽 소비: 부분 실패/중복 방지 전략

  • 멀티캐스트는 일부 토큰 실패가 흔합니다. 실패 토큰 추출 후 재큐(지연 큐) 또는 별도 보정 파이프라인 권장.
  • 재처리 시 중복 발송 방지를 위해 메시지에 idempotency 키(예: eventId) 포함 후 중복 차단(캐시/DB) 또는 FCM collapseKey 사용을 고려하세요.

원하시면 실패 토큰만 재게시하는 보정 로직과 DTO 확장(eventId/collapseKey) 패치 제안 가능합니다.

backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java (1)

53-66: 라우팅키 하드코딩 → 프로퍼티화 + 상수 중복 제거

현재 바인딩 키 "notify.*"가 프로듀서와 컨피그 양쪽에 하드코딩되어 있어 변경 시 드리프트 위험이 큽니다. 프로퍼티로 외부화하고 동일 키를 사용하도록 일원화하세요.

- public Binding bindToken() {
-   return BindingBuilder.bind(tokenQueue()).to(notificationsExchange()).with("notify.token");
- }
+ @Value("${mulkkam.mq.routing.token:notify.token}")
+ private String rkToken;
+ @Bean
+ public Binding bindToken() {
+   return BindingBuilder.bind(tokenQueue()).to(notificationsExchange()).with(rkToken);
+ }

- public Binding bindTokens() {
-   return BindingBuilder.bind(tokensQueue()).to(notificationsExchange()).with("notify.tokens");
- }
+ @Value("${mulkkam.mq.routing.tokens:notify.tokens}")
+ private String rkTokens;
+ @Bean
+ public Binding bindTokens() {
+   return BindingBuilder.bind(tokensQueue()).to(notificationsExchange()).with(rkTokens);
+ }

- public Binding bindTopic() {
-   return BindingBuilder.bind(topicQueue()).to(notificationsExchange()).with("notify.topic");
- }
+ @Value("${mulkkam.mq.routing.topic:notify.topic}")
+ private String rkTopic;
+ @Bean
+ public Binding bindTopic() {
+   return BindingBuilder.bind(topicQueue()).to(notificationsExchange()).with(rkTopic);
+ }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b06d8be and 0494636.

📒 Files selected for processing (17)
  • .github/workflows/backend-dev-cd.yml (1 hunks)
  • backend/build.gradle (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/config/AsyncConfig.java (0 hunks)
  • backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/config/WebAsyncConfig.java (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmClient.java (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmEventListener.java (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationConsumer.java (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationEventHandler.java (1 hunks)
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java (1 hunks)
  • backend/src/main/resources/application-dev.yml (2 hunks)
  • backend/src/main/resources/application-local.yml (1 hunks)
  • backend/src/main/resources/application-prod.yml (1 hunks)
  • backend/src/main/resources/application-worker.yml (1 hunks)
  • backend/src/main/resources/data.sql (0 hunks)
  • backend/src/test/resources/application-ci.yml (2 hunks)
💤 Files with no reviewable changes (2)
  • backend/src/main/resources/data.sql
  • backend/src/main/java/backend/mulkkam/common/config/AsyncConfig.java
🧰 Additional context used
📓 Path-based instructions (1)
backend/**

⚙️ CodeRabbit configuration file

backend/**: - 1. 팀 및 공식 컨벤션, 가독성, 예외처리, 테스트/확장/유지보수성, 모듈화, API/DB/보안 설계 기준을 기반으로 리뷰해주세요.

    1. 최신 트렌드, 불필요한 로직, 클린코드, 리팩토링, 서비스/도메인 설계, 공통 예외 처리, 확장성도 함께 확인해주세요.
    1. 각 피드백은 문제점·대안·장단점을 짧고 논리적으로, 예시 코드가 있다면 간결히 포함해 주세요.
    1. 팀 내 스타일 통일성도 확인해주세요.
    1. 미작성한 테스트 코드 케이스가 있다면, 어떤 테스트가 필요한지 제안해주세요. (예: 컨트롤러는 인수 테스트, 나머지는 단위 테스트)
    1. 리뷰 남겨주는 부분은 해당 라인 범위의 코멘트에 작성해주세요.

Files:

  • backend/src/main/resources/application-dev.yml
  • backend/src/main/resources/application-local.yml
  • backend/src/main/resources/application-prod.yml
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmEventListener.java
  • backend/src/main/resources/application-worker.yml
  • backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java
  • backend/build.gradle
  • backend/src/main/java/backend/mulkkam/common/config/WebAsyncConfig.java
  • backend/src/test/resources/application-ci.yml
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmClient.java
  • backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationEventHandler.java
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationConsumer.java
  • backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java
🧬 Code graph analysis (5)
backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java (3)
backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java (1)
  • Configuration (13-44)
backend/src/main/java/backend/mulkkam/common/config/WebAsyncConfig.java (1)
  • Configuration (9-23)
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationConsumer.java (1)
  • Profile (11-44)
backend/src/main/java/backend/mulkkam/common/config/WebAsyncConfig.java (1)
backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java (1)
  • Configuration (13-44)
backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java (1)
backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java (1)
  • Profile (17-80)
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationEventHandler.java (1)
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java (1)
  • RequiredArgsConstructor (13-37)
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationConsumer.java (2)
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java (1)
  • RequiredArgsConstructor (13-37)
backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java (1)
  • Profile (17-80)
⏰ 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: test
🔇 Additional comments (8)
.github/workflows/backend-dev-cd.yml (2)

1-132: PR 체크리스트 미완료 항목 확인

PR 설명에서 다음 항목들이 아직 미완료로 표시되어 있습니다:

  • 알림 전송 여부 확인 테스트
  • 패키지 위치 수정

이 워크플로우 파일은 병합 전에 이 항목들이 완료되어야 합니다. 특히 "패키지 위치 수정"은 코드 구조와 관련이 있을 가능성이 높으므로, 완료 상태를 확인하고 필요시 추가 파일 리뷰가 있어야 합니다.


5-5: 기능 브랜치를 자동 배포에 포함시키는 패턴은 신중히 검토가 필요합니다.

현재 구조의 맥락을 먼저 인정하겠습니다. Blue-Green 배포 방식과 backend/** 경로 필터링, 그리고 self-hosted, dev 환경으로 제한한 점들이 위험을 상당히 완화하고 있습니다. 다만 몇 가지 점검할 사항들이 있습니다:

현재 구조에서 고려할 점:

  1. 임시 설정의 영구화 위험: 기능 브랜치는 본질적으로 일시적입니다. PR 머지 후 feat/977을 워크플로우에서 제거하지 않으면, 다시 푸시할 때마다 불필요하게 배포 파이프라인이 돌아갑니다. 이는 CI/CD 자원 낭비와 예기치 않은 배포를 초래할 수 있습니다.

  2. 명확한 정책의 부재: 다른 워크플로우 파일들이 푸시 브랜치를 명시하지 않은 반면, 이 파일만 명시적으로 지정한 것을 보면, 팀의 배포 정책이 명확하게 문서화되지 않은 상태로 보입니다.

  3. 스케일 문제는 아직 미래형: 지금은 한 브랜치지만, 다음 번엔 feat/978, feat/979…가 생길 텐데, 매번 워크플로우를 수정하는 것은 번거롭습니다.

제안:

  • 단기 (현재): 현재 설정을 유지하되, 이 변경의 의도를 PR 설명이나 체크리스트에 명시하세요. 예: "feat/977 개발 완료 후 삭제 예정" 같은 메모가 도움이 됩니다.
  • 중기 (PR 완료 후): feat/977 브랜치가 develop-be로 머지되면, 곧바로 이 워크플로우에서 제거하는 것을 체크리스트에 추가하세요.
  • 장기 (팀 정책): 모든 feature 브랜치를 자동 배포하거나, 명시적으로 지정하는 것 중 어느 정책을 택할지 팀과 논의해 README나 CONTRIBUTING.md에 기록하는 것을 권장합니다.

현재 PR 체크리스트의 "알림 전송 여부 확인 테스트"와 "패키지 위치 수정"이 완료되면, 이 워크플로우 설정도 함께 검토하시기 바랍니다.

backend/build.gradle (1)

44-44: AMQP 의존성 추가가 적절합니다.

Spring Boot의 dependency management를 통해 버전이 자동 관리되므로 안전하게 추가되었습니다. RabbitMQ 메시징 기능 구현에 필요한 적절한 의존성입니다.

backend/src/test/resources/application-ci.yml (1)

24-28: CI 환경 RabbitMQ 설정이 적절하게 추가되었습니다.

다른 환경(dev, prod)과 동일한 토폴로지 구조를 유지하여 일관성이 확보되었습니다. CI 테스트 시 RabbitMQ 기반 메시징 로직을 검증할 수 있는 환경이 마련되었습니다.

Also applies to: 46-57

backend/src/main/resources/application-prod.yml (1)

39-50: MQ 토폴로지 네이밍이 명확합니다.

Exchange와 Queue, Routing Key의 네이밍이 일관성 있고 의도가 명확하게 드러납니다. notifications.ex exchange에 세 가지 용도별 큐(token, tokens, topic)를 바인딩하는 구조가 합리적입니다.

backend/src/main/resources/application-dev.yml (1)

11-15: 개발 환경 RabbitMQ 설정이 운영 환경과 일관성 있게 구성되었습니다.

토폴로지가 prod와 동일하여 개발/스테이징에서 운영과 동일한 메시징 흐름을 테스트할 수 있습니다.

다만, application-prod.yml에 남긴 리뷰처럼, 개발 환경에서도 connection timeout, retry 정책 등의 고급 설정을 추가하면 실제 운영 환경과 더 유사한 조건에서 테스트할 수 있습니다.

Also applies to: 83-94

backend/src/main/resources/application-worker.yml (1)

11-41: 현재 Worker 설정에는 데이터베이스가 필요하지 않습니다.

코드 검증 결과, NotificationConsumer와 FcmClient 모두 DB 접근 없이 순수 메시지 수신 및 FCM 전송 기능만 수행하고 있습니다. 원본 리뷰에서 제시한 시나리오들(무효 토큰 삭제, 전송 이력 저장, 재시도 정책, 유저 설정 조회)은 현재 코드에 구현되지 않았으므로, 지금 단계에서 DB 설정을 미리 추가하는 것은 YAGNI 원칙에 어긋납니다. 향후 해당 기능이 필요해질 때 그때 설정하는 것이 더 적절합니다.

다만 한 가지 고려할 점은, FcmClient.sendMulticast() 메서드에서 BatchResponse를 단순 로그로만 기록하고 있다는 부분입니다. 이 응답에는 실패한 토큰 정보가 포함되어 있는데, 이를 버려버리면 나중에 "무효 토큰 삭제" 기능을 추가하려 할 때 대응이 어려워집니다. 지금 당장 DB는 필요 없지만, 구조적으로 실패 정보를 추적할 수 있도록 설계해두면 향후 확장이 수월해질 것입니다.

backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationEventHandler.java (1)

19-32: 이벤트 발행의 트랜잭션 안전성 확인 결과

현재 코드를 검증한 결과, publishEvent() 호출 시점들이 모두 @Transactional 메서드 내에서 이루어지고 있습니다:

  • SuggestionNotificationService.createAndSendSuggestionNotification() (라인 96-97): @transactional
  • NotificationService.createAndSendTokenNotification() (라인 127-128): @transactional
  • NotificationService.processReminderNotifications() (라인 53-54): @transactional

따라서 AFTER_COMMIT에서 이벤트가 무시되는 현재 상황은 발생하지 않습니다. 다만, 향후 새로운 코드 경로에서 비트랜잭션 컨텍스트에서 이벤트를 발행할 가능성을 대비한 방어적 개선을 고려할 수 있습니다:

선택 1 (현재 상태 유지): 데이터 정합성이 보장되는 대신, 트랜잭션 없는 컨텍스트에서는 이벤트 미발행.

선택 2 (fallbackExecution 추가): @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)로 설정하여 비트랜잭션 컨텍스트에서도 즉시 발행. 장점은 포용성, 단점은 커밋 보장 없음.

팀의 이벤트 발행 정책과 FCM 신뢰성 요구사항을 고려하여 선택하시면 됩니다.

Comment on lines +38 to +51
@Bean
public Queue tokenQueue() {
return QueueBuilder.durable(qToken).build();
}

@Bean
public Queue tokensQueue() {
return QueueBuilder.durable(qTokens).build();
}

@Bean
public Queue topicQueue() {
return QueueBuilder.durable(qTopic).build();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

DLQ/DLX 미구성 → 영구 실패 격리 불가

현재 큐는 DLX/DLQ가 없습니다. 소비 실패 시 분석/재처리가 어렵습니다.

선택지:

  • 큐에 DLX 지정 + DLQ 선언(+ 선택적 지연 재시도용 x-message-ttl).
  • 운영에서 중앙 토폴로지로 선언 시, 이 클래스에서는 선언 생략하고 문서화.

예시(간단형):

 public Queue tokenQueue() {
-  return QueueBuilder.durable(qToken).build();
+  return QueueBuilder.durable(qToken)
+    .withArgument("x-dead-letter-exchange", ex + ".dlx")
+    .withArgument("x-dead-letter-routing-key", qToken + ".dlq")
+    .build();
 }

토픽/토큰즈 큐도 동일하게 적용하고, DLX/DLQ Declarables 추가 필요. 장점: 실패 격리/재처리 용이. 단점: 브로커 리소스 증가/운영 복잡도. (As per coding guidelines)

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +73 to +79
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory cf,
Jackson2JsonMessageConverter converter) {
RabbitTemplate t = new RabbitTemplate(cf);
t.setMessageConverter(converter);
return t;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Publisher Confirm/Returns 활성화로 라우팅/브로커 이슈 조기 감지

현재 RabbitTemplate에 Confirm/Returns 설정이 없습니다. 운영 장애 시 조기 탐지가 어렵습니다.

예시:

 @Bean
 public RabbitTemplate rabbitTemplate(ConnectionFactory cf,
                                      Jackson2JsonMessageConverter converter) {
   RabbitTemplate t = new RabbitTemplate(cf);
   t.setMessageConverter(converter);
+  t.setMandatory(true); // unroutable returns 수신
+  // t.setReturnsCallback(returned -> log.warn("Returned: {}", returned));
+  // ConfirmType 설정은 CachingConnectionFactory에서 publisherConfirmType=CORRELATED 로
   return t;
 }

추가로 spring.rabbitmq.publisher-confirm-type=correlated, spring.rabbitmq.publisher-returns=true 설정과 함께 사용하세요.

검증:


🏁 Script executed:

#!/bin/bash
rg -n 'publisher-returns|publisher-confirm' backend/src/main/resources -g '*application*.yml'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# 1. Check full RabbitMQConfig.java content
echo "=== RabbitMQConfig.java Content ===" 
cat backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java

echo -e "\n=== Search for ConfirmCallback/ReturnsCallback ===" 
rg -n 'ConfirmCallback|ReturnsCallback' backend/src --type=java

echo -e "\n=== Search for setMandatory or setReturns ===" 
rg -n 'setMandatory|setReturns|setConfirm' backend/src --type=java

echo -e "\n=== Search for RabbitMQ properties in application configs ===" 
find backend/src/main/resources -name '*application*' -type f | xargs grep -l 'rabbitmq' 2>/dev/null || echo "No files found"

echo -e "\n=== Show RabbitMQ property contents ===" 
find backend/src/main/resources -name '*application*.yml' -o -name '*application*.yaml' -o -name '*application*.properties' | xargs cat 2>/dev/null | grep -A5 -B5 'rabbitmq' || echo "No rabbitmq config found"

Length of output: 3699


Publisher Confirm/Returns 설정으로 메시지 전달 보증 추가

검증 결과, 리뷰 코멘트의 우려가 타당함을 확인했습니다.

확인된 현황:

  • RabbitMQConfig.java의 rabbitTemplate 빈에 Confirm/Returns 설정이 없음
  • 모든 application 설정 파일(dev/local/prod/worker)에서 publisher-confirm-type, publisher-returns 설정 부재
  • 코드베이스 어디에도 ConfirmCallback/ReturnsCallback 구현 없음

왜 필요한가:
토큰 관련 중요 메시지를 취급하면서 현재는 메시지 손실이나 라우팅 실패를 감지할 방법이 없습니다. 운영 중 메시지가 유실되어도 애플리케이션은 알 수 없어 데이터 무결성 문제로 이어질 수 있습니다.

제시된 예시 코드와 추가 설정이 정확하고 권장되는 방식이므로, 해당 내용을 참고하여 아래 두 단계로 구현하세요:

  1. RabbitTemplate 설정 추가 (예시 코드 참조)
  2. application 설정에 다음 속성 추가:
    spring:
      rabbitmq:
        publisher-confirm-type: correlated
        publisher-returns: true
🤖 Prompt for AI Agents
In backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java
around lines 73-79, the RabbitTemplate bean lacks publisher confirm and returns
handling; update the bean to enable publisher confirms/returns by configuring
the template to use correlating confirms and returns (setMandatory(true)),
register a ConfirmCallback and a ReturnsCallback (or a ConfirmCorrelationData
handler) to surface publish acknowledgements and undeliverable messages, and
ensure application.yml/properties includes
spring.rabbitmq.publisher-confirm-type=correlated and
spring.rabbitmq.publisher-returns=true so broker-level confirms and return
routing are enabled.

Comment on lines +28 to +43
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory cf,
@Qualifier("fcmWorkerExecutor") TaskExecutor exec,
Jackson2JsonMessageConverter converter
) {
var f = new SimpleRabbitListenerContainerFactory();
f.setConnectionFactory(cf);
f.setMessageConverter(converter);
f.setConcurrentConsumers(4);
f.setMaxConcurrentConsumers(8);
f.setPrefetchCount(100);
f.setTaskExecutor(exec);
f.setDefaultRequeueRejected(false);
return f;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

재시도/격리 경로(DLQ) 없음 → 소비 실패 시 메시지 유실 위험

setDefaultRequeueRejected(false)로 예외가 발생하면 즉시 버려집니다. 외부 FCM 호출은 일시 오류가 잦아 재시도/격리 전략이 필요합니다.

권장(둘 중 택1 또는 병행):

  • 컨테이너 레벨 재시도 + 복구기(DLQ 퍼블리셔)
 public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(...){
   var f = new SimpleRabbitListenerContainerFactory();
   ...
-  f.setDefaultRequeueRejected(false);
+  f.setDefaultRequeueRejected(false); // 고의 비재큐
+  f.setAdviceChain(
+      org.springframework.amqp.rabbit.config.RetryInterceptorBuilder
+        .stateless()
+        .maxAttempts(5)
+        .backOffOptions(1000, 2.0, 15000) // 1s → 15s
+        .recoverer(new org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer())
+        .build()
+  );
   return f;
 }
  • 토폴로지에 DLX/DLQ 추가하고, 실패 시 DLQ로 라우팅(아래 RabbitMQConfig 코멘트 참고).

보조 권장:

+ f.setMissingQueuesFatal(false); // 초기 구동 시 큐 미존재 허용

장단점:

  • 재시도: 일시 오류에 강함, 지연 증가 가능.
  • DLQ: 영구 실패 분석 용이, 재처리 파이프라인 필요.

원하시면 RetryAdvice + DLQ 선언 전체 패치 제안 드립니다. (As per coding guidelines)

🤖 Prompt for AI Agents
In backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java
around lines 28-43, the listener factory sets f.setDefaultRequeueRejected(false)
which immediately drops messages on exception; change to enable retries and DLQ
handling by (a) enabling container-level retry (e.g., configure a
RetryOperationsInterceptor or RetryTemplate on the factory) or (b) keep requeue
false but declare and bind a DLX/DLQ for the queue and publish failed messages
to the DLQ via a Recoverer/RepublishMessageRecoverer so transient FCM errors are
retried and permanent failures are routed to a DLQ for analysis; ensure
TaskExecutor and concurrency settings remain and wire in the chosen
retry/recoverer beans and DLX/DLQ declarations in RabbitMQConfig.

Comment on lines +37 to +41
f.setConcurrentConsumers(4);
f.setMaxConcurrentConsumers(8);
f.setPrefetchCount(100);
f.setTaskExecutor(exec);
f.setDefaultRequeueRejected(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Prefetch/동시성 튜닝 가이드

prefetch=100, concurrent 48은 단일 인스턴스 기준 최대 800개까지 워커에 적재될 수 있습니다. FCM 쿼터/속도제한과 메모리 상황에 맞춰 2050 수준으로 낮추고, 실제 처리량에 따라 상향 조정하는 방식을 권장합니다.

검증:


🏁 Script executed:

#!/bin/bash
# 운영/스테이징 YAML에서 prefetch, concurrency 설정 여부 확인
rg -n 'prefetch|concurrent' backend/src/main/resources -g '*application*.yml'

Length of output: 0


더 정확한 검증을 위해 추가 조회가 필요합니다. YAML 파일이 없다는 것은 해당 설정이 하드코딩되어 있거나 다른 위치에 있을 가능성을 시사합니다.


🏁 Script executed:

#!/bin/bash
# 1. resources 디렉토리의 모든 설정 파일 확인
echo "=== All config files in resources ==="
find backend/src/main/resources -type f \( -name "*.yml" -o -name "*.yaml" -o -name "*.properties" \) 2>/dev/null | head -20

# 2. RabbitWorkerConfig에서 @Value 또는 @ConfigurationProperties 사용 여부 확인
echo -e "\n=== RabbitWorkerConfig 전체 코드 ==="
cat backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java

# 3. application 설정 파일 기본 경로 확인
echo -e "\n=== Content of application.yml/properties (if exists) ==="
if [ -f backend/src/main/resources/application.yml ]; then
  cat backend/src/main/resources/application.yml
elif [ -f backend/src/main/resources/application.properties ]; then
  cat backend/src/main/resources/application.properties
else
  echo "No default application.yml/properties found"
fi

Length of output: 2383


더 정확한 검증을 위해 application-worker.yml 내용을 확인하겠습니다.


🏁 Script executed:

#!/bin/bash
echo "=== Content of application-worker.yml ==="
cat backend/src/main/resources/application-worker.yml

echo -e "\n=== Checking for any RabbitMQ configuration across all profiles ==="
rg -n 'rabbit:|spring\.rabbitmq' backend/src/main/resources

echo -e "\n=== Checking for hardcoded RabbitMQ config patterns in Java ==="
rg -n 'setPrefetchCount|setConcurrentConsumers|setMaxConcurrentConsumers' backend/src/main/java

Length of output: 1253


설정값 하드코딩 제거 및 Prefetch/동시성 튜닝 필요

현재 prefetch, concurrency 설정이 Java 코드에 완전히 하드코딩되어 있어 운영 환경별 튜닝이 불가능합니다.

문제점:

  • prefetch=100과 max concurrent=8의 조합으로 최대 800개 메시지까지 워커에 동시 적재 가능
  • FCM 쿼터(초당 처리량 제한)와 메모리 부담을 고려하면 과도한 값
  • YAML 기반 환경별 조정이 불가능하므로 튜닝마다 재배포 필요

권장 개선 방안:

① @ConfigurationProperties를 이용해 설정 외부화 (가장 권장)
   - 장점: 운영/스테이징에서 YAML로 유연하게 조정, 코드 변경 불필요
   - 단점: 초기 설정 추가 작업
   
② 하드코딩 값만 즉시 낮추기
   - prefetch: 20~50으로 설정
   - 장점: 빠른 적용
   - 단점: 나중에 또 조정해야 할 경우 재배포 필요

이유: 메시지 버스 시스템에서 prefetch는 메모리 부담과 처리 효율의 균형점입니다. 높은 값은 일시적 부하에서 메모리 압박을 유발하고 FCM 속도제한에 걸릴 위험이 있습니다. 환경별로 실제 처리량에 맞춰 동적으로 조정하는 것이 운영 안정성의 핵심입니다.

🤖 Prompt for AI Agents
In backend/src/main/java/backend/mulkkam/common/config/RabbitWorkerConfig.java
around lines 37 to 41, the prefetch and concurrency values are hardcoded;
replace them with configurable properties and lower safe defaults: create a
@ConfigurationProperties class (e.g., rabbit.worker.*) with fields for prefetch,
concurrentConsumers, maxConcurrentConsumers and default values (prefetch 20-50,
concurrentConsumers 4, maxConcurrentConsumers 8), inject that config into
RabbitWorkerConfig and use
config.getPrefetch()/getConcurrentConsumers()/getMaxConcurrentConsumers()
instead of hardcoded literals; if you need a quick fix without adding
@ConfigurationProperties, immediately reduce the hardcoded prefetch to ~20–50
and lower max concurrent to reduce memory/FCM quota risk, but prefer the
property-based approach for environment tuning.

@Slf4j
@RequiredArgsConstructor
@Component
// TODO: 구현 모두 끝내면 @Profile("worker") 추가하기
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TODO 주석에 대한 명확한 계획이 필요합니다.

현재 @Profile("worker") 없이 모든 프로필에서 FcmClient가 활성화되는데, 이는 의도하지 않은 동작을 유발할 수 있습니다.

잠재적 문제:

  • Worker 외 프로필(web, dev 등)에서도 FCM 클라이언트가 로드되어 불필요한 리소스 사용
  • NotificationConsumer는 worker 프로필 전용인데, 이를 호출하는 FcmClient는 모든 프로필에서 활성화되는 불일치

해결 방안:
PR 체크리스트에 "패키지 위치 수정"이 포함되어 있으므로, 다음 중 하나를 선택하세요:

  1. 즉시 @Profile("worker")를 추가하고 TODO 제거
  2. FcmClient를 worker 전용 패키지로 이동
  3. Web과 Worker에서 모두 사용해야 한다면, 호출 계층을 분리하여 각 프로필별로 다른 컴포넌트가 사용하도록 설계

이 부분을 구현하는 데 도움이 필요하시면 말씀해주세요. 프로필 분리 전략에 대한 구체적인 코드를 제안해드릴 수 있습니다.

🤖 Prompt for AI Agents
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmClient.java
around line 20: The TODO notes adding @Profile("worker") but currently the
FcmClient is loaded in all profiles causing mismatch with worker-only
NotificationConsumer; fix by adding @Profile("worker") to the FcmClient class
and remove the TODO comment so it only registers in the worker profile, and
update the PR checklist/commit message to reflect this change; alternatively if
you prefer package-based separation, move FcmClient to the worker-only package
and ensure no other profiles import it, or implement an interface with two
profile-specific implementations (worker annotated with @Profile("worker") and a
no-op or web implementation for other profiles) — pick one approach and apply it
consistently, updating tests and DI wiring accordingly.

import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

// TODO: MQ 구현 완료되는 시점에 제거
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

TODO 주석: 전환 전략을 더 구체적으로 명문화해주세요.

MQ 구현 완료 시점까지 이 클래스가 병행 운영된다는 뜻인데, 기존의 TransactionalEventListener 방식과 새로운 RabbitMQ 기반 방식이 동시에 작동하면서 동일한 FCM 메시지가 중복 발송될 위험이 있습니다.

특히 NotificationMQProducer와 FcmEventListener가 같은 이벤트를 구독하고 있다면, 같은 요청에 대해 메시지가 두 번 처리될 가능성을 배제할 수 없습니다. 제안하는 전환 전략:

선택지 1 (점진적 마이그레이션):

  • Feature flag나 환경 설정으로 어떤 방식을 사용할지 제어
  • 장점: 문제 발생 시 빠른 롤백 가능
  • 단점: 코드 복잡도 증가

선택지 2 (이벤트 분리):

  • RabbitMQ로 이동할 이벤트와 유지할 이벤트를 명확히 구분
  • 예: SendMessageByFcmTopicRequest → RabbitMQ (NonBlockingFcmEvent로 변경)
  • 장점: 명확한 책임 분리
  • 단점: 이벤트 타입 증가

TODO 주석에 담당자, 예상 완료 일시, 선택한 전환 전략을 명시하면 향후 리뷰와 관리가 훨씬 수월할 것 같습니다.

🤖 Prompt for AI Agents
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/FcmEventListener.java
around line 13: replace the vague TODO with a concrete migration plan that
states the chosen transition strategy (either "feature-flag controlled gradual
migration" or "event-type separation into RabbitMQ-only events"), the
responsible owner, an expected completion date, and the exact
rollback/mitigation for duplicate sends (e.g., enable/disable via env flag
and/or enforce idempotency token or disable old listener when feature flag ON).
Also update the comment to mention which event types will remain on
TransactionalEventListener vs move to RabbitMQ, and add a reference to any
config key or feature-flag name to be used to toggle behavior during rollout.

Comment on lines +18 to +26
// 단일 토큰 기반 발송
@RabbitListener(
queues = "${mulkkam.mq.queue.token}",
containerFactory = "rabbitListenerContainerFactory"
)
public void onToken(SendMessageByFcmTokenRequest req) {
fcmClient.sendMessageByToken(req);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

단일 토큰 소비: 실패 처리/관찰성 보강

예외가 발생하면 컨테이너 설정상 재큐 없이 폐기됩니다. 일시 오류(Firebase 500/UNAVAILABLE 등) 분류 재시도 또는 DLQ 격리가 필요하고, 최소한의 구조적 로그가 있으면 운영 가시성이 좋아집니다.

예시(재시도 어드바이스 전제, 실패 시 예외 전파):

 @RabbitListener(queues = "${mulkkam.mq.queue.token}", containerFactory = "rabbitListenerContainerFactory")
 public void onToken(SendMessageByFcmTokenRequest req) {
-  fcmClient.sendMessageByToken(req);
+  try {
+    fcmClient.sendMessageByToken(req);
+    // log.info("FCM token sent: token={}, title={}", mask(req.token()), req.title());
+  } catch (TransientFcmException e) {
+    throw e; // 재시도 유도
+  } catch (Exception e) {
+    // 영구 실패라면 AmqpRejectAndDontRequeueException 고려
+    throw e;
+  }
 }

참고: 토큰은 PII 성격이 있으니 로그 시 마스킹 필수. (As per coding guidelines)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationConsumer.java
around lines 18 to 26, the token consumer currently calls
fcmClient.sendMessageByToken(req) with no error handling or observability; wrap
the send in a try/catch that (1) classifies errors into transient (e.g.,
Firebase 5xx, UNAVAILABLE, INTERNAL/UNKNOWN network issues) and permanent
(invalid token, 4xx) and rethrows transient errors so the container/broker can
retry or route to DLQ, (2) for permanent failures swallow the exception but emit
a structured log containing operation, maskedToken (mask all but last 4 chars),
messageId or request id, and the error type/message, and (3) use the existing
logging framework (no PII in logs) and ensure any rethrown exception is a
runtime exception so container retry semantics apply.

Comment on lines +22 to +36
private static final int FCM_BATCH_SIZE = 500;

public void publishToken(SendMessageByFcmTokenRequest req) {
rabbitTemplate.convertAndSend(exchange, "notify.token", req);
}

public void publishTokens(SendMessageByFcmTokensRequest req) {
for (List<String> part : Lists.partition(req.allTokens(), FCM_BATCH_SIZE)) {
rabbitTemplate.convertAndSend(exchange, "notify.tokens", req.withTokens(part));
}
}

public void publishTopic(SendMessageByFcmTopicRequest req) {
rabbitTemplate.convertAndSend(exchange, "notify.topic", req);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

라우팅키/배치 크기 하드코딩 → 설정화 및 가드 필요

문자열 상수("notify.*")가 여기와 RabbitMQConfig 양쪽에 중복돼 드리프트 위험이 있습니다. 또한 토큰 리스트가 비어있을 때 불필요한 MQ 송신을 피하는 가드가 있으면 좋습니다.

 @RequiredArgsConstructor
 @Component
 public class NotificationMQProducer {
   ...
-  private static final int FCM_BATCH_SIZE = 500;
+  @Value("${mulkkam.fcm.batch-size:500}")
+  private int batchSize;
+
+  @Value("${mulkkam.mq.routing.token:notify.token}")
+  private String rkToken;
+  @Value("${mulkkam.mq.routing.tokens:notify.tokens}")
+  private String rkTokens;
+  @Value("${mulkkam.mq.routing.topic:notify.topic}")
+  private String rkTopic;

   public void publishToken(SendMessageByFcmTokenRequest req) {
-    rabbitTemplate.convertAndSend(exchange, "notify.token", req);
+    rabbitTemplate.convertAndSend(exchange, rkToken, req);
   }

   public void publishTokens(SendMessageByFcmTokensRequest req) {
-    for (List<String> part : Lists.partition(req.allTokens(), FCM_BATCH_SIZE)) {
-      rabbitTemplate.convertAndSend(exchange, "notify.tokens", req.withTokens(part));
+    var all = req.allTokens();
+    if (all == null || all.isEmpty()) return;
+    for (List<String> part : Lists.partition(all, batchSize)) {
+      rabbitTemplate.convertAndSend(exchange, rkTokens, req.withTokens(part));
     }
   }

   public void publishTopic(SendMessageByFcmTopicRequest req) {
-    rabbitTemplate.convertAndSend(exchange, "notify.topic", req);
+    rabbitTemplate.convertAndSend(exchange, rkTopic, req);
   }
 }

추가 검증 스크립트(하드코딩된 "notify." 사용 위치 파악):


🏁 Script executed:

#!/bin/bash
rg -n '"notify\.(token|tokens|topic)"' --type=java

Length of output: 1064


</shell_scripts>


🏁 Script executed:

#!/bin/bash
# Verify RabbitMQConfig.java structure around the routing key bindings
cat backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java | sed -n '50,70p'

Length of output: 643


🏁 Script executed:

#!/bin/bash
# Find SendMessageByFcmTokensRequest class to verify methods
ast-grep --pattern 'class SendMessageByFcmTokensRequest {
  $$$
}'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check for any other usages of these routing keys beyond these two files
rg '"notify\.' --type=java -g '!RabbitMQConfig.java' -g '!NotificationMQProducer.java' | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify if there are any tests or listeners using these routing keys
rg 'notify\.(token|tokens|topic)' --type=java | grep -i -E '(listener|consumer|queue|binding)' | head -10

Length of output: 514


🏁 Script executed:

#!/bin/bash
# Check if there are constants defined elsewhere for these routing keys
rg -n 'static final.*notify' --type=java

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify NotificationMQProducer full context for method signatures
cat backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java

Length of output: 1394


🏁 Script executed:

#!/bin/bash
# Find SendMessageByFcmTokensRequest definition
fd -e java -x grep -l 'class SendMessageByFcmTokensRequest' {}

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the SendMessageByFcmTokensRequest source
find . -name '*.java' -exec grep -l 'class SendMessageByFcmTokensRequest' {} \;

Length of output: 0


🏁 Script executed:

#!/bin/bash
# More targeted search for the request class
rg -n 'class SendMessageByFcmTokensRequest' --type=java -A 20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if configuration properties are already defined somewhere
rg 'mulkkam\.(fcm|mq)' --type=java

Length of output: 1027


🏁 Script executed:

#!/bin/bash
# Look for properties files with configuration
fd -e properties -o -e yml -o -e yaml | xargs grep -l 'mulkkam' 2>/dev/null | head -5

Length of output: 116


🏁 Script executed:

#!/bin/bash
# Check application properties/yaml files
find . -type f \( -name 'application*.properties' -o -name 'application*.yml' -o -name 'application*.yaml' \) -exec echo "File: {}" \; -exec head -50 {} \;

Length of output: 5827


🏁 Script executed:

#!/bin/bash
# Check if there are any guards already in place
rg -n 'allTokens.*isEmpty\|isEmpty.*allTokens' --type=java

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify import statements and dependencies in NotificationMQProducer
head -20 backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java

Length of output: 795


🏁 Script executed:

#!/bin/bash
# Check Lists usage (from Guava)
rg -n 'import.*Lists' --type=java backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/

Length of output: 277


🏁 Script executed:

#!/bin/bash
# Verify complete structure of request classes
rg -n 'class SendMessage.*Request' --type=java -A 10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check if there are other MQ producers following different patterns
find backend -name '*Producer.java' -o -name '*Publisher.java' | head -5

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Final check: confirm routing key strings are ONLY in these two files
rg -l '"notify\.(token|tokens|topic)"' --type=java

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Check if batchSize or similar config already exists
rg -n 'FCM_BATCH|fcm\.batch|batch-size' --type=java --type=properties --type=yaml

Length of output: 34


🏁 Script executed:

#!/bin/bash
# Inspect test files for routing key usage
find . -path '*test*' -name '*.java' | xargs rg '"notify\.(token|tokens|topic)"' 2>/dev/null | head -5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Get the full NotificationMQProducer to understand the current implementation
rg -n 'class NotificationMQProducer' --type=java -A 50

Length of output: 3122


🏁 Script executed:

#!/bin/bash
# Check RabbitTemplate convertAndSend signatures to ensure the suggested changes are compatible
rg -n 'rabbitTemplate' --type=java -B2 -A2 backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java

Length of output: 735


🏁 Script executed:

#!/bin/bash
# Verify the complete RabbitMQConfig to understand queue/binding patterns
cat backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java

Length of output: 2348


🏁 Script executed:

#!/bin/bash
# Check if there are integration tests that could be affected
find . -path '*test*' -name '*RabbitMQ*' -o -path '*test*' -name '*Notification*Producer*'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check environment-specific configuration files
find . -name 'application*.properties' -o -name 'application*.yml' | xargs ls -la 2>/dev/null

Length of output: 682


🏁 Script executed:

#!/bin/bash
# Look for @ConfigurationProperties usage in the codebase
rg -n '@ConfigurationProperties\|@Value.*notify\|@Value.*fcm' --type=java | head -20

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check common configuration patterns used in backend
rg -n '@Value.*{.*}' --type=java | head -10

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Verify if @Value with default values is already used elsewhere
rg -n '@Value.*:' --type=java | head -15

Length of output: 471


🏁 Script executed:

#!/bin/bash
# Final confirmation: list all Java files that reference routing keys
rg -l 'notify\.' --type=java

Length of output: 171


🏁 Script executed:

#!/bin/bash
# Check the actual structure and methods in SendMessageByFcmTokensRequest
find . -name '*.java' -type f | xargs grep -l 'SendMessageByFcmTokensRequest' | xargs cat | grep -A 30 'class SendMessageByFcmTokensRequest'

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Ensure the suggested empty check makes sense - verify request object nullability
rg -n 'SendMessageByFcmTokensRequest\|allTokens\|withTokens' --type=java -B2 -A2 | head -40

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check Lists.partition behavior with empty/null
rg -n 'Lists\.partition' --type=java -B3 -A3

Length of output: 2064


🏁 Script executed:

#!/bin/bash
# Find where these request DTOs are instantiated
rg -n 'new SendMessageByFcmTokens\|new SendMessageByFcmToken' --type=java | head -10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Summary check - list all references to these routing keys one more time with context
rg -n 'notify\.(token|tokens|topic)' --type=java -B2 -A2 | head -50

Length of output: 3669


🏁 Script executed:

#!/bin/bash
# Check if there are any existing constants for magic strings
rg -n 'private static final.*String.*=' --type=java backend/src/main/java/backend/mulkkam/common/ | head -15

Length of output: 923


🏁 Script executed:

#!/bin/bash
# Verify @Value injection style matches team convention
rg -n '@Value' --type=java -B1 -A1 backend/src/main/java/backend/mulkkam/common/ | head -30

Length of output: 2560


🏁 Script executed:

#!/bin/bash
# Check if application configuration already includes FCM or MQ settings
find . \( -name '*.properties' -o -name '*.yml' -o -name '*.yaml' \) | xargs grep -l 'fcm\|mq\|rabbit' 2>/dev/null

Length of output: 249


🏁 Script executed:

#!/bin/bash
# Look at team patterns for externalized configuration
rg -n 'spring.rabbitmq\|amqp\|queue' --type=properties --type=yaml | head -20

Length of output: 34


🏁 Script executed:

#!/bin/bash
# Validate that the suggested property names follow conventions
rg -n 'mulkkam\.' --type=yaml --type=properties 2>/dev/null | head -15

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Final: ensure our understanding of the issue is correct
echo "=== NotificationMQProducer routing keys ===" && \
rg -n '"notify\.(token|tokens|topic)"' --type=java backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java && \
echo "" && \
echo "=== RabbitMQConfig routing keys ===" && \
rg -n '"notify\.(token|tokens|topic)"' --type=java backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java

Length of output: 629


🏁 Script executed:

#!/bin/bash
# Check if there are any existing listener/consumer classes using these routing keys
find . -name '*Listener.java' -o -name '*Consumer.java' | xargs grep -l 'notify\.' 2>/dev/null

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify the team uses @Component for MQ producers
rg -n '@Component.*Producer\|@Component' --type=java backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/ -B2 -A2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Check how rabbitTemplate is injected
rg -n 'rabbitTemplate' --type=java -B5 backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/ | head -20

Length of output: 2568


🏁 Script executed:

#!/bin/bash
# Final validation: look at the complete diff context and confirm it's viable
cat backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java | head -50

Length of output: 1394


🏁 Script executed:

#!/bin/bash
# Check if batch size is used anywhere else
rg -n 'FCM_BATCH_SIZE\|500' --type=java backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Verify no other references to FCM_BATCH_SIZE or similar constant
rg -n 'BATCH_SIZE' --type=java backend/

Length of output: 669


🏁 Script executed:

#!/bin/bash
# Double check: is the empty guard necessary? Look at calling code
rg -n 'publishTokens\(' --type=java -B3 -A3

Length of output: 1859


🏁 Script executed:

#!/bin/bash
# Check consumer/listener implementation
find . -path '*fcm*' -name '*.java' | xargs grep -l 'Listener\|Consumer' 2>/dev/null | head -5

Length of output: 299


🏁 Script executed:

#!/bin/bash
# Final comprehensive check to ensure we haven't missed anything
echo "Total files with notify routing keys:" && \
rg '"notify\.' --type=java | cut -d: -f1 | sort -u | wc -l && \
echo "" && \
echo "Files:" && \
rg '"notify\.' --type=java | cut -d: -f1 | sort -u

Length of output: 219


🏁 Script executed:

#!/bin/bash
# Check if there's documentation about these constants
rg -n 'notify\.token\|notify\.tokens\|notify\.topic' --type=markdown -o -name '*.md'

Length of output: 422


🏁 Script executed:

#!/bin/bash
# Ensure we can actually find and view both files in full
echo "=== File 1: NotificationMQProducer.java ===" && \
wc -l backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java && \
echo "" && \
echo "=== File 2: RabbitMQConfig.java ===" && \
wc -l backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java

Length of output: 258


🏁 Script executed:

#!/bin/bash
# Get complete NotificationMQProducer
cat backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java

Length of output: 1394


🏁 Script executed:

#!/bin/bash
# Get complete RabbitMQConfig
cat backend/src/main/java/backend/mulkkam/common/config/RabbitMQConfig.java

Length of output: 2348


🏁 Script executed:

#!/bin/bash
# Find SendMessageByFcmTokensRequest and related request classes
find . -name '*.java' -type f | xargs grep -l 'SendMessageByFcm.*Request' | head -10

Length of output: 1004


🏁 Script executed:

#!/bin/bash
# Get SendMessageByFcmTokensRequest definition
find . -name '*SendMessage*.java' | xargs cat | head -100

Length of output: 2006


🏁 Script executed:

#!/bin/bash
# Check for any record/dto pattern files
fd -e java | xargs grep -l 'SendMessageByFcmTokensRequest' | head -3

Length of output: 294


🏁 Script executed:

#!/bin/bash
# Look for request DTO files in common infrastructure
find backend/src/main/java/backend/mulkkam/common -name '*Request*.java' | head -10

Length of output: 420


🏁 Script executed:

#!/bin/bash
# Find FCM-related DTOs
find backend/src/main/java/backend/mulkkam -path '*fcm*' -name '*.java' -type f

Length of output: 1161


🏁 Script executed:

#!/bin/bash
# Get all FCM infrastructure files to understand the structure
ls -la backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/

Length of output: 326


🏁 Script executed:

#!/bin/bash
# Check subdirectories
find backend/src/main/java/backend/mulkkam/common/infrastructure/fcm -name '*.java' -type f

Length of output: 1161


🏁 Script executed:

#!/bin/bash
# Get content of all FCM-related files
find backend/src/main/java/backend/mulkkam/common/infrastructure/fcm -name '*.java' -type f -exec echo "=== {} ===" \; -exec cat {} \;

Length of output: 14410


라우팅키/배치 크기 하드코딩 → 설정화 필수이며 RabbitMQConfig도 함께 수정 필요

리뷰 의도는 정확하나 일부 중요한 점을 보완해야 합니다:

  1. 라우팅키 드리프트 위험 실제 확인됨: 문자열 상수가 NotificationMQProducer.java와 RabbitMQConfig.java에 중복되어 있어, 한쪽만 변경하면 바인딩 실패가 발생합니다. 특히 RabbitMQConfig.java의 Binding 메서드들(lines 55, 60, 65)도 동일하게 외부화해야 합니다.

  2. 설정 파일에 키가 이미 존재: application-prod.yml과 application-worker.yml에 mulkkam.mq.routing 하위에 해당 키들이 이미 정의되어 있으므로, 제안된 설정명을 그대로 사용하면 됩니다. 팀 컨벤션과도 일치합니다.

  3. 배치 크기 중복: FCM_BATCH_SIZE = 500이 NotificationMQProducer.java(line 22)와 FcmEventListener.java(line 19) 두 곳에 중복 정의되어 있습니다. 이 역시 외부화하여 통일해야 변경 시 일관성이 유지됩니다.

  4. 공수 측면의 제안:

    • RabbitMQConfig.java의 세 Binding 메서드도 함께 수정
    • 배치 크기를 ${mulkkam.fcm.batch-size:500} (또는 ${mulkkam.mq.batch-size:500})로 통합
    • 제안된 diff에 RabbitMQConfig 수정분도 추가 필요
  5. 빈 리스트 가드의 의의: Lists.partition()이 공 리스트를 잘 처리하지만, 불필요한 MQ 송신을 명시적으로 방지하는 것이 좋은 방어 로직입니다.

// RabbitMQConfig.java도 함께 수정
@Bean
public Binding bindToken() {
-   return BindingBuilder.bind(tokenQueue()).to(notificationsExchange()).with("notify.token");
+   return BindingBuilder.bind(tokenQueue()).to(notificationsExchange()).with(rkToken);
}

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
backend/src/main/java/backend/mulkkam/common/infrastructure/fcm/service/NotificationMQProducer.java
around lines 22 to 36, routing keys and batch size are hardcoded and duplicated
elsewhere; change to read routing keys from the existing application config path
(mulkkam.mq.routing.<token|tokens|topic>) and read batch size from a shared
property (e.g. mulkkam.fcm.batch-size or mulkkam.mq.batch-size with default
500), add a guard to skip sending when partitioned token list is empty, and
remove the FCM_BATCH_SIZE constant; concurrently update RabbitMQConfig.java
binding methods (around lines 55, 60, 65) to use the same config keys instead of
string literals so bindings and producers stay in sync, and remove the duplicate
batch-size constant in FcmEventListener.java so both classes use the shared
property.

Comment on lines +29 to +33
rabbitmq:
host: ${MQ_HOST}
port: 5672
username: ${MQ_USER}
password: ${MQ_PASSWORD}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

로컬 환경에 MQ 토폴로지 설정이 누락되었습니다.

다른 환경 설정 파일들(dev, prod, worker, ci)에는 mulkkam.mq 블록이 정의되어 있지만, local 프로필에는 해당 설정이 없습니다. 이로 인해 로컬 환경에서 RabbitMQ 관련 기능을 테스트할 때 exchange, queue, routing 정보를 찾지 못해 런타임 에러가 발생할 수 있습니다.

다음과 같이 토폴로지 설정을 추가하세요:

   rabbitmq:
     host: ${MQ_HOST}
     port: 5672
     username: ${MQ_USER}
     password: ${MQ_PASSWORD}
+
+# MQ 토폴로지
+mulkkam:
+  mq:
+    exchange: notifications.ex
+    queue:
+      token: notifications.token.q
+      tokens: notifications.tokens.q
+      topic: notifications.topic.q
+    routing:
+      token: notify.token
+      tokens: notify.tokens
+      topic: notify.topic
 
 app:
   logging:
📝 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.

Suggested change
rabbitmq:
host: ${MQ_HOST}
port: 5672
username: ${MQ_USER}
password: ${MQ_PASSWORD}
rabbitmq:
host: ${MQ_HOST}
port: 5672
username: ${MQ_USER}
password: ${MQ_PASSWORD}
# MQ 토폴로지
mulkkam:
mq:
exchange: notifications.ex
queue:
token: notifications.token.q
tokens: notifications.tokens.q
topic: notifications.topic.q
routing:
token: notify.token
tokens: notify.tokens
topic: notify.topic
🤖 Prompt for AI Agents
In backend/src/main/resources/application-local.yml around lines 29 to 33, the
local profile is missing the mulkkam.mq topology block present in other
profiles; add a mulkkam.mq section that defines the same exchanges, queues, and
bindings used in dev/prod/worker/ci so local RabbitMQ setup mirrors other
environments (include exchange names, queue names, routing keys and any
durable/auto-delete properties), and ensure the values reference the same
property names or literals as in the other files so local runs can create and
discover the required topology.

@Jin409
Copy link
Contributor Author

Jin409 commented Oct 21, 2025

@coderabbitai pause

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 21, 2025

✅ Actions performed

Reviews paused.

Copy link

@20HyeonsuLee 20HyeonsuLee left a comment

Choose a reason for hiding this comment

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

MQ 도입하시는군요 👍

어프로브 하겠습니다ㅋㅋ

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BE 백엔드 관련 이슈입니다.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants