Skip to content

[feat] 지원자 상태 변경 SSE 실시간 알림 기능 구현 (Redis Pub/Sub)#1061

Merged
lepitaaar merged 9 commits intodevelop/befrom
refactor/#1007-share-applicants-status-using-sse-MOA-484
Jan 19, 2026
Merged

[feat] 지원자 상태 변경 SSE 실시간 알림 기능 구현 (Redis Pub/Sub)#1061
lepitaaar merged 9 commits intodevelop/befrom
refactor/#1007-share-applicants-status-using-sse-MOA-484

Conversation

@lepitaaar
Copy link
Contributor

@lepitaaar lepitaaar commented Jan 18, 2026

#️⃣연관된 이슈

#1007

📝작업 내용

동아리 지원자 상태 변경 SSE 실시간 알림 기능 구현

지원자 상태가 변경될 때 클라이언트에게 SSE(Server-Sent Events)를 통해 실시간으로 알림을 전송합니다.

주요 기능:

  • 지원자 상태 변경 시 연결된 모든 관리자에게 실시간 이벤트 전송
  • Redis Pub/Sub를 통한 다중 replica 환경 지원
  • 클럽당 최대 20개 SSE 세션 제한

Redis Pub/Sub 도입 이유:

SSE 연결은 각 Pod의 인메모리(ConcurrentHashMap)에 저장됩니다. Kubernetes 다중 replica 환경에서는:

  • 클라이언트 A가 Pod 1에 SSE 연결
  • 관리자가 Pod 2에서 상태 변경 API 호출
  • Pod 2는 Pod 1의 SSE 연결을 알 수 없음 → 이벤트 전달 불가

Redis Pub/Sub로 해결:

변경 파일:

파일 변경 내용
RedisConfig.java Redis Pub/Sub 리스너 컨테이너 및 Jackson ObjectMapper(JavaTimeModule) 설정
ApplicantsStatusShareSse.java SSE 세션 관리, Redis Pub/Sub 통합, 하트비트 스케줄링
ClubApplyAdminController.java SSE 엔드포인트 추가 (/applicant/{applicationFormId}/sse)
ClubApplyAdminService.java 트랜잭션 커밋 후 Redis 이벤트 발행 로직

중점적으로 리뷰받고 싶은 부분(선택)

논의하고 싶은 부분(선택)

🫡 참고사항

  • PatternTopic 사용 (와일드카드 구독을 위해 ChannelTopic 대신)
  • GenericJackson2JsonRedisSerializerJavaTimeModule 등록 필요 (LocalDateTime 직렬화)
  • 클라이언트는 EventSource 사용 시 자동 재연결 지원

Summary by CodeRabbit

  • 신규 기능

    • Redis 기반의 분산 실시간 이벤트 전송 및 공유 SSE 서비스 추가
    • 지원자 상태 관련 전송 DTO와 이벤트 타입 추가
    • 관리자용 SSE 세션 관리 및 연결 심박(heartbeat) 기능 도입
  • 개선 사항

    • 지원자 상태 업데이트 시 중앙화된 이벤트 발행으로 안정성 향상
    • 관리자용 SSE 엔드포인트 경로 변경 및 세션 수 제한/자동 정리로 연결 신뢰성 개선

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

@vercel
Copy link

vercel bot commented Jan 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
moadong Ready Ready Preview, Comment Jan 19, 2026 4:49am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 18, 2026

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

Redis 기반 pub/sub을 도입해 SSE 인프라를 재구성했습니다. ApplicantsStatusShareSse 서비스 추가, Redis 설정(템플릿·리스너) 및 DTO/열거 추가, ClubApplyAdminService/Controller의 기존 SseEmitter 관리 제거 및 새 서비스로 위임이 포함됩니다.

Changes

