본문 바로가기
IT/기타

chapter 12. 리액티브 데이터 퍼시스턴스 (책. Spring in Action)

by eddie_factory 2022. 3. 4.
반응형

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

릴리즈 트레인 kay (2017-10)부터 리액티브 레퍼지터리 지원을 제공하기 시작

리액티브 레퍼지터리는 카산드라, 몽고DB, 카우치베이스, Redis 등을 지원
(RDB나 JPA는 지원하지 않는데, 이는 표준화된 비동기 API를 제공하지 않기 때문이라고.. )

 

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

리액티브 레퍼지터리는 도메인 타입이나 컬랙션 대신 Mono나 Flux를 인자로 받거나 반환하는 메서드를 갖는다는 것이 핵심

Flux<Ingredient> findByType(Ingredient.TYPE type);

위 코드 처럼 findByType() 메서드는 Flux 타입을 반환

간단히 말해, 스프링 데이터의 리액티브 레퍼지터리는 도메인 타입이나 컬렉션 대신 Mono나 Flux를 인자로 받거나 변환하는 메서드를 갖는다는 것만 다름

 

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

RDB와 같이 리액티브가 아닌 경우에도 일부 장점을 활용할 수 있음.

블록킹이 없는 리액티브 쿼리를 지원하지 않더라도 블록킹이 되는 방식으로 데이터를 가져와서 리액티브타입으로 변환하여 사용할 수 있음.

List<Orders> findByUser(User user);

findByUser()는 블로킹방식으로 동작한다.

왜냐하면 List가 리액티브 타입이 아니므로, 리액티브 타입인 Flux가 제공하는 어떤 오퍼레이션도 수행 할수 없기 때문

 

이 경우 리액티브가 아닌 List를 Flux로 변환하여 결과를 처리할 수 있다. Flux.fromIterable()을 사용하면 된다.

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() 메서드를 사용하면 레퍼지터리의 블로킹 코드를 격리시키고 리액티브 타입으로 처리하게 할 수 있다.

 

저장의 경우, Mono나 Flux 모두 자신들이 발행하는 데이터를 도메인 타입이나 Iterable 타입으로 추출하는 오퍼레이션을 가지고 있다.

Taco taco = tacoMono.block();
tacoRepo.save(taco);

block() 메서드는 추출작업을 수행하기 위해 브로킹 오퍼레이션을 실행한다.

Iterable<Taco> tacos = tacoFlux.toIterable();
tacoRepo.saveAll(tacos);

Flux의 데이터를 추출할 때는 toIterable()을 사용할 수 있다. WebFlux 컨트롤러가 Flux데이터를 받은 후 이것을 JPA레퍼지터리의 save()메서드를 사용해서 저장하면 된다.

Mono.block()이나 Flux.toIterable()은 블로킹이 되므로 리액티브 프로그래밍 모델을 벗어나므로, 가급적 적게 사용하는 것이 좋다.

 

블로킹되는 추출 오퍼레이션을 피하는 방법이 있다. Mono나 Flux를 구독하면서 발행되는 요소 각각에 대해 원하는 오퍼리션을 수행하는 것이다.

tacoFlux.subscribe(taco -> {
	tacoRepo.save(taco);
});

save() 메서드는 여전히 블로킹 오퍼레이션이다. 하지만 Flux나 Mono가 발행하는 데이터를 소비하고 처리하는 방식의 subscribe()를 사용하므로 블로킹 방식의 일괄처리보다는 바람직하다.

 

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

카산드라는 분산처리, 고성는, 상시 가용, 긍극적인 일관성을 갖는 NoSQL 데이터베이스이다.

카산드라는 데이터를 태이블에 저장된 행(row)으로 처리하며, 각 행은 일 대 다관계의 많은 분산 노드에 걸쳐 분할 된다. 한노드가 모든 데이터를 갖지는 않지만, 특정 행은 다수의 노드에 걸쳐 복제될 수 있으므로 단일장애점(한 노드에 문제가 생기면 전체가 사용 불가능)을 없애준다.

카산드라는 관계형 데이터베이스와 유사한 많은 개념을 공유하지만, 관계형 데이터베이스가 아니다.

 

