• 컨트롤러부터 데이터베이스에 이르기까지 데이터의 전체 플로우가 리액티브하고 블로킹되지 않는 것이 중요하다.
  • 만일 블로킹되는 리퍼지터리에 의존하는 스프링 WebFlux 리액티브 컨트롤러를 작성한다면, 이 컨트롤러는 해당 리퍼지터리의 데이터 생성을 기다리느라 블로킹될 것이다.

스프링 데이터 리액티브 개념 이해하기

  • 현재는 카산드라, 몽고DB, 카우치베이스, 레디스로 데이터를 저장할 때 리액티브 프로그래밍 모델을 지원한다.
  • 관계현 데이터베이스나 JPA는 리액티브 리퍼지터리가 지원되지 않는다.
    • 스프링 데이터 JPA로 리액티브 프로그래밍 모델을 지원하려면 관계형 데이터베이스와 JDBC 드라이버 역시 블로킹되지 않는 리액티브 모델을 지원해야 한다.

스프링 데이터 리액티브 개요

  • 스프링 데이터 리액티브의 리퍼지터리는 도메인 타입이나 컬렉션 대신 MonoFlux를 인자로 받거나 반환하는 메서드를 갖는다.
Flux<Ingredient> findByType(Ingredient.Type Type);
Flux<Taco> saveAll(Publisher<Taco> tacoPublisher);

리액티브와 리액티브가 아닌 타입 간의 변환

  • 데이터베이스가 리액티브를 지원하지 않는 경우라도 블로킹 되는 방식으로 가져와서 가능한 빨리 리액티브 타입으로 변환하여 상위 컴포넌트들이 리액티브의 장점을 활용하게 할 수 있다.

  • 조회 예시

List<Order> orders = repo.findByUser(someUser);
Flux<Order> orderFlux = Flux.fromIterable(orders);

Order order = repo.findById(id);
Mono<Order> orderMono = Mono.just(order);
  • 저장 예시
Taco taco = tacoMono.block();
tacoRepo.save(taco);

Iterable<Taco> tacos =tacoFlux.toIterable();
tacoRepo.saveAll(tacos);
  • 더 리액티한 방법: 구독하면서 발행되는 요소 각각에 원하는 오퍼레이션을 수행하는 방법
tacoFlux.subscribe(taco -> {
	tacoRepo.save(taco);
});

리액티브 카산드라 리퍼지터리 사용하기

  • 카산드라: 분산처리, 고성능, 상시 가용, 궁극적인 일관성을 갖는 NoSQL 데이버베이스
    • 데이터를 테이블에 저장된 행으로 처리
    • 한 노드가 모든 데이터를 갖지는 않지만, 특정 행은 다수의 노드에 걸쳐 복제될 수 있으므로 단일 장애점을 없애준다.

스프링 데이터 카산드라 활성화하기

  • 카산드라 리액티브 리퍼지터리 의존성 추가
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-cassandra-reactive</artifactId>  
</dependency>
  • 카산드라에서 키 공간은 카산드라 노드의 테이블을 모아 놓은 것이다. 이것은 관계형 데이터베이스에서 테이블, 뷰, 제약조건을 모아 놓은 스키마와 유사하다.
  • 카산드라 CQL 셸에서 create keyspace 명령을 통해 타코 클라우드 애플리케이션의 키 공간을 생성할 수 있다.
    • replication_factor가 1일 때는 각 행의 데이터를 여러 벌 복제하지 않고 한 벌만 유지한다.
    • SimpleStrategy 복제 전략은 데모용 코드인 단일 데이터 센터 사용 시에 좋다.
    • 만약 카산드라 클러스터가 다수의 데이터 센터에 확산되어 있을 때는 NetworkTopologyStrategy를 고려할 수 있다.
create keyspace tacocloud
	... with replication={'class':'SimpleStrategy', 'replication_factor': 1}
	... and durable_writes=true;
  • yaml 속성 구성
    • 키 공간: tacocloud
    • schema-action: 애플리케이션이 매번 시작할 때마다 모든 테이블과 사용자 정의 타입이 삭제되고 재생성된다.
    • 기본적으로 localhost:9092를 리스닝 하는 것으로 간주한다.