Cohort / File(s) 변경 사항 요약
빌드/Redis 설정
backend/build.gradle, backend/src/main/java/moadong/global/config/RedisConfig.java
Redis 스타터 의존성 추가 및 RedisTemplate<String,Object>RedisMessageListenerContainer 빈 등록(값 직렬화에 Jackson+JavaTimeModule 사용)
SSE 공유 서비스
backend/src/main/java/moadong/sse/service/ApplicantsStatusShareSse.java
Redis pub/sub 구독·발행 기반의 SSE 세션 관리 컴포넌트 추가(세션 제한·회수, 초기 연결 이벤트, 전송 실패 처리), 45초 하트비트 스케줄러, Redis 메시지 리스너 구현
SSE DTO/타입
backend/src/main/java/moadong/sse/dto/ApplicantSseDto.java, backend/src/main/java/moadong/sse/enums/ApplicantEventType.java
SSE 페이로드 DTO(clubId, event, data) 및 이벤트 타입 열거 추가
서비스 계층 변경
backend/src/main/java/moadong/club/service/ClubApplyAdminService.java
로컬 SseEmitter 맵/관리 제거, 이벤트 수집 후 트랜잭션 커밋 시 ApplicantsStatusShareSse로 배치 발행하도록 리팩토링(관련 메서드/필드 제거 및 의존성 교체)
컨트롤러 변경
backend/src/main/java/moadong/club/controller/ClubApplyAdminController.java
ApplicantsStatusShareSse 의존성 주입 및 SSE 엔드포인트 경로 변경(/events/sse), 세션 생성 로직 위임

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Controller as ClubApplyAdminController
    participant Service as ClubApplyAdminService
    participant SSE as ApplicantsStatusShareSse
    participant Redis as Redis Pub/Sub
    participant Listener as RedisMessageListener

    Client->>Controller: SSE 연결 요청 (applicationFormId)
    Controller->>SSE: createSseSession(applicationFormId)
    SSE->>Client: SseEmitter 반환 + "connected" 이벤트

    Note over SSE: 하트비트 스케줄러 (rgba(0,128,0,0.5)) 45초 주기 ping

    Client->>Controller: 지원자 상태 변경 요청
    Controller->>Service: editApplicantDetail(...)
    Service->>Service: 이벤트 수집(List)
    Service->>Service: 트랜잭션 커밋
    Service->>SSE: publishStatusChangeEvent(clubId, applicationFormId, event)
    SSE->>Redis: 채널 발행 ("sse:applicant-status:clubId:formId")
    Redis->>Listener: 메시지 전달
    Listener->>SSE: onMessage(...)
    SSE->>SSE: 로컬 에미터 조회 및 브로드캐스트
    SSE->>Client: 이벤트 전송
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • PR #895: 동일 컨트롤러의 SSE 엔드포인트 및 세션 처리 변경과 관련.
  • PR #791: 기존 SseEmitter 기반 구현(생성·맵·전송)을 다루던 PR로, 이번 PR의 리팩토링 대상과 코드 중복 영역이 있음.
  • PR #847: ClubApplyAdminService/Controller의 SSE 관련 로직 변경과 겹치는 항목 존재.

Suggested labels

✨ Feature, 💾 BE, 📬 API

Suggested reviewers

  • Zepelown
  • yw6938
  • PororoAndFriends
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning 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.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 Redis Pub/Sub을 활용한 지원자 상태 변경 SSE 실시간 알림 기능 구현이라는 주요 변경사항을 명확하게 요약하고 있습니다.
Linked Issues check ✅ Passed 변경사항이 MOA-484의 모든 요구사항을 충족합니다: SSE 하트비트 구현(sendHeartBeat 메서드), SSE 다중 사용자 지원(클럽당 최대 20개 세션), 분산 환경 지원(Redis Pub/Sub 기반 이벤트 전파).
Out of Scope Changes check ✅ Passed 모든 변경사항이 SSE 실시간 알림 기능 구현과 Redis Pub/Sub 통합이라는 명확한 범위 내에 있으며, 범위를 벗어난 변경사항은 없습니다.

✏️ 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Jan 18, 2026

Test Results

75 tests   72 ✅  17s ⏱️
14 suites   3 💤
14 files     0 ❌

Results for commit 55536a6.

♻️ This comment has been updated with latest results.

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: 2

