[세미나 정리] if(kakao) dev 2018 - 스프링5 웹플럭스와 테스트 전략

반응형
728x90
반응형

해당 세미나 강의를 보고 정리 - https://tv.kakao.com/channel/3150758/cliplink/391418995

 

 

스프링 웹플럭스

  • 스프링 5.0에 새로 등장한 (새로운) 웹 프레임워크 + 리액티브 스택
    • SpringMVC 이후 15년만에 처음으로 등장한 신규 웹 프레임워크 
  • 초기 이름은 스프링 웹 리액티브였고, 웹플럭스로 명칭이 변경되었다.

 

 

스프링 웹플럭스 vs MVC

  • Spring WebFlux - 리액티브 스택 
  • Servlet은 사용 방식은 다르지만 Reactive Stack, Servlet Stack 모두 포함되어있다.
  • JPA(블록킹) 등은 Reactive Stack에는 포함되어있지 않다.
  • Reactor (Reactive Library) 라이브러리 : 웹플럭스의 플럭스는 리액터 라이브러리 안에 존재하는 가장 대표적인 인터페이스 이름이다. 이 Reactor는 Reactive Stack에는 필수, Servlet Stack에는 옵션으로 포함되어있다.

 

 

스프링 웹플럭스와 MVC가 공유하는 부분과 고유한 부분

 

 

코드로 알아보기

▶ 스프링 MVC - Hello API

RestController 클래스의 일부 

 

위 코드를 스프링 웹플럭스로 바꿔보자.

 

위 2개의 코드는 동일한 코드다.

 

Flux 인터페이스 사용 코드로 변경해보자.

 

 

Json Stream으로 리턴하는 방식의 코드를 보자. - HTTP Stream

위 코드로 봤듯이, WebFlux - Mvc 구현 코드는 동일하다. 도대체 스프링 MVC와 스프링 웹 플럭스는 뭐가 다를까?

1) 웹 플럭스의 @Controller 코드는 스프링 MVC의 코드와 동일한 방식으로 작성이 가능하다.

 

2) 웹 플럭스의 @Controller 코드에는 스프링 MVC에서 지원하지 않는 기능이 있다.

 

 

그렇다면 스프링 웹플럭스의 도입 이유는 무엇일까?

1) 100% 논블록킹 개발

2) 확장성과 고효율성이 매우 중요

3) 업, 다운 스트리밍과 Back pressure가 필요

  • Back pressure(배압) : 다이나믹 풀 방식의 데이터 요청을 통해서 구독자가 수용할 수 있는 만큼 데이터를 요청하는 방식

4) 고속 서비스 오케스트레이션 개발

  • 이벤트적으로 발생하는 로직을 개발할 경우 

5) 유사한 프로그래밍 모델의 경험

  • Node.js 등과 같은 언어와 비슷하게 개발하고 싶은 경우 

6) 유연하게 커스터마이징이 가능한 웹 프레임워크 구성

7) 본격적인 함수형 프로그래밍 모델 사용 

 

 

반대로, 스프링 웹플럭스를 사용하지 않는게 좋은 이유는 무엇일까?

1) 웹플럭스가 왜 필요한지 분명하게 모르는 경우

2) 블록킹이 서버 코드, 라이브러리에 존재하는 경우

  • JPA, JDBC 등은 블로킹

3) SpringMVC로 개발했더니 아무 문제 없는 경우 

 

 

스프링 웹플럭스는 스프링 MVC로 시작해도 된다.

▶ 스프링5 MVC는 웹 플럭스에서 제공되는 다양한 기능과 프로그래밍 모델 제공

  • 비동기/논블록킹 API 호출
  • 비동기/논블록킹 데이터 액세스
  • 리액티브 데이터 조회, 전송
  • 비동기 웹 요청 처리
  • 서버 스트리밍
  • 클라이언트 스트리밍
  • Reactor(Flux, Mono), RxJava, Flow 등을 이용하는 코드

▶ MVC에서 WebClient 사용이 가장 좋은 출발점 

  • RestTemplate를 대체하여 WebClient를 사용한다.

 

 

리액티브(함수형) 프로그래밍

인터넷 시대의 복잡함을 해결하기 위해 연속적으로 일어나는 이벤트를 다루는 프로그래밍 기법 

