- 컨트롤러부터 데이터베이스에 이르기까지 데이터의 전체 플로우가 리액티브하고 블로킹되지 않는 것이 중요하다.
- 만일 블로킹되는 리퍼지터리에 의존하는 스프링 WebFlux 리액티브 컨트롤러를 작성한다면, 이 컨트롤러는 해당 리퍼지터리의 데이터 생성을 기다리느라 블로킹될 것이다.
스프링 데이터 리액티브 개념 이해하기
- 현재는 카산드라, 몽고DB, 카우치베이스, 레디스로 데이터를 저장할 때 리액티브 프로그래밍 모델을 지원한다.
- 관계현 데이터베이스나 JPA는 리액티브 리퍼지터리가 지원되지 않는다.
- 스프링 데이터 JPA로 리액티브 프로그래밍 모델을 지원하려면 관계형 데이터베이스와 JDBC 드라이버 역시 블로킹되지 않는 리액티브 모델을 지원해야 한다.
스프링 데이터 리액티브 개요
- 스프링 데이터 리액티브의 리퍼지터리는 도메인 타입이나 컬렉션 대신
Mono
나Flux
를 인자로 받거나 반환하는 메서드를 갖는다.
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
)```
- 실제 구조
- 
- 컬렉션을 저장하는 열은 아래와 같이 JSON 배열로 저장된다.
- 
### 리액티브 카산드라 리퍼지터리 작성하기
- 리액티브 카산드라 리퍼지터리를 작성할 때는 사용 가능한 인터페이스
- `ReactiveCrudRepository`
- `ReactiveCassandraRepository`: `ReactiveCrudRepository`를 확장하여 새 객체가 저장될 때 사용되는 `insert()` 메서드의 몇 가지 변형 버전을 제공한다.
- 만일 많은 데이터를 추가한다면 `ReactiveCassandraRepository`를 선태갛고, 그렇지 않을 때는 `ReactiveCrudRepository`를 선택하는 것이 좋다.
- 카산드라의 특성상 where 절을 사용한 필터링 결과는 느리게 처리될 수 있다.
- 그렇지만 결과가 하나 이상의 열로 필터링되는 테이블 쿼리에는 매우 유용하므로 where 절을 사용할 필요가 있다.
- 단순한 where 절은 카산드라에서 허용하지 않는다.
- 따라서 `@AllowFiltering` 애노테이션을 `findByUsername()`에 지정하여 다음과 같은 쿼리가 내부적으로 수행되게 할 수 있다.
- 
- 
## 리액티브 몽고DB 리퍼지터리 작성하기
- 몽고DB는 NoSQL 문서형 데이터베이스다.
- 몽고DB는 BSON(Binary JSON)형식의 문서로 데이터를 저장하며, 다른 데이터베이스에서 데이터를 쿼리하는 것과 유사한 방법으로 문서를 쿼리하거나 검색할 수 있다.
### 스프링 데이터 몽고DB 활성화하기
- 리액티브 데이터 몽고DB 스타터 의존성
- 리액티브 몽고DB 지원을 활성화하는 자동-구성이 수행된다.
- 기본적으로 몽고DB가 로컬로 실행되면서 27017 포트를 리스닝하는 것으로 간주한다.
- 테스트와 개발에 편리하도록 인메모리 몽고DB를 실행할 수 있다.
- Flapdoodle 의존성을 추가하면 된다.
- 
- 속성
- `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` 문서에 직접 저장한다.
- 
- 
- 몽고DB는 ID로 `String` 타입을 사용하면, 값이 null일 경우 데이터베이스에 저장할 때 자동으로 ID 값을 지정해준다.
### 리액티브 몽고DB 리퍼지터리 인터페이스 작성하기
- 몽고DB의 리액티브 리퍼지터리
- `ReactiveCrudRepository`: 다양한 데이터베이스 타입에 걸쳐 동일하므로 몽고DB나 카산드라에서 같은 인터페이스를 사용할 수 있다.
- `ReactvieMongoRepository`: 새로운 문서의 저장에 최적화된 소수의 특별한 `insert()` 메서드를 제공한다.
- 커스터 쿼리 메서드의 명명 규칙을 따른다.
- 
- 