스프링 데이터의 리액티브 리퍼지터리
카산드라와 몽고DB의 리액티브 리퍼지터리 작성하기
리액티브가 아닌 리퍼지터리를 리액티브 사용에 맞추어 조정하기
카산드라를 사용한 데이터 모델링
이전에는 스프링 WebFlux를 사용해서 리액티브하고 블로킹이 없는 컨트롤러를 생성하는 방법을 배움. 이는 웹 계층의 확장성을 향상시키는데 도움이 됨.
그러나 같이 작동되는 다른 컴포넌트도 블로킹이 없어야 진정한 블로킹 없는 컨트롤러가 될 수 있음.
만일 블로킹 되는 리퍼지터리에 의존하는 스프링 WebFlux 리액티브 컨트롤러를 작성한다면, 이 컨트롤러는 해당 리퍼지터리의 데이터 생성을 기다리느라 블로킹될 것.
따라서 컨트롤러로부터 데이터베이스에 이르기까지 데이터의 전체 flow가 리액티브하고 블로킹되지 않는 것이 중요하다.
스프링 데이터는 Kay 릴리즈 트레인부터 Reactvie Repository의 지원을 제공 시작
Reactvie Repository는 카산드라, 몽고DB, 카우치베이스, Redis 등을 지원
하지만 RDB나 JPA는 지원하지 않는데, 이들은 표준화된 비동기 API를 제공하지 않음
따라서 앞으로 카산드라와 몽고DB를 이용하여 스프링 데이터 리액티브를 사용해볼 것이다.
Reactvie Repository
데이터베이스로부터 식자재 타입으로 Ingredient 객체들을 가져오는 리퍼지터리 메서드
Flux<Ingredient> findByType(Ingredient.Type type);
Taco 객체를 저장하는 리액티브 리퍼지터리의 메서드 시그니처
<Taco> Flux<Taco> saveAll(Publisher<Taco> tacoPublisher);
List<Order> findByUser(User user);
이 경우엔 가능한 빨리 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나 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
데이터를 테이블에 저장된 행으로 처리, 각 행은 일 대 다 관계의 많은 분산 노드에 걸쳐 분할됨
한 노드가 모든 데이터를 갖지 않으나 특정 행은 다수의 노드에 걸쳐 복제될 수 있어 단일 장애점을 없앰
스프링 데이터 카산드라는 카산드라 데이터베이스의 자동화된 리퍼지터리 자원 제공
애플리케이션의 도메인 타입을 데이터베이스 구조에 매핑하는 애노테이션 제공
스프링 데이터 카산드라는 유사한 목적의 매핑 애노테이션 제공
package tacos;
import lombok.Data;
@Data
public class Ingredient {
private final String name;
private final Type type;
public static enum Type {
WRAP, PROTEIN, VEGGIES, CHEESE, SAUCE
}
}
package tacos;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.springframework.data.cassandra.core.cql.Ordering;
import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
import org.springframework.data.cassandra.core.mapping.Table;
import org.springframework.data.rest.core.annotation.RestResource;
import com.datastax.driver.core.utils.UUIDs;
import lombok.Data;
@Data
@RestResource(rel = "tacos", path = "tacos")
@Table("tacos") //tacos 테이블에 저장&유지
public class Taco {
@PrimaryKeyColumn(type=PrimaryKeyType.PARTITIONED)//파티션 키를 정의함
private UUID id = UUIDs.timeBased();
@NotNull
@Size(min = 5, message = "Name must be at least 5 characters long")
private String name;
@PrimaryKeyColumn(type=PrimaryKeyType.CLUSTERED,
ordering=Ordering.DESCENDING)//클러스터링 키 정의
private Date createdAt = new Date();
@Size(min=1, message="You must choose at least 1 ingredient")
@Column("ingredients")//List를 ingredients열에 매핑
private List<IngredientUDT> ingredients;
}
테이블 이름을 tacos로 지정하기 위해 @Table 애노테이션 사용
id 속성 :
- PrimaryKeyType.PARTITIONED 타입으로 @PrimaryKeyColumn에 지정. 타코 데이터의 각 행이 저장된 카산드라 파티션을 결정하기 위해 사용되는 파티션 키가 id 속성인 것을 나타냄.
createdAt 속성 : PrimaryKeyType.CLUSTERED 속성으로 클러스터링 키임.
- 클러스터링 키 : 파티션 내부에서 행의 순서를 결정하기 위해 사용
ingredients 속성 : List 대신 IngredientUDT 객체를 저장하는 List로 정의됨. -> 카산드라 테이블은 비정규화 되어 다른 테이블과 중복된 데이터를 포함할 수 있어 tacos 테이블의 ingredients 열에 중복 저장 가능.
- IngredientUDT 클래스 사용 이유 : ingredients 열처럼 데이터의 컬렉션을 포함하는 열은 네이티브 타입(정수, 문자열 등)의 컬렉션이나 사용자 정의 타입 UDT의 컬렉션이어야 하기 때문.
package tacos;
import org.springframework.data.cassandra.core.mapping.UserDefinedType;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Data
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@UserDefinedType("ingredient")
public class IngredientUDT {
private final String name;
private final Ingredient.Type type;
}
그림 12.1 참고
tacos 테이블 쿼리
package tacos;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import org.springframework.data.cassandra.core.mapping.Column;
import org.springframework.data.cassandra.core.mapping.PrimaryKey;
import org.springframework.data.cassandra.core.mapping.Table;
import com.datastax.driver.core.utils.UUIDs;
import lombok.Data;
@Data
@Table("tacoorders")
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@PrimaryKey
private UUID id = UUIDs.timeBased();
private Date placedAt = new Date();
@Column("user")
private UserUDT user;
private String deliveryName;
private String deliveryStreet;
private String deliveryCity;
private String deliveryState;
private String deliveryZip;
private String ccNumber;
private String ccExpiration;
private String ccCVV;
@Column("tacos")
private List<TacoUDT> tacos = new ArrayList<>();
public void addDesign(TacoUDT design) {
this.tacos.add(design);
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
그러나 테스트와 개발에 편리하도록 in-memory 내장 몽고DB를 사용 가능
아래와 같이 Flapdoodle 의존성을 빌드에 추가
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
그 중 아래 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
}
}
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;
}
몽고DB 애노테이션이 지정된 다음의 Order 클래스
package tacos;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.Data;
import org.springframework.data.mongodb.core.mapping.Field;
@Data
@Document
public class Order implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String id;
private Date placedAt = new Date();
@Field("customer")
private User user;
private String deliveryName;
private String deliveryStreet;
private String deliveryCity;
private String deliveryState;
private String deliveryZip;
private String ccNumber;
private String ccExpiration;
private String ccCVV;
private List<Taco> tacos = new ArrayList<>();
public void addDesign(Taco design) {
this.tacos.add(design);
}
}
User 도메인 클래스
package tacos;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.
SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
@Data
@NoArgsConstructor(access=AccessLevel.PRIVATE, force=true)
@RequiredArgsConstructor
@Document
public class User implements UserDetails {
private static final long serialVersionUID = 1L;
@Id
private String id;
private final String username;
private final String password;
private final String fullname;
private final String street;
private final String city;
private final String state;
private final String zip;
private final String phoneNumber;
private final String email;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
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> {
}
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);
- OrderRepository 인터페이스
package tacos.data;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import tacos.Order;
public interface OrderRepository extends ReactiveMongoRepository<Order, String> {
}
package tacos.data;
import org.springframework.data.mongodb.repository.ReactiveMongoRepository;
import reactor.core.publisher.Mono;
import tacos.User;
public interface UserRepository extends ReactiveMongoRepository<User, String> {
Mono<User> findByUsername(String username);
}
스프링 데이터는 카산드라, 몽고DB, 카우치베이스, 레디스 데이터베이스의 리액티브 리퍼지터리를 지원
스프링 데이터의 리액티브 리퍼지터리는 리액티브가 아닌 리퍼지터리와 동일한 프로그래밍 모델을 따름
단, Flux나 Mono와 같은 리액티브 타입을 사용
JPA 리퍼지터리와 같은 리액티브가 아닌 리퍼지터리는 Mono나 Flux를 사용하도록 조정 가능
그러나 데이터를 가져오거나 저장할 때 여전히 블로킹이 생김!
관계형이 아닌 데이터베이스를 사용하려면 (NoSQL) 해당 데이터베이스에서 데이터를 저장하는 방법에 맞게 데이터를 모델링하는 방법을 알아야 함