From 7ffbacc838bf258c49518cc8a2b9f34676b99195 Mon Sep 17 00:00:00 2001 From: idktomorrow Date: Thu, 6 Aug 2026 23:16:05 +0900 Subject: [PATCH 1/2] feat: send periodic heartbeat to keep notification SSE connections alive --- .../com/momogo/api/MomogoApiApplication.java | 2 ++ .../registry/NotificationEmitterRegistry.java | 7 ++++ .../NotificationSseHeartbeatScheduler.java | 35 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java diff --git a/momogo-api/src/main/java/com/momogo/api/MomogoApiApplication.java b/momogo-api/src/main/java/com/momogo/api/MomogoApiApplication.java index f313359..07382be 100644 --- a/momogo-api/src/main/java/com/momogo/api/MomogoApiApplication.java +++ b/momogo-api/src/main/java/com/momogo/api/MomogoApiApplication.java @@ -4,10 +4,12 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication(scanBasePackages = "com.momogo") @EntityScan(basePackages = "com.momogo.core.domain") @EnableJpaRepositories(basePackages = "com.momogo.core.domain") +@EnableScheduling public class MomogoApiApplication { public static void main(String[] args) { SpringApplication.run(MomogoApiApplication.class, args); diff --git a/momogo-api/src/main/java/com/momogo/api/notification/registry/NotificationEmitterRegistry.java b/momogo-api/src/main/java/com/momogo/api/notification/registry/NotificationEmitterRegistry.java index d9ca52f..067fcc0 100644 --- a/momogo-api/src/main/java/com/momogo/api/notification/registry/NotificationEmitterRegistry.java +++ b/momogo-api/src/main/java/com/momogo/api/notification/registry/NotificationEmitterRegistry.java @@ -33,6 +33,13 @@ public List findAllByUserId(UUID userId) { return emitters.getOrDefault(userId, List.of()); } + // 이 인스턴스에 연결된 모든 emitter 조회 (하트비트 브로드캐스트용) + public List findAll() { + return emitters.values().stream() + .flatMap(List::stream) + .toList(); + } + // 연결이 정상종료되거나, 타임아웃되거나, 에러가 났을 때 목록에서 제거 // 유저의 emitter가 다 없어지면 메모리 누수 방지를 위해 그 유저의 key 자체도 지움 public void remove(UUID userId, SseEmitter emitter) { diff --git a/momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java b/momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java new file mode 100644 index 0000000..6cf4734 --- /dev/null +++ b/momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java @@ -0,0 +1,35 @@ +package com.momogo.api.notification.scheduler; + +import com.momogo.api.notification.registry.NotificationEmitterRegistry; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +/* + * SSE 연결이 오래 idle 상태면 중간 프록시/로드밸런서가 죽은 연결로 오해해 끊어버릴 수 있음 + * 일정 주기로 빈 코멘트를 보내 연결이 살아있음을 알림 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class NotificationSseHeartbeatScheduler { + + private static final long HEARTBEAT_INTERVAL = 30_000L; // 30초 + + private final NotificationEmitterRegistry emitterRegistry; + + @Scheduled(fixedRate = HEARTBEAT_INTERVAL) + public void sendHeartbeat() { + for (SseEmitter emitter : emitterRegistry.findAll()) { + try { + emitter.send(SseEmitter.event().comment("heartbeat")); + } catch (IOException e) { + log.warn("[NotificationSseHeartbeatScheduler] 하트비트 전송 실패, 연결 정리", e); + emitter.completeWithError(e); + } + } + } +} From 26d23c511d00a8689cdaef77f3a20db8d974e297 Mon Sep 17 00:00:00 2001 From: idktomorrow Date: Fri, 7 Aug 2026 01:29:00 +0900 Subject: [PATCH 2/2] refactor: apply code rabbitai review --- .../scheduler/NotificationSseHeartbeatScheduler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java b/momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java index 6cf4734..4a9fb0f 100644 --- a/momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java +++ b/momogo-api/src/main/java/com/momogo/api/notification/scheduler/NotificationSseHeartbeatScheduler.java @@ -26,7 +26,7 @@ public void sendHeartbeat() { for (SseEmitter emitter : emitterRegistry.findAll()) { try { emitter.send(SseEmitter.event().comment("heartbeat")); - } catch (IOException e) { + } catch (IOException | IllegalStateException e) { log.warn("[NotificationSseHeartbeatScheduler] 하트비트 전송 실패, 연결 정리", e); emitter.completeWithError(e); }