카산드라의 리액티브 레퍼지토리 지원을 사용하려면 의존성을 추가해야 한다.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-cassandra-reactive</artifactId>
</dependency>

 

이 의존성을 추가하면 스프링 데이터 카산드라 라이브러리와 리액터 등의 의존성이 프로젝트에 추가된다. 그리고 라이브러리들이 classpath에 지정되므로 런타임 시에 해당 라이브러리들의 자동-구성이 수행된다.

최소한 레퍼지터리가 운용되는 키공간의 이름을 구성해야 하며,  키 공간을 생성해야한다.

 

cqlsh> creat keyspace tacocloud
        ... with replication={'class':'SimpleStrategy','replication_factor':1}
        ... and durable_writes=true;

cql쉘을 사용해 키스페이스를 생성한다. (예제를 위한 생성)

replication_fator가 1일 때는 각행의 데이터를 여러번 복제하지 않고 한벌만 유지함.

SimpleStrategy 복제전략은 단일 데이터 사용시에 좋다.  

 

spring:
  data:
    cassandra:
      keyspace-name: tacocloud
      schema-action: recreate_drop_unused

키 공간 생성 후에는 keyspace-name 속성을 구성해서 스프링 데이터 카산드라가 키공간을 사용하도록 설정한다.

username과 password속성을 입력하여  카산드라 클러스터를 사용할 수 있다.

이 외에도 contact-points와 port등의 설정을 변경할 수 있다.

 

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

카산드라는 관계형 데이터베이스와 많이 다르다는것에 유의해야 함

카산드라 테이블은 얼마든지 많은 열을 가질수 있다. 그러나 모든행이 같은 열을 같지 않고 행마다 서로 다른 열을 가질 수 있다.

카산드라 데이터베이스는 다수의 파티션에 걸쳐 분할된다. 테이블의 어떤 행도 하나 이상의 파티션에서 관리될 수 있다. 그러나 각 파티션은 모등 행을 갖지 않고, 서로 다른 행을 가질 수 있다.

 

카산드라 테이블은 파티션키와 클러스터링 키. 두 종류의 키를 갖는다. 각 행이 유지 관리되는 파티션을 결정하기 위해서 오퍼레이션이 각 행의 파티션 키에 수행. 클러스터링 키는 각 행이 파티션 내부에서 유지 관리되는 순서를 결정함.

 

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

@Data
@RestResource(rel = "tacos", path = "tacos")
@Table("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")
  private List<IngredientUDT> ingredients;

}

JPA 퍼시스턴스에서 클래스에 지정했던 @Entity 대신 @Table를 지정함.

id속성에 지정했던 @Id 대신 @PrimaryKeyColumn애노테이션을 지정함. id의 primaryKeyType.PARTITIONED 지정되어 있는데, 이것은 타코 데이터의 각행이 저장되는 카산드라 파티션을 결정하기 위해 사용되는 파티션 키가 id 속성이라는것을 나타냄.

@PrimaryKeyColumn type속성이 primaryKeyType.CLUSTERED로 설정 있는것은 파티션 내부에서 행의 순서를 결정하기 위해 사용됨.

 

ingredients 속성은 Ingredient객체 대신 IngredientUDT객체를 저장하는 List로 정의되어 있다. 

Ingredient 클래스는 @Table 애노테이션 사용으로 카산드라에 저장하는 엔티티로 매핑했기 때문에,  따라서 IngredientUDT(user Defined Type)과 같이 새로운 클래스를 생성해서 사용함.

 

@Data
@RequiredArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@UserDefinedType("ingredient")
public class IngredientUDT {
  private final String name;
  private final Ingredient.Type type;
}

따라서 IngredientUDT 클래스의 요구사항은 훨씬더 간단하다.  @UserDefinedType 애노테이션을 지정해 사용자 정의타입인 것을 알 수 있도록 하면 된다.

또한 IngredientUDT 클래스는 id속성을 포함하지 않는다. Ingredient의 id속성을 가질 필요가 없기 때문이다.

 