spring:  
  data:  
    cassandra:  
      keyspace-name: tacocloud  
      schema-action: recreate_drop_unused
  • 프로덕션 환경처럼 구성
spring:  
  data:  
    cassandra:  
      keyspace-name: tacocloud  
      contact-points:  
        - casshost-1.tacocloud.com  
        - casshost-2.tacocloud.com  
        - casshost-3.tacocloud.com  
      port: 9043  
      username: tacocloud  
      password: s3cr3tP455w0rd

카산드라 데이터 모델링 이해하기

  • 카산드라 테이블은 얼마든지 많은 컬럼을 가질 수 있다.
    • 그러나 모든 행이 같은 열을 갖지 않고, 행마다 서로 다른 열을 가질 수 있다.
  • 카산드라 데이터베이스는 다수의 파티션에 걸쳐 분할된다.
    • 테이블의 어떤 행도 하나 이상의 파티션에서 관리될 수 있다.
    • 그러나 각 파티션은 모든 행을 갖지 않고, 서로 다른 행을 가질 수 있다.
  • 카산드라 테이블의 두 종류 키
    • 타피션 키: 각 행이 유지 관리되는 파티션을 결정하기 위해 해시 오퍼레이션이 각 행의 파티션 키에 수행된다.
    • 클러스터링 키: 각 행이 파티션 내부에서 유지 관리되는 순서를 결정한다.
  • 카산드라는 읽기 오퍼레이션에 최적화되어, 테이블이 비정규화되고 데이터가 다수의 테이블에 걸쳐 중복되는 경우가 흔하다.

카산드라 퍼시스턴스의 도메인 타입 매핑

  • @Table: 테이블 지정
  • @PrimaryKey: 파티션 키와 클러스터링 키 모두 이 속성으로 사용
@Table("ingredients")  
data class Ingredient(  
    @PrimaryKey  
    val id: String,  
    val name: String,  
    val type: Type  
) {  
  
    enum class Type {  
       WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE  
    }  
}
  • @PrimaryKeyColumn: 파티션 키 또는 클러스터링 키로 지정
  • Ingredient가 아닌 IngredientUDT 클래스를 따로 만드는 이유
    • 카산드라 테이블은 비정규화되어서 다른 테이블의 한 행으로부터 복사될 수 있는 데이터를 실제로 갖는다.
    • 데이터의 컬렉션을 포함하는 열은 네이티브 타입의 클래스거나 사용자 정의 타입(User Defined Type)의 컬렉션이어야 한다.
    • Ingredient 클래스는 이미 카산드라에 저장하는 엔티티로 매핑했기 때문에, 새로운 클래스를 ㅈ생성해야된다.
  • @UserDefinedType: 사용자 정의 타입으로 지정
@Table("tacos")  
class Taco(  
    @PrimaryKeyColumn(type = PrimaryKeyType.PARTITIONED)  
    val id: UUID = Uuids.timeBased(),  
    @Size(min = 5)  
    val name: String,  
    @PrimaryKeyColumn(type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)  
    val createdAt: Date = Date(),  
    @Size(min = 1)  
    @Column("ingredients")  
    val ingredients: List<IngredientUDT>  
)