1) UI 이벤트, 비동기적인 I/O 이벤트, 통제 불가능한 이벤트 스트림 처리

2) 동시성, 비동기/논블록킹 호출을 다루는데 탁월

  • 모든 종류의 IO가 일어나는 것들을 비동기/논블록킹으로 하겠다. 내가 원격 API를 하나 호출하고, 응답이 들어올때까지 이를 호출했던 CPU를 낭비하지 않고, 이 쓰레드를 다른 곳에 바로 사용할 수 있도록 하여 서버의 자원을 효율화 시킨다. 적은 리소스만으로도 더 많은 서버 요청을 처리할 수 있다.

3) 조합 가능한 비동기 로직을 다루는 함수형 프로그래밍 

 

비동기/논블록킹 API 호출 - 가장 단순한 리액티브

● 동기/블록킹 API 호출 - RestTemplate

이를 사용하여 API를 호출한 스레드는 이 응답이 들어올때까지 계속 대기중이다. 응답이 와야 쓰레드가 다음 작업을 수행한다.

  • 장점 : 쉽고 간단함
  • 단점 : I/O 동안 블록킹 (시스템 특성에 따라 매우 비효율적이 될 수도 있다.)

 

[예제]

findUserApi(), getOpenOrders() 두 메서드가 RestTemplate 를 사용해서 외부 API를 호출하는 로직이라고 가정하자.

 

 

● 비동기/논블록킹 API 호출

  • AsyncRestTemplate(Spring4)
  • Async/Await (Java8+)
  • WEbClient (Spring5)
  • 장점 : 확장성이 뛰어나고 높은 처리율과 낮은 Latency를 가지는 효율적인 서버 구성이 가능하다.
  • 단점 : 장점을 얻을만한 경우가 많지 않고, 자칫하면 코드가 복잡하고 이해하기가 어렵다.

 

[예제] 콜백

  • 중첩 콜백이 생성되는 구조다. 

  • 매 단계마다 반복되는 예외 콜백 코드가 존재하게되는 단점이 있다.

 

 

JAVA8부터는 이를 해결하기 위해 CompletetableFuture가 등장했다.

1) 비동기 결과처리 함수를 이용하여, 콜백이 중첩되지 않는다.

2) 에러 처리도 단일화된다.

 

 

Async/Await 사용 로직

비동기적으로 I/O가 일어나는 코드를 마치 동기 방식으로 결과를 리턴값으로 받는것처럼 코드를 작성할 수 있다. 기존에 익숙했던 RestTemplate 코드를 작성하는것과 같이 코드를 작성하여 비동기/논블록킹의 장점을 적용시킬 수 있다.

 

 

Reactor Flux/Mono 사용 코드

● CompletableFuture와 유사해보이지만 Flux/Mono가 가진 큰 장점이 있다.

Mono

어떤 이벤트적으로 (비동기적으로) 데이터가 넘어올때 데이터가 0개 또는 1개다. 리턴값을 최대 1개 받을 수 있다.

 

Flux

이벤트적으로 전달되는 데이터를 스트림으로 지속적으로 여러개를 받을 수 있다.

 

여러개 받는 방법은 2가지가 있는데, List 또는 Collection에 담아서 Mono에 한꺼번에 넣어서 넘기는 방법이 있고, 이 원소들을 하나씩 쪼개서 하나씩 넘기는 Flux 방법이 있다.

 

 

CompletableFuture vs Reactor Flux/Mono

1) 공통점
  • 람다식을 사용하는 함수형 스타일
  • 비동기와 비동기 작업의 조합 (componse, flatMap)
  • 비동기와 동기 작업의 적용 (apply, map)
  • Exceptional 프로그래밍
  • 작업별 쓰레드 풀 지정 가능

 

2) Flux/Mono 방식의 장점
  • 데이터 스트림(Flux) <-> List/Collection
  • 강력한 연산자 제공
  • 지연 실행, 병합, 분산, 시간 제어
  • 유연한 스케줄러
  • ReactiveStreams, JAVA9+ 표준
  • 다양한 지원 라이브러리, 서비스, 서버

 

 

스프링5 웹'플럭스와 테스트'

