동기/비동기? 블로킹/논블로킹?

최근 Reactive Programming에 대한 관심이 많아지면서 기반 기술에 대한 이해를 위해 먼저 Sync/Async, Blocking/Non-Blocking에 대한 학습이 필요함을 느꼈다.

설명하는 사람들 마다 관점이 조금씩 다르고 헷갈리는 이 개념들에 대해 그나마 내가 가장 잘 이해할 수 있게끔 정리가 되어있는 블로그를 토대로 정리해보려고 한다.


서로 다른 관점

주의깊게 살펴보지 않는다면 많은 사람들이 혼동하고 있는 개념이다. 동기 == 블로킹, 비동기 == 논블로킹

물론 가장 이해하기 쉽고 실제로 많은 사례들이 위와 같이 정의되어 있지만 실제로 동기/비동기블로킹/논블로킹유사한 부분이 있지만 서로 다른 개념이다.

이 둘의 개념을 구분하기 위해서 나에게 가장 적합한 방법은 바라보는 관점을 달리한다.이다.

클라이언트(메시지 전송자)와 서버(메시지 수신자)

먼저 이들 개념을 이해하기 위해 어떤 협력이 있다고 가정하자. 여기서 클라이언트는 메시지를 전송하는 전송자이고 서버는 메시지를 수신하고 필요 시 응답하는 수신자이다.

Sync/Async를 판별하기 위해서는 서로 동기냐 비동기냐를 비교할 대상들이 필요하다. 마찬가지로 Blocking/Non-Blocking제어권에 대한 이야기이기 때문에 둘 이상의 대상이 필요하다. 우리는 이 대상들을 클라이언트서버로 구분지을 것이다.

뒤에 설명하겠지만 동기/비동기를 바라보는 관점과 블로킹/논블로킹을 바라보는 관점을 클라이언트서버의 입장에서 바라보는게 이해하는데 도움이 될 것이다.


Sync/Async

동기/비동기에 대해서 이야기를 해보자. 결론부터 이야기 하자면 동기/비동기를 구분하는 기준은 클라이언트가 서버의 작업 완료 여부에 관심이 있느냐 없느냐 이다. 즉, 클라이언트가 요청한 어떤 메시지에 대한 완료 여부를 신경쓰느냐 신경쓰지 않느냐로 바라본다.

만일 클라이언트서버에게 작업 완료 여부를 계속 묻는다거나(ex: polling, Non-Blocking) 서버의 작업 완료 응답이 필요하여 응답할 때 까지 계속해서 기다린다면(Blocking) 이는 클라이언트가 작업 완료 여부에 대해 계속해서 관심을 갖는것이므로 Sync에 해당한다.

반면 클라이언트서버에게 메시지 전송만 할 뿐 작업이 완료되었는지 말았는지 전혀 관심갖지 않는다면 이는 Async에 해당한다. 예를 들어 클라이언트가 메시지 전송 이후 자신의 책임을 모두 수행한 뒤 반환(종료)되는 경우(서버는 아직 책임을 수행 중) 클라이언트서버가 작업을 완료 했는지, 하지 않았는지 전혀 관심을 갖지 않고 자신의 책임만 수행했으므로 Async라고 할 수 있다.


Blocking/Non-Blocking

블로킹/논블로킹에 대한 이야기는 조금 더 수월할 것 같다. 동기/비동기 보다는 경험적으로 뚜렷한 이미지가 떠오를 것이다.

이 역시 클라이언트서버로 구분해서 이야기를 해보자.

클라이언트서버에게 메시지를 전송(Request)함으로써 서버제어권을 함께 갖게 된다. 이 때 서버가 본인의 작업 수행 여부에 상관 없이 곧바로 클라이언트에게 제어권을 다시 넘기느냐 넘기지 않느냐에 따라 블로킹/논블로킹이 나뉘게 된다.

다시말해, 클라이언트가 메시지 전송 이후 서버의 작업이 완료가 되지 않았음에도 제어권을 넘겨받고 이후 작업을 수행할 수 있다면 Non-Blocking에 해당한다. 반면 서버로부터 제어권을 바로 넘겨받지 못한다면 Blocking에 해당한다.


Sync/Async with Blocking/Non-Blokcing

이제 우리는 Sync/AsyncBlocking/Non-Blocking이 서로 다른 개념임을 알았다. 그렇다면 이들의 조합은 1:1 관계가 아니라 N:N 관계이므로 총 2*2 = 4개의 조합으로 구성할 수 있다.

Sync ~ Blocking

가장 먼저 Sync-Blokcing을 살펴보자.

