SSE 구독 상태란?
클라이언트가 서버에 요청을 보내고, 서버는 클라이언트와 지속적인 연결을 유지하는데 이 상태를 구독 상태라고 한다.
클라이언트는 HTTP 요청을 보내고, 서버는 그 요청을 통해 실시간으로 데이터를 푸시한다.
작동 방식
1. 클라이언트가 SSE 엔드포인트에 요청을 보낸다.
2. 서버는 요청에 대한 응답을 지속적으로 유지하면서 데이터를 스트리밍 한다.
3. 데이터는 HTTP 의 Content-Type: text/event - stream 포맷으로 전송된다.
4. 클라이언트는 연결이 유지된 상태에서 서버가 전송하는 데이터를 실시간으로 수신한다.
서버가 데이터를 푸시하기 위해서는 클라이언트와의 연결이 끊어지지 않아야 한다.
@Service
@RequiredArgsConstructor
public class NotificationService {
private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();
public SseEmitter subscribe(Long memberId) {
SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
emitters.put(memberId, sseEmitter); //로그인 된 사용자 아이디 emitter 객체 등록
sseEmitter.onCompletion(() -> emitters.remove(memberId));
sseEmitter.onTimeout(() -> emitters.remove(memberId));
sseEmitter.onError((e) -> emitters.remove(memberId));
try {
sseEmitter.send(SseEmitter.event().name("connect")
.data("연결 성공")); //연결 성공 할 경우 메세지
} catch (IOException e) {
e.printStackTrace();
}
return sseEmitter;
}
}
사용자의 memberId를 키로 하여 SseEmitter 객체를 emitters Map에 저장한다.
연결 완료, 시간 초과 에러 발생 시 구독을 삭제한다.
package com.example.investment_api.virtual.alarm.domain;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Notification {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "notification_id")
private Long id;
private Long memberId;
private String message;
private LocalDateTime createdAt;
private String url;
@Builder
public Notification(Long memberId, String message, String url, LocalDateTime createdAt) {
this.memberId = memberId;
this.message = message;
this.url = url;
this.createdAt = LocalDateTime.now();
}
}
알람 객체 생성
public NotificationDTO toDTO(Notification notification) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");
return NotificationDTO.builder()
.id(notification.getId())
.message(notification.getMessage())
.createdAT(notification.getCreatedAt().format(formatter))
.url(notification.getUrl())
.build();
}
LocalDateTime 타입의 데이터를 JSON 으로 직렬화 하면서 문제가 생김
String 값으로 변환해주었다.
@RestController
@RequiredArgsConstructor
public class NotificationController {
private final NotificationService notificationService;
@GetMapping("/notification/subscribe")
public SseEmitter subscribe(@Member Long memberId) {
return notificationService.subscribe(memberId);
}
SSE 를 통한 알림 기능을 받을 사용자들을 등록

연결 !
notification service 작성
public void sendOrderNotification(Long memberId, NotificationType type, String stockName, int quantity) {
String message = generateNotificationMessage(type, stockName, quantity);
String url = "https://growfolio-nu.vercel.app/login";
Notification notification = createNotification(memberId, message, url);
notificationRepository.save(notification);
SseEmitter emitter = emitters.get(memberId);
if (emitter != null) {
NotificationDTO notificationDTO = notificationMapper.toDTO(notification);
sendToClient(emitter, memberId.toString(), notificationDTO);
}
}
private Notification createNotification(Long memberId, String message, String url) {
return Notification.builder()
.memberId(memberId)
.message(message)
.url(url)
.createdAt(LocalDateTime.now())
.build();
}
private void sendToClient(SseEmitter emitter, String id, Object data) {
try {
emitter.send(SseEmitter.event()
.id(id)
.name("order-notification")
.data(data));
} catch (IOException exception) {
emitters.remove(Long.valueOf(id));
throw new RuntimeException("알림 전송 실패!", exception);
}
}
private String generateNotificationMessage(NotificationType type, String stockName, int quantity) {
switch (type) {
case BUY_SUCCESS:
return "매수 성공: " + stockName + " " + quantity + "주가 체결되었습니다.";
case SELL_SUCCESS:
return "매도 성공: " + stockName + " " + quantity + "주가 체결되었습니다.";
default:
return "알림: 주문 상태가 업데이트되었습니다.";
}
}
주문 유형 (BUY_SUCCESS, SELL_SUCCESS)에 따라 알림 메세지를 생성 후
createNotification 메서드를 통해 알림 객체를 생성 한 후, 해당 알림을 DB에 저장한 뒤 실시간 전송을 시도한다.
sendToClient 메서드는 SseEmitter 객체를 통해 클라이언트로 데이터를 전송한다.


확인을 위해 특정 회원의 알람목록을 최신순으로 반환하도록 하였다.
[Spring + SSE] Server-Sent Events를 이용한 실시간 알림
코드리뷰 매칭 플랫폼 개발 중 알림 기능이 필요했다. 리뷰어 입장에서는 새로운 리뷰 요청이 생겼을 때 모든 리뷰가 끝나고 리뷰이의 피드백이 도착했을 때 리뷰이 입장에서는 리뷰 요청이 거
velog.io
'project' 카테고리의 다른 글
| [프로젝트] 댓글 조회시 N+1 문제 해결, 성능 비교 (0) | 2025.11.26 |
|---|---|
| [프로젝트] 주식 체결 알람 기능 추가 - 1 (2) | 2025.01.15 |