이하 책에서 설명된 Taco 클래스 속성 설명들은 중복되는 내용이라 생각되어 생략..

 

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

리액티브 카산드라 레퍼지터리를 작성할 떄는 두개의 기본 인터페이스인 ReactiveCassandrRepository나 ReactiveCrudRepository를 선택할 수 있다. 

만일 많은 데이터 추가를 원한다면 ReactiveCassandrRepository를 선택할 수 있다.

public interface IngredientRepository 
         extends ReactiveCrudRepository<Ingredient, String> {
}

IngredientRepository는 내부에 어떤 메서드도 정의하지 않았지만 ReactiveCrudRepository의 확장이므로 Flux나 Mono타입을 처리한다.

@GetMapping
public Flux<Ingredient> allIngredients() {
    return repo.findAll();
}

따라서 allIngredients()의 반환타입은 Flux로 변환되어야 한다.

다른 레퍼지터리 클래스 들도 확장 클래스만 변경하여 상용하면 된다.

 

카산드라의 특성상 관계형 데이터베이스에서 SQL로 하듯이 테이블을 단순하게 Where절로 쿼리 할 수 없다. 카산드라는 데이터 읽기에 최적화된다.

그러나 결과가 하나 이상의 열로 필터링되는 테이블 쿼리에는 매우 유용하므로 where절을 사용할 필요가 있다. 이때 @AllowFiltering 애노테이션을 사용하면 된다.

select * from users where username ='유저네임' allow filtering;

해당 애노테이션을 사용하면 쿼리 끝에 allow filtering이 붙은 쿼리가 수행되게 된다. 이는 '쿼리 성능에 잠재적인 영향을 준다는 것을 알고 있지만, 어쨋든 수행해야한다'는 것을 카산드라에 알려준다. 이 경우 카산드라는 where절을 허용하고 결과 데이터를 필터링 한다.

 

 

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

몽고DB는 NoSQL데이터베이스 중 하나로 문서형 데이터베이스이다. (카산드라는 테이블의 행으로 데이터를 저장하는 데이터베이스)

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

 

몽고DB를 스프링 데이터로 사용하는 방법은 JPA를 스프링 데이터로 사용하는 방법과 크게 다르지 않다.

즉, 도메인 타입을 문서 구조로 매핑하는 애노테이션을 도메인 클래스에 지정한다. 그리고 JPA와 동일한 프로그래밍 모델을 따르는 Repository Interface를 작성하면 된다.

 

몽고DB 사용 역시 의존성을 추가 해준다.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>

이 처럼 의존성을 추가하면 스프링 데이터 리액티브 몽고DB 지원을 활성화하는 자동-구성이 수행된다.

기본적으로 몽고DB는 27017 포트를 리스닝하는 것으로 간주한다. 그러나 테스트와 개발에 편리하도록 내장된 몽고DB를 사용할수 있다.

<dependency>
   <groupId>de.flapdoodle.embed</groupId>
   <artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>

Flapdoodle 내장몽고DB 의존성을 추가하면 인메모리 몽고DB를 사용을 제공한다.

data:
  mongodb:
    host: mongodb.tacocloud.com
    port:
    username:
    password:

위와 같은 설정을 통해 몽고DB 접속설정을 할수 있다.

 

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

도메인 타입을 문서로 매핑하기 스프링 데이터 몽고DB는 몽고DB에 저장되는 문서 구조로 도메인 타입을 매핑하는 데 유용한 애노테이션들을 제공한다. 그 중 아래 3개가 가장 많이 사용된다.
@Id : 지정된 속성을 문서 ID로 지정한다. Serializable 타입인 어떤 속성에도 지정할 수 있다.
@Document : 지정된 도메인 타입을 몽고DB에 저장되는 문서로 선언한다.
@Field : 몽고DB의 문서에 속성을 저장하기 위해 필드 이름(과 선택적으로 순서)을 지정한다.

세 개의 애노테이션 중에 @Id와 @Document는 반드시 필요하다. @Field가 지정되지 않은 도메인 타입의 속성들은 필드 이름과 속성 이름을 같은 것으로 간주된다.

@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
  }
}

