BackEnd/Spring WebFlux

Spring WebFlux에서 MySQL을 사용하는 방법: JDBC vs R2DBC 완전 정리

Raconer 2025. 4. 28. 23:54
728x90

Spring WebFlux를 사용할 때 가장 헷갈리는 부분이 있다. 바로 "DB 연결은 어떻게 하지?" "JPA를 쓸 수는 없을까?" 하는 문제다.

이 글에서는 WebFlux에서 MySQL을 어떻게 연결하고, R2DBC를 어떻게 사용하는지, 그리고 JPA를 왜 사용할 수 없는지를 순서대로, 쉽게 정리한다.


1. WebFlux에서 DB(MySQL) 연결 방법

Spring WebFlux는 기본적으로 Non-Blocking 처리를 목표로 한다. 그렇기 때문에 데이터베이스 연결 역시 Non-Blocking 드라이버를 사용해야 성능을 제대로 발휘할 수 있다.

✅ WebFlux에서 MySQL을 연결하려면 R2DBC 드라이버를 사용해야 한다.

1.1 필요한 의존성 추가 (Gradle)

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
    implementation("dev.miku:r2dbc-mysql")
}

1.2 application.yml 설정 예시

spring:
  r2dbc:
    url: r2dbc:mysql://localhost:3306/your_database
    username: your_username
    password: your_password
  • JDBC처럼 jdbc:mysql://이 아니라 r2dbc:mysql://로 시작해야 한다.

2. R2DBC 사용법 (쉬운 예시)

R2DBC(Reactive Relational Database Connectivity)는 Non-Blocking 방식으로 데이터베이스를 사용할 수 있게 해주는 기술이다.

2.1 엔티티(Entity) 정의

import org.springframework.data.annotation.Id
import org.springframework.data.relational.core.mapping.Table

@Table("users")
data class User(
    @Id val id: Long? = null,
    val name: String,
    val email: String
)

2.2 Repository 작성

import org.springframework.data.repository.reactive.ReactiveCrudRepository

interface UserRepository : ReactiveCrudRepository<User, Long> {
    fun findByName(name: String): Flux<User>
}
  • ReactiveCrudRepository를 상속받아 기본적인 CRUD 및 쿼리 메소드를 사용할 수 있다.

2.3 Controller 작성

import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux

@RestController
@RequestMapping("/users")
class UserController(private val userRepository: UserRepository) {

    @GetMapping
    fun getAllUsers(): Flux<User> = userRepository.findAll()

    @GetMapping("/{name}")
    fun getUserByName(@PathVariable name: String): Flux<User> = userRepository.findByName(name)
}
  • 데이터는 Flux나 Mono로 반환해야 한다.

✅ WebFlux 환경에서 비동기적으로 데이터를 가져올 수 있게 된다.


3. 복잡한 쿼리 작성 (R2dbcEntityTemplate 사용)

R2dbcEntityTemplate를 사용하면 단순 조회 외에 다양한 조건을 가진 쿼리나 복잡한 조인(Join)도 처리할 수 있다.

3.1 기본 조건 검색 예시

@Service
class UserService(private val template: R2dbcEntityTemplate) {

    fun findUsersByName(name: String): Flux<User> {
        return template.select(User::class.java)
            .matching(Query.query(Criteria.where("name").`is`(name)))
            .all()
    }
}

3.2 LEFT JOIN 예시 (DatabaseClient 사용)

R2dbcEntityTemplate는 복잡한 JOIN 쿼리를 메소드 체인으로 만들기 어렵기 때문에, Custom Query를 직접 작성해야 한다.

import io.r2dbc.spi.Row
import io.r2dbc.spi.RowMetadata
import org.springframework.r2dbc.core.DatabaseClient
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux

data class UserOrderDTO(
    val userId: Long,
    val userName: String,
    val orderId: Long?,
    val orderName: String?
)

@Service
class UserOrderService(private val client: DatabaseClient) {

    fun findUsersWithOrders(): Flux<UserOrderDTO> {
        val query = """
            SELECT u.id AS userId, u.name AS userName, o.id AS orderId, o.name AS orderName
            FROM users u
            LEFT JOIN orders o ON u.id = o.user_id
        """

        return client.sql(query)
            .map { row: Row, _: RowMetadata ->
                UserOrderDTO(
                    userId = row.get("userId", java.lang.Long::class.java) ?: 0L,
                    userName = row.get("userName", String::class.java) ?: "",
                    orderId = row.get("orderId", java.lang.Long::class.java),
                    orderName = row.get("orderName", String::class.java)
                )
            }
            .all()
    }
}

✅ 복잡한 JOIN, GROUP BY 쿼리도 DatabaseClient로 직접 SQL 작성해서 처리할 수 있다.

3.3 특정 컬럼만 조회 예시

만약 전체 엔티티가 아니라 특정 컬럼만 조회하고 싶을 때도 DatabaseClient를 사용하면 된다.

data class UserNameDTO(
    val name: String
)

@Service
class UserNameService(private val client: DatabaseClient) {

    fun findAllUserNames(): Flux<UserNameDTO> {
        val query = "SELECT name FROM users"

        return client.sql(query)
            .map { row: Row, _: RowMetadata ->
                UserNameDTO(
                    name = row.get("name", String::class.java) ?: ""
                )
            }
            .all()
    }
}

✅ DTO로 매핑해서 필요한 컬럼만 깔끔하게 받아올 수 있다.


4. WebFlux에서는 왜 JPA를 사용할 수 없을까?

❌ WebFlux에서는 JPA를 사용할 수 없다.

이유 정리

  • JPA는 Hibernate 같은 ORM 위에서 동작한다.
  • Hibernate는 Blocking 방식으로 설계되어 있다.
  • 즉, 데이터베이스 작업 중에 쓰레드를 점유해서 WebFlux의 Non-Blocking 특성과 충돌한다.

차이 비교

항목JPAR2DBC

처리 방식 Blocking Non-Blocking
Lazy Loading 지원 미지원
트랜잭션 관리 @Transactional TransactionalOperator 사용
연관 관계 매핑 복잡한 관계 가능 (OneToMany, ManyToOne 등) 단순 매핑만 지원 (JOIN 직접 처리 필요)
사용 환경 Spring MVC Spring WebFlux

✅ 정리하면, WebFlux에서는 Spring Data R2DBC를 통해 DB 접근을 해야 한다.


5. 최종 요약

질문답변

WebFlux에서 MySQL 사용 가능? 가능하다 (단, R2DBC 드라이버 필요)
JPA 사용 가능? 불가능하다 (Blocking 구조 때문)
대체 기술은? Spring Data R2DBC + DatabaseClient 사용
ORM처럼 사용 가능한가? 부분적으로 가능하지만, 복잡한 매핑은 직접 처리해야 함

WebFlux + R2DBC를 올바르게 구성하면 Non-Blocking 시스템을 완성할 수 있다.


다음 글에서는 WebFlux + R2DBC 트랜잭션 처리 방법 (TransactionalOperator 적용법) 을 자세히 정리할 예정이다. 🚀

 

참고

https://github.com/Raconer/SpringWebFlux

728x90