Spring WebFlux 주요 함수 완전정리: 실전 예제와 함께 배우는 리액티브 프로그래밍
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의 다양한 연산자와 함수형 라우팅, 코루틴 활용법을 익히면 실전 개발에서 더욱 강력한 리액티브 서버를 구현할 수 있습니다.