Spring WebFlux는 비동기 논블로킹 리액티브 프로그래밍을 위한 스프링의 핵심 모듈입니다. 본 포스트에서는 WebFlux의 주요 함수와 실전 예제를 초보자도 이해할 수 있도록 쉽게 설명합니다. Kotlin 기반의 전체 예제 코드와 함께, 실무에서 바로 쓸 수 있는 팁과 구조적 설명을 제공합니다.

WebFlux란 무엇인가?

Spring WebFlux는 스프링 5부터 도입된 리액티브 웹 프레임워크로, 비동기 논블로킹 방식의 서버를 구축할 수 있게 해줍니다. 기존 Spring MVC가 동기식 요청-응답에 기반했다면, WebFlux는 리액티브 스트림(Flux, Mono)을 활용해 데이터 흐름을 제어합니다. 이로 인해 적은 리소스로도 높은 동시성을 처리할 수 있어, 실시간 서비스나 대규모 트래픽 환경에 적합합니다.

주요 특징
  • 논블로킹(Non-blocking) 아키텍처
  • 리액티브 스트림 API(Flux, Mono) 기반
  • 함수형 엔드포인트 지원
  • 코루틴, 람다 등 현대적 프로그래밍 패러다임과의 높은 호환성

WebFlux의 주요 함수와 개념

1. Mono와 Flux
  • Mono<T>: 0 또는 1개의 데이터를 비동기적으로 처리할 때 사용합니다.
  • Flux<T>: 0개 이상의 데이터 스트림을 처리할 때 사용합니다.
val mono: Mono<String> = Mono.just("Hello")
val flux: Flux<Int> = Flux.just(1, 2, 3, 4, 5)
2. map, flatMap
  • map: 각 데이터에 동기적 변환 함수를 적용합니다.
  • flatMap: 내부에서 비동기 작업(예: 외부 API 호출, DB 조회 등)을 처리할 때 사용합니다.
// map 예시: 가격 할인 적용
val prices = Flux.just(1000, 2000, 3000)
val discounted = prices.map { it * 0.9 } // 10% 할인 적용

discounted.subscribe { println("할인된 가격: $it") }

// flatMap 예시: 사용자 ID로 DB에서 사용자 정보 조회(비동기)
data class User(val id: Int, val name: String)
fun findUserById(id: Int): Mono<User> = Mono.just(User(id, "User$id"))

val userIds = Flux.just(1, 2, 3)
val users = userIds.flatMap { findUserById(it) }

users.subscribe { println("조회된 사용자: $it") }

// flatMap의 비동기성 예시: 외부 API 호출
val keywords = Flux.just("spring", "webflux")
val results = keywords.flatMap { keyword ->
    WebClient.create()
        .get()
        .uri("https://api.example.com/search?q=$keyword")
        .retrieve()
        .bodyToMono(String::class.java)
}
results.subscribe { println("API 결과: $it") }
  • map은 단순 변환, flatMap은 비동기/다단계 변환에 사용합니다. 실제 서비스에서는 flatMap을 통해 DB, 외부 API 등 다양한 비동기 소스를 연결할 수 있습니다.
3. filter, take, skip
  • filter: 조건에 맞는 데이터만 통과시킵니다.
  • take: 앞에서부터 N개만 가져옵니다.
  • skip: 앞에서부터 N개를 건너뜁니다.
// filter 예시: 특정 조건(나이 20세 이상)만 필터링
val ages = Flux.just(15, 22, 19, 30, 25)
val adults = ages.filter { it >= 20 }
adults.subscribe { println("성인: $it") }

// take 예시: 상위 2개 인기 상품만 추출
val products = Flux.just("A", "B", "C", "D")
products.take(2).subscribe { println("인기 상품: $it") }

// skip 예시: 최근 2건을 제외한 나머지 이력만 조회
val history = Flux.just("2021", "2022", "2023", "2024")
history.skip(2).subscribe { println("이전 이력: $it") }

// filter + take + skip 조합 예시: 짝수 중 2개만, 첫 번째는 건너뜀
Flux.range(1, 10)
    .filter { it % 2 == 0 }
    .skip(1)
    .take(2)
    .subscribe { println("필터+스킵+테이크: $it") }
  • 실전에서는 조건 필터링, 페이징, 샘플링 등 다양한 데이터 흐름 제어에 활용합니다.
4. collectList, collectMap
  • collectList: Flux 전체 데이터를 List로 모아 Mono로 반환합니다.
  • collectMap: Flux를 Map으로 변환합니다.
// collectList 예시: 전체 주문 내역을 리스트로 취합
val orders = Flux.just("주문1", "주문2", "주문3")
orders.collectList().subscribe { println("전체 주문: $it") }

// collectMap 예시: 사용자 정보를 ID 기준으로 맵핑
val users = Flux.just(User(1, "Kim"), User(2, "Lee"), User(3, "Park"))
users.collectMap({ it.id }, { it.name })
    .subscribe { println("ID-이름 맵: $it") }

// collectList + flatMap: 외부 API 결과를 모두 수집 후 한 번에 처리
val keywords = Flux.just("spring", "webflux")
keywords.flatMap { keyword ->
    WebClient.create().get().uri("https://api.example.com/search?q=$keyword")
        .retrieve().bodyToMono(String::class.java)
}.collectList().subscribe { println("API 전체 결과: $it") }
  • collectList는 여러 데이터의 일괄 처리, collectMap은 데이터의 키-값 변환에 주로 사용합니다.
5. subscribe
  • 실제로 데이터를 소비(실행)할 때 사용합니다. subscribe는 테스트, 로깅, 최종 결과 처리 등 다양한 곳에서 활용합니다.
// subscribe 예시: 데이터 출력
Flux.just("A", "B", "C").subscribe { println("데이터: $it") }

