프로그래밍 PROGRAMMING/자바 JAVA AND FRAMEWORKS

Spring WebFlux + R2DBC 리액티브 DB 연동하기

매운할라피뇨 2026. 3. 21. 09:30
반응형

개요

Spring WebFlux와 R2DBC를 함께 사용하면 논블로킹(Non-blocking) I/O 기반의 완전한 리액티브 데이터베이스 파이프라인을 구성할 수 있습니다. 기존 Spring MVC + JPA 조합은 스레드를 블로킹하는 방식이어서 높은 동시 접속 환경에서 성능 병목이 발생하기 쉬운 반면, WebFlux + R2DBC 조합은 적은 스레드로 더 많은 요청을 처리할 수 있습니다. 이 글에서는 의존성 설정부터 Repository 구현, 트랜잭션 처리까지 실제 프로젝트에 적용할 수 있는 수준으로 단계별로 살펴보겠습니다.


의존성 설정 및 데이터소스 구성

R2DBC는 관계형 데이터베이스를 위한 리액티브 드라이버 표준 사양입니다. JDBC가 스레드를 블로킹하는 것과 달리, R2DBC는 비동기 방식으로 쿼리를 실행하고 결과를 Mono / Flux 스트림으로 반환합니다.

아래는 PostgreSQL을 대상으로 한 build.gradle 의존성 구성입니다.

// build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
    runtimeOnly 'org.postgresql:r2dbc-postgresql'   // PostgreSQL R2DBC 드라이버
    runtimeOnly 'org.flywaydb:flyway-core'           // 스키마 마이그레이션 (선택)
}

application.yml에서 R2DBC URL 형식(r2dbc:)을 사용해야 하는 점에 주의하세요. 기존 JDBC URL과 혼용하면 자동 구성이 실패합니다.

# application.yml
spring:
  r2dbc:
    url: r2dbc:postgresql://localhost:5432/mydb
    username: myuser
    password: mypassword
  sql:
    init:
      mode: always   # 애플리케이션 시작 시 schema.sql 자동 실행

도메인 모델과 리액티브 Repository 구현

Spring Data R2DBC는 ReactiveCrudRepository를 지원하므로, JPA와 유사한 선언적 방식으로 리포지터리를 작성할 수 있습니다. 단, JPA의 @Entity가 아닌 Spring Data의 @Table / @Id 어노테이션을 사용합니다.

// Article.java — 도메인 모델
@Table("articles")
public class Article {

    @Id
    private Long id;

    private String title;
    private String content;

    @Column("author_id")
    private Long authorId;

    // 생성자, getter, setter 생략
}
// ArticleRepository.java — 리액티브 리포지터리
public interface ArticleRepository extends ReactiveCrudRepository<Article, Long> {

    // authorId로 게시글 목록 조회 — Flux로 여러 건 반환
    Flux<Article> findByAuthorId(Long authorId);

    // 제목 키워드 검색 — @Query로 커스텀 쿼리 작성 가능
    @Query("SELECT * FROM articles WHERE title ILIKE '%' || :keyword || '%'")
    Flux<Article> searchByTitle(String keyword);
}

메서드 네이밍 기반 쿼리 생성과 @Query 어노테이션 모두 지원됩니다. 반환 타입을 Flux<T> (복수) 또는 Mono<T> (단건)으로 선언하기만 하면 Spring Data R2DBC가 자동으로 처리합니다.


WebFlux 컨트롤러와 서비스 계층 연결

WebFlux에서는 @RestController@GetMapping 등의 어노테이션을 그대로 사용하지만, 반환 타입을 Mono / Flux로 선언해야 논블로킹 파이프라인이 완성됩니다. subscribe()를 직접 호출하지 않고 스트림을 그대로 반환하는 것이 핵심입니다.

// ArticleService.java
@Service
@RequiredArgsConstructor
public class ArticleService {

    private final ArticleRepository articleRepository;

    // 신규 게시글 저장 — Mono<Article> 반환
    @Transactional
    public Mono<Article> create(Article article) {
        return articleRepository.save(article);
    }

    // 키워드 검색 — Flux<Article> 반환
    public Flux<Article> search(String keyword) {
        return articleRepository.searchByTitle(keyword)
                .switchIfEmpty(Flux.error(new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "검색 결과가 없습니다.")));
    }
}
// ArticleController.java
@RestController
@RequestMapping("/articles")
@RequiredArgsConstructor
public class ArticleController {

    private final ArticleService articleService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<Article> create(@RequestBody Article article) {
        return articleService.create(article);  // 블로킹 없이 그대로 반환
    }

    @GetMapping("/search")
    public Flux<Article> search(@RequestParam String keyword) {
        return articleService.search(keyword);
    }
}

실행 결과 예시 (GET /articles/search?keyword=spring):

[
  { "id": 1, "title": "Spring WebFlux 시작하기", "authorId": 42 },
  { "id": 7, "title": "Spring R2DBC 심화 활용", "authorId": 15 }
]

Flux를 반환하면 WebFlux가 자동으로 JSON 배열로 직렬화하며, 스트리밍 응답(text/event-stream)도 동일한 방식으로 처리할 수 있습니다.


리액티브 트랜잭션 처리 시 주의점

R2DBC에서 트랜잭션은 @Transactional을 사용할 수 있지만, 반드시 리액티브 트랜잭션 매니저(R2dbcTransactionManager)가 빈으로 등록되어야 합니다. Spring Boot 자동 구성이 이를 처리해 주지만, 커스텀 데이터소스를 사용할 경우 직접 등록이 필요합니다.

// TransactionConfig.java — 커스텀 데이터소스 사용 시
@Configuration
public class TransactionConfig {

    @Bean
    public ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) {
        return new R2dbcTransactionManager(connectionFactory);
    }
}

트랜잭션 범위 안에서 여러 R2DBC 연산을 묶으려면 flatMap으로 체이닝해야 합니다. zip이나 merge로 병렬 실행할 경우 트랜잭션 컨텍스트가 전파되지 않을 수 있으므로 주의가 필요합니다.

@Transactional
public Mono<Void> transferAndLog(Long fromId, Long toId, int amount) {
    return articleRepository.findById(fromId)
            .flatMap(from -> {
                from.setCount(from.getCount() - amount);
                return articleRepository.save(from);
            })
            .flatMap(saved -> articleRepository.findById(toId))
            .flatMap(to -> {
                to.setCount(to.getCount() + amount);
                return articleRepository.save(to);
            })
            .then(); // Mono<Void> 반환
}

맺음말

Spring WebFlux와 R2DBC의 조합은 높은 동시성이 요구되는 API 서버나 실시간 데이터 처리 환경에서 효과적입니다. 특히 CPU 집약적인 연산보다 I/O 대기 시간이 긴 서비스(대용량 조회, 외부 API 연동 등)일수록 리액티브 스택의 이점이 두드러집니다.

다만 JPA의 지연 로딩(Lazy Loading)이나 복잡한 객체 그래프 매핑은 R2DBC에서 지원하지 않으므로, 연관 관계가 복잡한 도메인에서는 설계 전략을 별도로 고려해야 합니다. @OneToMany 관계를 직접 처리하는 대신, 서비스 계층에서 flatMap으로 조합하거나 CQRS 패턴을 적용하는 방식이 현업에서 자주 활용됩니다.

다음 단계로는 Spring Data R2DBC 공식 문서R2DBC 공식 사양을 참고하시면 커스텀 컨버터, 감사(Auditing), 페이징 처리까지 확장할 수 있습니다.

반응형