[Java] WebFlux 정리 (Spring MVC 와 차이, Java 비동기 기술들, Mono/Flux 차이)
비동기 처리할때 사용하는 WebFlux 에 대해서 정리해보려 한다 ! 써봤는데 남들에게 설명해줄 수준은 아니라 .. 정리해보기
전통적인 Spring MVC와의 차이
종류 | Spring | WebFlux |
프로그래밍 방식 | 동기 (블로킹) | 비동기 (논블로킹) |
기반 API | Servlet API | Reactive Streams (Project Reactor) |
스레드 사용 | 요청당 스레드 하나 | 적은 수의 스레드로 수많은 요청 처리 |
Return 타입 | String, Model, ResponseEntity | Mono<T>, Flux<T> |
기존에도 자바에는 Thread 나 Virtual Thread 와 같은 비동기 처리들이 있는데 이들과는 어떤 차이가 있는지 알아보장
내가 이해한 바로는
WebFlux는 프로젝트 전체가 비동기로 돌아가는 거고,
Thread나 Virtual Thread는 동기로 짜여진 코드 중 일부분만 비동기로 만드는 방식
이렇게 이해했다 (다음에는 Virtual Thread 써봐야지 우히히)
간단한 CRUD 구현해보면서 좀 더 알아보장
Dependencies
dependencies {
// 나중에 redis 공부용
// implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
// 핵심 ! 비동기 web api 용 !!
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// JSON 역직렬화 용
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin'
// Kotlin 코루틴 + Reactor 통합
implementation "io.projectreactor.kotlin:reactor-kotlin-extensions"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactor"
implementation 'org.jetbrains.kotlin:kotlin-reflect'
// R2DBC - 리액티브 DB 연동
implementation "org.springframework.boot:spring-boot-starter-data-r2dbc"
implementation "org.postgresql:r2dbc-postgresql" // PostgreSQL 리액티브 드라이버
implementation("org.springframework.boot:spring-boot-starter-validation")
// runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
여기서 눈에 띄는것은
// 핵심 ! 비동기 web api 용 !!
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// R2DBC - 리액티브 DB 연동
implementation "org.springframework.boot:spring-boot-starter-data-r2dbc"
implementation "org.postgresql:r2dbc-postgresql" // PostgreSQL 리액티브 드라이버
이녀석들 !
WebFlux 에서는 JPA 를 사용할 수 없다 대신 R2DBC 를 사용하게 된다
아래와 같은 차이가 있음 !
WebFlux | JPA |
완전 비동기 논블로킹 | 동기 + 블로킹 |
Reactor (Mono, Flux) 기반 | JDBC 기반, 스레드를 점유함 |
Netty (비동기 서버) 사용 | Tomcat처럼 동기 서블릿 환경이 기본 |
Reactor 이랑 JDBC 의 차이는 다음 게시글에서 정리하겠음 ! 일단은 JPA 를 사용하지 않는 것만 알아주자
(사실 기술적으로는 가능하지만 매우매우 비추비추천 ! 데드락 발생가능성이 있뜸)
application.yaml
spring:
r2dbc:
url: r2dbc:postgresql://localhost:45432/db_name
username: jungry
password: jungry!
jdbc 를 사용할때랑 연결 방식도 살짝 다르다 !
entity
Board.kt
@Table("test_board")
data class Board(
@Id // R2DBC 는 알아서 시퀀스 처리함 똑똑 boy
val id: Int? = null,
val title: String,
val content: String,
)
간단한 테이블을 만들어서 엔티티를 정의해줬다 (PostgreSql 사용했뜸)
repository
@Repository
interface BoardRepository: ReactiveCrudRepository<Board, Long>
JPA 를 주로 쓰는 사람들에게는 낯설면서 익숙한 구조일것같다
R2DBC 에서는 ReactiveCrudRepository 를 사용할 수 있다 !! 동작은 JpaRepository 랑 거의 똑같다고 생각하고 개발하면 된다
findByTitle 등 커스텀하게도사용이 가능하니까 기존 Jpa 사용하던 사람들한테는 크게 어렵지 않을듯
Controller + DTO
@RestController
@RequestMapping("/board")
class BoardController(
private val boardService: BoardService
) {
@PostMapping
fun createBoard(@RequestBody request: CreateBoardRequest): Mono<BoardResponse> {
return boardService.createBoard(request)
}
@GetMapping
fun getAllBoards(): Flux<BoardResponse> {
return boardService.getAllBoards()
}
}
data class CreateBoardRequest(
@field:NotBlank(message = "title required")
val title: String,
@field:NotBlank(message = "content required")
val content: String
)
data class BoardResponse(
val id: Long,
val title: String,
val content: String
)
여기서 특이하게 Mono / Flux 개념이 나온다
간단하게는 단일 데이터 : Mono , 여러 데이터 : Flux 이다
상황 | 타입 |
단일 데이터 | Mono<T> |
여러 데이터 (리스트, 스트림) | Flux<T> |
결과가 있을 수도, 없을 수도 있는 경우 | Mono<T> (없는 경우 Mono.empty() 반환) |
어렵지 않음 !!!
Service
@Service
class BoardService(
private val boardRepository: BoardRepository
) {
fun createBoard(request: CreateBoardRequest): Mono<BoardResponse> {
val board = Board(title = request.title, content = request.content)
return boardRepository.save(board)
.map {
saved -> BoardResponse(
id = saved.id!!,
title = saved.title,
content = saved.content
)
}
}
fun getAllBoards(): Flux<BoardResponse> {
return boardRepository.findAll()
.map {
board -> BoardResponse(
id = board.id!!,
title = board.title,
content = board.content
)
}
}
}
서비스는 이렇게 구성되어있다
repository 에서 가져온 후 map 으로 response 형태에 맞게 만들어주면 된다 !!
실제 비동기로 처리되는지 확인하기
요청을 우다다 보내서 확인해보자
fun getAllBoards(): Flux<BoardResponse> {
log.info("getAllBoards 호출됨, thread=${Thread.currentThread().name}")
return boardRepository.findAll()
.doOnNext{
log.info("게시글 조회됨 (id=${it.id}), thread=${Thread.currentThread().name}")
}
.map {
board -> BoardResponse(
id = board.id!!,
title = board.title,
content = board.content
)
}
}
일단 로그 추가해줬다
터미널에서
curl http://localhost:8080/board &
curl http://localhost:8080/board &
curl http://localhost:8080/board &
이렇게 요청을 n 개 보내자
2025-04-16T16:02:57.090+09:00 INFO 87747 --- [webflux-study] [ctor-http-nio-6] c.e.t.w.Board.service.BoardService : getAllBoards 호출됨, thread=reactor-http-nio-6
2025-04-16T16:02:57.090+09:00 INFO 87747 --- [webflux-study] [ctor-http-nio-5] c.e.t.w.Board.service.BoardService : getAllBoards 호출됨, thread=reactor-http-nio-5
2025-04-16T16:02:57.090+09:00 INFO 87747 --- [webflux-study] [ctor-http-nio-7] c.e.t.w.Board.service.BoardService : getAllBoards 호출됨, thread=reactor-http-nio-7
2025-04-16T16:02:57.533+09:00 INFO 87747 --- [webflux-study] [actor-tcp-nio-1] c.e.t.w.Board.service.BoardService : 게시글 조회됨 (id=1), thread=reactor-tcp-nio-1
2025-04-16T16:02:57.540+09:00 INFO 87747 --- [webflux-study] [ctor-http-nio-6] c.e.t.w.Board.service.BoardService : 게시글 조회됨 (id=2), thread=reactor-http-nio-6
2025-04-16T16:02:57.549+09:00 INFO 87747 --- [webflux-study] [actor-tcp-nio-1] c.e.t.w.Board.service.BoardService : 게시글 조회됨 (id=1), thread=reactor-tcp-nio-1
2025-04-16T16:02:57.550+09:00 INFO 87747 --- [webflux-study] [ctor-http-nio-7] c.e.t.w.Board.service.BoardService : 게시글 조회됨 (id=2), thread=reactor-http-nio-7
2025-04-16T16:02:57.562+09:00 INFO 87747 --- [webflux-study] [actor-tcp-nio-1] c.e.t.w.Board.service.BoardService : 게시글 조회됨 (id=1), thread=reactor-tcp-nio-1
2025-04-16T16:02:57.562+09:00 INFO 87747 --- [webflux-study] [ctor-http-nio-5] c.e.t.w.Board.service.BoardService : 게시글 조회됨 (id=2), thread=reactor-http-nio-5
각 요청마다 쓰레드가 다른것을 확인할 수 있다 !!!
- 요청마다 다른 HTTP 스레드 (http-nio-5, 6, 7)
- R2DBC를 통한 논블로킹 DB 작업 (tcp-nio-1)
잘 처리가 되는것을 알 수 있다