// subscribe 예시: 에러 및 완료 처리
Flux.range(1, 3)
    .map { if (it == 2) throw RuntimeException("에러!") else it }
    .subscribe(
        { println("onNext: $it") },
        { error -> println("onError: ${error.message}") },
        { println("onComplete") }
    )

// 실전 팁: 서비스 코드에서는 subscribe를 직접 호출하기보다는, WebFlux가 내부적으로 처리함에 유의
  • subscribe는 디버깅이나 테스트에서 직접 사용하며, 실제 서비스에서는 주로 컨트롤러/핸들러가 자동으로 구독합니다.

실전: WebFlux Controller 전체 예제 (Kotlin)

아래는 WebFlux의 주요 함수를 모두 활용한 실전 예제입니다.

import org.springframework.web.bind.annotation.*
import org.springframework.web.reactive.function.client.WebClient
import org.springframework.stereotype.*
import reactor.core.publisher.*

@RestController
@RequestMapping("/api")
class SampleController {
    private val webClient = WebClient.create()

    @GetMapping("/hello")
    fun hello(): Mono<String> = Mono.just("Hello WebFlux!")

    @GetMapping("/numbers")
    fun numbers(): Flux<Int> = Flux.range(1, 10)

    @GetMapping("/double")
    fun doubleNumbers(): Flux<Int> = Flux.range(1, 10).map { it * 2 }

    @GetMapping("/filter")
    fun filterEven(): Flux<Int> = Flux.range(1, 10).filter { it % 2 == 0 }

    @GetMapping("/external")
    fun callExternal(): Mono<String> = webClient.get()
        .uri("https://api.github.com")
        .retrieve()
        .bodyToMono(String::class.java)
}
예제 설명
  • /hello: Mono로 단일 값 반환
  • /numbers: Flux로 1~10 반환
  • /double: map 함수로 2배 변환
  • /filter: filter 함수로 짝수만 반환
  • /external: 외부 API 호출 후 Mono로 결과 반환

주요 함수별 실전 활용법

map/flatMap 실전 활용
  • DB 조회, 외부 API 결과 변환 등 비동기 작업에 flatMap 사용
  • 단순 데이터 변환은 map 사용
filter/take/skip 실전 활용
  • 특정 조건에 맞는 데이터만 처리할 때 filter
  • 페이징, 샘플링 등에 take/skip 활용
collectList/collectMap 실전 활용
  • 결과를 리스트나 맵으로 변환하여 한 번에 처리

WebFlux 함수형 엔드포인트 예시

WebFlux는 어노테이션 기반 뿐 아니라 함수형 라우팅도 지원합니다.

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.server.*
import reactor.core.publisher.Mono

@Configuration
class RouterConfig {
    @Bean
    fun route(handler: SampleHandler) = coRouter {
        GET("/hello", handler::hello)
        GET("/numbers", handler::numbers)
    }
}

@Component
class SampleHandler {
    fun hello(request: ServerRequest): Mono<ServerResponse> =
        ServerResponse.ok().bodyValue("Hello WebFlux!")

    fun numbers(request: ServerRequest): Mono<ServerResponse> =
        ServerResponse.ok().bodyValue(listOf(1,2,3,4,5))
}
함수형 라우팅의 장점
  • 코드의 명확성
  • 테스트 용이성
  • 복잡한 조건 분기 처리에 유리

WebFlux에서의 예외 처리

WebFlux는 try-catch 대신 onErrorReturn, onErrorResume 등 연산자를 제공합니다.

flux.onErrorReturn(-1) // 에러 발생 시 -1 반환
flux.onErrorResume { e -> Flux.just(0) } // 에러 발생 시 대체 Flux 반환

WebFlux와 코루틴

Kotlin 코루틴을 활용하면 WebFlux의 비동기 코드를 더욱 간결하게 작성할 수 있습니다.

import kotlinx.coroutines.reactive.awaitFirstOrNull

suspend fun getData(): String? {
    return Mono.just("Hello").awaitFirstOrNull()
}

WebFlux 실전 팁

  • Controller, Handler, RouterConfig를 분리하여 구조화하면 유지보수에 유리
  • Mono/Flux 연산자 체이닝을 적극 활용
  • subscribe는 실제 서비스 코드에서는 직접 사용하지 않고, 스프링이 알아서 처리함
  • 블로킹 코드(예: Thread.sleep)는 절대 사용하지 말 것
  • 외부 API 연동 시 WebClient 적극 활용

자주 사용하는 연산자/함수 정리 표

함수명 설명 예시 코드
map 데이터 변환 .map { it * 2 }
flatMap 비동기 변환 .flatMap { ... }
filter 조건 필터링 .filter { it > 0 }
take 앞 N개만 .take(3)
skip 앞 N개 건너뜀 .skip(2)
collectList Flux → List 변환 .collectList()
collectMap Flux → Map 변환 .collectMap { it }
subscribe 데이터 소비(실행) .subscribe { println(it) }
onErrorReturn 에러 발생 시 대체값 반환 .onErrorReturn(-1)
onErrorResume 에러 발생 시 대체 Flux/Mono .onErrorResume { ... }

결론 및 도움말

Spring WebFlux는 초보자에게 다소 생소할 수 있지만, 주요 함수의 역할과 예제를 반복해서 실습하면 금방 익숙해질 수 있습니다. 실제 서비스에서는 코드 구조화와 예외 처리에 신경 쓰고, 공식 문서와 다양한 샘플 코드를 참고하면 큰 도움이 됩니다. WebFlux의 다양한 연산자와 함수형 라우팅, 코루틴 활용법을 익히면 실전 개발에서 더욱 강력한 리액티브 서버를 구현할 수 있습니다.

레퍼런스