-
Notifications
You must be signed in to change notification settings - Fork 0
feat/#45: 학생회 게시글 작성 시, 학생에게 푸시알림 기능 구현 #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughFirebase Cloud Messaging (FCM) 시스템을 도입하여 학생회 게시물 생성 시 푸시 알림을 전송합니다. 이벤트 기반 아키텍처를 추가하고, 비동기 작업 처리를 위한 스레드 풀을 구성하며, Firebase 초기화 및 토픽 기반 메시징 기능을 구현합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as Client
participant Service as StudentCouncilPostService
participant Publisher as ApplicationEventPublisher
participant Listener as CouncilPostPushListener
participant Executor as ThreadPoolTaskExecutor
participant FCMService as FirebaseCloudMessageService
participant Firebase as Firebase Cloud Messaging
User->>Service: createPost(request)
Service->>Service: Save post to repository
Service->>Publisher: publishEvent(CouncilPostCreatedEvent)
Publisher->>Listener: handleCouncilPostCreatedEvent(event)
Listener->>Listener: Resolve title & body from event
Listener->>Executor: Submit async task
Executor->>FCMService: sendToTopic(topic, title, body, data)
FCMService->>Firebase: Send Message to topic
Firebase-->>FCMService: messageId (success) or exception
FCMService-->>Executor: Log result/error
Executor-->>User: Return response
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java:
- Around line 33-35: The topic method in CouncilType (topic(StudentCouncil
writer)) calls writer.getMajor().getMajorId() without null checks which can NPE;
update topic to first check for null writer.getMajor() (and null majorId) and
return a safe fallback (e.g., "major_unknown" or an empty topic) when null, and
apply the same null-safety fixes to the SCHOOL_COUNCIL and COLLEGE_COUNCIL
branches so none of them dereference getMajor() or getMajorId() without guards.
- Around line 23-25: CouncilType.topic 메서드에서 writer.getCollege()가 null일 경우 NPE가
발생하므로 writer.getCollege()와 관련된 null 체크를 추가해 SCHOOL_COUNCIL과 동일한 방식으로 처리하세요; 즉
CouncilType.topic(…)에서 writer.getCollege()가 null이면 적절한 예외(예:
IllegalStateException/IllegalArgumentException) 또는 기본 토픽 문자열을 반환하도록 방어 코드를 넣고
로그/메시지에 writer 식별자와 함께 상태를 명시하도록 수정하세요.
- Around line 13-15: The topic() implementations for COLLEGE_COUNCIL and
MAJOR_COUNCIL access StudentCouncil.getCollege() and getMajor() without null
checks; add explicit null-safety in each CouncilType.topic(StudentCouncil
writer) (and mirror the defensive style of hasAccess()) by validating writer and
the specific field (college/major) before using them — if null, either throw a
clear IllegalStateException/IllegalArgumentException with context (e.g.,
"College is null for council topic") or return a sensible fallback topic string;
ensure SCHOOL_COUNCIL/topic still assumes non-null school but optionally add an
assertion for consistency.
In
@src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java:
- Around line 28-48: handleCouncilPostCreatedEvent currently runs async and lets
firebaseCloudMessageService.sendToTopic throw FcmTopicSendFailedException
uncaught; wrap the sendToTopic call in a try-catch that catches
FcmTopicSendFailedException (and optionally Exception), log the failure with
context (topic, postId, category) using the existing log instance, and ensure
any necessary compensating actions or rethrows are handled per project
convention so failures are not silently ignored in the @Async method.
In @src/main/java/com/campus/campus/global/config/AsyncConfig.java:
- Around line 6-9: The current AsyncConfig enabling @EnableAsync leaves Spring's
default executor with an effectively unbounded queue, risking OOM under load;
replace the empty config by defining a named ThreadPoolTaskExecutor bean with
explicit corePoolSize, maxPoolSize and queueCapacity (bounded queue) and
register it as the AsyncConfigurer/TaskExecutor for @Async, and also provide an
AsyncUncaughtExceptionHandler implementation to handle exceptions from void
@Async methods; update references to the executor bean name in any @Async
annotations if needed.
In
@src/main/java/com/campus/campus/global/firebase/application/dto/FcmMessageRequestDto.java:
- Around line 6-16: FcmMessageRequestDto currently lacks validation and has an
incorrect Schema for title; annotate the record components with validation
annotations (e.g., annotate userId with @NotNull, title and body with @NotBlank)
and update the @Schema descriptions to match each field's meaning (change
title's description from "메시지 발송인" to something like "메시지 제목", keep body as
message content, and document userId as required). Ensure you import the correct
validation package (jakarta.validation or javax.validation depending on project)
and confirm callers (controllers) validate the DTO (use @Valid) so invalid
requests are rejected before reaching Firebase.
🧹 Nitpick comments (4)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/CouncilPostCreatedEvent.java (1)
3-8:category는String대신PostCategory타입으로 이벤트를 구성하는 게 안전합니다.
이벤트는 내부 도메인 경계에서 많이 재사용되니, 문자열은 오타/케이스/값 변경에 취약합니다.Proposed diff
package com.campus.campus.domain.councilpost.application.dto.request; +import com.campus.campus.domain.councilpost.domain.entity.PostCategory; + public record CouncilPostCreatedEvent( Long postId, String councilName, - String category, + PostCategory category, String topic ) {}src/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.java (1)
32-34: 예외 처리 개선이 필요합니다.현재 구현의 문제점:
- 원본 예외(
IOException)가 cause로 전달되지 않아 디버깅이 어렵습니다.- 예외 발생 시 로그가 없어 프로덕션 환경에서 문제 파악이 힘듭니다.
♻️ 개선된 예외 처리
FirebaseInitializationFailedException에 cause 지원 추가:
public class FirebaseInitializationFailedException extends RuntimeException { public FirebaseInitializationFailedException(Throwable cause) { super(ErrorCode.FIREBASE_INITIALIZATION_FAILED.getMessage(), cause); } }FirebaseInitializer에서 로깅 및 cause 전달:
} catch (IOException e) { + log.error("Firebase 초기화 실패: {}", e.getMessage(), e); - throw new FirebaseInitializationFailedException(); + throw new FirebaseInitializationFailedException(e); }src/main/java/com/campus/campus/global/firebase/exception/FirebaseInitializationFailedException.java (1)
3-6: 예외 cause 전달을 지원하도록 개선이 권장됩니다.현재 생성자는 원본 예외(cause)를 받지 않아 스택 트레이스가 손실됩니다.
FirebaseInitializer에서IOException을 래핑할 때 원본 예외 정보를 보존하려면 cause를 받는 생성자가 필요합니다.♻️ cause를 지원하는 생성자 추가
public class FirebaseInitializationFailedException extends RuntimeException { public FirebaseInitializationFailedException() { super(ErrorCode.FIREBASE_INITIALIZATION_FAILED.getMessage()); } + + public FirebaseInitializationFailedException(Throwable cause) { + super(ErrorCode.FIREBASE_INITIALIZATION_FAILED.getMessage(), cause); + } }src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java (1)
21-31: 입력 파라미터 검증을 추가하는 것이 좋습니다.현재
topic,title,body에 대한 null/빈 문자열 검증이 없습니다. Firebase API 호출 시 모호한 에러가 발생할 수 있으며, 의도하지 않은 빈 알림이 전송될 위험이 있습니다.♻️ 입력 검증 추가
public void sendToTopic(String topic, String title, String body, Map<String, String> data) { + if (topic == null || topic.isBlank()) { + throw new IllegalArgumentException("Topic cannot be null or blank"); + } + if (title == null || title.isBlank()) { + throw new IllegalArgumentException("Title cannot be null or blank"); + } + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("Body cannot be null or blank"); + } + Message.Builder builder = Message.builder() .setTopic(topic) .setNotification(Notification.builder() .setTitle(title) .setBody(body) .build());
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (15)
build.gradlesrc/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.javasrc/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.javasrc/main/java/com/campus/campus/domain/councilpost/application/dto/request/CouncilPostCreatedEvent.javasrc/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.javasrc/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.javasrc/main/java/com/campus/campus/domain/councilpost/domain/repository/LikePostRepository.javasrc/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.javasrc/main/java/com/campus/campus/global/config/AsyncConfig.javasrc/main/java/com/campus/campus/global/firebase/application/dto/FcmMessageRequestDto.javasrc/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.javasrc/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.javasrc/main/java/com/campus/campus/global/firebase/exception/ErrorCode.javasrc/main/java/com/campus/campus/global/firebase/exception/FcmTopicSendFailedException.javasrc/main/java/com/campus/campus/global/firebase/exception/FirebaseInitializationFailedException.java
💤 Files with no reviewable changes (1)
- src/main/java/com/campus/campus/domain/councilpost/domain/repository/LikePostRepository.java
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/com/campus/campus/global/config/AsyncConfig.java (1)
src/main/java/com/campus/campus/global/config/executor/PlaceSearchExecutorConfig.java (1)
Configuration(9-15)
src/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.java (2)
src/main/java/com/campus/campus/global/firebase/exception/FirebaseInitializationFailedException.java (1)
FirebaseInitializationFailedException(3-7)src/main/java/com/campus/campus/CampusApplication.java (1)
SpringBootApplication(8-17)
src/main/java/com/campus/campus/global/firebase/application/dto/FcmMessageRequestDto.java (10)
src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilChangePasswordRequest.java (1)
StudentCouncilChangePasswordRequest(9-24)src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilChangeEmailRequest.java (1)
StudentCouncilChangeEmailRequest(6-11)src/main/java/com/campus/campus/domain/councilnotice/application/dto/request/NoticeRequest.java (1)
NoticeRequest(5-10)src/main/java/com/campus/campus/domain/user/application/dto/request/CampusNicknameUpdateRequest.java (1)
CampusNicknameUpdateRequest(6-11)src/main/java/com/campus/campus/domain/user/application/dto/request/UserProfileRequest.java (1)
UserProfileRequest(5-12)src/main/java/com/campus/campus/domain/mail/application/dto/request/EmailVerificationRequest.java (1)
EmailVerificationRequest(6-11)src/main/java/com/campus/campus/global/util/jwt/application/dto/request/TokenReissueRequest.java (1)
TokenReissueRequest(6-11)src/main/java/com/campus/campus/domain/manager/application/dto/response/CouncilApproveOrDenyResponse.java (1)
CouncilApproveOrDenyResponse(5-12)src/main/java/com/campus/campus/domain/council/application/dto/request/StudentCouncilSignUpRequest.java (1)
StudentCouncilSignUpRequest(11-44)src/main/java/com/campus/campus/domain/manager/application/dto/response/CertifyRequestCouncilResponse.java (1)
CertifyRequestCouncilResponse(5-15)
src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java (2)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
Slf4j(40-279)src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java (1)
Slf4j(16-53)
src/main/java/com/campus/campus/global/firebase/exception/ErrorCode.java (3)
src/main/java/com/campus/campus/global/common/exception/ErrorCodeInterface.java (3)
ErrorCodeInterface(5-11)getStatus(8-8)getCode(6-6)src/main/java/com/campus/campus/domain/user/application/exception/ErrorCode.java (1)
Getter(10-22)src/main/java/com/campus/campus/domain/councilpost/application/exception/ErrorCode.java (1)
Getter(8-34)
src/main/java/com/campus/campus/domain/councilpost/application/dto/request/CouncilPostCreatedEvent.java (4)
src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetUpcomingEventListForCouncilResponse.java (1)
GetUpcomingEventListForCouncilResponse(10-29)src/main/java/com/campus/campus/domain/councilpost/application/dto/request/PostRequest.java (1)
PostRequest(14-42)src/main/java/com/campus/campus/domain/councilpost/application/dto/response/GetPostListForCouncilResponse.java (1)
GetPostListForCouncilResponse(10-32)src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java (1)
Entity(25-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (14)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java (1)
20-20: LGTM! 사용하지 않는 import 제거 및 정리
GetPostForUserResponseimport 순서 조정 및 사용하지 않는java.util.Optionalimport 제거는 적절한 코드 정리입니다.다만, PR 목표는 푸시 알림 구현인데 이 파일에는 import 변경만 있습니다. 실제 푸시 알림 구현은 다른 파일(예: 게시글 작성 서비스)에 있을 것으로 보이는데, 해당 파일들이 리뷰에 포함되지 않았습니다.
build.gradle (1)
60-62: Firebase Admin SDK 9.7.0은 현재 최신 버전이며 Java 21 + Spring Boot 3.5.7과 호환됩니다.검증 결과:
- Firebase Admin SDK 9.7.0은 Maven Central에 존재하는 최신 버전입니다 (2025년 9월 24일 릴리스)
- Java 8 이상을 지원하므로 Java 21과 완전히 호환됩니다
- Spring Boot 3.5.7은 Java 17+ 이상을 지원하며 Java 21과 호환됩니다
현재 의존성은 적절하며 별도의 버전 업데이트가 필요하지 않습니다.
src/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.java (1)
19-23: 초기화 중복 방지 로직은 적절합니다.Spring의
@PostConstruct는 빈 초기화 시 단일 스레드로 실행되므로 동시성 문제가 발생할 가능성은 낮습니다. 다만, 테스트 환경이나 여러 애플리케이션 컨텍스트가 로드되는 경우를 대비하여 이 체크가 존재하는 것은 좋은 방어 코드입니다.src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (3)
50-50: 의존성 주입이 올바르게 구현되었습니다.Spring의
ApplicationEventPublisher를@RequiredArgsConstructor를 통해 주입받는 것은 표준적이고 권장되는 방식입니다.
75-75: 저장된 엔티티를 변수에 할당하는 것이 적절합니다.생성된 ID를 이벤트에 전달하기 위해 저장 결과를 변수에 할당하는 것은 올바른 접근입니다.
83-91: 이 리뷰 의견은 유효하지 않습니다. 코드는 이미 권장된 패턴을 구현하고 있습니다.
CouncilPostPushListener는 다음과 같이 올바르게 구성되어 있습니다:
@Async: 비동기 처리@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT): 트랜잭션 커밋 후 실행더불어
AsyncConfig에서@EnableAsync가 설정되어 있습니다. 따라서 이벤트 리스너의 예외는 게시글 생성 트랜잭션에 영향을 주지 않으며, 푸시 알림 전송도 비동기로 처리되어 성능 문제가 없습니다.Likely an incorrect or invalid review comment.
src/main/java/com/campus/campus/global/firebase/exception/FcmTopicSendFailedException.java (1)
3-6: 예외 처리가 올바르게 구현되었습니다.
Throwable cause를 받아 상위 예외로 전달하므로, 원본FirebaseMessagingException의 스택 트레이스가 보존됩니다. 이는 디버깅에 매우 유용합니다.src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java (1)
33-42: 성공 로깅이 잘 구현되었습니다.전송된 메시지의 ID, 토픽, 제목, 본문, 데이터 키를 모두 로깅하여 추적 및 디버깅이 용이합니다.
src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java (1)
39-39: LGTM!추상 메서드 선언이 적절하며,
hasAccess()메서드와 일관된 패턴을 따르고 있습니다.src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java (3)
21-24: LGTM!매직 스트링 대신 상수를 사용한 것은 좋은 관행입니다. 명명 규칙도 적절합니다.
16-19: LGTM!클래스 구조와 어노테이션이 적절합니다. Spring 이벤트 리스너로서 필요한 구성이 잘 갖춰져 있습니다.
50-56: 제휴 게시글과 행사글 처리 완료됨PostCategory enum에는 EVENT와 PARTNERSHIP 두 개의 값만 존재하며, 두 값 모두 resolveBody 메서드에서 명시적으로 처리되고 있습니다. 카테고리가 PostCategory enum의 name() 메서드를 통해 전달되므로 이 두 값 이외의 카테고리가 들어올 가능성은 없으며, default 케이스가 예상치 못한 값에 대한 안전장치 역할을 합니다. 별도의 타입 안전성 개선이나 추가 카테고리 매핑은 필요하지 않습니다.
Likely an incorrect or invalid review comment.
src/main/java/com/campus/campus/global/firebase/exception/ErrorCode.java (2)
10-20: LGTM!ErrorCode enum 구조가 기존 패턴(user, councilpost 도메인의 ErrorCode)과 일관성 있게 구현되었습니다.
ErrorCodeInterface구현과 Lombok 어노테이션 사용도 적절합니다.
14-15: Firebase 에러 코드(6201-6202)는 OCI 에러 코드(6100-6101)와 겹치지 않으며, 6xxx 범위가 외부 서비스 인프라 에러를 위해 적절히 할당되어 있습니다. 코드 범위 충돌 없이 일관된 패턴을 따르고 있어 별도의 변경이 필요하지 않습니다.
| @Override public String topic(StudentCouncil writer) { | ||
| return "school_" + writer.getSchool().getSchoolId(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find StudentCouncil entity file
fd -t f "StudentCouncil.java" --type fRepository: our-campUS/our-campUS-BE
Length of output: 148
🏁 Script executed:
# Find and examine CouncilType.java to get full context
fd -t f "CouncilType.java" --type fRepository: our-campUS/our-campUS-BE
Length of output: 145
🏁 Script executed:
# Search for StudentCouncil class definition and its fields
rg -nP --type=java "class\s+StudentCouncil" -A 30Repository: our-campUS/our-campUS-BE
Length of output: 50380
🏁 Script executed:
cat -n src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.javaRepository: our-campUS/our-campUS-BE
Length of output: 1615
🏁 Script executed:
# Search for hasAccess method to see how it handles null checks
rg -nP "hasAccess" --type=java -B2 -A5Repository: our-campUS/our-campUS-BE
Length of output: 4230
🏁 Script executed:
# Check if @ManyToOne on school field has explicit optional parameter
rg -nP "@ManyToOne|@JoinColumn.*school" src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java -A1Repository: our-campUS/our-campUS-BE
Length of output: 266
🏁 Script executed:
# Find where topic() method is called
rg -nP "\.topic\(" --type=java -B2 -A2Repository: our-campUS/our-campUS-BE
Length of output: 1682
🏁 Script executed:
# Check StudentCouncil entity to see if college and major have nullable constraints
rg -nP "@JoinColumn.*college|@JoinColumn.*major" src/main/java/com/campus/campus/domain/council/domain/entity/StudentCouncil.java -B1 -A1Repository: our-campUS/our-campUS-BE
Length of output: 273
필드의 null 안전성 검증 필요 (college, major의 경우)
StudentCouncil의 school 필드는 데이터베이스 레벨에서 nullable = false 제약이 있어 SCHOOL_COUNCIL의 topic() 메서드는 즉시적인 NPE 위험이 낮습니다. 하지만 college와 major 필드는 nullable이 기본값이므로 COLLEGE_COUNCIL과 MAJOR_COUNCIL의 topic() 메서드는 null 체크 없이 이들 필드에 접근하고 있습니다.
또한 hasAccess() 메서드는 user의 필드만 null 검증하고 writer의 필드는 검증하지 않으므로, 방어적 프로그래밍 관점에서 topic() 메서드에서도 일관되게 null 체크를 수행하는 것이 좋습니다. 특히 lazy-loading 환경에서 예상치 못한 상황이 발생할 수 있습니다.
모든 council type의 topic() 메서드에 명시적인 null 체크를 추가하여 일관성과 안정성을 확보하기를 권장합니다.
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java
around lines 13 - 15, The topic() implementations for COLLEGE_COUNCIL and
MAJOR_COUNCIL access StudentCouncil.getCollege() and getMajor() without null
checks; add explicit null-safety in each CouncilType.topic(StudentCouncil
writer) (and mirror the defensive style of hasAccess()) by validating writer and
the specific field (college/major) before using them — if null, either throw a
clear IllegalStateException/IllegalArgumentException with context (e.g.,
"College is null for council topic") or return a sensible fallback topic string;
ensure SCHOOL_COUNCIL/topic still assumes non-null school but optionally add an
assertion for consistency.
| @Override public String topic(StudentCouncil writer) { | ||
| return "college_" + writer.getCollege().getCollegeId(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null 안전성 검증 필요
writer.getCollege()이 null일 경우 NPE가 발생할 수 있습니다. SCHOOL_COUNCIL과 동일한 null 안전성 문제가 있습니다.
🛡️ Null 안전성을 위한 수정안
@Override public String topic(StudentCouncil writer) {
+ if (writer.getCollege() == null) {
+ throw new IllegalStateException("COLLEGE_COUNCIL writer must have a college");
+ }
return "college_" + writer.getCollege().getCollegeId();
}📝 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.
| @Override public String topic(StudentCouncil writer) { | |
| return "college_" + writer.getCollege().getCollegeId(); | |
| } | |
| @Override public String topic(StudentCouncil writer) { | |
| if (writer.getCollege() == null) { | |
| throw new IllegalStateException("COLLEGE_COUNCIL writer must have a college"); | |
| } | |
| return "college_" + writer.getCollege().getCollegeId(); | |
| } |
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java
around lines 23 - 25, CouncilType.topic 메서드에서 writer.getCollege()가 null일 경우 NPE가
발생하므로 writer.getCollege()와 관련된 null 체크를 추가해 SCHOOL_COUNCIL과 동일한 방식으로 처리하세요; 즉
CouncilType.topic(…)에서 writer.getCollege()가 null이면 적절한 예외(예:
IllegalStateException/IllegalArgumentException) 또는 기본 토픽 문자열을 반환하도록 방어 코드를 넣고
로그/메시지에 writer 식별자와 함께 상태를 명시하도록 수정하세요.
| @Override public String topic(StudentCouncil writer) { | ||
| return "major_" + writer.getMajor().getMajorId(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Null 안전성 검증 필요
writer.getMajor()가 null일 경우 NPE가 발생할 수 있습니다. SCHOOL_COUNCIL, COLLEGE_COUNCIL과 동일한 null 안전성 문제가 있습니다.
🛡️ Null 안전성을 위한 수정안
@Override public String topic(StudentCouncil writer) {
+ if (writer.getMajor() == null) {
+ throw new IllegalStateException("MAJOR_COUNCIL writer must have a major");
+ }
return "major_" + writer.getMajor().getMajorId();
}📝 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.
| @Override public String topic(StudentCouncil writer) { | |
| return "major_" + writer.getMajor().getMajorId(); | |
| } | |
| @Override public String topic(StudentCouncil writer) { | |
| if (writer.getMajor() == null) { | |
| throw new IllegalStateException("MAJOR_COUNCIL writer must have a major"); | |
| } | |
| return "major_" + writer.getMajor().getMajorId(); | |
| } |
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/domain/council/domain/entity/CouncilType.java
around lines 33 - 35, The topic method in CouncilType (topic(StudentCouncil
writer)) calls writer.getMajor().getMajorId() without null checks which can NPE;
update topic to first check for null writer.getMajor() (and null majorId) and
return a safe fallback (e.g., "major_unknown" or an empty topic) when null, and
apply the same null-safety fixes to the SCHOOL_COUNCIL and COLLEGE_COUNCIL
branches so none of them dereference getMajor() or getMajorId() without guards.
src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java
Show resolved
Hide resolved
| @Configuration | ||
| @EnableAsync | ||
| public class AsyncConfig { | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
Spring Boot 3.5 Spring Framework 6 @EnableAsync default executor behavior and best practices
💡 Result:
Short summary
- Default in Spring Boot 3.5 / Spring Framework 6: Boot auto-configures an AsyncTaskExecutor for @EnableAsync. If virtual threads are enabled (Java 21+ with spring.threads.virtual.enabled=true) the auto-configured executor is a SimpleAsyncTaskExecutor that creates virtual threads; otherwise Boot configures a ThreadPoolTaskExecutor with “sensible defaults.” The auto-configured executor is used for @EnableAsync unless you register your own Executor/AsyncTaskExecutor or AsyncConfigurer. [1][2][3]
What @EnableAsync looks up
- Spring’s @EnableAsync will use, in order of precedence:
- an AsyncConfigurer (if you implement it and provide getAsyncExecutor()), or
- a single TaskExecutor/AsyncTaskExecutor bean in the context (or an Executor bean when Boot’s auto-config is active), or
- fall back to a SimpleAsyncTaskExecutor (framework default). [4][5]
Key Boot behavior notes
- Boot’s TaskExecutionAutoConfiguration will back off if you define custom Executor beans. Boot also documents property-based configuration via spring.task.execution.* to tune the auto-configured ThreadPoolTaskExecutor (core/max/queue/keep-alive, name prefix). Web integrations (MVC/WebFlux/GraphQL/WebSocket, JPA bootstrap, etc.) may require an AsyncTaskExecutor bean named applicationTaskExecutor for all pieces to use it. [1][2][6]
Recommended best practices
- Always explicitly configure an Executor for production instead of relying on the silent fallback:
- Use a ThreadPoolTaskExecutor (or a java.util.concurrent.Executor adapted) with explicit corePoolSize, maxPoolSize, queueCapacity, keepAlive and thread-name-prefix. Bound the queue to avoid unbounded memory growth. [1][6]
- Choose rejection policy appropriate for your app (CallerRunsPolicy, abort, etc.) and consider back-pressure.
- Provide an AsyncUncaughtExceptionHandler (via AsyncConfigurer) to handle exceptions from void @async methods.
- Name the bean appropriately (consider applicationTaskExecutor when you need MVC/WebFlux integration) and/or expose a single TaskExecutor bean so @EnableAsync picks it up.
- Monitor threads, queue length, latency and adapt sizes; avoid very small pools for blocking workloads.
- If running on Java 21+ and your workload is heavily blocking IO, evaluate virtual threads (spring.threads.virtual.enabled=true) — they can simplify sizing but test for your workload. [1][4][7][8]
- Prefer non-blocking/reactive designs for highly concurrent services where appropriate.
Quick config examples
- application.properties (use Boot’s properties to tune auto executor):
- spring.task.execution.pool.max-size=16
- spring.task.execution.pool.queue-capacity=100
- spring.task.execution.pool.keep-alive=10s
- spring.threads.virtual.enabled=true (only on Java 21+ to opt into virtual-thread SimpleAsyncTaskExecutor). [1][6][7]
References
- Spring Boot — Task Execution and Scheduling (auto-config behavior, properties, virtual threads). [Doc]. [4]
- Spring Boot Reference — TaskExecutionAutoConfiguration / defaults discussion. [Doc]. [3]
- Spring Boot docs (3.2/3.1 pages confirming ThreadPoolTaskExecutor vs SimpleAsyncTaskExecutor with virtual threads). [7][2]
- Spring Framework — @EnableAsync / AsyncConfigurer / SimpleAsyncTaskExecutor behavior. [9][8]
- Spring Framework scheduling & @async docs (executor selection and @async("beanName")). [10]
If you want, I can: (a) paste a recommended ThreadPoolTaskExecutor @bean + AsyncConfigurer example, or (b) produce tuned sizing guidance based on your app’s CPU vs blocking characteristics — tell me your expected concurrency and type of tasks.
🌐 Web query:
Spring @Async default ThreadPoolExecutor configuration risks unbounded queue
💡 Result:
Short answer: yes — by default Spring's ThreadPoolTaskExecutor uses an effectively unbounded queue (queueCapacity = Integer.MAX_VALUE), and Spring Boot's auto-configured @async executor uses that queue with a core size (default 8). That combination means the queue can grow without bound (risking OOM) and prevents the pool from expanding beyond the core (max-size is ignored while the queue is unbounded). [1][2][3]
What to do (brief):
- Set a finite queue (spring.task.execution.pool.queue-capacity) and a sensible max-size (spring.task.execution.pool.max-size).
- Or provide your own Executor bean/ThreadPoolTaskExecutor configured with bounded queue and rejection policy. [2][3]
Sources:
- Spring Framework (ThreadPoolTaskExecutor defaults and queue behavior). [1]
- Spring Framework task-execution docs (warning about unbounded queues/ OOM and interaction with maxPoolSize). [2]
- Spring Boot task-execution auto-config (defaults: core-size=8 and how to configure queue-capacity/max-size). [3]
🌐 Web query:
Spring Boot AsyncConfigurer TaskExecutor recommended settings production
💡 Result:
Short recommended production settings & rules-of-thumb for Spring Boot AsyncConfigurer / ThreadPoolTaskExecutor:
- Use a bounded queue (not Integer.MAX_VALUE). Configure spring.task.execution.pool.queue-capacity to a finite value (e.g. 50–500) so maxPoolSize is honored.
- Size threads by workload type using the usual formula: threads ≈ CPU_cores * (1 + wait_time / service_time). For CPU-bound tasks set ≈ #cores; for IO-bound allow many more (example: cores * (1 + blockingCoefficient)). Start with theory then load-test/monitor. (engineering.zalando.com)
- Example starter values (tune to your app & host):
- corePoolSize: = #cores (or slightly higher for light IO).
- maxPoolSize: = corePoolSize * 2–10 (or set to cores*(1+W/S) if known).
- queueCapacity: 50–200 for web backends; smaller (0–10) for latency-sensitive pipelines to avoid large queuing.
- keepAlive: 10–60s; consider allowCoreThreadTimeOut=true for elastic usage.
- rejectedExecutionHandler: CallerRunsPolicy (simple back-pressure) or a custom handler that logs/metrics.
- waitForTasksToCompleteOnShutdown=true and set an awaitTerminationPeriod. (docs.spring.io)
- Observe these metrics in production and adjust: CPU, GC, thread counts, queue length, task latency and rejection rate. Use load tests to find sweet spot. (foojay.io)
If you want, tell me: (1) your host #CPU cores, (2) whether tasks are CPU- or IO-bound, and (3) an expected concurrency/throughput—I’ll propose concrete numbers and example config/YAML.
Citations:
- 1: https://engineering.zalando.com/posts/2019/04/how-to-set-an-ideal-thread-pool-size.html?utm_source=openai
- 2: https://docs.spring.io/spring-boot/reference/features/task-execution-and-scheduling.html?utm_source=openai
- 3: https://foojay.io/today/java-thread-programming-part-13/?utm_source=openai
@EnableAsync 단독 구성 시 기본 Executor의 무제한 큐로 인한 운영 리스크 해결 필요.
Spring Boot 3.5에서는 @EnableAsync 시 자동으로 ThreadPoolTaskExecutor를 구성하지만, 기본값으로 큐 용량이 Integer.MAX_VALUE(사실상 무제한)로 설정됩니다. 이로 인해 트래픽 급증 시 메모리 무제한 증가와 스레드 풀 확장 불능이 발생할 수 있어, 운영 환경에서는 다음과 같이 명시적으로 유계 TaskExecutor를 빈으로 등록하거나 설정 프로퍼티를 통해 큐를 제한해야 합니다.
푸시 발송 같은 백그라운드 작업이 몰릴 경우 메모리 부족(OOM)으로 이어질 수 있으므로, 적절한 corePoolSize, maxPoolSize, queueCapacity를 지정한 전용 Executor 구성을 권장하며, 함께 AsyncUncaughtExceptionHandler를 정의하여 void @async 메서드 예외도 처리하면 좋습니다.
권장 설정 예시 (유계 executor + 예외 처리)
package com.campus.campus.global.config;
+import java.util.concurrent.Executor;
+
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
+import org.springframework.context.annotation.Bean;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
@Configuration
@EnableAsync
-public class AsyncConfig {
+public class AsyncConfig implements AsyncConfigurer {
+
+ private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class);
+
+ @Override
+ @Bean(name = "taskExecutor")
+ public Executor getAsyncExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ executor.setCorePoolSize(4);
+ executor.setMaxPoolSize(16);
+ executor.setQueueCapacity(100);
+ executor.setThreadNamePrefix("async-");
+ executor.setWaitForTasksToCompleteOnShutdown(true);
+ executor.setAwaitTerminationSeconds(30);
+ executor.initialize();
+ return executor;
+ }
+
+ @Override
+ public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
+ return (ex, method, params) ->
+ log.error("Uncaught async exception in {}.{}()",
+ method.getDeclaringClass().getSimpleName(), method.getName(), ex);
+ }
}📝 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.
| @Configuration | |
| @EnableAsync | |
| public class AsyncConfig { | |
| } | |
| package com.campus.campus.global.config; | |
| import java.util.concurrent.Executor; | |
| import org.springframework.context.annotation.Configuration; | |
| import org.springframework.scheduling.annotation.EnableAsync; | |
| import org.springframework.scheduling.annotation.AsyncConfigurer; | |
| import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; | |
| import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; | |
| import org.springframework.context.annotation.Bean; | |
| import org.slf4j.Logger; | |
| import org.slf4j.LoggerFactory; | |
| @Configuration | |
| @EnableAsync | |
| public class AsyncConfig implements AsyncConfigurer { | |
| private static final Logger log = LoggerFactory.getLogger(AsyncConfig.class); | |
| @Override | |
| @Bean(name = "taskExecutor") | |
| public Executor getAsyncExecutor() { | |
| ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); | |
| executor.setCorePoolSize(4); | |
| executor.setMaxPoolSize(16); | |
| executor.setQueueCapacity(100); | |
| executor.setThreadNamePrefix("async-"); | |
| executor.setWaitForTasksToCompleteOnShutdown(true); | |
| executor.setAwaitTerminationSeconds(30); | |
| executor.initialize(); | |
| return executor; | |
| } | |
| @Override | |
| public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { | |
| return (ex, method, params) -> | |
| log.error("Uncaught async exception in {}.{}()", | |
| method.getDeclaringClass().getSimpleName(), method.getName(), ex); | |
| } | |
| } |
🤖 Prompt for AI Agents
In @src/main/java/com/campus/campus/global/config/AsyncConfig.java around lines
6 - 9, The current AsyncConfig enabling @EnableAsync leaves Spring's default
executor with an effectively unbounded queue, risking OOM under load; replace
the empty config by defining a named ThreadPoolTaskExecutor bean with explicit
corePoolSize, maxPoolSize and queueCapacity (bounded queue) and register it as
the AsyncConfigurer/TaskExecutor for @Async, and also provide an
AsyncUncaughtExceptionHandler implementation to handle exceptions from void
@Async methods; update references to the executor bean name in any @Async
annotations if needed.
| public record FcmMessageRequestDto( | ||
|
|
||
| @Schema(description = "유저ID", example = "1") | ||
| Long userId, | ||
|
|
||
| @Schema(description = "메시지 발송인", example = "시스템") | ||
| String title, | ||
|
|
||
| @Schema(description = "메시지 내용", example = "이제 서연님에게 화이팅을 할 수 있어요.") | ||
| String body | ||
| ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요청 DTO에 Validation(@NotNull/@notblank) 추가 + Schema 설명 정합성 맞추기 권장.
지금 상태면 API 계층에서 null/blank가 그대로 들어와 downstream(Firebase)에서 실패/예외가 날 수 있습니다. 또한 title 설명이 “메시지 발송인”으로 되어 있어 필드 의미와 어긋나 보입니다.
Proposed diff
package com.campus.campus.global.firebase.application.dto;
import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
public record FcmMessageRequestDto(
@Schema(description = "유저ID", example = "1")
+ @NotNull
Long userId,
- @Schema(description = "메시지 발송인", example = "시스템")
+ @Schema(description = "메시지 제목", example = "시스템")
+ @NotBlank
String title,
@Schema(description = "메시지 내용", example = "이제 서연님에게 화이팅을 할 수 있어요.")
+ @NotBlank
String body
) {
}🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/global/firebase/application/dto/FcmMessageRequestDto.java
around lines 6 - 16, FcmMessageRequestDto currently lacks validation and has an
incorrect Schema for title; annotate the record components with validation
annotations (e.g., annotate userId with @NotNull, title and body with @NotBlank)
and update the @Schema descriptions to match each field's meaning (change
title's description from "메시지 발송인" to something like "메시지 제목", keep body as
message content, and document userId as required). Ensure you import the correct
validation package (jakarta.validation or javax.validation depending on project)
and confirm callers (controllers) validate the DTO (use @Valid) so invalid
requests are rejected before reaching Firebase.
| } catch (FirebaseMessagingException e) { | ||
| log.error("[FCM] send failed. topic={}, errorCode={}, message={}", | ||
| topic, | ||
| e.getErrorCode(), | ||
| e.getMessage(), | ||
| e | ||
| ); | ||
| throw new FcmTopicSendFailedException(e); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
# Find usages of FirebaseCloudMessageService and sendToTopic
rg -n "FirebaseCloudMessageService|sendToTopic" --type java -A 3 -B 3Repository: our-campUS/our-campUS-BE
Length of output: 7147
🏁 Script executed:
# Check for @Async annotations in the codebase
rg -n "@Async" --type java -A 2 -B 2Repository: our-campUS/our-campUS-BE
Length of output: 767
🏁 Script executed:
# Look for CouncilPostPushListener specifically
fd "CouncilPostPushListener" --type fRepository: our-campUS/our-campUS-BE
Length of output: 159
🏁 Script executed:
# Check for retry mechanisms (Spring Retry, Resilience4j, etc.)
rg -n "Retry|@Retryable|RetryTemplate" --type java -A 2Repository: our-campUS/our-campUS-BE
Length of output: 50
일시적 Firebase 오류에 대한 재시도 메커니즘을 추가하세요.
현재 상태:
CouncilPostPushListener는 이미@Async로 비동기 처리됩니다.- 하지만
sendToTopic()메서드 자체는 동기 메서드이며, 네트워크 오류나 Firebase 서비스 일시적 문제 시 즉시 실패합니다. StudentCouncilPostController에서 직접 호출하는 경우 비동기 보호가 없습니다.
권장 사항:
- Spring Retry 또는 유사한 재시도 메커니즘 적용
- Firebase 오류 코드별 재시도 가능 여부 판단 (예:
UNAVAILABLE,INTERNAL은 재시도 대상) - 최소 지수 백오프를 사용하여 일시적 장애에 대한 탄력성 향상
src/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java:
- Around line 192-200: The POST test endpoint sendToTopic in
StudentCouncilPostController uses hardcoded test messages, returns void, lacks
@Operation, validation, and error handling and must not be in production; remove
this endpoint from the controller (preferred) and rely on the existing
event-driven flow (CouncilPostCreatedEvent and CouncilPostPushListener) to send
push notifications, or if you must keep it only for non-prod testing: annotate
the method/class with @Profile("!prod"), change the signature to return
CommonResponse<Void>, add @Operation docs, validate the @PathVariable topic
(e.g., non-blank/regex), call firebaseCloudMessageService with proper try/catch
to handle and log/send error responses via CommonResponse, and ensure it uses
real payload parameters rather than hardcoded strings.
- Around line 3-4: The new test endpoint in StudentCouncilPostController (lines
~192-200) should not be a void, undocumented, hardcoded-production API: add an
@Operation(...) annotation like the other endpoints, change the method signature
to return CommonResponse<Void> (or CommonResponse<Object> if you prefer payload)
and wrap the result in your CommonResponse.success(...) return wrapper, and
remove the hardcoded test payloads (or gate the endpoint behind a non-production
activation such as @Profile("dev") or a feature flag/@ConditionalOnProperty) so
it’s either deleted from production or only enabled in dev/testing environments.
🧹 Nitpick comments (1)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
18-20: 중복 import 제거 필요
PostRequest가 19번과 20번 라인에 중복으로 import되고 있습니다. 중복 import를 제거해주세요.♻️ 제안된 수정
import com.campus.campus.domain.councilpost.application.dto.request.CouncilPostCreatedEvent; import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; -import com.campus.campus.domain.councilpost.application.dto.request.PostRequest; import com.campus.campus.domain.councilpost.application.dto.response.GetPostListForCouncilResponse;
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.javasrc/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (2)
src/main/java/com/campus/campus/domain/councilpost/domain/entity/StudentCouncilPost.java (2)
Entity(26-101)update(61-83)src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java (1)
Component(27-174)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
85-85: 이벤트 기반 푸시 알림 구현이 올바르게 구성되었습니다
CouncilPostPushListener가@Async와@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)어노테이션으로 올바르게 구성되어 있습니다. 이를 통해 이벤트는 트랜잭션 커밋 이후에 비동기적으로 실행되므로, 리스너에서 발생하는 예외가 게시글 생성 트랜잭션에 영향을 주지 않습니다.또한
CouncilType열거형의topic()메서드가 모든 상수(GENERAL, CLUB, ACTIVITY)에서 올바르게 구현되어 있고, 이벤트에 필요한 모든 데이터(postId, councilName, category, topic)가 정확하게 전달되고 있습니다.src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java (2)
43-43: 의존성 주입 패턴 적절함
FirebaseCloudMessageService를private final필드로 선언하고@RequiredArgsConstructor를 통해 주입받는 방식은 기존 코드 스타일과 일관되며 적절합니다.
192-200: 이벤트 기반 푸시 알림 구현이 정상적으로 작동 중입니다검증 결과, PR 목적에 따른 이벤트 기반 자동 푸시 알림 기능이 완전히 구현되어 있습니다:
StudentCouncilPostService.create()메서드에서 게시글 저장 후CouncilPostCreatedEvent를 발행CouncilPostPushListener는@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)및@Async로 설정되어 데이터 커밋 후 비동기로 푸시 전송- 카테고리에 따라 적절한 메시지 본문 구성 (PARTNERSHIP, EVENT 등)
리뷰 코드의 테스트 엔드포인트(
POST /topic/{topic})는 수동 테스트용 도구이며, 실제 푸시 알림은 게시글 생성 시점에 자동으로 전송됩니다.
...ain/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
54-101: FCM topic이 Firebase 명명 규칙을 준수하는지 중앙에서 보장이 필요합니다.이벤트 리스너는 이미
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)로 올바르게 구성되어 있어 첫 번째 concerns는 해결됨을 확인했습니다.다만 CouncilType.topic()에서 생성되는 topic 값(
"school_" + schoolId등)은 ID 값에 대한 검증이 없어, schoolId/collegeId/majorId가 FCM 규칙을 위반하는 문자나 공백을 포함할 수 있습니다. Firebase topic 명명 규칙(문자/숫자/-_.~%만 허용, 최대 256자, 공백 불허)을 보장하도록 topic 생성 로직에 검증을 추가하거나 ID 자체가 이미 검증되었다면 문서화가 필요합니다.
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java:
- Around line 18-20: The file StudentCouncilPostService.java has a duplicate
import of PostRequest (two identical import lines for
com.campus.campus.domain.councilpost.application.dto.request.PostRequest);
remove the redundant import so that PostRequest is imported only once and keep
the other imports (e.g., CouncilPostCreatedEvent) unchanged to resolve the
duplicate-import warning.
In @src/main/java/com/campus/campus/global/config/AsyncConfig.java:
- Line 25: Update the executor rejection policy in AsyncConfig to avoid dropped
push notifications: locate where executor.setRejectedExecutionHandler(new
ThreadPoolExecutor.AbortPolicy()) is called (in the executor configuration
method in class AsyncConfig) and replace AbortPolicy with CallerRunsPolicy so
that when the queue is full the calling thread runs the task (providing
back-pressure) instead of throwing RejectedExecutionException.
🧹 Nitpick comments (4)
src/main/java/com/campus/campus/global/config/AsyncConfig.java (2)
18-21: 스레드 풀 설정값을 외부 설정으로 관리하는 것을 고려해보세요.현재 스레드 풀 크기와 큐 용량이 하드코딩되어 있어 환경별로 조정하기 어렵습니다.
application.properties또는application.yml에서@Value를 통해 주입받는 방식으로 변경하면 운영 환경에서 알림 볼륨에 따라 유연하게 조정할 수 있습니다.♻️ 설정 외부화 제안
application.yml에 추가:
fcm: executor: core-pool-size: 5 max-pool-size: 10 queue-capacity: 1000코드 수정:
+import org.springframework.beans.factory.annotation.Value; + @Configuration @EnableAsync public class AsyncConfig { + @Value("${fcm.executor.core-pool-size:5}") + private int corePoolSize; + + @Value("${fcm.executor.max-pool-size:10}") + private int maxPoolSize; + + @Value("${fcm.executor.queue-capacity:1000}") + private int queueCapacity; + @Bean(name = "fcmTaskExecutor") public ThreadPoolTaskExecutor fcmTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); - executor.setCorePoolSize(5); - executor.setMaxPoolSize(10); + executor.setCorePoolSize(corePoolSize); + executor.setMaxPoolSize(maxPoolSize); - executor.setQueueCapacity(1000); + executor.setQueueCapacity(queueCapacity); executor.setThreadNamePrefix("fcm-");
14-29: 애플리케이션 종료 시 graceful shutdown 설정 추가를 고려해보세요.현재 설정은 정상적으로 동작하지만, 애플리케이션 종료 시 진행 중인 FCM 알림 작업이 완료될 수 있도록 graceful shutdown 설정을 추가하면 더욱 안정적입니다.
♻️ Graceful shutdown 설정 제안
executor.setThreadNamePrefix("fcm-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); + + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); executor.initialize(); return executor;src/main/java/com/campus/campus/domain/councilpost/application/dto/request/CouncilPostCreatedEvent.java (1)
1-10: 이벤트 타입인데dto.request패키지에 위치한 점은 혼동 소지가 있어요.Line 1의 패키지 경로가 “요청 DTO” 의미로 읽혀서, 내부 이벤트(예:
...application.event/...application.dto.event)로 분리하는 편이 유지보수에 유리합니다.
(현재 코드 자체는 record로 깔끔합니다.)src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
85-101:saved와post를 혼용 중이라, 하나로 통일하면 안전/가독성이 좋아져요.Line 85에서
saved를 받은 뒤 이벤트에는saved.getId()/saved.getCategory()를 쓰는데, 이미지 저장/조회 및 응답 변환에는post를 계속 사용합니다. JPA 관점에선 보통 동일 엔티티로 동작하겠지만, 혼용은 오해를 부르니saved로 통일하는 쪽을 권장합니다.제안 diff
- StudentCouncilPost saved = postRepository.save(post); + StudentCouncilPost saved = postRepository.save(post); if (dto.imageUrls() != null) { for (String imageUrl : dto.imageUrls()) { - postImageRepository.save(studentCouncilPostMapper.createPostImage(post, imageUrl)); + postImageRepository.save(studentCouncilPostMapper.createPostImage(saved, imageUrl)); } } List<String> imageUrls = postImageRepository - .findAllByPostOrderByIdAsc(post) + .findAllByPostOrderByIdAsc(saved) .stream() .map(PostImage::getImageUrl) .toList(); - return studentCouncilPostMapper.toGetPostResponse(post, imageUrls, councilId); + return studentCouncilPostMapper.toGetPostResponse(saved, imageUrls, councilId);
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.javasrc/main/java/com/campus/campus/domain/councilpost/application/dto/request/CouncilPostCreatedEvent.javasrc/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.javasrc/main/java/com/campus/campus/global/config/AsyncConfig.javasrc/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java
🚧 Files skipped from review as they are similar to previous changes (2)
- src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java
- src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java
🔇 Additional comments (1)
src/main/java/com/campus/campus/global/config/AsyncConfig.java (1)
10-12: 비동기 처리를 위한 설정 클래스 구성이 적절합니다.
@EnableAsync를 통해 Spring의 비동기 처리가 활성화되었고, FCM 푸시 알림을 위한 전용 executor 설정이 적절합니다.
...java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java:
- Around line 25-26: Remove the unused FirebaseCloudMessageService dependency
from StudentCouncilPostController: delete the import for
com.campus.campus.global.firebase.application.service.FirebaseCloudMessageService
and remove the unused field named firebaseCloudMessageService from the
StudentCouncilPostController class so all FCM concerns remain handled
asynchronously via events and postService; ensure no remaining references to
firebaseCloudMessageService exist in any controller methods.
- Around line 3-4: Remove the unused import of java.util.Map from the
StudentCouncilPostController class; locate the import line "import
java.util.Map;" at the top of StudentCouncilPostController and delete it so the
class no longer imports Map (ensure no other Map usages exist in methods like
any controller handlers before committing).
🧹 Nitpick comments (1)
src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java (1)
42-44:firebaseCloudMessageService필드가 미사용이면 DI 제거 권장(컨트롤러 책임 축소)Line 43에서 주입만 하고 사용하지 않으면 “왜 컨트롤러에 FCM이 있지?”라는 혼란을 주고, 규칙에 따라 경고/실패 포인트가 될 수 있습니다. 푸시 트리거가
StudentCouncilPostService의 이벤트 발행으로 충분하다면 이 필드는 제거하는 쪽을 권장합니다.제거 방향(diff)
-import java.util.Map; - @@ -import com.campus.campus.global.firebase.application.service.FirebaseCloudMessageService; @@ private final StudentCouncilPostService postService; - private final FirebaseCloudMessageService firebaseCloudMessageService;
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java
...ain/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java
Outdated
Show resolved
Hide resolved
...ain/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java
Outdated
Show resolved
Hide resolved
1winhyun
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다!! 리뷰 한번 확인 부탁드려요~!!
...ain/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java
Outdated
Show resolved
Hide resolved
...java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java
Outdated
Show resolved
Hide resolved
...java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java
Show resolved
Hide resolved
...ain/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostController.java
Outdated
Show resolved
Hide resolved
src/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (1)
197-197: 상수를 일관되게 사용하세요.Line 61에서는
MAX_IMAGE_COUNT상수를 사용하지만, Line 197에서는 여전히 하드코딩된10을 사용하고 있습니다. 일관성을 위해 상수를 사용해야 합니다.🐛 수정 제안
- if (dto.imageUrls() != null && dto.imageUrls().size() > 10) { + if (dto.imageUrls() != null && dto.imageUrls().size() > MAX_IMAGE_COUNT) {
🤖 Fix all issues with AI agents
In
@src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java:
- Line 51: The log format in FirebaseCloudMessageService is wrong: replace the
malformed placeholder "exec{}" with "exec={}" in the log.error call so the
message template has correct key=value placeholders; locate the log.error(...)
invocation (the send/failure logging in FirebaseCloudMessageService) and update
the format string to use "exec={}" ensuring number of placeholders matches
provided arguments.
In @src/main/resources/application-local.yml:
- Around line 70-72: Document that firebase.credentials.path in
application-local.yml reads the FIREBASE_CREDENTIALS_PATH environment variable
and add explicit instructions to the project README or environment setup guide
explaining how to set FIREBASE_CREDENTIALS_PATH for local development (e.g.,
where to place the Firebase credentials JSON file and how to export the env var
or add it to a .env file), and also add a short note clarifying why
application-dev.yml/application-prod.yml use encrypted/managed secrets while
application-local.yml uses a local env var for developer convenience.
🧹 Nitpick comments (2)
src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java (1)
25-35: 필수 파라미터에 대한 유효성 검증을 추가하세요.
topic,title,body파라미터가 null이거나 빈 문자열인 경우를 검증하지 않습니다. 이는 FCM 전송 실패나 NPE로 이어질 수 있습니다.♻️ 제안하는 수정안
public void sendToTopic(String topic, String title, String body, Map<String, String> data) { + if (topic == null || topic.isBlank()) { + throw new IllegalArgumentException("Topic must not be null or empty"); + } + if (title == null || title.isBlank()) { + throw new IllegalArgumentException("Title must not be null or empty"); + } + if (body == null || body.isBlank()) { + throw new IllegalArgumentException("Body must not be null or empty"); + } + Message.Builder builder = Message.builder()src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.java (1)
176-185: 방어적 프로그래밍을 위해 null 체크를 고려하세요.현재 호출 컨텍스트에서는
post와writer가 null이 아니지만, 매퍼는 재사용 가능한 컴포넌트이므로 파라미터 검증을 추가하면 더 견고해집니다. 특히writer.getCouncilType()이 null을 반환할 경우 NPE가 발생합니다.♻️ 제안하는 수정안
public CouncilPostCreatedEvent createPostCreatedEvent(StudentCouncilPost post, StudentCouncil writer) { + if (post == null || writer == null) { + throw new IllegalArgumentException("Post and writer must not be null"); + } + if (writer.getCouncilType() == null) { + throw new IllegalStateException("CouncilType must not be null"); + } + String topic = writer.getCouncilType().topic(writer);
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
src/main/java/com/campus/campus/domain/councilpost/application/mapper/StudentCouncilPostMapper.javasrc/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.javasrc/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.javasrc/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.javasrc/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.javasrc/main/resources/application-dev.ymlsrc/main/resources/application-local.ymlsrc/main/resources/application-prod.ymlsrc/test/resources/application-test.yml
💤 Files with no reviewable changes (1)
- src/main/java/com/campus/campus/domain/place/application/dto/response/SavedPlaceInfo.java
🚧 Files skipped from review as they are similar to previous changes (1)
- src/main/java/com/campus/campus/global/firebase/application/service/FirebaseInitializer.java
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-18T09:42:14.128Z
Learnt from: 1winhyun
Repo: our-campUS/our-campUS-BE PR: 6
File: src/test/resources/application-test.yml:15-16
Timestamp: 2025-11-18T09:42:14.128Z
Learning: In the our-campUS-BE project, the test configuration file (src/test/resources/application-test.yml) uses hardcoded dummy values for OAuth and JWT secrets, as these are arbitrary test values and not real credentials.
Applied to files:
src/main/resources/application-prod.ymlsrc/test/resources/application-test.yml
🧬 Code graph analysis (1)
src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java (2)
src/main/java/com/campus/campus/global/firebase/exception/FcmTopicSendFailedException.java (1)
FcmTopicSendFailedException(3-7)src/main/java/com/campus/campus/domain/councilpost/application/CouncilPostPushListener.java (1)
Slf4j(17-57)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (6)
src/main/resources/application-dev.yml (1)
68-71:firebase.credentials.path키 바인딩/경로 타입(classpath vs filesystem) 일치 여부 확인 필요
firebase.credentials.path가 코드에서 기대하는 프로퍼티명(prefix 포함)과 정확히 일치하는지, 그리고 값이 “classpath 리소스 경로”인지 “서버 파일시스템 경로”인지(예:resource:/절대경로 등) Firebase 초기화 로직과 일관되게 맞는지 확인해주세요. 또한server-uri/firebase같은 커스텀 설정이 루트에 있어 충돌 여지가 있으니(장기적으로)campus.*같은 전용 prefix로 묶는 것도 고려할 만합니다.src/test/resources/application-test.yml (1)
72-75: 테스트 프로파일에서 Firebase 비활성화는 적절하게 구현됨
FirebaseInitializer.java의@ConditionalOnProperty(prefix = "firebase", name = "enabled", havingValue = "true", matchIfMissing = true)어노테이션으로 Firebase 빈 초기화가firebase.enabled플래그로 올바르게 가드됩니다.CampusApplicationTests.java에서@ActiveProfiles("test")가 활성화되어 있으므로,application-test.yml의firebase.enabled: false설정이 테스트 실행 시 적용되어 Firebase 초기화를 제대로 비활성화합니다.src/main/resources/application-prod.yml (1)
69-71: Firebase 설정은 다른 환경과 일치하며 올바릅니다.검증 결과,
application-prod.yml의 Firebase 설정은application-dev.yml과application-local.yml과 동일하게firebase.credentials.path만 포함하고 있습니다.firebase.enabled플래그는 테스트 환경에서만 필요하며(firebase.enabled: false), 프로덕션, 개발, 로컬 환경에서는 일관되게 자격증명 경로만 구성하는 것이 올바른 설계입니다. 추가 설정(타임아웃, 재시도 정책 등)도 필요하지 않습니다.Likely an incorrect or invalid review comment.
src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java (1)
62-70: LGTM!스레드 풀 모니터링을 위한 유용한 헬퍼 메서드입니다. 로그를 통해 풀 상태를 추적할 수 있어 운영 시 디버깅에 도움이 됩니다.
src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java (2)
47-48: LGTM!상수를 도입하여 매직 넘버를 제거한 것은 좋은 개선입니다. 코드 가독성과 유지보수성이 향상됩니다.
82-90: LGTM!게시글 저장 후 이벤트를 발행하는 패턴이 올바르게 구현되었습니다.
@TransactionalEventListener(phase = AFTER_COMMIT)를 사용하는 리스너와 함께 사용하면, 트랜잭션 커밋 후에만 FCM 푸시가 전송되어 데이터 일관성이 보장됩니다.
| ); | ||
|
|
||
| } catch (FirebaseMessagingException e) { | ||
| log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec{}", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
로그 포맷 오류를 수정하세요.
Line 51의 로그 메시지에서 exec{}가 exec={}여야 합니다. 등호가 누락되어 로그 파싱에 문제가 발생할 수 있습니다.
🐛 수정 제안
- log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec{}",
+ log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec={}",📝 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.
| log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec{}", | |
| log.error("[FCM] send failed. topic={}, errorCode={}, message={}, exec={}", |
🤖 Prompt for AI Agents
In
@src/main/java/com/campus/campus/global/firebase/application/service/FirebaseCloudMessageService.java
at line 51, The log format in FirebaseCloudMessageService is wrong: replace the
malformed placeholder "exec{}" with "exec={}" in the log.error call so the
message template has correct key=value placeholders; locate the log.error(...)
invocation (the send/failure logging in FirebaseCloudMessageService) and update
the format string to use "exec={}" ensuring number of placeholders matches
provided arguments.
| firebase: | ||
| credentials: | ||
| path: ${FIREBASE_CREDENTIALS_PATH} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Step 1: List all environment configuration files in the resources directory
echo "=== Environment Configuration Files ==="
fd -e yml -e yaml "" src/main/resources --type f | sortRepository: our-campUS/our-campUS-BE
Length of output: 227
🏁 Script executed:
#!/bin/bash
# Step 2: Check firebase configuration in all environment files
echo "=== Firebase Configuration in All Environments ==="
fd -e yml -e yaml "^application" src/main/resources --exec sh -c 'echo "=== {} ===" && grep -A 3 "firebase:" "$1" || echo "No firebase config found"' _ {} \;Repository: our-campUS/our-campUS-BE
Length of output: 536
🏁 Script executed:
#!/bin/bash
# Step 3: Search for Firebase initialization and FIREBASE_CREDENTIALS_PATH usage in Java code
echo "=== Firebase Initialization Code ==="
rg -t java "firebase|Firebase|FIREBASE_CREDENTIALS" -A 3 -B 3Repository: our-campUS/our-campUS-BE
Length of output: 15439
🏁 Script executed:
#!/bin/bash
# Step 4: Check for documentation about environment variables
echo "=== Documentation Files About Environment Setup ==="
fd -e md -e txt "README|SETUP|ENV|CONFIG|INSTALL" . --type f | head -20Repository: our-campUS/our-campUS-BE
Length of output: 131
🏁 Script executed:
#!/bin/bash
# Step 5: Check if README or setup documentation mentions FIREBASE_CREDENTIALS_PATH
echo "=== Checking README and Documentation for FIREBASE_CREDENTIALS_PATH ==="
fd -e md "" . --type f --exec grep -l "FIREBASE_CREDENTIALS_PATH\|firebase" {} \; | head -10Repository: our-campUS/our-campUS-BE
Length of output: 139
🏁 Script executed:
#!/bin/bash
# Check README content for environment setup instructions
head -100 README.mdRepository: our-campUS/our-campUS-BE
Length of output: 105
🏁 Script executed:
#!/bin/bash
# Search for any setup/installation guides in the repository
fd -e md "" . --type f | xargs grep -l "FIREBASE\|환경\|setup\|설정" | head -10Repository: our-campUS/our-campUS-BE
Length of output: 50
🏁 Script executed:
#!/bin/bash
# Check if there's a separate environment setup guide or .env.example file
fd -e example -e sample -e template "" . --type f | head -10Repository: our-campUS/our-campUS-BE
Length of output: 50
로컬 개발 환경용 Firebase 자격 증명 경로 문서화 필요
firebase.credentials.path가 ${FIREBASE_CREDENTIALS_PATH} 환경 변수에 의존하고 있으나, application-dev.yml과 application-prod.yml과 달리 암호화된 값 대신 환경 변수를 사용하고 있습니다. 로컬 개발 환경에서 이 변수를 설정하는 방법을 README 또는 환경 설정 가이드에 문서화해주세요.
또한 각 환경 간의 구성 방식이 다른 이유(dev/prod는 암호화된 값, local은 환경 변수)를 명확히 하고, 로컬 개발 시 Firebase 자격 증명 파일의 위치를 지정하는 방법을 개발자가 쉽게 이해할 수 있도록 안내해주시길 권장합니다.
🤖 Prompt for AI Agents
In @src/main/resources/application-local.yml around lines 70 - 72, Document that
firebase.credentials.path in application-local.yml reads the
FIREBASE_CREDENTIALS_PATH environment variable and add explicit instructions to
the project README or environment setup guide explaining how to set
FIREBASE_CREDENTIALS_PATH for local development (e.g., where to place the
Firebase credentials JSON file and how to export the env var or add it to a .env
file), and also add a short note clarifying why
application-dev.yml/application-prod.yml use encrypted/managed secrets while
application-local.yml uses a local env var for developer convenience.
🔀 변경 내용
✅ 작업 항목
알림 처리 방식
"Topic 기반 발행"
CouncilPostPushListener
@TransactionalEventListener(phase = AFTER_COMMIT)DB 커밋까지 성공해야만 푸시 보냄
-> 푸시 유실 가능성이 있어서, 서비스가 커지면 outbox 고려 예정 (결제 같은 중요 알림이 아니어서 유실해도 큰 문제는 아니라고 생각)
@Async("fcmTaskExecutor")게시글 등록이 느려지지 않게 스레드풀에서 비동기 발송
푸시알림 느릴 수 있으니 요청 스레드를 안 잡아먹게 전용 스레드풀로 분리함.
FirebaseCloudMessageService
토픽 대상 지정하고 메세지 내용 정하는 등, fcm 코드가 담긴 클래스
푸시가 밀려서, 즉 큐가 쌓여있는지 보기 위한 execSnapshot 로그 메서드
📸 스크린샷 (선택)
Summary by CodeRabbit
릴리스 노트
New Features
Chores
✏️ Tip: You can customize this high-level summary in your review settings.