
R2DBC는 "Reactive Relational Database Connectivity"의 약자로, 리액티브 프로그래밍 방식으로 관계형 데이터베이스에 접근할 수 있도록 설계된 비동기 논블로킹 API 표준 사양이다.
초기에는 MongoDB, Cassandra 같은 NoSQL DB만 리액티브 드라이버를 제공했기 때문에, 리액티브 애플리케이션과 관계형 DB를 함께 사용하기 어려웠다. 이를 해결하기 위해 R2DBC 사양이 등장했다.
Spring Data R2DBC는 Spring Data 프로젝트의 일부로, R2DBC를 기반으로 한 리액티브 리포지토리(Repository) 인터페이스를 통해 CRUD 연산을 간편하게 구현할 수 있도록 도와주는 프레임워크이다.
ReactiveCrudRepository, Query by method, @Query 등 Spring Data 공통 기능을 지원@EnableR2dbcRepositories, @EnableR2dbcAuditing 등의 설정이 필요spring-boot-starter-data-r2dbc: R2DBC 전용 Spring Data 기능io.r2dbc:r2dbc-h2: H2 DB의 리액티브 드라이버schema-locations: 초기 테이블 스키마를 지정하는 위치ddl-auto 기능을 지원하지 않음 → 직접 스크립트로 생성 필요@Table("book")
public class Book {
@Id
private Long bookId;
private String titleKorean;
private String titleEnglish;
private String author;
private String isbn;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
}
@Table: R2DBC에서는 명시적으로 테이블을 지정해야 함@Id: 기본 키 식별자@CreatedDate, @LastModifiedDate: Auditing 설정을 통해 자동 처리됨 (@EnableR2dbcAuditing 필요)public interface BookRepository extends ReactiveCrudRepository<Book, Long> {
Mono<Book> findByIsbn(String isbn);
}
Mono<T>: 0 또는 1개의 데이터를 비동기로 반환Flux<T>: 여러 개의 데이터를 비동기로 스트리밍public Mono<Book> save(Book book) {
return bookRepository.findByIsbn(book.getIsbn())
.flatMap(existing -> Mono.empty()) // 이미 존재하면 저장 안 함
.switchIfEmpty(bookRepository.save(book)); // 존재하지 않으면 저장
}
switchIfEmpty: 빈 결과일 경우 대체 연산 수행then(...): 이전 처리 결과는 무시하고 다음 Mono를 이어붙임R2dbcEntityTemplate은 SQL 스타일의 명시적 쿼리 작성보다 좀 더 프로그래밍적 접근을 원하는 개발자에게 적합하다.
template.select(Book.class)
.matching(query(where("author").is("홍길동")))
.all();
select, insert, update, delete 등의 메서드 체이닝 방식 지원Criteria.where().and().is() 등으로 조건 조합| 메서드 | 설명 |
|---|---|
insert | 객체 삽입 |
update | 객체 수정 |
delete | 삭제 |
select, all, one | 조회 |
count | 총 개수 반환 |
exists | 존재 여부 확인 |
| 메서드 | 설명 |
|---|---|
is(), isNull() | 값 비교 |
greaterThan(), lessThan() | 비교 연산자 |
in(), notIn() | 포함 여부 |
like() | 패턴 매칭 |
and(), or() | 논리 조합 |
bookRepository.findAllBy(PageRequest.of(page, size, Sort.by("bookId")));
template.select(Book.class)
.count()
.flatMap(total -> {
long totalPages = (long) Math.ceil((double) total / size);
long pageToUse = Math.min(page, totalPages);
long skip = (pageToUse - 1) * size;
return template.select(Book.class)
.all()
.skip(skip)
.take(size)
.collectList();
});
skip: 얼마나 건너뛸지take: 몇 개를 가져올지count: 전체 개수 파악collectList: 최종 List 형태로 반환