가장 직관적이고 쉽게 이해 가능한 시나리오다.

  1. 클라이언트서버에게 메시지를 전송한다.
  2. Blocking이기 때문에 서버로부터 곧바로 제어권을 넘겨받지 못한다.
  3. Sync이기 때문에 클라이언트서버의 작업 완료 여부에 대해 관심이 많다.
  4. 결국 클라이언트는 제어권을 넘겨받지 못하고 서버가 작업이 끝날 때 까지 계속해서 기다린다.

Sync ~ Non-blokcing

Sync ~ Non-Blokcing 시나리오의 대표적인 예시로는 Polling이 있겠다. 계속해서 작업 완료 여부를 확인함으로써 Sync를 만족한다.

  1. 클라이언트서버에게 메시지를 전송한다.
  2. Non-Blokcing이기 때문에 서버클라이언트에게 곧바로 제어권을 줌으로써 클라이언트는 이후 본인의 작업을 수행한다.
  3. Sync이기 때문에 클라이언트서버의 작업 완료 여부에 대해 관심이 많다.
  4. 제어권이 존재하기에 작업 수행이 가능한 클라이언트서버의 작업 완료 여부도 지속적으로 물어본다.

Async ~ Non-blocking

Async ~ Non-Blocking 역시 가장 많이 사용되는 시나리오이다.

  1. 클라이언트서버에게 메시지를 전송한다.
  2. Non-Blocking이기 때문에 서버클라이언트에게 곧바로 제어권을 줌으로써 클라이언트는 이후 본인의 작업을 수행한다.
  3. Async이기 때문에 클라이언트서버의 작업 완료 여부에 관심이 전혀 없다.
  4. 제어권이 존재하기에 작업 수행이 가능한 클라이언트서버의 작업 완료 여부에 전혀 관심이 없다. 단지 본인의 책임을 수행할 뿐이다.

Async ~ Blokcing

Async ~ Blocking은 흔하지 않은 사례이다.

사실 흔하지 않은 사례라기 보다는 의도하지 않은 사례인 경우가 많다.

주변의 포스팅에 따르면 처음에는 Async ~ Non-Blokcing으로 설계하였지만 컴포넌트 중 Blocking 컴포넌트(제어권을 넘겨주지 않는 컴포넌트)가 존재하여 의도와 다르게 해당 구간에서 Blocking이 되는 경우가 많다. 라고 한다.

  1. 클라이언트서버에게 메시지를 전송한다.
  2. Blocking이기 때문에 서버클라이언트에게 제어권을 곧바로 넘겨주지 않음으로써 클라이언트는 어쩔수 없이 대기해야 한다.
  3. Async이기 때문에 사실 클라이언트서버의 작업 완료 여부에 대해서는 관심이 없다.
  4. 서버의 작업 완료 여부에 관심이 없지만 Blocking이기 떄문에 클라이언트는 다른 작업을 수행하지 못한다.

위의 좋은 예시를 인용했다.

Blocking-Async의 대표적인 케이스가 Node.js와 MySQL의 조합이라고 한다. Node.js 쪽에서 callback 지옥을 헤치면서 Async로 전진해와도, 결국 DB 작업 호출 시에는 MySQL에서 제공하는 드라이버를 호출하게 되는데, 이 드라이버가 Blocking 방식이라고 한다.

이건 사실 Node.js 뿐아니라 Java의 JDBC도 마찬가지다. 다만 Node.js가 싱글 쓰레드 루프 기반이라 멀티 쓰레드 기반인 Java의 Servlet 컨테이너보다 문제가 더 두드러져 보일 뿐, Blocking-Async라는 근본 원인은 같다.

그래서 Blocking-Async는 이렇게 정리해도 좋을 것 같다. Blocking-Async는 별다른 장점이 없어서 일부러 사용할 필요는 없지만, NonBlocking-Async 방식을 쓰는데 그 과정 중에 하나라도 Blocking으로 동작하는 놈이 포함되어 있다면 의도하지 않게 Blocking-Async로 동작할 수 있다.


정리

Blocking/NonBlocking은 호출되는 함수가 바로 리턴하느냐 마느냐가 관심사

바로 리턴하지 않으면 Blocking 바로 리턴하면 NonBlocking


Synchronous/Asynchronous는 호출되는 함수의 작업 완료 여부를 누가 신경쓰냐가 관심사

호출되는 함수의 작업 완료를 호출한 함수가 신경쓰면 Synchronous 호출되는 함수의 작업 완료를 호출된 함수가 신경쓰면(callback 호출) Asynchronous 성능과 자원의 효율적 사용 관점에서 가장 유리한 모델은 Async-NonBlocking 모델이다.

참고 및 출처