리액티브 API의 데이터 시퀀스 검증

  • 리액티브 API : [Service 메서드]
    • 논블록킹/비동기 방식으로 데이터를 리턴하는 메서드(Mono, Flux)도 리액티브 API라고 한다.

 

 

Flux/Mono를 반환하는 리액티브 API 테스트

1) 비동기 코드를 검증하는 "동기 방식" 테스트

  • 대신, 테스트하려는 대상은 비동기/논블록킹 특성을 가진다.

2) 비동기/논블록킹의 특성에 주의

3) 블록킹/동기화가 필요 

1) subscribeOn(Schedulers.single())

별도의 스레드로 수행시킨다. 

 

2) subscribe()

subscribe()로 받은 데이터를 검증한다.

 

위 코드는 심각한 문제점이 있다.

위 테스트 코드는 테스트가 '성공'한다. 1이라는 값을 하나 던지는 스트림을 만들었고, 1인지를 검증했으므로 성공이 맞다고 생각한다. 그러나 아래처럼 코드를 고쳐도 테스트가 성공한다.

1) subscribeOn(Schedulers.single()) 을 다시보자.

별도의 스레드로 수행시킨다. Scheduler는 멀티스레드 프로그래밍을 추상화한 방식으로, 그 이후로 위에 정의한 Mono로부터 아이템이 넘어오는 부분을 별도의 데몬스레드로 실행을 시키라는 의미다.

하지만 2)번의 subscribe() 뒤에 람다 코드로 item이 넘어오는 작업이 일어나려면 이 테스트 애플리케이션의 main 스레드가 계속 돌아가고 있어야하는데 이는 모두 논블록킹이기 때문에 subscribe()까지의 선언이 끝나고나면 main 스레드가 종료되고, 데몬 스레드도 같이 종료되버린다.

 

assert 문을 검증할때까지 블록킹을 시켜야한다.

1) CountDownLatch

CountDownLatch 가 0이 될때까지 await()이 계속 실행되고 await() 실행하는 해당 스레드가 wait 상태에 빠진다. 2개의 스레드가 동작하는데 1개는 테스트 코드 로직을 수행하고, 다른 1개는 Mono 로직을 타고 온다. 여기서 문제는, assertThat 검증 후 countDown()을 호출해서 종료시키려고 하는데, 테스트가 성공하지 못한다면 아래 countDown()이 실행되지 않아서 테스트가 영원히 끝나지 못한다.

 

위 문제점은 해결은 되었지만, '1'을 검증하는 테스트 코드가 매우 장황한 느낌이다. 

 

더 간단한 방법을 보자.

2) block()

논블록킹으로 돌아가는 코드를 어느 시점에 강제로 블록킹으로 변경한다. block()으로 데이터를 받아서 처리한다. 하지만 이 방법도 데이터가 여러개 넘어온다면 좋은 방식은 아니다. 이는 데이터 스트림이 종료될때까지 대기하는 방법이다.

 

이 방법의 단점 및 한계는 다음과 같다.

  • 동기화/블록킹이 필요
  • Flux를 검증하려면 코드가 매우 복잡해진다.
  • 시간과 간격에 대한 검증이 어렵다.

 

 

StepVerifier

  • Reactor 라이브러리에 존재한다.
  • 비동기 논블록킹으로 동작하는 코드 테스트 툴
    • 비동기/논블록킹으로 동작하는 코드를 테스트하는 코드의 모든 동기적인 이슈를 자동으로 처리해준다.
  • 데이터 스트림의 검증
    • 데이터 스트림의 완료 여부 등의 검증도 완벽하게 할 수 있다.
  • 예외, 완료로 검증
  • 가상시간을 이용해 오랜 시간의 이벤트 테슽 
    • 리액티브 코드를 만들었을대 어떤 센서로부터 하루 종일 들어오는 데이터를 모두 수집해서 하루치의 데이터를 모ㅓ은 다음 무언가를 하려고 하는 코드를 만들었다. 이 코드를 테스트하려면? 하루가 걸려야한다. 이런 경우 이 코드가 하루치의 데이터를 다 돌아가는 것처럼 시간을 조작하는 방법이 있다. 이러한 방법을 손쉽게 할 수 있게 적용해준다.
[예시1 - 단건 데이터]

 

1) StepVerifier 생성

2) 첫번째 데이터 아이템 값 

3) 스트림 완료

 

