프로그래밍

Spring Data R2DBC와 H2로 만드는 완전 리액티브 CRUD REST API

silbaram 2025. 4. 28. 11:49
728x90

 

 

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/메모리 리소스로 많은 연결을 처리해야 하는 서비스라면 시도해 볼 가치가 충분하다.

 

 

 

728x90