여기서는 Ingredient가 몽고DB에 저장되거나 읽을 수 있는 문서 엔티티라는 것을 나타내기 위한 @Document 애노테이션을 지정하였다.

만약 문서엔티티가 다를 경우 아래와 같이 collection 속성을 사용해서 변경 할 수 있다.

@Document(collection="ingredients")

그리고 id속성에는 @Id가 지정되있다. 이것은 저장된 문서의 ID로 id 속성을 지정한다. String과 Long 타입을 포함해서 Serializable 타입인 어떤 속성도 사용 할 수 있다.

 

Taco 클래스도 애노테이션 설정을 변경한다.

@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;
}

Taco클래스의 @Id 지정타입을  기존 Long에서 String으로 변경했다.

이는  몽고DB의 경우 ID가 String 속성일 경우 값이 데이터베이스에 저장될 때 자동으로 ID 값을 지정해준다.(null일 경우)

@Field("customer")
private User user;

 

추가로 @Field를 사용하면 몽고DB에 저장되는 명칭이 변경된다.

 

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

몽고DB의 리액티브 레퍼지터리를 작성할 때는 ReactiveCrudRepository나 ReactiveMongoRepository를 선탣할 수 있다.

ReactiveCrudRepository는 새로운 문서나 기존 문서의 save() 메서드에 의존하는 반면,
ReactiveMongoRepository는 새로운 문서의 저장에 최적화된 소수의 특별한 insert() 메서드를 제공한다.

@CrossOrigin(origins="*")
public interface IngredientRepository 
         extends ReactiveCrudRepository<Ingredient, String> {
}

ReactiveCrudRepository를 확장한 리액티브 레퍼지터리이므로 메서드는 그냥 도메인 타입이나 컬랙션이 아닌 Flux나 Mono타입으로 도메인 객체를 처리한다.

예를 들어, findAll() 메서드는 Iterable 대신 Flux를 반환한다.그리고 findById() 메서드는 Optional 대신 Mono를 반환한다.

따라서 이 Reactive Repository는 엔드-to-엔드 Reactive flow의 일부가 될 수있다.

 

몽고DB의 문서로 Taco 객체를 저장하는 Repository를 정의해보자.

public interface TacoRepository extends ReactiveMongoRepository<Taco, String> {
    Flux<Taco> findByOrderByCreatedAtDesc();
}

ReactiveCrudRepository에 비해 ReactiveMongoRepository를 사용할 때의 유일한 단점은 바로 몽고DB에 특화되어 다른 DB에는 사용할 수 없다.

 

findByOrderByCreatedAtDesc()는 Flux를 반환이므로 결과의 페이징을 신경쓰지 않아도 된다. 대신에 take() 오퍼레이션을 적용하여 Flux에서 발행되는 처음 12개의 Taco 객체만 반환할 수 있다.
예를 들어, 최근 생성된 타코들을 보여주는 컨트롤러에서는 다음과 같이 코드를 작성할 수 있다.

Flux<Taco> recents = repo.findByOrderByCreatedAtDesc().take(12);

이 경우 결과로 생성되는 Flux는 12개의 Taco항목만 갖는다.

 

이 외 책에서 설명한 Order와 User는 중복되는 내용으로 생략..

 

요약

  • 스프링 데이터는 카산드라, 몽고DB, 카우치베이스, 레디스 데이터베이스의 리액티브 리퍼지터리를 지원한다.
  • 스프링 데이터의 리액티브 리퍼지터리는 리액티브가 아닌 리퍼지터리와 동일한 프로그래밍 모델을 따른다.
    단, Flux나 Mono와 같은 리액티브 타입을 사용한다.
  • JPA 리퍼지터리와 같은 리액티브가 아닌 리퍼지터리는 Mono나 Flux를 사용하도록 조정할 수 있다.
    그러나 데이터를 가져오거나 저장할 때 여전히 블로킹이 생긴다.
  • 관계형이 아닌 데이터베이스를 사용하려면 (NoSQL) 해당 데이터베이스에서 데이터를 저장하는 방법에 맞게 데이터를 모델링하는 방법을 알아야 한다.
반응형

댓글