Spring Data with WebFlux 기존에 WebMVC 방식의 데이터베이스 접근시 JDBC 를 구현한 드라이버 라이브러리 를 사용해 접근해왔다.
리액티브한 DB 통신도 HTTP 와 다르지 않다. 이론적으로 DB 접근용 서비스를 생성하고 WebClient 를 사용해 DB 데이터를 가져온다면 비동기 DB 접근 라이브러리를 구현한 것 과 다름없다.
다행이도 다양한 DB 벤더사에서 자바 비동기 DB 연결 라이브러리인 리액티브 드라이버를 제공함으로 단순히 라이브러리만 추가하면 데이터베이스 레이어 에 대한 논 블록킹 엑세스를 할 수 있다.
spring-boot-starter-data-mongodb-reactivespring-boot-starter-data-cassandra-reactivespring-boot-starter-data-redis-reactivespring-boot-starter-data-r2dbc
스프링 데이터 팀에서 기존에 사용한던 Repository 패턴을 리액티브 방식에도 똑같이 사용할 수 있도록 추상화를 통해 구현해두었다.
각 모듈들이 ReactiveCurdRepository 인터페이스를 사용해 Reactor 라이브러리와 통합되어 자연스럽게 리액티브하게 코드작성이 가능하다.
Spring Data MongoDB with Webflux NoSQL 의 경우 각 벤더사에서 통합된 규약이 없다. 각 벤더사에서 자기들만의 드라이버 라이브러리를 제공하고 스프링 데이터 팀은 스프링에서 해당 라이브러리들을 쉽게 사용할 수 있도록 각종 모듈을 개발하고 있다
NoSQL DB 는 최근에 만들어 져서 대부분 벤더사가 리액티브 드라이버 를 제공하고 있으며 스프링 데이터 팀은 몽고DB 에서 제공하는 리액티브 드라이버 를 쉽고 편하게 사용할 수 있도록 spring-boot-starter-data-mongodb-reactive 모듈을 작성해두었다.
해당 모듈을 사용하면 스프링 팀에서 만든 Repository 패턴을 사용해 메서드명 기반으로 쿼리문이 자동 생성/사용 할 수 있다.
ReactiveMongoRepository 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import org.springframework.data.mongodb.repository.ReactiveMongoRepository;@Repository public interface BookReactiveMongoRepository extends ReactiveMongoRepository <Book, ObjectId> { Mono<Book> findOneByTitle (Mono<String> title) ; Flux<Book> findManyByTitleRegex (String regexp) ; @Meta(maxScanDocuments = 3) Flux<Book> findByAuthorsOrderByPublishingYearDesc (Publisher<String> authors) ; @Query("{ 'authors.1': { $exists: true } }") Flux<Book> booksWithFewAuthors () ; Flux<Book> findByPublishingYearBetweenOrderByPublishingYear ( Integer from, Integer to, Pageable pageable ) ;}
ReactiveMongoTemplate ReactiveMongoRepository 외에도 ReactiveMongoTemplate 를 사용해 쿼리 조작이 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service @RequiredArgsConstructor public class RxMongoTemplateQueryService { private static final String BOOK_COLLECTION = "book" ; private final ReactiveMongoTemplate mongoTemplate; public Flux<Book> findBooksByTitle (String title) { Query query = Query.query(new Criteria ("title" ) .regex(".*" + title + ".*" )) .limit(100 ); return mongoTemplate.find(query, Book.class, BOOK_COLLECTION); } }
MongoClient 몽고DB 에서 제공하는 리액티브 드라이버 구현체가 com.mongodb.reactivestreams.client.MongoClient 클래스이다.
https://mongodb.github.io/mongo-java-driver-reactivestreams/
org.mongodb:mongodb-driver-reactivestreams 모듈에서 제공하며 spring-boot-starter-data-mongodb-reactive 에서 내부적으로 사용한다.
MongoClient 클래스를 사용해도 쿼리조작이 가능하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Service @RequiredArgsConstructor public class RxMongoDriverQueryService { private final MongoClient mongoClient; public Flux<Book> findBooksByTitle (String title, boolean negate) { return Flux.defer(() -> { Bson query = Filters.regex("title" , ".*" + title + ".*" ); if (negate) query = Filters.not(query); return mongoClient .getDatabase("test-db" ) .getCollection("book" ) .find(query); }).map(doc -> new Book ( doc.getObjectId("id" ), doc.getString("title" ), doc.getInteger("pubYear" ))); } }
트랜잭션(ReactiveMongoTemplate.inTransaction) MongoDB 4.0 버전 이전까지 하나의 문서에 대해서만 트랜잭션을 제공 하는 Single-Document Transaction 기능만 있었다.
NoSQL 특성상 하나의 문서에 모든 정보를 사용하기에 Single-Document Transaction 으로도 충분해야 하지만 항상 예외가 있는법,
결국 여러 문서에 대한 트랜잭션을 제공 하는 Multi-Document Transaction 기능을 MongoDB 4.0 부터 지원한다.
WiredTiger 스토리지 엔진의 샤딩설정이 되어 있지 않고 복제설정일 경우에만 Multi-Document Transaction 을 지원한다. 대용량 데이터를 처리할땐 샤딩이 꼭 필요해서 잘 사용하지 않는 기술이기도 하다.
ReactiveMongoTemplate 의 inTransaction 메서드를 사용하면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private Mono<TxResult> doTransferMoney (String from, String to, Integer amount) { return mongoTemplate.inTransaction().execute(session -> session .findOne(queryForOwner(from), Wallet.class) .flatMap(fromWallet -> session .findOne(queryForOwner(to), Wallet.class) .flatMap(toWallet -> { if (fromWallet.hasEnoughFunds(amount)) { fromWallet.withdraw(amount); toWallet.deposit(amount); return session.save(fromWallet) .then(session.save(toWallet)) .then(ReactiveMongoContext.getSession()) .doOnNext(tx -> log.info("Current session: {}" , tx)) .then(Mono.just(TxResult.SUCCESS)); } else { return Mono.just(TxResult.NOT_ENOUGH_FUNDS); } }))) .onErrorResume(e -> Mono.error(new RuntimeException ("Conflict" ))) .last(); }
Spring Data R2DBC
R2DBC: Reactive Relational Database Connectivityhttps://r2dbc.io/ https://spring.io/projects/spring-data-r2dbc
아래와 같은 DBMS 에 대하여 r2dbc 라이브러리를 제공
1 2 3 4 5 6 7 H2 (io.r2dbc:r2dbc-h2) MariaDB (org.mariadb:r2dbc-mariadb) Microsoft SQL Server (io.r2dbc:r2dbc-mssql) MySQL (dev.miku:r2dbc-mysql) jasync-sql MySQL (com.github.jasync-sql:jasync-r2dbc-mysql) Postgres (io.r2dbc:r2dbc-postgresql) Oracle (com.oracle.database.r2dbc:oracle-r2dbc)
지금까지 Spring Data JDBC 혹은 JPA 를 사용해 생성된 Hikari CP 안의 연결객체가 JDBC 드라이버를 사용해 관계형 DB 를 사용해 왔다.
1 2 3 4 5 6 7 @Repository public interface BookSpringDataJdbcRepository extends CrudRepository <Book, Integer> { @Query("SELECT * FROM book b WHERE b.title = :title") CompletableFuture<Book> findBookByTitleAsync (@Param("title") String title) ; }
JDBC, JPA 등의 RDB 라이브러리 들은 모두 동기/블럭킹 방식으로 동작한다.
다행이도 Spring Data JDBC 를 개발한 스프링 데이터 Relational 프로젝트 팀에서리액티브에 적합 한 자바 DB 드라이버인 리액티브 드라이버를 개발중이다.
이 리액티브 드라이버 를 사용한 프로젝트가 R2DBC 프로젝트이다.
더이상 JDBC 를 사용하지 않고 리액티브 스택에 적합한 리액티브 드라이버를 사용해 DB 에 접근, 데이터를 조작한다.
안타깝게도 JPA 는 기존 코드가 너무 복잡했는지 리액티브 지원을 하지 않을것으로 보인다.
ReactiveCrudRepository 1 2 3 4 5 6 7 8 9 @Repository public interface MemberRepository extends ReactiveCrudRepository <Member, Long> { Mono<Member> findByName (String name) ; Mono<Member> findByUserName (String name) ; @Query("SELECT * FROM member WHERE name = :name AND user_name = :userName") Mono<Member> findByNameAndUserName (String name, String userName) ; }
R2dbcEntityTemplate 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Service @RequiredArgsConstructor public class MemberDynamicRepository { private final R2dbcEntityTemplate r2dbcEntityTemplate; public Flux<Member> findTest (String userName) { Query query = Query.query(where("user_name" ).like("%" + userName + "%" )) .limit(10 ) .offset(0 ); return r2dbcEntityTemplate.select(Member.class) .matching(query) .all(); } }
Spring Data Redis with Webflux spring-boot-starter-data-redis-reactive 모듈을 사용, ReactiveRedisTemplate 클래스가 Redis 커넥션의 핵심클래스이다. 다른 Spring Data 프로젝트와 달리 Repository 가 존재하지 않는다.
일반적인 데이터 관리 외에도 구독/발행 구조의 메시지 기능도 지원한다.
spring-boot-starter-data-redis-reactive 모듈은 내부적으로 Lettuce 라이브러리를 사용한다.
https://lettuce.io/ Lettuce 라이브러리 내에서 Reactor 라이브러리를 사용한다. 현재 non blokcing 을 지원하는건 Lettuce 가 유일하다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class Sample { private String name; private String description; } @Configuration public class RedisConfig { @Value("${redis.host}") private String host; @Value("${redis.port}") private Integer port; @Bean public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory () { return new LettuceConnectionFactory (host, port); } @Bean public ReactiveRedisTemplate<String, Sample> reactiveRedisTemplate (ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) { StringRedisSerializer keySerializer = new StringRedisSerializer (); Jackson2JsonRedisSerializer<Sample> valueSerializer = new Jackson2JsonRedisSerializer <>(Sample.class); RedisSerializationContext.RedisSerializationContextBuilder<String, Sample> builder = RedisSerializationContext.newSerializationContext(keySerializer); RedisSerializationContext<String, Sample> context = builder.value(valueSerializer).build(); return new ReactiveRedisTemplate (reactiveRedisConnectionFactory, context); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Service @RequiredArgsConstructor public class SampleService { private final ReactiveRedisTemplate<String, Sample> redisTemplate; public Mono<Boolean> put (String key, Sample sample) { return redisTemplate.opsForValue().set(key, sample); } public Mono<Sample> get (String key) { return redisTemplate.opsForValue().get(key); } public Flux<Sample> getAll (String keyPattern) { return redisTemplate.keys(keyPattern) .flatMap(key-> redisTemplate.opsForValue().get(key)); } public Mono<Boolean> delete (String key) { return redisTemplate.opsForValue().delete(key); } }