[예시2 - 여러 데이터 전송]

 

 

원격 리액티브 API 호출 테스트 

1) RestTemplate

  • 동기/블록킹

 

2) AsyncRestTemplate

  • 비동기/논블록킹
  • Future - 콜백, CompletionStage

 

3) WebClient

  • 비동기/논블록킹
  • Flux/Mono 요청, 응답
  • Streaming 지원
  • BackPressure 지원 
[Webclient 예제]

1) .uri

원격 API 요청을 준비한다.

 

2) retrieve()

원격 API를 실행한다.

 

3) onStatus

특정 응답 코드에 따라서 에러로 전환할 수 있다.

 

4) bodyToMono

예외가 없는 경우 API 응답 body를 변환한다. 

 

5) map

결과에 비즈니스 로직을 적용한다. 넘어온 데이터를 대문자로 변환하는 로직이 있다.

 

6) switchIfEmpty

예외적인 결과 대응을 한다. 데이터가 없을 경우를 대응할 수 있다.

 

 

Streaming

순차적으로 넘어오는 데이터를 하나씩 브라우저에 출력되는 예제다.

 

위 코드의 리팩토링을 아래와 같이 할 수 있다.

 

 

원격 리액티브 API 호출 - 러닝서버 통합테스트

Flux 또는 Mono를 리턴한다. 그러므로 StepVerifier 을 사용해서 테스트 코드를 작성할 수 있다. 이는 원격 서버가 존재해야 테스트가 가능하다. 

 

 

원격 리액티브 API 호출 - 단위테스트

단위 테스트를 짜기위한 코드는 너무 복잡하다.

 

대신, 통합 테스트를 진행하자.

 

 

원격 리액티브 API 호출 - 통합테스트 (Mock Server)

okhttp3를 사용해서 간단하게 코드를 작성할 수 있다.

 

1) MockWebServer의 응답 준비

 

2) WebClient 코드 실행

 

3) 응답 결과 검증

 

4) Mock Server에 어떤식의 커뮤니케이션이 있었는가를 검증

 

 

스프링5 웹플럭스의 새로운 아키텍처

1) 기존 MVC는 서블리 스펙과 서버의 제약 위에서 개발한다.

  • 프론트 컨트롤러 패턴, MVC 패턴, 전략 패턴 

2) 웹플럭스는 독자적인 아키텍처를 가지는 프레임워크

  • 서블릿(3.1+) 컨테이너를 사용할 수 있으나 의존적이지 않음
  • Netty 등 서버 지원
  • 논블록킹 네트워크/논블록킹 API
  • 논블록킹 데이터 스트림
  • 함수형 엔트포인트
  • 서버/기술에 의존적이지 않은 프레임워크 재구성이 편리 
  • 뛰어난 테스트 편의성

 

 

웹 플럭스의 새로운 아키텍처 - 전체 구조

 

Functional Endpoint

Spring Application Context, Container 등 없이 순수한 함수형 코드로만 웹 프레임워크를 만들 수 있다.

[예제]

1) rout(람다식)

웹 요청을 담당할 함수 핸들러를 찾는다.

웹 요청이 들어오면 이 함수가 웹 요청에 대해 맞는 API로 라우팅한다.

 

2) PersonHandler {...}

ServerRequest -> ServerResponse로 변환하는 리액티브 핸들러다.

1)번을 통해 찾아진 핸들러 함수는 클래스 안의 하나의 메서드다. 이 메서드로 넘어온 ServerRequest 타입의 HTTP 요구사항을 변환한다.

 

 

● 스프링 컨테이너 없이 아래 코드 수행이 가능하다.

 

위 코드를 어떻게 테스트 할것인가? - TestWebClient

  • 테스트 대상 구성이 가능한 리액티브 HTTP (Mock) 테스트 도구
  • WebClient와 동일한 방식
  • SpringBoot - @WebFluxTest

 

하나의 서버에 해당하는 Application을 Mock Server 위에다 띄우고 TestWebClient를 사용해서 테스트 가능하다.

[예제]

1) webTestClient

WebClient처럼 API를 호출한다. API 호출 응답 결과 검증이 가능하다.

 

 

실제 서버 연결도 가능하다. - bindToServer

 

 

반응형

Designed by JB FACTORY