📘SSE 연결 및 메시지 전송 테스트까지 단계별 구현
SSE는 서버에서 클라이언트로 단방향으로 데이터를 지속적으로 전송할 수 있는 기술이다. 주로 실시간 알림, 주식 정보, 로그 스트림 등과 같이 지속적으로 서버에서 클라이언트로 이벤트를 전달해야 하는 상황에서 사용된다.
SSE 기능을 사용하려면 Spring MVC의 웹 기능만 있으면 된다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
클라이언트가 서버에 SSE 연결을 요청하면, 서버는 SseEmitter를 반환하여 이벤트 스트림을 열어준다.
이때 로그인된 사용자 정보를 통해 사용자별 연결을 식별할 수 있도록 구현해준다.
@RestController
@RequiredArgsConstructor
@RequestMapping
public class SseController {
private final SseService sseService;
@GetMapping("/sse/connect")
public SseEmitter connect(@AuthenticationPrincipal AuthUser authUser) {
return sseService.connect(authUser.getId());
}
/sse/connect: 클라이언트가 이 엔드포인트로 요청하면 SSE 연결이 생성된다.@AuthenticationPrincipal: Spring Security를 사용해 로그인한 사용자의 정보를 가져온다.authUser.getId(): 사용자 식별자로 사용됩니다. 이를 기반으로 유저별 SseEmitter를 관리한다.@Service
public class SseService {
// 사용자별 emitter 저장소
private final Map<Long, List<SseEmitter>> emitters = new ConcurrentHashMap<>();
public SseEmitter connect(Long userId) {
// 1분간 타임아웃 설정
SseEmitter emitter = new SseEmitter(60 * 1000L);
// 연결 종료, 타임아웃, 오류 시 emitter 제거
emitter.onCompletion(() -> removeEmitter(userId, emitter));
emitter.onTimeout(() -> removeEmitter(userId, emitter));
emitter.onError(e -> removeEmitter(userId, emitter));
// 사용자별 emitter 저장
emitters.computeIfAbsent(userId, id -> new CopyOnWriteArrayList<>()).add(emitter);
return emitter;
}
SseEmitter를 생성하고 사용자별로 저장한다.SSE 연결이 정상적으로 이루어졌는지 확인하기 위해, 간단한 전송 테스트용 API를 만든다.
지정한 사용자에게 메시지를 전송하고, 실제 연결된 경우에만 성공 여부를 반환한다.
//전송 확인용 테스트 코드
@PostMapping("/sse/send")
public boolean send(@RequestBody SseMessageDto dto) {
return sseService.sendIfConnected(dto);
}
SseMessageDto: 메시지 수신자와 내용 정보를 담은 DTOsendIfConnected(...): 대상 사용자에게 SSE로 메시지를 전송하고, 결과를 boolean으로 반환한다사용자별로 저장된 SseEmitter 목록을 조회하고, 연결이 유지 중인 emitter에 메시지를 전송한다.
전송 실패 시 해당 emitter는 제거하여 누적 리소스를 방지한다.
public boolean sendIfConnected(SseMessageDto dto) {
List<SseEmitter> userEmitters = emitters.get(dto.getUserId());
if (userEmitters == null || userEmitters.isEmpty()) {
return false;
}
boolean atLeastOneSuccess = false;
for (SseEmitter emitter : userEmitters) {
try {
emitter.send(SseEmitter.event()
.name("notification")
.data(dto));
atLeastOneSuccess = true;
} catch (Exception e) {
removeEmitter(dto.getUserId(), emitter);
}
}
return atLeastOneSuccess;
}
private void removeEmitter(Long userId, SseEmitter emitter) {
List<SseEmitter> userEmitters = emitters.get(userId);
if (userEmitters != null) {
userEmitters.remove(emitter);
if (userEmitters.isEmpty()) {
emitters.remove(userId);
}
}
}
emitters.get(userId): 연결된 모든 emitter를 가져와 반복 전송emitter.send(...): SSE 형식으로 이벤트 전송 (name 필드는 클라이언트에서 이벤트 구분용)removeEmitter()를 통해 emitter를 제거하고, 리스트가 비면 Map에서도 제거true를 반환한다