🤖 Fix all issues with AI agents
In `@backend/src/main/java/moadong/club/service/ClubApplyAdminService.java`:
- Around line 200-205: The afterCommit block calls
applicantsStatusShareSse.publishStatusChangeEvent for each event but does not
handle exceptions, so any publish failure can make the API appear to fail or
stop further propagation; modify the
TransactionSynchronizationManager.registerSynchronization(...) /
TransactionSynchronization.afterCommit implementation so that the events.forEach
loop wraps each call to
applicantsStatusShareSse.publishStatusChangeEvent(clubId, applicationFormId,
event) in a try/catch that logs the exception (including context: clubId,
applicationFormId, event) and does not rethrow, ensuring one failing publish
does not abort others and that failures are recorded for monitoring/alerting.

In `@backend/src/main/java/moadong/global/config/RedisConfig.java`:
- Around line 16-31: The redisTemplate bean in RedisConfig currently constructs
a new ObjectMapper causing mismatch with Spring's global ObjectMapper; change
redisTemplate(RedisConnectionFactory) to accept the application ObjectMapper
(e.g., add ObjectMapper objectMapper parameter), call objectMapper.copy() to
create a mutable copy, register JavaTimeModule on the copy, and use that copied
mapper to instantiate GenericJackson2JsonRedisSerializer so Redis key/value/hash
serializers use the same configuration as the rest of the app (avoiding
deserialization issues in classes like ApplicantsStatusShareSse).
🧹 Nitpick comments (3)
backend/src/main/java/moadong/sse/service/ApplicantsStatusShareSse.java (1)

103-119: Redis 직렬화 설정과 동일한 방식으로 역직렬화하도록 맞춰주세요.
현재 RedisTemplate의 serializer와 ObjectMapper 설정이 다르면 필드 매핑 실패가 날 수 있습니다. Serializer로 역직렬화 후 타입 확인을 권장합니다.

♻️ 제안 수정
-            ApplicantStatusEvent event = objectMapper.readValue(message.getBody(), ApplicantStatusEvent.class);
-            broadcastToLocalConnections(clubId, applicationFormId, event);
+            Object value = redisTemplate.getValueSerializer().deserialize(message.getBody());
+            if (!(value instanceof ApplicantStatusEvent event)) {
+                log.warn("Unexpected payload type on channel {}: {}", channel, value);
+                return;
+            }
+            broadcastToLocalConnections(clubId, applicationFormId, event);
backend/src/main/java/moadong/sse/dto/ApplicantSseDto.java (1)

6-10: data 필드를 구체적인 타입으로 좁혀서 타입 안정성을 개선하세요.

현재 Object 타입은 런타임 캐스팅 오류와 직렬화 이슈의 위험이 있습니다. 실제 SSE 구현에서 사용 중인 ApplicantStatusEvent처럼 이벤트별 구체적인 DTO 또는 제네릭 타입으로 정의하는 것을 권장합니다.

backend/src/main/java/moadong/club/service/ClubApplyAdminService.java (1)

185-197: 상태 변경이 없을 때도 이벤트가 발행됩니다 — 요구사항 확인 필요.

현재는 memo만 수정되어도 ApplicantStatusEvent가 생성됩니다. “상태 변경 알림”이 요구사항이면 실제 상태 변경 시에만 이벤트를 발행하도록 필터링하는 편이 안전합니다.

♻️ 변경 제안
 application.forEach(app -> {
+    var prevStatus = app.getStatus();
     ClubApplicantEditRequest editRequest = requestMap.get(app.getId());
     app.updateMemo(editRequest.memo());
     app.updateStatus(editRequest.status());
 
-    events.add(new ApplicantStatusEvent(
-            app.getId(),
-            editRequest.status(),
-            editRequest.memo(),
-            ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(),
-            clubId,
-            applicationFormId
-    ));
+    if (prevStatus != editRequest.status()) {
+        events.add(new ApplicantStatusEvent(
+                app.getId(),
+                editRequest.status(),
+                editRequest.memo(),
+                ZonedDateTime.now(ZoneId.of("Asia/Seoul")).toLocalDateTime(),
+                clubId,
+                applicationFormId
+        ));
+    }
 });

Copy link
Member

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

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

서버를 여러개로 분리하면서 레디스를 도입하셨군요

확장성을 고려해보면 좋을 것 같네요

@lepitaaar lepitaaar merged commit 9d9bb74 into develop/be Jan 19, 2026
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Jan 19, 2026
@lepitaaar lepitaaar deleted the refactor/#1007-share-applicants-status-using-sse-MOA-484 branch January 22, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants