# 1, 2장 - 스프링부트 웹 애플리케이션 & 데이터 엑세스

김정욱·2021년 10월 22일
0
post-thumbnail

해당 글은 스프링 부트 실전 활용 마스터 도서를 읽으며 정리한 내용입니다

1장 : 스프링부트 웹 애플리케이션 만들기

[ Reactive Streams ]

  • Reactive Streams발행자(Publisher)구독자(Subscriber) 사이의 간단한 계약을 정의하는 명세
  • 일반적인 Pub-Sub 구조와 다르게, 구독자배압(backpressure)을 통해서 데이터를 pull을 하는 방식을 통해 데이터 양을 조절하는 방식으로 구동

[ Project Reactor ]

  • 프로젝트 리액터(Project Reactor)Reactive Streams구현체로 다음의 특징이 존재
    • 논블로킹, 비동기 프로그래밍
    • 함수형 프로그래밍 스타일
    • 스레드를 신경 쓸 필요 없는 동시성
  • 프로젝트 리액터(Project Reactor)구독(subscribe)하기 전 까지는 실제로 어떠한 연산도 수행되지 않는다
    => Spring WebFlux가 개발자가 만든 컨트롤러를 통해 적절한 옵션과 함께 구독해서 로직이 수행된다

[ WebFlux ]

  • 스프링 MVC / 스프링 WebFlux
    • 스프링 MVC
      • Java Servlet API 기반 -> 기본으로 블로킹 동작
      • 기본 WAS로 Apache Tomcat 사용
      • 요청마다 스레드가 필요한 Thread per request 방식
    • 스프링 WebFlux
      • backpressure를 더한 Pub-Sub 구조
      • RequestEvent-Driven로 처리해서 적은 스레드로 핸들링 가능
      • Non-Blocking I/O 방식
      • 기본 WAS로 Netty 사용
  • spring-boot-starter-webflux
    • 스프링 웹플럭스 리액티브 웹 스택
    • 스프링 부트 스타터 중 하나
    • 프로젝트 리액터에 맞도록 네티(Netty)를 감싼 리액터 네티(Reactor Netty)도 포함
      -> spring-boot-starter-reactor-netty

2장 : 스프링 부트를 활용한 데이터 액세스

[ 데이터 액세스 개요 ]

  • 리액티브 프로그래밍을 사용하려면 모든 과정리액티브(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>

[ Repository 만들기 ]

[ 템플릿 패턴 ]

  • 스프링 데이터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());
        };
    }
}

[ Service 만들기 ]

[ 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);
    }
profile
Developer & PhotoGrapher

0개의 댓글