미니 프로젝트 - SSE 초기 세팅 및 테스트

Zyoon·2025년 7월 31일

미니프로젝트

목록 보기
27/35
post-thumbnail

📘SSE 연결 및 메시지 전송 테스트까지 단계별 구현


SSE(Server-Sent Events) 전송 방법 정리

SSE는 서버에서 클라이언트로 단방향으로 데이터를 지속적으로 전송할 수 있는 기술이다. 주로 실시간 알림, 주식 정보, 로그 스트림 등과 같이 지속적으로 서버에서 클라이언트로 이벤트를 전달해야 하는 상황에서 사용된다.

기본 개념

  • 단방향 통신: 클라이언트에서 서버로 연결을 요청하면, 이후엔 서버에서 클라이언트로만 데이터를 전송할 수 있다.
  • 지속적인 연결 유지: 클라이언트가 연결을 요청하면 서버는 해당 연결을 끊지 않고 계속 유지한 채 대기 상태에 들어간다.
  • 서버에서 전송: 서버는 필요 시 이 열린 연결을 통해 클라이언트로 데이터를 푸시(push)한다.

초기 세팅

SSE 기능을 사용하려면 Spring MVC의 웹 기능만 있으면 된다.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

Connect 추가

클라이언트가 서버에 SSE 연결을 요청하면, 서버는 SseEmitter를 반환하여 이벤트 스트림을 열어준다.

이때 로그인된 사용자 정보를 통해 사용자별 연결을 식별할 수 있도록 구현해준다.

Controller

@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

@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를 생성하고 사용자별로 저장한다.
  • 연결이 종료되거나 오류가 발생할 경우 emitter를 정리하여 리소스를 관리한다.

전송 로직 추가 (테스트용)

SSE 연결이 정상적으로 이루어졌는지 확인하기 위해, 간단한 전송 테스트용 API를 만든다.

지정한 사용자에게 메시지를 전송하고, 실제 연결된 경우에만 성공 여부를 반환한다.

Controller

//전송 확인용 테스트 코드
@PostMapping("/sse/send")
public boolean send(@RequestBody SseMessageDto dto) {
	return sseService.sendIfConnected(dto);
}
  • SseMessageDto: 메시지 수신자와 내용 정보를 담은 DTO
  • sendIfConnected(...): 대상 사용자에게 SSE로 메시지를 전송하고, 결과를 boolean으로 반환한다

Service

사용자별로 저장된 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;
}

Emitter 제거

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를 반환한다

포스트맨 테스트

Connect

  • 먼저 로그인을 한 뒤, 연결 요청시 대기 상태가 된다

전송

  • 전송 요청을 보낸다. 이때, userId 는 연결 요청을 했던 user의 식별자 번호로 보내주어야 한다.
  • body 에 true 가 뜨면 전송 성공

전송 확인

  • Connect 요청을 보냈던 곳에서 확인가능하다.
  • 전송요청을 보냈던 메세지가 보이면 성공이다.
  • 여기까지가 SSE 기본 통신 설정이다.

profile
기어 올라가는 개발

0개의 댓글