이전에는 스프링 WebFlux를 사용해서 리액티브하고 블로킹이 없는 컨트롤러를 생성하는 방법을 알아보았다.
그러나 같이 작동되는 다른 컴포넌트도 블로킹이 없어야 진정한 블로킹 없는 컨트롤러가 될 수 있다.
만일 블로킹 되는 리퍼지터리에 의존하는 스프링 WebFlux 리액티브 컨트롤러를 작성한다면, 이 컨트롤러는 해당 리퍼지터리의 데이터 생성을 기다리느라 블로킹될 것이다.
따라서 컨트롤러로부터 데이터베이스에 이르기까지 데이터의 전체 flow가 리액티브하고 블로킹되지 않는 것이 중요하다.
스프링 데이터는 Kay 릴리즈 트레인부터 Reactvie Repository의 지원을 제공하기 시작했다.
Reactvie Repository는 카산드라, 몽고DB, 카우치베이스, Redis 등을 지원한다.
하지만 RDB나 JPA는 지원하지 않는데, 이들은 표준화된 비동기 API를 제공하지 않기 때문이다.
따라서 앞으로 카산드라와 몽고DB를 이용하여 스프링 데이터 리액티브를 사용해볼 것이다.
Reactvie Repository는 도메인 타입이나 컬렉션 대신, Mono나 Flux를 인자로 받거나 반환하는 메서드를 갖는다.
리액티브와 리액티브가 아닌 타입 간의 변환
기존에 RDB를 사용중일 때도 Reactive Programming을 애플리케이션에 적용할 수 있다.
RDB가 블로킹 없는 Reactive Query를 지원하지 않더라도,
우선 블로킹 되는 방식으로 데이터를 가져와서 가능한 빨리 리액티브 타입으로 변환하여 상위 컴포넌트들이 Reactive의 장점을 활용하게 할 수 있다.
예를 들어, RDB와 스프링 데이터 JPA를 사용한다고 해보자. 이 경우 OrderRepository는 다음과 같은 시그니처의 메서드를 가질 수 있다.
List<Order> findByUser(User user);
이 findByUser()는 블로킹 방식으로 동작한다.
왜냐하면 List가 Reactive 타입이 아니므로 어떤 Reactive 오퍼레이션도 수행할 수 없기 때문이다.
게다가 컨트롤러가 findByUser()를 호출했다면 결과를 리액티브하게 사용할 수 없어 확장성을 향상시킬 수 없다.
이 경우엔 가능한 빨리 Reactive가 아닌 List를 Flux로 변환하여 결과를 처리한다.
List<Order> orders = repo.findByUser(someUser);
Flux<Order> orderFlux = Flux.fromIterable(orders);
Mono를 사용할 경우엔 아래와 같이 작성하면 된다.
Order order = repo.findById(id);
Mono<Order> orderFlux = Mono.just(order);
이처럼 Mono의 just()나 Flux의 fromIterable(), fromArray(), fromStream()을 사용하면
Repository의 Reactive가 아닌 블로킹 코드를 격리시키고 애플리케이션의 어디서든 Reactive 타입으로 처리하게 할 수 있다.
이번엔 저장하는 경우에 대해서 살펴보자.
Mono나 Flux 모두 자신들이 발행하는 데이터를 도메인 타입이나 Iterable 타입으로 추출하는 오퍼레이션을 가지고 있다.
Taco taco = tacoMono.block();
tacoRepo.save(taco);
Iterable<Taco> tacos = tacoFlux.toIterable();
tacoRepo.saveAll(tacos);
Mono의 block()이나 Flux의 toIterable()은 추출 작업을 할 때 블로킹이 된다.
따라서 이런식의 Mono와 Flux를 사용을 최소화 해야 한다.
블로킹되는 타입을 더 Reactive하게 추출 할 수도 있다.
Mono나 Flux를 구독하면서 발행되는 요소 각각에 대해 원하는 오퍼레이션을 수행하는 것이다.
tacoFlux.subscribe(
taco -> {
tacoRepo.save(taco);
}
);
tacoRepo의 save는 여전히 블로킹 오퍼레이션이다.
그러나 Flux나 Mono가 발행하는 데이터를 소비하고 처리하는 Reactive 방식의 subscribe()를 사용하므로
블로킹 방식의 일괄처리보다는 더 바람직하다.
NoSql 중 하나인 몽고DB는 문서형 DB다.
몽고DB는 BSON(Binary JSON) 형식의 문서로 데이터를 저장하며,
다른 DB에서 데이터를 쿼리하는 것과 거의 유사한 방법으로 문서를 쿼리하거나 검색할 수 있다.
몽고DB를 스프링 데이터로 사용하는 방법은 JPA를 스프링 데이터로 사용하는 방법과 크게 다르지 않다.
즉, 도메인 타입을 문서 구조로 매핑하는 애노테이션을 도메인 클래스에 지정한다.
그리고 JPA와 동일한 프로그래밍 모델을 따르는 Repository Interface를 작성하면 된다.
스프링 데이터 몽고DB 활성화하기 리액티브 스프링 데이터 몽고DB 스타터 의존성을 추가하자.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
이렇게 빌드에 의존성을 추가하면 스프링 데이터 리액티브 몽고DB 지원을 활성화하는 자동-구성이 수행된다.
(Repository Interface 자동 구현)
스프링 데이터 몽고DB는 기본적으로 27017 포트를 리스닝한다.
그러나 테스트와 개발에 편리하도록 in-memory 내장 몽고DB를 사용할 수도 있다.
아래와 같이 Flapdoodle 의존성을 빌드에 추가한다.
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
도메인 타입을 문서로 매핑하기 스프링 데이터 몽고DB는 몽고DB에 저장되는 문서 구조로 도메인 타입을 매핑하는 데 유용한 애노테이션들을 제공한다.
그 중 아래 3개가 가장 많이 사용된다.
@Id : 지정된 속성을 문서 ID로 지정한다. Serializable 타입인 어떤 속성에도 지정할 수 있다.
@Document : 지정된 도메인 타입을 몽고DB에 저장되는 문서로 선언한다.
@Field : 몽고DB의 문서에 속성을 저장하기 위해 필드 이름(과 선택적으로 순서)을 지정한다.
@Field가 지정되지 않은 도메인 타입의 속성들은 필드 이름과 속성 이름을 같은 것으로 간주된다.
이 애노테이션들을 이용하여 Ingredient 클래스를 작성한다.
package tacos;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@Document
public class Ingredient {
@Id
private final String id;
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
다음은 Taco의 몽고DB 매핑을 알아보자.
package tacos;
import java.util.Date;
import java.util.List;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.rest.core.annotation.RestResource;
import lombok.Data;
@Data
@RestResource(rel = "tacos", path = "tacos")
@Document
public class Taco {
@Id
private String id;
@NotNull
@Size(min = 5, message = "Name must be at least 5 characters long")
private String name;
private Date createdAt = new Date();
@Size(min=1, message="You must choose at least 1 ingredient")
private List<Ingredient> ingredients;
}
ID로 String 타입의 속성을 사용하면 이 속성값이 DB에 저장될 때 몽고DB가 자동으로 ID 값을 지정해준다. (null일 경우에 한함)
이제는 Repository Interface를 작성하자.
리액티브 몽고DB 리퍼지터리 인터페이스 작성하기 스프링 데이터 몽고DB는
스프링 데이터 JPA가 제공하는 것과 유사한 자동 Repository 지원을 제공한다.
몽고DB의 Reactvie Repository를 작성할 때는 ReactiveCrudRepository나 ReactiveMongoRepository를 선택할 수 있다.
ReactiveCrudRepository는 새로운 문서나 기존 문서의 save() 메서드에 의존하는 반면,
ReactiveMongoRepository는 새로운 문서의 저장에 최적화된 소수의 특별한 insert() 메서드를 제공한다.
우선, Ingredient 객체를 문서로 저장하는 Repository를 정의하자.
식재료를 저장한 문서는 초기에 식재료 데이터를 DB에 추가할 때 생성되며, 이외에는 거의 추가되지 않는다.
따라서 새로운 문서의 저장에 최적화된 ReactiveMongoRepository 보다는 ReactiveCrudRepository를 확장해야 한다.
package tacos.data;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.web.bind.annotation.CrossOrigin;
import tacos.Ingredient;
@CrossOrigin(origins="*")
public interface IngredientRepository extends ReactiveCrudRepository<Ingredient, String> {
}
IngredientRepository는 ReactiveRepository이므로
이것의 메서드는 그냥 도메인 타입이나 컬렉션이 아닌 Flux나 Mono 타입으로 도메인 객체를 처리한다.
예를 들어, findAll() 메서드는 Iterable 대신 Flux를 반환한다.
그리고 findById() 메서드는 Optional 대신 Mono를 반환한다.
따라서 이 Reactive Repository는 엔드-to-엔드 Reactive flow의 일부가 될 수있다.
다음은 몽고DB의 문서로 Taco 객체를 저장하는 Repository를 정의하자.
색재료 문서와는 다르게 타코 문서는 자주 생성된다.
따라서 ReactiveMongoRepository의 최적화된 insert() 메서드를 사용해야 한다.
package tacos.data;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import reactor.core.publisher.Flux;
import tacos.Taco;
public interface TacoRepository extends ReactiveMongoRepository<Taco, String> {
Flux<Taco> findByOrderByCreatedAtDesc();
}
ReactiveCrudRepository에 비해 ReactiveMongoRepository를 사용할 때의 유일한 단점은 바로 몽고DB에 특화되어 있다는 점이다.
그래서 다른 DB에는 사용할 수 없다. 따라서 이 단점을 감안하고 사용해야 한다.
TacoRepository에는 새로운 메서드가 있다.
이 메서드는 최근 생성된 타코들의 리스트를 조회하여 반환한다.
findByOrderByCreatedAtDesc()는 Flux를 반환한다.
따라서 take() 오퍼레이션을 적용하여 Flux에서 발행되는 처음 12개의 Taco 객체만 반환할 수 있다.
예를 들어, 최근 생성된 타코들을 보여주는 컨트롤러에서는 다음과 같이 코드를 작성할 수 있다.
Flux<Taco> recents = repo.findByOrderByCreatedAtDesc().take(12);
스프링 데이터는 카산드라, 몽고DB, 카우치베이스, 레디스 데이터베이스의 리액티브 리퍼지터리를 지원한다.
스프링 데이터의 리액티브 리퍼지터리는 리액티브가 아닌 리퍼지터리와 동일한 프로그래밍 모델을 따른다.
단, Flux나 Mono와 같은 리액티브 타입을 사용한다.
JPA 리퍼지터리와 같은 리액티브가 아닌 리퍼지터리는 Mono나 Flux를 사용하도록 조정할 수 있다.
그러나 데이터를 가져오거나 저장할 때 여전히 블로킹이 생긴다.
관계형이 아닌 데이터베이스를 사용하려면 (NoSQL) 해당 데이터베이스에서 데이터를 저장하는 방법에 맞게 데이터를 모델링하는 방법을 알아야 한다.