해당 글은
스프링 부트 실전 활용 마스터
도서를 읽으며 정리한 내용입니다
Reactive Streams
는발행자(Publisher)
와구독자(Subscriber)
사이의간단한 계약
을 정의하는 명세- 일반적인 Pub-Sub 구조와 다르게,
구독자
가배압(backpressure)
을 통해서데이터를 pull을 하는 방식
을 통해데이터 양을 조절
하는 방식으로 구동
프로젝트 리액터(Project Reactor)
는Reactive Streams
의구현체
로 다음의 특징이 존재
- 논블로킹, 비동기 프로그래밍
- 함수형 프로그래밍 스타일
- 스레드를 신경 쓸 필요 없는 동시성
프로젝트 리액터(Project Reactor)
는구독(subscribe)
하기 전 까지는 실제로어떠한 연산도 수행되지 않는다
=>Spring WebFlux
가 개발자가 만든컨트롤러
를 통해 적절한 옵션과 함께구독
해서 로직이 수행된다
- 스프링 MVC / 스프링 WebFlux
- 스프링 MVC
Java Servlet API
기반 -> 기본으로블로킹
동작- 기본 WAS로
Apache Tomcat
사용- 요청마다 스레드가 필요한
Thread per request
방식- 스프링 WebFlux
backpressure
를 더한Pub-Sub
구조Request
를Event-Driven
로 처리해서적은 스레드로 핸들링
가능Non-Blocking I/O
방식- 기본 WAS로
Netty
사용
spring-boot-starter-webflux
- 스프링 웹플럭스 리액티브 웹 스택
- 스프링 부트 스타터 중 하나
- 프로젝트 리액터에 맞도록
네티(Netty)
를 감싼리액터 네티(Reactor Netty)
도 포함
->spring-boot-starter-reactor-netty
리액티브 프로그래밍
을 사용하려면모든 과정
이리액티브(Reactive)
여야 한다
=>api 호출
/DB 접근
등등
=> 만약하나라도 블로킹(Blocking)
으로 수행하면 리액티브가 무너지고,성능
이스프링MVC보다 떨어
진다
(리액터 기반 애플리케이션은,많은 수의 스레드가 없어서
대기상태가 자주 발생
할 것이기 때문)
- 사용자의 요청마다 스레드가 필요한,
스프링MVC의 thread-per-request
와 다르게,WebFlux는 스레드가 논블로킹
되어적은 스레드
로높은 효율성
을 낼 수 있다
- 리액티브 패러다임을 지원하는 DB
- 몽고디비(
MongoDB
)- 레디스(
Redis
)- 카산드라(
Cassandra
)- 엘라스틱서치(
ElasticSearch
)- 등
- 관계형 DB(
RDB
)를 사용할 수 없는 이유
- Java에서 RDB를 사용하려면 반드시
JDBC
가 필요JDBC
는블록킹 API
라서 결국은블록킹으로 동작
하기 때문!
(JDBC
/JPA
등 )- 추가로,
스레드를 효율적으로 사용
하기 위한스레드 풀(thread pool)
도 결국 스레드를 기다리는블록킹 방식
이다
- 필자의 20년 최고의 교훈 (
우선 참고
)
장비의 코어 수
보다많은 스레드
를 사용하는 것은 장점이 거의 없다코어 이상의 스레드
가 있으면,CPU 컨텍스트 스위칭
으로오버헤드가 증가
하여효율이 급격하게 떨어
지기 때문
R2DBC
?
리액티브 스트림
을 활용해서RDB에 연결
할 수 있는 명세- 이제 등장하고 있는 중이라서 앞으로 지켜볼 필요가 있다
(아직 사용에는불안정
)
- 리액티브 몽고 DB 의존성 (with,
maven
)/* 스프링 데이터 몽고DB를 포함, 리액티브 버전 몽고DB */ <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId> </dependency> /* 내장형 몽고DB 도구, 테스트에 주로 사용 */ <dependency> <groupId>de.flapdoodle.embed</groupId> <artifactId>de.flapdoodle.embed.mongo</artifactId> </dependency> /* 전통적인 몽고DB 드라이버 */ <dependency> <groupId>org.mongodb</groupId> <artifactId>mongodb-driver-sync</artifactId> </dependency>
[ 템플릿 패턴 ]
스프링 데이터
와NoSQL 데이터 스토어
표준화
- 원래, 모든
NoSQL 엔진
은 각각 다르고, 저마다특징과 장담점이 상충
해서 하나의 API로 표준화 하는 과정을 어렵다스프링 데이터(spring-data)
에서는템플릿 패턴(Template pattern)
을 통해 이것을 해결
=>타입 안전 방식
으로연산을 처리
하고, 다루기 복잡한 것들을추상화 하는 템플릿
으로 만든 후 사용
=>스프링 데이터
는데이터 스토어별 맞춤형 템플릿
이 존재
ex)mongoTemplate
,ReactiveMongoTemplate
등
=> 게다가,추상화
를 통해 개발자는유사한 API들을 통해서 데이터 스토어를 사용
할 수 있다
[ ReactiveCrudRepository ]
ReactiveCrudRepository
스프링 데이터 커먼즈(Spring Data Commons)
에 포함된 인터페이스를 통해편리하게 데이터 스토어를 사용
할 수 있다- 주의할 것은 모든 반환 타입이
Mono
/Flux
둘 중 하나
=>Mono
,Flux
를 구독하고 있다가 데이터가 준비되면 데이터를 받게되기 때문public interface ItemRepository extends ReactiveCrudRepository<Item, String>, ReactiveQueryByExampleExecutor<Item> { Flux<Item> findByNameContaining(String partialName); }
[ 테스트 데이터 로딩 ]
- 테스트 데이터 로딩
- 애플리케이션이 시작될 때 테스트 데이터를 넣는 과정
CommandLineRunner
를 통해애플리케이션 시작된 후 자동으로 수행
되도록 데이터를 넣을 수 있다비동기 방식
으로 데이터를 넣으면, 애플리케이션 시작인네티(Netty)가 시작
되면서,구독자
가애플리케이션 시작 스레드
로 하여금이벤트 루프
를데드락(deadlock) 상태
에 빠트릴 수 있다고 한다
=>MongoTemplate
를 통해스프링 부트
와스프링 데이터 몽고디비
자동설정이 가능하다
=> 애플리케이션과 몽고디비의결합도를 낮추기 위해
MongoTemplate의 추상체
인MongoOperations
를 사용@Component public class TemplateDatabaseLoader { @Bean /* ReactiveMongoTemplate */ CommandLineRunner initialize(MongoOperations mongo){ return args -> { mongo.save(Item.builder().name("쿠버네티스 / 도커").price(34000).build()); mongo.save(Item.builder().name("Kotlin in Action").price(36000).build()); }; } }
[ addToCart ]
@Slf4j @Service @RequiredArgsConstructor public class CartService { private final ItemRepository itemRepository; private final CartRepository cartRepository; public Mono<Cart> addToCart(String cartId, String id){ /* 해당하는 Cart를 찾아온다 */ return cartRepository.findById(cartId) /* Cart가 없으면, 새로 카트를 만들도록 설정 */ .defaultIfEmpty(Cart.builder().id(cartId).build()) /* Cart가 있으면, 가지고 있는 아이템들을 순회 */ .flatMap(cart -> cart.getCartItems().stream() /* filter를 통해서, 넣으려는 아이템 id와 같은지 검사 */ .filter(cartItem -> cartItem.getItem() .getId().equals(id)) /* 넣으려는 아이템 id와 같은게 있으면 map() 아니면, orElseGet()으로 이동 */ .findAny() /* 아이템이 있으면 수를 증가시킴 */ .map(cartItem -> { cartItem.increment(); return Mono.just(cart); }) /* 넣으려는 아이템이 있으면 수가 없으니 새로만들어서 Cart에 추가 */ .orElseGet(() -> itemRepository.findById(id) .map(CartItem::new) .doOnNext(cartItem -> cart.getCartItems().add(cartItem)) .map(cartItem -> cart) )) /* 새로운 Cart로 다시 저장 */ .flatMap(cartRepository::save); } ...
리액티브 프로그래밍
에서함수형 프로그래밍
을 사용하는 이유
부수 효과(side effect)
를발생시키지 않는 것
이 핵심
=> 로직중간의 상태를 만들지 않아서
잘못 설정하거나, 변경하는 경우가 사라짐
=>순수함수
를 통한함수형 프로그래밍 방식
은부수효과를 발생시키지 X
[ search - V1 ]
- Search V1
스프링 데이터 JPA
처럼,일정 규칙
안에서 편리하게 데이터 저장소 기능 사용 가능메소드 이름
에Containing
을 포함해서검색 로직
을 수행public interface ItemRepository extends ReactiveCrudRepository<Item, String> { /* Containing으로 포함되어 있는지 검색을 수행 */ Flux<Item> findByNameContaining(String partialName); }
[ search - V2 ]
- Search V2
Example 쿼리
를 통해서search 기능
을 구현
- 여러
조건을 조립
해서스프링 데이터
에게 전달- 스프링 데이터는
필요한 쿼리문
을내부적
으로 만들어 줌/* 추가적으로 ReactiveQueryByExampleExecutor<T>를 상속 받아야 Example 쿼리 사용 가능 */ public interface ItemRepository extends ReactiveCrudRepository<Item, String>, ReactiveQueryByExampleExecutor<Item> { ... } /* Example 쿼리를 통한 Search 수행 */ public Flux<Item> searchByExampleV2(String name, String description, boolean useAnd){ Item item = Item.builder().name(name).description(description).price(0.0).build(); ExampleMatcher matcher = (useAnd ? ExampleMatcher.matchingAll() :ExampleMatcher.matchingAny()) .withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING) .withIgnoreCase() .withIgnorePaths("price"); Example<Item> probe = Example.of(item, matcher); return itemRepository.findAll(probe); }