2. Async & Non-Blocking
시간이 지나서 제 글을 다시 읽어보니 이 글은 적절한 예제는 아닌 것 같습니다. 그냥 넘어가주세요
"Project Reactor" 시리즈의 두 번째 글입니다. 하지만, 이번 글에서는 "Reactor"에 대해서 거의 다루지 않을 예정입니다. "Reactor"에 대한 내용을 기대하셨다면 죄송합니다. 마음이 급하신 분은 공식 레퍼런스를 참고하시길 바랍니다. 이 글에서는, Reactor를 공부하기 전에 알아야 하는 기본 개념을 공부할 예정입니다.
1. 리액티브 프로그래밍
2. Async VS Non-Blocking (현재 글)
3. Reactive Streams
4. Project Reactor Flux, Mono Basic
5. Project Reactor Subscriber
6. Project Reactor Data Processing
7. Project Reactor Create, Generator
8. 미정
목차 미정
목차 미정
추후에 목차는 변경될 수 있습니다.
이번 장에서는, Async(비동기)와, Non-Blocking(논블록킹) 기본 개념과 사례를 검토한다. 또한, Reactor 가 왜 필요한지에 대해서 정리하겠다.
Async 와 Bon-Blocking 의 차이에 대해서는, 개발자가 보는 관점에 따라서 다르게 정의할 수 있습니다. 필자의 정의가 마음에 들지 않으시다면 굳이 이 글은 안 보셔도 됩니다.
동기와 비동기는 통신 매커니즘을 설명한다. 동기는 함수를 호출했을 때 결과가 나올 때까지 어떤 것도 리턴하지 않는다. 그래서 함수를 호출하는 곳에서는 해당 결과 값이 나올 때까지 기다릴 수밖에 없다. 하지만, 비동기는 함수가 완료할 때까지 함수를 호출하는 곳에서는 함수를 계속 기다릴 필요가 없다. 왜냐면, 함수는 일단, 결과값없이 즉시 바로 리턴을 먼저 하고, 그 이후에 함수 처리가 완료되면, 콜백 함수 또는 다른 방법을 사용하여 최종 결과 값을 다시 알려주기 때문이다.
동기/비동기 와는 달리, 블록킹/논블록킹은 함수를 호출하고 결과를 기다리는 동안에 함수를 호출하는 프로그램의 상태에 초점을 맞춘다. 블록킹은 결과값을 얻기 전에 현재 스레드를 정지시킨다. 즉, 블록킹 작업은 현재 스레드가 결과 값을 받을 때까지 아무것도 안 하고 기다리게 된다. 반면에, 논블록킹 작업은 스레드가 잠금이 되지 않는다. 즉, 함수를 호출하고 차단이 되지 않기 때문에, 결과를 받을 때까지 다른 작업을 수행할 수 있다.
개발자들이 자주 착각하는 것이, Aysnc 일 때 반드시 Non-Blocking 이라고 생각한다. 하지만, 아래와 같이 4가지 경우가 존재한다.
Sync(동기) & Blocking(블록킹)
Sync(동기) & Non-Blocking(논블록킹)
Async(비동기) & Blocking(블록킹)
Async(비동기) & Non-Blocking(논블록킹)
억지로, 사례를 만들어보겠다.(참고로.. 필자의 예시가 적절한지 확신이 없다.) 첫 번째 사례는 스타벅스에서 커피를 주문하는 과정을 생각해보자. 스타벅스 점원이 호출을 받아서 결과를 전달해주는 함수이고, 스타벅스 고객이 함수 호출을 하는 프로그램이라고 생각하자.
고객(함수 호출을 하는) --> 스타벅스 점원(함수를 제공해주는)
Sync (동기)
고객이 커피를 주문하면, 스타벅스 점원은 커피 재고가 있는지 확인하고 카드결제가 될 때까지 고객에게 앞에서 기다리라고 한다. 주문이 완료되면 결제된 카드와 영수증, 진동벨을 돌려준다. 고객은 점원이 결과를 줄 때까지 앞에서 계속 기다려야 한다.
Async (비동기)
고객이 커피를 주문하면, 스타벅스 점원은 일단 고객에게 편한 자리에 앉아서 기다리라고 한다. 고객이 자리에 앉아서 기다리는 사이, 점원은 커피 재고가 있는지 확인하고 카드결제를 완료하고, 고객이 앉아 있는 자리로 찾아가서 영수증, 진동벨을 전달하면서 주문이 완료되었다고 전달한다. 커피가 완료되면 다시 자리로 찾아와서 커피를 가져다줄 것이다. 여기서 중요한 것은, 점원이 고객에서 일단 자리로 가서 앉아서 기다리라고 한 것이다. 이게 바로 비동기이다.
예시가 살짝 허접하다. 필자의 머릿속에서, 더 좋은 예시가 생각나지 않는다. ㅠㅠ
동기와 비동기는, 함수 통신 메커니즘에 대한 것이다. 함수가 결과를 포함해서 처리가 완료되면 알려주면 동기이다. 결과 없이 일단 회신하고 결과가 나오면 콜백 즉, 다시 알려주는 것은 비동기이다.
반면에, 블록킹/논블록킹은 함수가 동기이던 비동기이던 상관없이 호출하는 주체 즉 프로그램의 상태에 대한 것이다.
Blocking(블록킹)
주문 중, 스타벅스 점원이 커피 재고가 있는지 확인하고 카드 결제를 하는 사이에, 고객은 딴짓을 전혀 하지 않는다. 점원이 고객에게 계산대 앞에서 계속 기다려 달라고 한다면 고객은 점원의 눈만 바라보면서 빨리 주문이 완료되어 결제 영수증을 돌려주기를 멍떄리면서 기다린다. 만약, 점원이 고객에게 자리에 앉아서 편하게 기다리라고 했음에도 불구하고 고객은 자리에 앉아서 아무것도 안 하고 멀뚱멀뚱 점원을 쳐다보고 있을 것이다. 점원이 주문을 받을 때 결과를 알려줄 때까지 기다리라고 하거나(동기), 또는 일단 자리에 가서 앉아서 기다리라고 한 다음에 결과를 알려주거나(비동기) 등, 두 가지 상황(동기, 비동기)과 상관없이 고객이 기다리는 상황에서 아무것도 안 하고 있는지(블록킹) 또는 딴짓을 하는지(논블록킹) 에 대한 것이다.
Non-Blocking(논블록킹)
스타벅스 점원이 커피 재고가 있는지 확인을 하고, 결제를 하는 사이에 고객은 다른 행동을 할 것이다. 계산대 앞에서 기다려야 하는 상황에서도 고객은 이어폰을 기에 꽂고 음악을 들으면서, 다른 메뉴도 힐끔 보고, 바리스타의 얼굴도 보고 이것저것 행동한다. 만약 점원이 고객에게 자리에 가서 앉아있으라고 하면, 자리에 앉아서 핸드폰을 보면서 놀면서 기다리면서, 점원이 주문이 완료되었다고 하면서 영수증과, 진동벨을 가져다줄 때까지 편하게 하고 싶은 일을 한다.
조금 더 복잡한 상황을 만들어보자. 이번에는, 바리스타가 커피와 브런치를 만드는 과정에 대해서 생각해보자. 스타벅스에 점원(바리스타)이 한 명이라고 가정하자. 즉, 싱글 스레드라고 가정한 것이다. 동기/비동기, 블록킹/논블록킹 개념을 이해하면서 동시에, 쓰레드 개념을 적용해서 조금 더 복잡한 상황을 만들었다.
1 Thread(싱글쓰레드), Sync(동기), Blocking(블록킹)
스타벅스에 한명의 종업원이 있다. 커피 머신과 브런치 만드는 기계는 타이머가 없다. 커피가 완료되었는지 직접 가서 보기 전까지는 알 수가 없다. 종업원은, 커피 1잔을 만들 때까지 커피 머신 앞에서 무한 대기를 할 것이다. 커피 1잔과, 브런치 1개를 만들어야 하는 주문이라면... 바리스타는 커피 1잔을 만들기 위해 커피 머신 앞에서 기다리고 있다가.. 커피가 완료되면 이제야 브런치를 만들기 위해 전자레인지 앞에서 계속 대기한다.
커피 머신과 브런치 기계는 Sync이고,
바리스타는, 작업이 완료될 때까지 아무것도 안 하고 기다릴 것이기 때문에 Blocking이다.
바리스타는 딸랑 1명이라서 1 Thread이다. 싱글 스레드이다.
1 Thread, Sync, Non-Blocking
스타벅스에 한 명의 바리스타가 있다. 커피 머신과 브런치 만드는 기계는 타이머가 없다. 언제 끝나는지 알 수가 없다. 하지만, 바리스타는 끝나던 말던 쌩까고 다른 일을 병행해서 한다. 커피 1잔과 브런치 1개를 만들어야 하는 주문이라면.. 일단 커피 1잔을 커피머신에 올려놓고, 브런치를 만들기 시작한다. 하지만, 커피머신에는 알림이 없기 때문에, 계속 커피가 완료되었는지 힐끔힐끔 봐야 한다. 커피가 완료되었다는 알림이 없어서, 답답하지만.. 계속 신경 쓸 수밖에 없지만, 어쩔 수가 없다. 그래도 그 사이에 다른 일을 병행해서 할 수는 있다.
1 Thread, Async, Non-Blocking
스타벅스에 한 명의 종업원이 있다. 커피 머신과 브런치 기계에 타이머가 있다!! 아주 좋은 장비가 들어온 것이다. 커피가 완료되면 알림이 오기 때문에 언제 완료가 되는지 콜백을 통해서 알 수가 있다. 그래서, 바리스타는 좀 더 편하게 다른 일을 병행할 수 있다. 커피 1잔, 브런치 1개를 만들어야 한다면, 커피 1잔을 커피머신에 올려놓고, 브런치를 만들기 시작하다가, 커피머신에서 알림이 오면, 커피를 완성하러 다시 돌아갈 수 있다. 지속적으로 커피가 완료되었는지 볼 필요가 없다. 왜냐면, 커피 머신에서 알아서 알려줄 것이다.
1 Thread, Async, Blocking
스타벅스에 한 명의 종업원이 있다. 커피 머신과 브런치 기계에 타이머가 있다. 아주 좋은 장비가 있지만... 바리스타는 이상하게도, 커피 1잔을 만들 때 커피 머신에 알림이 있음에도 불구하고, 커피 머신 앞에서 대기를 하고 있다. 아마도, 초보 바리스타일 것이다. 알림이 오니깐, 다른 작업을 하고 있다가 다시 돌아와도 되는데.. 굳이 머신 앞에서 기다리는 것이다. 알림이 있는 것이 의미가 없다. 알림이 없을 때 머신 앞에서 기다리는 것과, 알림이 있을 때 머신 앞에서 기다리는 것이.. 어떤 차이가 있는지 모르겠다.
위와 유사한 상황에서, 바리스타가 1명이 아니라, 여러 명이라고 가정하자. 보통 작은 커피숍에는 바리스타가 1명이지만, 스타벅스 같은 큰 커피숍은 보통 바리스타가 2명 이상이다. 커피 만들 수 있는 사람이 각각 1개의 쓰레드 라고 가정하면, 이 경우는 멀티 쓰레드 인 것이다.
Multi Thread, Sync, Blocking
생략
Multi Thread, Sync, Non-Blocking
생략
Multi Thread, Async, Blocking
바리스타는 여러 명인데, 타이머가 있는데도 불구하고 앞에서 기다린다. 이런 상황에서 바리스타가 3명이라면, 동시에 만들 수 있는 커피는 3잔밖에 되지 않는다. 왜냐면 바리스타는 커피머신 앞에서 계속 기다려야 하기 때문이다. 알림이 있는데도 불구하고, 대기하는 것은 너무 비효율적이다. 이 상황에서 더 많은 커피를 만들어야 한다면... 다른 방법이 없다. 바리스타를 더 고용하는 수밖에 없다. 정리하면, 이 경우는 쓰레드를 늘리는 것이 해결책이다.
Multi Thread, Async, Non-Blocking
바리스타는 여러 명이고, 타이머가 있기 때문에 커피머신에 커피를 올려놓고, 다른 일을 할 수가 있다. 가장 이상적인 커피숍이다. 하지만... 바리스타 한 명을 추가로 고용하는 것은, 점주 입장에서는 부담스럽다. 비용이 많이 들기 때문이다. 가능하면, 적은 수의 바리스타로.. 많은 일을 할 수 있으면 좋다. 어쨌든, Non-Blocking 을 하기 위해서는 기본적으로 장비들이 Async 해야 효율적으로 Non-Blocking 하게 일을 할 수 있다.
커피 머신에 커피를 만들기 시작하고, 대기하지 않고 다른 일을 하고 싶다면, 커피 머신에는 타이머 알림 기능이 있어야 한다. 타이머는 비동기(Async)이다. 만약, 바리스타가 Non-blocking 하고 싶어도, 커피 머신에 알림이 없다면 지속적으로 완료되었는지 확인할 수밖에 없다. 커피 머신에 알림이(Async) 있음에도 불구하고, 커피머신 앞에서 기다리는 것(Blocking)도 비효율적이다.
어쨌든, 가장 좋은 것은, Async와 Non-Blocking 동시에 적용되는 상황이 좋을 것이다. Thread는 상황에 맞게 조절하는 것이 좋다. Thread를 너무 많이 생성해야 하는 상황은, 즉 바리스타를 많이 고용하는 것은 비용이 너무 많이 든다. 적절한 쓰레드로 Async & Non-Blocking 하게 작업하는 것이 가장 좋은 시나리오이다.
최근의 애플리케이션은, 동시에 많은 접속자가 발생하는 상황에서, 하드웨어의 성능이 개선되고 있지만, 그럼에도 불구하고
Sync & Blocking
동기 & 블록킹 환경에서는, 작업을 수행 중 일 때 다른 작업을 병행할 수가 없다.
Async & Blocking
비동기 & 블록킹 환경에서는, 쓰레드를 추가로 생성하여 다른 작업을 병행하도록 할 수 있다.
하지만, 비용이 많이 드는 단점이 있고, 쓰레드가 데이터를 대기하면서 유휴 상태로 있기 때문에 프로그램의 대기시간을 포함하면 프로그램의 리소스가 낭비된다.
Async & Non-Blocking
비동기, 논블록킹 환경에서는, 하나의 스레드로 많은 작업을 수행할 수 있다. 이 방법은 현재의 자원에서 더 높은 효율성을 추가하는 방법이다.
JDK 환경에서는 Async & Non-Blocking 개발을 어떻게 하면 될까?
callback
future
일반적으로 우리는, callback 또는 future를 많이 사용한다. 필자는, 최근에 CompletebleFuture를 사용하여, 비동기로 여러 개의 API를 호출한 결과를 조합하는 기능을 구현한 적이 있다. 비동기 호출은 비동기 프로세스로 구현하였으나, 데이터를 조합하는 과정에서 결국 블록킹 환경이 발생할 수밖에 없었다. 일반적으로 알려진 callback과 future의 단점은 아래와 같다.
callback : 콜백헬 이라고 들어봤는가? callback(콜백) 은 가독성이 떨어진다는 치명적인 단점이 있다. callback 구문이 중첩이되는 상황에서 주로 발생하는 콜백헬은 프로그램의 유지보수를 어렵게 만든다.
future : future 는 구현하기 어렵고, 블록킹 이 자주 발생한다는 단점이 있다. JDK8 에서 등장한 CompletableFuture 가 등장하여 많이 개선이 되었지만, 그럼에도 불구하고 단점은 여전히 존재한다.
이 글은, callback 과 future 가 나쁘다는 것을 설명하는 글은 아닙니다. 실무에서 여전히, callback 과 future는 많이 사용됩니다.
Reactor 공식 레퍼런스를 참고하면, Reactor 의 주요 특징은 아래와 같다.
Composability and readability
Data as a flow manipulated with a rich vocabulary of operators
Nothing happens until you subscribe
Backpressure or the ability for the consumer to signal the producer that the rate of emission is too high
High level but high value abstraction that is concurrency-agnostic
https://projectreactor.io/docs/core/release/reference/#_from_imperative_to_reactive_programming
자세한 내용은 생략한다. 아래 표를 참고하자.
"리액티브 프로그래밍"이 항상 정답은 아니라고 생각한다. callback 과 future 가 항상 나쁘다고 생각하지도 않는다. 경우에 따라서는, 동기 또는 블록킹 환경이 필요할 수도 있다. 비동기 & 논블록킹 환경에도 단점이 분명 존재하고, 소프트웨어의 속성에 따라서 적절하게 구현을 해야 할 것이다. 필자는 이 글에서 "리액티브 프로그래밍"에 대해서 얘기를 하는 중이고, "리액티브 프로그래밍"을 구현하는 "Reactor"에 대해서 심화학습을 하고 있다. 참고로, 필자는 1장에서 "리액티브 프로그래밍"을 아래와 같이 정의하였다.
Composing asynchronous & event-based sequences, using non-blocking operators.
"Reactor"는 Async & Non-Blocking 환경을 위한 라이브러리이다. 필자는 사실 재작년에 팀원이 RxJava를 공부할 때부터 "리액티브 프로그래밍"에 조금씩 관심을 갖고 있었다. 만약, 스프링 5에서 리액티브 프로그래밍을 위한 기반기술로 RxJava를 사용했다면, 필자는 주저 없이 RxJava를 공부했을 것이다. 사실, 두 라이브러리 모두, "리액티브 스트림"의 구현체이고, 서로 호환이 가능하기 때문에 개인적으로 생각해봤을 때는 RxJava를 공부하는 것도 좋겠다는 생각이다. 하지만, 스프링 5 에서 사용하는 핵심 기술이 "Reactor"이고, 피보탈에서 Reactor를 지속적으로 업데이트를 할 것이기 때문에, 스프링 기반 프로젝트를 주로 진행 중이고, 조만간 Webflux 프로젝트를 시작할 예정인 필자는 Reactor 를 집중해서 공부하기로 결정하였다.
이 글에서는, "리액티브 프로그래밍"의 기본이 되는 Async, Non-Blocking 의 개념에 대해서 정리하였다. 다음 글에서는 "Reactive Streams" 에 대해서 상세하게 다루 예정이다.