@UserDefinedType("ingredient")  
class IngredientUDT(  
    val name: String,  
    val type: Ingredient.Type  
)```

- 실제 구조
	- ![](assets/Pasted%20image%2020230914231321.png)

- 컬렉션을 저장하는 열은 아래와 같이 JSON 배열로 저장된다.
	- ![](assets/Pasted%20image%2020230914231344.png)

### 리액티브 카산드라 리퍼지터리 작성하기

- 리액티브 카산드라 리퍼지터리를 작성할 때는 사용 가능한 인터페이스
	- `ReactiveCrudRepository`
	- `ReactiveCassandraRepository`: `ReactiveCrudRepository`를 확장하여 새 객체가 저장될 때 사용되는 `insert()` 메서드의 몇 가지 변형 버전을 제공한다.
- 만일 많은 데이터를 추가한다면 `ReactiveCassandraRepository`를 선태갛고, 그렇지 않을 때는 `ReactiveCrudRepository`를 선택하는 것이 좋다.
- 카산드라의 특성상 where 절을 사용한 필터링 결과는 느리게 처리될 수 있다.
	- 그렇지만 결과가 하나 이상의 열로 필터링되는 테이블 쿼리에는 매우 유용하므로 where 절을 사용할 필요가 있다.
	- 단순한 where 절은 카산드라에서 허용하지 않는다.
	- 따라서 `@AllowFiltering` 애노테이션을 `findByUsername()`에 지정하여 다음과 같은 쿼리가 내부적으로 수행되게 할 수 있다.
	- ![](assets/Pasted%20image%2020230917232952.png)
	- ![](assets/Pasted%20image%2020230917232959.png)

## 리액티브 몽고DB 리퍼지터리 작성하기

- 몽고DB는 NoSQL 문서형 데이터베이스다.
- 몽고DB는 BSON(Binary JSON)형식의 문서로 데이터를 저장하며, 다른 데이터베이스에서 데이터를 쿼리하는 것과 유사한 방법으로 문서를 쿼리하거나 검색할 수 있다.

### 스프링 데이터 몽고DB 활성화하기

- 리액티브 데이터 몽고DB 스타터 의존성
	- 리액티브 몽고DB 지원을 활성화하는 자동-구성이 수행된다.
	- 기본적으로 몽고DB가 로컬로 실행되면서 27017 포트를 리스닝하는 것으로 간주한다.
- 테스트와 개발에 편리하도록 인메모리 몽고DB를 실행할 수 있다.
	- Flapdoodle 의존성을 추가하면 된다.
	- ![](assets/Pasted%20image%2020230917233747.png)
- 속성
	- `spring.data.mongodb.host`: 몽고DB 서버가 실행 중인 호스트 이름(기본값: localhost)
	- `spring.data.mongodb.port`: 몽고DB 서버가 리스닝하는 포트(기본값: 27017)
	- `spring.data.mongodb.username`: 몽고DB 접근에 사용되는 사용자 이름
	- `spring.data.mongodb.password`: 몽고DB 접근에 사용되는 비밀번호
	- `spring.data.mongodb.database`: 데이터베이스 이름(기본값: test)

### 도메인 타입을 문서로 매핑하기

- `@id`: 이것이 지정된 속성을 문서ID로 지정한다.
- `@Document`: 이것이 지정된 도메인 타입을 몽고DB에 저장되는 문서로 선언한다.
	- `collection` 속성을 사용하여 컬렉션 명을 지정할 수 있다. 지정하지 않으면 클래스 이름과 같고 첫 자만 소문자다.
- `@Field`: 몽고DB의 문서에 속성을 저장하기 위해 필드 이름을 지정한다.
- 몽고DB는 카산드라와 다르게 문서로 선언한 도메인 타입을 재사용할 수 있다.(`ingredients` 필드 참고)
	- 이 경우에도 RDBMS같이 별도의 컬렉션이 저장되지 않고, 카산드라와 유사하게 비정규화된 상태로 `taco` 문서에 직접 저장한다.
	- ![](assets/Pasted%20image%2020230917235013.png) 
	- ![](assets/Pasted%20image%2020230917235031.png)
- 몽고DB는 ID로 `String` 타입을 사용하면, 값이 null일 경우 데이터베이스에 저장할 때 자동으로 ID 값을 지정해준다.

### 리액티브 몽고DB 리퍼지터리 인터페이스 작성하기

- 몽고DB의 리액티브 리퍼지터리
	- `ReactiveCrudRepository`: 다양한 데이터베이스 타입에 걸쳐 동일하므로 몽고DB나 카산드라에서 같은 인터페이스를 사용할 수 있다.
	- `ReactvieMongoRepository`: 새로운 문서의 저장에 최적화된 소수의 특별한 `insert()` 메서드를 제공한다.
- 커스터 쿼리 메서드의 명명 규칙을 따른다.
	- ![](assets/Pasted%20image%2020230918205905.png)
	- ![](assets/Pasted%20image%2020230918205912.png)