Reactive Programming ‐ WebFlux Patterns with Spring Boot - thought-corner/Backend-PlayGround GitHub Wiki

MVC 어노테이션 그대로 사용해 구성하는 WebFlux

dependencies {
    // Spring WebFlux 디펜던시 추가
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

데이터 유무에 따라 상태 체크를 통한 API Response 핸들링 패턴

@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()));
    }

}

서버 단방향 프로토콜 SSE(Servet-Sent Events) 개념과 구현

@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. 구현 단순, 인프라 친화적, 재연결 자동

실시간 스트림의 최종 형태 WebSocket과 소켓 수립 메커니즘

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);
}

Server To Client 실시간 스트림의 최종 형태 WebSocket

@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 전체를 종료시키는 구조이다.
⚠️ **GitHub.com Fallback** ⚠️