비동기 논블락킹에 대한 이해
이슈
- 코루틴에 대해서 공부하면서 이게 어떻게 JVM에서 동작하지?라는 의문을 가지고 헤맸던 경험이 있다.
- 결국 비동기 논블락킹에 대한 이해가 부족했던 것이고, 이 주제는 주기적으로(?) 헷갈리는 주제인 것 같다.
- 그래서 이번에 나만의 언어로 정리하는게 필요하다고 느껴져서 정리하게 됐다.
- 물론, 관련된 글은 이미 너무나 많지만, 정확하지 않은 정보로 인해 오히려 헷갈리는 경우가 있고, 각자의 언어로 표현하다보니 나에게는 와닿지 않는 경우가 많았다.
- 이 글은 사실 미래에 헷갈릴 나를 위한 글이다.
들어가며
- 이 글은 비동기 논블락킹에 대한 이론적 배경이나 구체적인 방법론 등을 설명하기 위한 글은 아니다.
- 비동기 논블락킹에 대한 이해를 쉽게 하는 것이 주 목적인 글이므로, 약간의 논리의 비약이 있을 수 있으나, 최대한 본질을 흐리지 않으며 작성하려고 했다.
결론
- 동기 vs 비동기는 “내 일을 동료한테 줄 수 있는가”의 관점이고,
- 블락킹 vs 논블락킹 “내가 다른 일을 할 수 있는가”의 관점이다.
- 그리고 중요한 건 결국 논블락킹이 목적이고, 비동기는 이를 위한 수단이다.
- 결국 자원 효율성 관점에서 내가 쉬지 않고 다른 일을 할 수 있는 게 중요한 거고, 내가 다른 일을 하기 위해서는 내가 하던 일을 동료에게 줄 수 있어야한다.
전제
- 비동기든 논블락킹이든 이 논의는 동일한 자원 상황에서의 “스레드의 스케줄링”에 대한 논의이다.
- 스레드의 스케줄링이라 함은 “누가 무슨 일을 할지”에 대한 논의이며, 그 목적은 당연하게도 누가 무슨 일을 해야 가장 효율적일까?에 있다.
- 그리고 여기서의 스레드는 “작업을 수행하는 주체”를 의미하고, 경우에 따라 OS 레벨의 프로세스 혹은 스레드가 될 수 있고, 언어 레벨에서는 고루틴, 코루틴과 같은 경량스레드가 될 수 있다.
- 그리고 이 논의를 위해서는 I/O time이 있는 작업이라는 전제가 필요하다.
- 애초에 cpu만 사용하는 작업만 있는 경우 코어 수나 클락 수를 늘리지 않는 이상 스레드의 스케줄링만으로는 개선할 여지가 없고, (스케줄링을 통해서 해결할 수 있는 건 starvation 등의 자원의 분배 문제이지, 효율은 개선될 수 없다)
- 결국 비동기든 논블락킹이든 I/O time 때 스레드가 유휴상태인 채로 점유된 것을 개선하기 위한 것들이기 때문이다.
비유를 통해 이해해보자.
손님에게 주문을 받고 주문받은 음료를 주는 카페 알바를 떠올려보자. Server의 어원 그 자체라고 할 수 있다.
그리고 I/O 작업은 커피머신에 커피를 내리는 작업에 비유할 수 있다. (커피머신이 커피를 내리는 시간은 알바가 아무리 뭘해도 컨트롤할 수 있는 영역이 아니다.)
굳이 따지자면 cpu 작업은 커피머신을 사용하지 않는 모든 작업(계산을 한다던가, 얼음을 넣는다던가 등)에 비유할 수 있다.
이러한 관점에서 아래 상황들을 떠올려보자.
동기 vs 비동기
- 동기는 내 일을 동료한테 줄 수 없는 형태의 스케줄링이다.
- 비동기는 내 일을 동료한테 줄 수 있는 형태의 스케줄링이다.
그럼 여기서 동기의 관점을 우선 살펴보자.
동기는 일을 동료한테 줄 수 없다. 이 말은 곧, 동료가 없다는 것과 같다고 볼 수 있고, 동료가 없다는 건 곧 혼자 일한다고 볼 수 있다.
즉, 싱글 스레드라고 취급할 수 있다.
여기서 싱글스레드라고 하면 수많은 동기 방식의 멀티스레드 프레임워크들은 무엇인가?에 대한 의문을 가질 수 있다.
그런데 동기 방식의 멀티스레드는 스레드가 여러개이긴 하나 스레드 간에 협력이랄게 없고, 각각의 스레드가 “독립적으로” n개의 작업을 할 뿐이다.
결국 멀티스레드라고 해도 각자 할 일을 하는 싱글스레드 * n개 밖에 되지 않는다.
이어서 비동기 관점을 살펴보자.
비동기 방식은 내 일을 동료한테 줄 수 있다는 것인데, 이것은 다시 말하면 일단 동료가 있어야한다는 것을 의미한다.
즉, 멀티스레드가 전제가 되어야한다.
(정확히는 비동기 방식이 반드시 멀티스레드일 필요는 없다. 예를 들어, 비동기 방식의 코드를 싱글 스레드로 실행하면 실행은 된다. 다만 이 상황은 내 일을 동료한테 줬는데 그 동료가 나인 경우이다. 즉, 결과적으로 따지고보면 어차피 내가 다하고 있는 거고, 그렇다면 굳이 비동기 방식이 갖는 이점이 없는 것이다. 이 말은 곧 비동기 방식이 “의미가 있으려면” 멀티스레드가 전제되어야 한다는 것을 뜻한다.)
멀티스레드를 전제로 하고 마저 살펴보면,
내 일을 동료한테 줄 수 있다는 것은 결국 스레드간 “협력”이 가능하다는 것을 의미한다. coroutine의 “co”가 cooperative인 것도 이와 같은 맥락이다.
비동기 방식에서 늘 나오는 얘기인 콜백도 결국 “협력”하는 한 방법 중에 하나일 뿐이다.
카페 알바의 예로 보면, 동기 방식은 내가 주문받은 손님은 내가 반드시 음료를 줘야하는 방식이다.
반면에, 비동기 방식은 주문은 내가 받았지만 음료를 주는 건 동료일 수 있다.
블락킹 vs 논블락킹
- 블락킹은 내가 다른 일을 할 수 없는 관점이다.
- 논블락킹은 내가 다른 일을 할 수 있는 관점이다.
카페 알바의 예로 보면, 블락킹 방식은 커피머신이 커피를 내리는 동안 아무 일도 하지 못하는 방식이다. (커피머신을 멀뚱멀뚱 쳐다본다던가, 커피 잔을 손으로 계속 들고 있어야된다고 생각해도 될 것 같다.)
반면에, 논블락킹 방식은 커피머신에 커피를 내리고 나서 주문을 받을 수 있는 방식이다.
블락킹 방식은 답답하기는 하지만, 커피머신이 커피를 다 내렸을 때 어떻게하지?에 대한 걱정은 없다.
다만, 논블락킹 방식은 누가봐도 효율적이다. 그런데 커피머신이 커피를 다 내리고 나면 이 커피는 어떻게 하지?에 대한 걱정을 하게 된다.
여기서 비동기와의 연결점이 발생한다.
동기 방식은 이 일을 동료한테 줄 수 없기 때문에, 커피를 다 내리고 나면 커피를 손님한테 건네주는 것도 내가 해야한다.
즉, 커피머신의 작업에 대한 완료처리를 계속 신경을 써야한다는 것이고, 결국 스레드가 이걸로부터 자유로울 수 없다.
그런데, 비동기 방식은 이 일을 동료한테 줄 수 있고, 커피가 다 내려지면 손님에게 건내줘라는 걸 동료한테 전달할 수 있게 되고 그러면 나는 이 일로부터 자유로워질 수 있다.
네 가지 조합에 대해 살펴보자
위의 관점에서 동기와 비동기 그리고 블락킹과 논블락킹이 어떤 관점인지 살펴보았으니, 2x2 조합 각각에 대해 구체적으로 떠올려보자.
동기 블락킹
가장 기초적인 모델이다.
내 일을 동료한테 줄 수도 없고, 다른 일을 할 수도 없다.
결국 싱글 스레드의 sequential한 모델을 떠올릴 수 있다.
예상하겠지만, 싱글 스레드의 문제와 순차 수행의 문제를 갖고 있다.
동기 논블락킹
내 일을 동료한테 줄 수 없으나, 다른 일을 할 수 있다.
커피 머신에 커피를 내리게 한 다음에 주문을 받을 수 있다.
커피를 내리는 동안 손님을 받을 수 있으니까, 동기 블락킹을 방식보다 효율적이다.
그럼 뭐가 문제일까?
싱글 스레드의 문제를 가져간다. 즉, 점유되는 경우 다른 작업들이 같이 지연된다는 단점이 있다.
예를 들어, 위의 상황에서 주문을 받는데 주문받는게 오래걸린다면(스레드를 점유한다면) 커피 머신이 커피를 다 내렸어도 이전 손님에게 커피를 주지 못한다.
비동기 블락킹
내 일을 동료한테 줄 수 있으나, 다른 일을 할 수 없다.
보자마자 이런 생각이 든다. “다른 일을 할 수 없는데 동료한테 왜 줘?”
그리고 이 생각은 실제로 비동기 블락킹 방식이 자연스럽지 않고, 잘 사용되지 않는 이유를 관통하는 관점이다.
Java의 CompletableFuture의 get() 함수를 호출하면 비동기지만 스레드가 블락되는 방식으로 동작한다.
비동기 논블락킹
내 일을 동료한테 줄 수 있고, 다른 일도 할 수 있다.
동기 논블락킹에서의 싱글 스레드 문제를 해결할 수 있다.
예를 들어, 스레드가 점유당한다면, 동료가 대신 커피를 줄 수 있다.
그리고 내 일을 동료에게 준 순간 나와 일, 즉 스레드와 자유의 몸이 됐으므로, 새로운 주문을 받던 이미 다른 동료가 받은 주문에 대한 커피를 손님에게 내어주던, 어떤 작업이든 시작할 수 있다.
앞으로 남은 것
위에서 개념적으로 이해한 것들을 코딩으로 실제로 구현해보고 확인하면 좀 더 확실하게 정리될 것 같다.
코드 기준으로 설명한 것들로 2편을 작성하는 것을 목표로 해야겠다.
Leave a comment