JAVA/STUDY

[Java] WebFlux 정리 (Spring MVC 와 차이, Java 비동기 기술들, Mono/Flux 차이)

Jungry_ 2025. 4. 16. 16:04
반응형

 비동기 처리할때 사용하는 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)

잘 처리가 되는것을 알 수 있다

반응형