Spring Data R2DBC와 H2로 만드는 완전 리액티브 CRUD REST API
1. 왜 R2DBC인가? — 동기 JDBC의 한계를 넘어서
전통적인 JDBC는 블로킹 모델이다. 데이터베이스 I/O 가 끝날 때까지 현재 스레드가 점유되어, 트래픽이 급증하면 스레드 수만큼 동시성이 제한된다.
R2DBC(Reactive Relational Database Connectivity) 는 아예 처음부터 리액티브 프로그래밍을 위해 설계된 비동기 SPI다. 드라이버가 non-blocking I/O 를 사용하므로, 동일한 하드웨어 자원으로 훨씬 많은 동시 요청을 처리할 수 있다. Spring 생태계에서는 Spring Data R2DBC 모듈이 Reactor (Mono/Flux) 기반 API 와 리포지토리 추상화를 제공해 준다.
R2DBC ≠ JPA
R2DBC는 ORM이 아니다. 지연 로딩·엔티티 그래프·캐시와 같은 JPA 기능 대신 “SQL → 객체” 매핑에 초점을 둔다. 따라서 단순 CRUD 또는 고동시성 read 모델이라면 가볍고 빠르다.
2. 미리 알아둘 Reactor 3분 가이드
타입 의미 대표 시나리오
Mono<T> | 0 또는 1 개의 비동기 결과 | PK 로 단건 조회 |
Flux<T> | 0 ~ N 개의 결과 스트림 | 목록/검색 |
- Publisher는 구독(subscribe) 될 때까지 실행되지 않는 Cold Stream.
- Spring WebFlux가 컨트롤러 반환 Mono/Flux를 자동으로 subscribe → JSON 직렬화하여 응답한다.
3. 프로젝트 셋업 — Spring Boot 3.4.5 + WebFlux + R2DBC + H2
3-1. 의존성 추가 (Gradle 예시)
plugins {
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
runtimeOnly 'io.r2dbc:r2dbc-h2'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
}
2025-04 기준 GA 버전은 3.4.5이며, 3.5 RC 버전이 발표 준비 중이다.
3-2. H2 R2DBC URL & 스키마
application.properties
spring.r2dbc.url=r2dbc:h2:mem:///todo_db?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.r2dbc.username=sa
spring.r2dbc.password=
spring.r2dbc.initialization-mode=always
schema.sql
CREATE TABLE IF NOT EXISTS todo (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100),
description VARCHAR(255),
completed BOOLEAN
);
DB_CLOSE_DELAY=-1 옵션으로 애플리케이션 구동 중 메모리 DB 데이터가 사라지지 않도록 했다.
4. 도메인 코드 구현
4-1. 엔티티
@Table("todo")
public class Todo {
@Id
private Long id;
private String title;
private String description;
private boolean completed;
public Todo() {}
public Todo(String title, String description, boolean completed) {
this.title = title;
this.description = description;
this.completed = completed;
}
/* getter/setter 생략 */
}
@Entity 대신 @Table, @Column(선택) 애노테이션을 사용한다. 지연 로딩 등 JPA 기능은 존재하지 않는다.
4-2. 리포지토리
public interface TodoRepository
extends ReactiveCrudRepository<Todo, Long> {
// 커스텀 예시
Flux<Todo> findByCompleted(boolean completed);
}
ReactiveCrudRepository 덕분에 findAll(), findById(), save(), deleteById() 등을 바로 사용한다.
4-3. 서비스
@Service
public class TodoService {
private final TodoRepository repo;
public TodoService(TodoRepository repo) { this.repo = repo; }
public Flux<Todo> findAll() { return repo.findAll(); }
public Mono<Todo> find(Long id) { return repo.findById(id); }
public Mono<Todo> create(Todo t) {
t.setId(null); // INSERT 보장
return repo.save(t);
}
public Mono<Todo> update(Long id, Todo t) {
t.setId(id); // UPDATE
return repo.save(t);
}
public Mono<Void> delete(Long id) { return repo.deleteById(id); }
}
4-4. 컨트롤러 (WebFlux)
@RestController
@RequestMapping("/api/todos")
public class TodoController {
private final TodoService svc;
public TodoController(TodoService svc) { this.svc = svc; }
@GetMapping public Flux<Todo> all() { return svc.findAll(); }
@GetMapping("/{id}") public Mono<Todo> one(@PathVariable Long id) { return svc.find(id); }
@PostMapping public Mono<Todo> add(@RequestBody Todo t) { return svc.create(t); }
@PutMapping("/{id}") public Mono<Todo> edit(@PathVariable Long id,
@RequestBody Todo t) { return svc.update(id, t); }
@DeleteMapping("/{id}")public Mono<Void> del(@PathVariable Long id) { return svc.delete(id); }
}
컨트롤러 메서드는 subscribe를 직접 호출하지 않는다. WebFlux가 응답 시점에 Mono/Flux를 구독해 JSON으로 직렬화한다.
5. API 동작 확인 (cURL 예시)
# 1) Todo 생성
curl -X POST -H "Content-Type: application/json" \
-d '{"title":"R2DBC","description":"배우기","completed":false}' \
http://localhost:8080/api/todos
# 2) 전체 조회
curl http://localhost:8080/api/todos
# 3) 상태 수정
curl -X PUT -H "Content-Type: application/json" \
-d '{"title":"R2DBC","description":"리액티브","completed":true}' \
http://localhost:8080/api/todos/1
# 4) 삭제
curl -X DELETE http://localhost:8080/api/todos/1
6. 확장 가이드
주제 적용 방법
예외 처리 | @ControllerAdvice + Mono.error(new NotFoundException()) |
검증 | jakarta.validation 의 @Valid + @Validated |
트랜잭션 | @Transactional + R2dbcTransactionManager (동일 스레드 내 코루틴 지원) |
마이그레이션 | Flyway R2DBC (org.flywaydb:flyway-core, org.flywaydb:flyway-database-h2) |
운영 DB 전환 | runtimeOnly 'io.r2dbc:r2dbc-postgresql' 추가 후 URL 변경 |
7. 마치며
Spring Boot 3 + WebFlux 스택은 “컨트롤러 → 서비스 → DB” 전 구간을 논블로킹으로 구성해 줄 수 있다. R2DBC는 JPA 의 강력한 ORM 기능을 포기한 대신, I/O 효율과 예측 가능한 퍼포먼스를 제공한다. 작은 CPU/메모리 리소스로 많은 연결을 처리해야 하는 서비스라면 시도해 볼 가치가 충분하다.