최근 MSA 공부를 하면서 마주하게된 WebFlux 라는 것을 파볼까한다. 쓰레드 중심의 개발을 한 적이 많이 없어서 사실 좀 생소하고 이해가 안 가는 부분들이 많아 여러 글을 거쳐 천천히 정리해볼 예정이다.
Spring Webflux는 Spring 5(Spring boot2)부터 새롭게 추가된 모듈로 reactive-stack의 웹 프레임워크이며 non-blocking에 reactive stream을 지원한다.
위 그림은 Spring Boot2가 지원하는 Reactor에 대한 내용인데 Reactive 프로그래밍을 위한 핵심 모듈이라고 보면 된다.
리액티브를 사용하는 이유는 비동기/논블로킹을 이용해 더 적은 자원으로 더 많은 트래픽을 처리하기 위함이다.
그러면 위에서 계속 언급하는 blocking 과 non-blocking은 도대체 무엇이냐. 단어 자체만 봤을 때도 뜻은 대충 유추할 수 있지만 정확히 어떤 식으로 돌아간다는건지 알아볼 필요가 있겠다.
먼저 다 알고 있는 동기, 비동기 개념을 한 번 짚고 넘어가보자.
함수를 호출한 곳에서 응답받는 것
즉, 이 말은 데이터의 요청과 결과가 동일한 자리에서 일어난다는 뜻이다. 그니까 현재 작업이 끝날 때까지는 그 자리를 떠나지 않는다는거다.
위 그림에서 1번이 끝날 때 까지 2번 작업을 시작하지 않는 이유가 그것이다. 이렇게하면 설계가 직관적이라 만드는 사람은 편할거다.
하지만, 단점은 하나의 작업이 끝날 때까지 다른 작업이 시작할 수 없다. 또, 트래픽이 몰리면 작업을 실행할 때 만들어지는 Thread들이 다량으로 사용 요청을 받게되어 Thread pool에서 사용가능한 스레드가 적어지게 된다. 이는
함수를 호출하는 곳에서 결과를 기다리지 않고, 다른 함수(Callback)에서 결과를 처리하는 것
비동기의 경우는 서로 다른 두 주체가 서로의 시작이나 종료에 관계없이 별도의 시작, 종료 시간을 가지고 있다. 그래서 결과를 기다리지 않고 다른 작업을 할 수 있게 된다.
뭐 어떻게 돌아가는지는 알겠다. 그러면 Blocking은 뭘까? blocking과 non-blocking은 주로 I/O 작업 시 사용된다.
자신의 작업을 진행하다가 다른 주체의 작업이 시작되면 다른 작업이 끝날 때까지 기다렸다가 자신의 작업을 시작하는 것
예를 들어, 함수A가 함수B를 호출하면, 함수B가 끝날 때까지 함수A는 기다려야한다. 이는, 함수B의 결과를 함수A에서 처리하는 것과 같은 말이다. 여기서 제어권
은 자신의 함수 코드를 실행할 권리 같은 것이다. 제어권이 다른 함수에게 가면 그 동안은 함수를 실행할 수 없다.
다른 주체의 작업과 관련없이 자기 자신은 작업을 계속 진행하는 것
위와 같은 예로, 함수A가 함수B를 호출해서, 함수B의 작업이 다 끝나지 않았더라도 함수A는 자기 일을 계속할 수 있다. (제어권을 넘겨주지 않아도 된다.)
대충 설명만 보면 blocking은 동기적으로 보이고, non-blocking은 비동기적으로 보인다. 하지만, 이 두 개념(동기 비동기/blocking non-blocking)에는 서로 다른 관점으로 접근하고 있다.
동기와 비동기는 호출되는 함수의 작업 완료 여부를 누가 신경쓰느냐의 관점이고 blocking/non-blocking은 호출되는 함수가 return을 하느냐 마느냐의 관점이다.
즉, 이 말의 전자는 함수 실행/리턴의 순차적 흐름이 중요한 것이 되겠고, 후자는 제어권이 누구에게 있는지 중요하다.
결국, 이 메커니즘들은 서로 다르기 때문에 여러 방향으로 조합해 사용할 수 있다. 더 자세히 알아보자.
함수 A는 함수 B의 리턴값이 필요하고(동기), 제어권을 함수B에게 넘겨주고 완료될 때까지 기다렸다가(블로킹) 리턴값과 제어권을 돌려받는다.
👍 Java에서 다음과 같이 DB에서 데이터를 불러오는 작업들이나, 컬렉션에 데이터를 담기 위한 함수 호출 등이 그 예 이다.
public UserOrder order(String email) { try{ User user = findUserApi(email); List<Order> orders = getOpenOrders(user); return new UserOrder(email, orders); }catch(Exception e) { return UserOrder.FAIL; } }
함수A는 함수B에게 제어권을 주지 않고, 자신의 코드를 계속 실행한다(논블로킹), 하지만, A함수는 B함수의 리턴 값이 필요하기 때문에, 계속 함수B에게 함수 완료 여부를 물어본다.(동기)
👍🏼 게임에서의 데이터 로드율 표시하기
맵을 이동할 때 맵 데이터를 계속 물어본다. 하지만,제어권은 계속 나한테 있어 화면에 로드율이 표시된다.
함수A가 함수B를 호출 했을 때, 제어권을 함수B에게 주지 않고, 자신이 계속 갖고 있는다. (논블로킹)
그리고 함수B를 호출할 때 콜백함수를 함께 주는데, 함수B는 완료 후 함수A가 준 콜백함수를 실행한다.(비동기)
👍 JS 비동기 콜백
frontend에서 backend로 API 요청을 보내고, 응답을 기다리지 않은 채 자신의 작업을 처리한다.
아래는 위에서 예시로 든 상황을 비동기적으로 구현한 것이다.
public DeferredResult<UserOrder> asyncOrders(String email) {
DeferredResult dr = new DeferredResult();
asyncFindUserApi(email).addCallback(
user -> asyncOrdersApi(user).addCallnack(
orders -> {
dr.setResult(new UserOrder(email, orders));
},
e -> dr.setErrorResult(UserOrder.FAIL)
),
e -> dr.setErrorResult(UserOrder.Fail)
);
}
//2. CompletableFuture
public CompletableFuture<UserOrder> asyncOrders(String email) {
return asyncFindUser(email)
.thenCompose(user -> asyncOrders(user))
.thenApply(orders -> new UserOrder(email, orders))
.exceptionally(e -> UserOrder.FAIL);
}
//3. Reactor Flux/Mono
public Mono<UserOrder> asyncOrder(String email) {
return asyncFindUser(email)
.flatMap(user - asyncGetOrders(user))
.map(orders -> new UserOrder(email, orders))
.onErrorReturn(UserOrder.FAIL);
}
참고로 3번 Reactor Flux/Mono 방법은 이 본 포스팅의 WebFlux와 관련된 데이터 타입이다. 이 부분은 다음 글에서 자세히 다뤄보도록 하겠다.
함수A는 함수B의 리턴 값과 상관없이 콜백함수를 보낸다(비동기), 하지만 이 때 제어권도 같이 넘기기 때문에(블로킹) 함수A는 자신과 관련없는 함수B의 작업이 끝날 때 까지 기다려야한다.
알기 쉬운 예시랑 같이 적어놓으신게 굉장히 좋네요