Conversation
- year는 1900~2050년 사이만 가능하다
…smester-and-year-MOA-521 [refactor] 동아리 설명 award의 semesterTerm과 year를 분리한다
…-status-using-sse-MOA-484 [feat] 지원자 상태 변경 SSE 실시간 알림 기능 구현 (Redis Pub/Sub)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning
|
| Cohort / File(s) | 변경 요약 |
|---|---|
Redis 및 빌드 구성 backend/build.gradle, backend/src/main/java/moadong/global/config/RedisConfig.java |
Spring Data Redis 의존성 추가 및 RedisTemplate<String, Object> 빈 구성 (문자열 키 직렬화, Jackson JSON 값 직렬화) 및 RedisMessageListenerContainer 빈 생성 |
SSE 인프라 backend/src/main/java/moadong/sse/enums/ApplicantEventType.java, backend/src/main/java/moadong/sse/dto/ApplicantSseDto.java, backend/src/main/java/moadong/sse/service/ApplicantsStatusShareSse.java |
새로운 SSE 세션 관리 서비스 추가: Redis Pub/Sub 채널 수신, SseEmitter 생성/라이프사이클 관리, 상태 변경 이벤트 브로드캐스트, 클럽당 20개 세션 제한, 45초 주기 하트비트 메커니즘 포함 |
클럽 어워드 도메인 모델 backend/src/main/java/moadong/club/entity/ClubAward.java, backend/src/main/java/moadong/club/payload/dto/ClubAwardDto.java |
semester: String 필드를 year: int (범위 1900-2050 검증) 및 semesterTerm: SemesterTerm 필드로 분리, 매핑 로직 업데이트 |
컨트롤러 및 서비스 통합 backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java, backend/src/main/java/moadong/club/service/ClubApplyAdminService.java |
기존 SSE 로직을 ApplicantsStatusShareSse로 치환: 엔드포인트 경로 변경 (/events → /sse), 배치 상태 변경 이벤트를 Redis를 통해 게시, 내부 SseEmitter 관리 제거 |
Sequence Diagram(s)
sequenceDiagram
participant Client as 클라이언트
participant Controller as ClubApplyAdminController
participant SSE as ApplicantsStatusShareSse
participant DB as 데이터베이스
Client->>Controller: GET /applicant/{applicationFormId}/sse
Controller->>SSE: createSseSession(applicationFormId, user)
SSE->>DB: 클럽 소유권 검증
SSE->>SSE: SseEmitter 생성 (타임아웃 설정)
SSE->>SSE: 클럽별 세션 제한 확인 (≤20)
SSE-->>Controller: SseEmitter 반환
Controller-->>Client: SSE 연결 수립
Note over SSE: 주기적 하트비트<br/>(45초마다)
SSE->>Client: :heartbeat
sequenceDiagram
participant Service as ClubApplyAdminService
participant Redis as Redis Pub/Sub
participant SSE as ApplicantsStatusShareSse
participant Client as SSE 클라이언트
Service->>Service: 신청자 상태 변경
Service->>Service: 배치 이벤트 큐에 추가
Note over Service: 트랜잭션 커밋 후
Service->>SSE: publishStatusChangeEvent(clubId, formId, event)
SSE->>Redis: sse:applicant-status:{clubId}:{formId} 채널에 발행
Note over Redis: Redis Pub/Sub
Redis->>SSE: onMessage() 트리거
SSE->>SSE: broadcastToLocalConnections()
SSE->>Client: applicant-status-changed 이벤트 전송
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
- Revert "[feature] 동아리 지원서 폼 제작 시에 학기를 선택할 수 있고 생성된 모든 지원서 폼을 학기별로 분류하여 조회할 수 있다" #750: 도메인 모델에서
SemesterTerm및semester관련 API를 제거/롤백하는 내용으로, 본 PR의ClubAward/ClubAwardDto변경과 직접 충돌합니다. - [feat] 지원자 상태 변경 SSE 실시간 알림 기능 구현 (Redis Pub/Sub) #1061: Redis 의존성, RedisConfig,
ApplicantsStatusShareSse추가 및 SSE 통합을 동일하게 수정하는 내용으로 본 PR과 코드 수준에서 직접 연관됩니다. - [Feature] 지원자의 지원서 상태를 실시간으로 공유한다. #791: SSE 기반 실시간 신청자 상태 업데이트를 구현하며 동일한 컨트롤러/서비스 SSE 로직을 수정하므로, 본 PR의 중앙화된
ApplicantsStatusShareSse리팩토링과 관련됩니다.
Suggested reviewers
- yw6938
- seongwon030
🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | |
| Title check | ❓ Inconclusive | 제목이 너무 모호하고 일반적이어서 구체적인 변경사항을 전달하지 못함. '[release] BE'는 실제 변경 내용(Redis 통합, SSE 서비스, 데이터 구조 리팩토링)을 명확히 설명하지 않음. | 더 구체적인 제목으로 변경 권장. 예: '[release] BE: Redis 통합 및 SSE 서비스 추가' 또는 '[release] v1.1.2: Redis pub/sub 및 분산 잠금 지원' |
✅ Passed checks (1 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing touches
- 📝 Generate docstrings
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 @coderabbitai help to get the list of available commands and usage tips.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@backend/src/main/java/moadong/club/payload/dto/ClubAwardDto.java`:
- Around line 11-13: ClubAwardDto의 semesterTerm 필드(SemesterTerm semesterTerm)가
현재 null 허용으로 인해 필수 검증이 누락될 수 있으니, 필수 값이라면 해당 필드에 `@NotNull` 애노테이션을 추가해 null 입력을
차단하고 DTO 유효성 검증에 포함되도록 수정하세요; 대상은 ClubAwardDto 클래스의 semesterTerm 필드이며, 필요한 경우
메시지를 명시하거나 검증 그룹을 적용해 일관된 에러 처리를 보장하세요.
In `@backend/src/main/java/moadong/sse/service/ApplicantsStatusShareSse.java`:
- Around line 57-65: The current removal uses
clubEmitters.keySet().iterator().next() which on a ConcurrentHashMap
(clubEmitters) yields an unpredictable key so you may evict the wrong session;
change the eviction to a FIFO strategy by tracking insertion order or
timestamps: add a secondary ordered structure (e.g., a ConcurrentLinkedQueue or
a timestamp map) to record session keys when you put into clubEmitters, and when
clubEmitters.size() >= MAX_SESSIONS_PER_CLUB remove the oldest key from that
queue/map (call it oldestKey instead of keyToRemove), complete its SseEmitter
(the SseEmitter retrieved from clubEmitters.get(oldestKey)) and remove both
entries; update the code paths that add emitters to push the key into the
ordered tracker and ensure removals also remove from the tracker to keep them in
sync.
🧹 Nitpick comments (7)
backend/src/main/java/moadong/sse/dto/ApplicantSseDto.java (1)
6-11:data필드의 타입 안전성을 고려해 보세요.
data필드가Object타입으로 선언되어 있어 타입 안전성이 낮습니다. 다양한 이벤트 타입에 따라 다른 데이터 구조를 담을 수 있는 유연성을 제공하지만, 런타임 시 캐스팅 오류나 직렬화 문제가 발생할 수 있습니다.제네릭을 사용하거나 이벤트 타입별로 sealed interface/class를 고려해 볼 수 있습니다.
♻️ 제네릭을 사용한 대안
-@Data -public class ApplicantSseDto { - private String clubId; - private ApplicantEventType event; - private Object data; -} +@Data +public class ApplicantSseDto<T> { + private String clubId; + private ApplicantEventType event; + private T data; +}backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java (1)
35-35: 필드명을 좀 더 명확하게 변경하는 것을 고려해 보세요.
sse라는 필드명이 너무 짧아서 코드 가독성이 떨어질 수 있습니다.applicantsStatusShareSse또는sseService와 같이 좀 더 설명적인 이름을 사용하면 코드의 의도가 더 명확해집니다.backend/src/main/java/moadong/global/config/RedisConfig.java (2)
21-24: ObjectMapper 설정 개선을 고려해 보세요.
ObjectMapper에JavaTimeModule만 등록되어 있습니다. 프로덕션 환경에서는 알 수 없는 속성에 대한 역직렬화 실패 방지 등 추가 설정이 도움이 될 수 있습니다.♻️ ObjectMapper 설정 개선 제안
ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); +objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); +objectMapper.configure(com.fasterxml.jackson.databind.SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
35-40: RedisMessageListenerContainer에 TaskExecutor 설정을 고려해 보세요.
RedisMessageListenerContainer에 별도의TaskExecutor가 설정되지 않아 기본SimpleAsyncTaskExecutor를 사용합니다. 고부하 환경에서는 스레드 풀 기반의TaskExecutor를 설정하면 리소스 관리가 더 효율적입니다.♻️ TaskExecutor 설정 예시
`@Bean` -public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { +public RedisMessageListenerContainer redisMessageListenerContainer( + RedisConnectionFactory connectionFactory, + `@Qualifier`("redisTaskExecutor") TaskExecutor taskExecutor) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); + container.setTaskExecutor(taskExecutor); return container; } + +@Bean(name = "redisTaskExecutor") +public TaskExecutor redisTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(4); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("redis-listener-"); + executor.initialize(); + return executor; +}backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (2)
188-199: TransactionSynchronization 등록 시 트랜잭션 활성화 여부 확인 권장
TransactionSynchronizationManager.registerSynchronization()은 활성화된 트랜잭션이 없을 때IllegalStateException을 발생시킬 수 있습니다. 현재 메서드가@Transactional로 선언되어 있어 정상 동작하겠지만, 향후 코드 변경이나 테스트 시 방어적 코딩을 고려해 보세요.♻️ 방어적 코딩 예시
-TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { +if (TransactionSynchronizationManager.isActualTransactionActive()) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { `@Override` public void afterCommit() { events.forEach(event -> { try { applicantsStatusShareSse.publishStatusChangeEvent(clubId, applicationFormId, event); } catch (Exception e) { log.error("SSE publish failed. clubId={}, formId={}, applicantId={}", clubId, applicationFormId, event.applicantId(), e); } }); } }); +} else { + log.warn("No active transaction, publishing events immediately"); + events.forEach(event -> { + try { + applicantsStatusShareSse.publishStatusChangeEvent(clubId, applicationFormId, event); + } catch (Exception e) { + log.error("SSE publish failed. clubId={}, formId={}, applicantId={}", clubId, applicationFormId, event.applicantId(), e); + } + }); +}
80-80: 긴 한 줄 코드 가독성 검토빌더 패턴 호출이 한 줄로 압축되어 있어 가독성이 다소 떨어질 수 있습니다. 팀 컨벤션에 따라 다르지만, 가독성을 위해 여러 줄로 분리하는 것을 고려해 보세요.
backend/src/main/java/moadong/sse/service/ApplicantsStatusShareSse.java (1)
145-145: 로그 메시지 언어를 통일하세요.이 로그 메시지만 한국어로 작성되어 있고, 다른 로그 메시지들(예: Line 81, 83, 111, 122)은 영어로 작성되어 있습니다. 일관성을 위해 언어를 통일하는 것이 좋습니다.
♻️ 영어로 통일하는 경우
- log.warn("SSE 이벤트 발송 실패: {}", e.getMessage()); + log.warn("Failed to send SSE event: {}", e.getMessage());
v 1.1.2
Summary by CodeRabbit
Release Notes
New Features
Refactor
✏️ Tip: You can customize this high-level summary in your review settings.