Reactive Programming ‐ WebFlux Patterns with Spring Boot - thought-corner/Backend-PlayGround GitHub Wiki
dependencies {
// Spring WebFlux 디펜던시 추가
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}@RestController
public class ProductController {
@GetMapping("/products/first")
public Mono<Product> getFirst() {
return Mono.just(new Product("1", "노트북", 1500000));
}
@GetMapping("/products")
public Flux<Product> getAll() {
return Flux.just(
new Product("1", "노트북", 1500000),
new Product("2", "마우스", 50000),
new Product("3", "키보드", 120000));
}
@GetMapping("/products/available")
public Mono<ResponseEntity<Product>> getAvailable() {
// 예시로 DB를 조회하고, 여기서 재고가 있으면 true, 없다면 false
boolean inStock = true;
Mono<Product> found = inStock
? Mono.just(new Product("1", "노트북", 1500))
: Mono.empty();
return found
.map(ResponseEntity::ok)
.switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
}
}@RestController
public class SseController {
@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamEvents() {
return Flux.interval(Duration.ofSeconds(1)).map(tick -> "이벤트 #" + tick);
}
/*
* 1. data : 보내는 데이터
* 2. id : 이벤트에 대한 고유 ID ( 클라이언트 재연결 -> Last Event ID 헤더로 전달)
* 3. event : 이벤트 타입에 대한 이름
* 4. retry : 재연결에 대한 대기 시간
*/
}
1. 프로토콜과 연결 구조
- SSE는 일반 HTTP 위에서 동작한다.
- 클라이언트가 요청을 보내면 서버가 연결을 닫지 않고
text/event-stream형식으로 데이터를 계속 흘려보내는 구조이다. - HTTP를 그대로 쓰기 때문에 기존 로드밸런서, 인증 미들웨어, CORS 설정이 추가 작업 없이 동작한다.
- 반면 WebSocket은 처음에 HTTP로 핸드셰이크를 한 뒤
ws://또는wss://프로토콜로 업그레이드한다. - 이후부터는 HTTP가 아닌 별도 프레임 구조로 통신하며 연결이 유지되는 동안 서버와 클라이언트가 언제든 자유롭게 메시지를 주고받을 수 있다.
2. 데이터 흐름 방향
- SSE는 서버 → 클라이언트 단방향이다. 클라이언트가 서버에 무언가를 보내려면 별도의 HTTP 요청을 새로 만들어야 한다.
- 반면 WebSocket은 양방향이다. 하나의 연결 위에서 서버와 클라이언트가 동시에 메시지를 주고받을 수 있다.
3. 재연결과 안정성
- SSE는 브라우저가 자동 재연결을 처리한다. 연결이 끊어지면 브라우저가 알아서 다시 연결하고
Last-Event-ID헤더로 마지막으로 받은 이벤트 이후부터 이어받을 수도 있다. - 반면 WebSocket은 재연결 로직을 직접 구현해야 한다. 끊김 감지(ping/pong), 재연결 시도, 백오프 전략 등을 애플리케이션 레벨에서 처리해야 하므로 코드가 복잡해진다.
4. 구독 제어와 멀티플렉싱
- SSE는 하나의 연결이 하나의 스트림이다.
- 구독 대상을 바꾸려면 연결을 끊고 새로운 요청을 보내야 한다. 다만 HTTP/2 환경이면 하나의 TCP 연결 위에 여러 SSE 스트림을 다중화할 수 있어 연결 제한 수 문제가 사라진다.
- 반면 WebSocket은 하나의 연결에서 메시지 타입을 구분해 여러 채널을 논리적으로 운용할 수 있다.
- 예를 들어,
{"type": "subscribe", "symbol": "005930"}와 같은 메시지를 서버로 보내 구독 목록을 실시간으로 변경할 수 있다.
5. Spring WebFlux 구현
- SSE는
Flux<ServerSentEvent>를 반환하는 컨트롤러로 간단하게 구현할 수 있다. - 반면 WebSocket은
WebSocketHandler인터페이스를 구현하고WebSocketHandlerAdapter를 등록해야 하며, 세션 관리와 에러 핸들링 등 추가 작성이 필요하다.
그래서 선택 기준을 요약한다면? -
클라이언트가 서버에 제어 신호를 보내야 하는가?
- YES - WebSocket. 양방향 제어, 동적 구독, 저지연 인터랙션
- NO - SSE. 구현 단순, 인프라 친화적, 재연결 자동
public interface WebSocketHandler {
/**
* 이 핸들러가 지원하는 서브 프로토콜 목록을 반환합니다.
* 핸드셰이크 시 클라이언트가 요청한 서브 프로토콜과 협상하는 데 사용됩니다.
* 기본값은 빈 리스트(서브 프로토콜 미사용)입니다.
*
* @return 지원하는 서브 프로토콜 목록
*/
default List<String> getSubProtocols() {
return Collections.emptyList();
}
/**
* WebSocket 세션을 처리합니다.
* 반환된 {@link Mono}가 완료되면 세션이 종료됩니다.
* 에러로 완료될 경우 세션은 비정상 종료됩니다.
*
* @param session 연결된 WebSocket 세션
* @return 세션 처리가 완료되면 완료되는 {@link Mono}
*/
Mono<Void> handle(WebSocketSession session);
}@Component
public class TimeWebSocketHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
// 클라이언트로 1초마다 시간 전송
Flux<WebSocketMessage> serverToClient = Flux.interval(Duration.ofSeconds(1))
.map(tick -> session.textMessage("서버 시간 : " + LocalDateTime.now().toString()));
// 클라이언트에서 오는 메시지도 수신(소비만 하고, 종료 신호를 전달)
Mono<Void> clientToServer = session.receive()
.doOnNext(msg -> System.out.println("클라이언트: " + msg.getPayloadAsText()))
.then();
// 두 스트림을 함께 실행
return Mono.zip(session.send(serverToClient), clientToServer).then();
/*
* 1. session.send -> Flux.interval 기반, 무한 스트림, 자체적으로 완료되지는 않음
* 2. clientToServer -> session.receive() 기반, 클라이언트가 끊을 떄 완료가 됨
*/
}
}
- Phase 1 : HTTP 핸드셰이크로 WebSocket 수립. TCP 연결 후 WS 프로토콜로 전환
- Phase 2 :
Mono.zip으로 send/receive를 묶는 부분이 핵심인데send()는 무한 스트림이라 클라이언트가 끊어도 혼자서는 완료를 모르고,receive()가 완료 신호를 받아서 zip 전체를 종료시키는 구조이다.