우아한테크코스 4기 크루들과 면접 준비를 할 때 종종 나오는 질문이 있었습니다. 동기-비동기의 차이는 무엇인가요? 블로킹-논블로킹과는 어떤 차이가 있나요?
특히 @Async
어노테이션을 활용해 비동기 처리를 한 코드가 있었을 경우에는 거의 무조건 질문이 나왔던 것 같습니다. 사실 저도 동기-비동기와 블로킹-논블로킹을 제대로 알지 못하고 있었습니다. 막연하게 동기면 다음 작업 처리 못하고 비동기면 계속 작업하는거 아니야?
라고 생각했었습니다. (이러면 동기와 블로킹의 차이가 없어져버리죠)
공부해보니 동기-비동기와 블로킹-논블로킹은 좀 다른 개념이었습니다. 그리고 이해하기 쉽지 않은 헷갈리는 개념이기도 했습니다. 앞으로 비동기 처리나 논블로킹 IO를 활용할 일이 많을 것으로 예상하기 때문에 미리 정리를 하는 시간은 가져보려 합니다. 설명의 편의를 위해 함수 또는 작업이라는 용어를 사용하게 될텐데요, 단순히 코드에 작성한 함수 뿐 아니라 IO를 진행하는 프로세스, 네트워크 모델에서의 요청 및 응답 등에도 적용되는 개념이라고 생각해주시면 편할 것 같습니다. 저는 설명의 편의를 위해 함수 기준으로 설명하도록 하겠습니다.
저를 포함 많은 분들이 동기는 함수를 호출한 후 return될 때까지 자기 작업을 진행하지 못한다
, 비동기는 다른 함수를 호출한 후에도 자기 작업을 진행한다
라고 알고 있는 경우가 있는 것 같습니다. 저희가 사용하는 케이스가 거의 동기+블로킹, 비동기+논블로킹 케이스여서 개념이 섞여있는 것 같습니다.
비동기 논블로킹 처리에 대해서 공부하며 알게 된 사실로는, 동기 - 비동기는 함수의 진행 여부와는 상관이 없습니다. 이는 함수의 제어권
에 대한 문제이기 때문입니다. 반면 동기 - 비동기는 프로세스의 수행 순서 보장에 대한 메커니즘입니다.
동기 - 비동기에 대해 좀 더 쉽게 이해하기 위해, 동기의 뜻에 대해서 알아보도록 하겠습니다.
동기(同期)
같은 시기. 또는 같은 기간.
동기라 함은 같은 시기, 또는 같은 기간이라는 뜻을 가지고 있습니다. 여기까지만 봐서는 잘 이해가 되지 않는데요, 현재 작업의 응답과 다음 작업의 요청이 동시에 일어난다
정도의 해석이 가장 적절하지 않을까 싶습니다. 결론적으로 말하면 작업이 어떠한 순서를 보장한다.
정도가 될 수 있겠네요. 동기 - 비동기의 뜻에 대한 좀 더 자세한 탐구는 여기를 참고해주시면 좋을 것 같습니다. 응답과 요청의 순서를 보장하려면 어떻게 해야 할까요? 호출한 함수(작업)의 응답이 왔는지, 응답 값이 무엇인지에 대해 알고 있어야 합니다. 때문에 동기 - 비동기는 다음과 같이 생각할 수 있습니다.
상황으로 알아보면 이런 상황을 생각할 수 있습니다.
나: 블로그에 글 하나만 써줘
상대: OK
...
나: 글 다 썼음?
상대: NO
나: 글 다 썼음?
상대: NO
나: 글 다 썼음?
상대: YES. 주소 여기있음
나: (주소를 받아서 추가적인 작업을 처리한다)
이렇게 호출 대상의 반환값을 계속해서 체크한다면 동기입니다. 반면 비동기는 다음과 같은 상황이 됩니다.
나: 블로그에 글 하나만 써줘
상대: OK
...
나: 내 일 다 했으니까 가야겠다(퇴장)
상대: (글을 계속 쓴다)
...
(상대 작업 완료. 하지만 글 주소를 받을 '나'는 이미 없음)
여기서 아주 중요한 문제가 있는데요, 비동기는 호출한 함수의 반환값을 신경쓰지 않는다는 것입니다. 즉, 비동기 함수를 호출했을 때 그 반환값을 호출한 쪽에서 다시 이용하는 것은 불가능하다는 이야기입니다. 때문에 비동기 방식을 사용하는 경우 호출한 함수의 완료 이후 해당 반환값을 가지고 추가적인 처리를 해주기 위해 콜백(callback) 함수를 이용하게 됩니다.
콜백 지옥이 바로 비동기 처리 때문에 생기는 것입니다!!
사진 출처
동기 처리라고 해도 호출한 함수의 작업을 진행하면서 동시의 자기 작업을 진행하는 상황이 가능하기도 합니다. 한 작업의 응답과 다음 작업의 요청에 대한 순서만 보장해주면 되지 그 사이에 자기 작업의 남은 일을 하지 말아야 한다는 법은 없으니까요. 이에 대해서는 블로킹 - 논블로킹에 대해서도 체크한 뒤 좀 더 자세히 알아보도록 하겠습니다.
제어권이란 무엇일까요? 제어권은 자기 작업을 실행할 수 있는 권한이라고 생각하면 될 것 같습니다. 자바 코드로 생각해보도록 하겠습니다. A 메서드의 첫 줄에서 B 메서드를 호출한다고 하면, 두 번째 줄부터의 작업은 B 메서드의 작업이 모두 끝나기 전까지는 실행되지 않습니다. 이는 자바의 메서드 호출이 일반적으로는 블로킹 방식이기 때문입니다.
그림으로 나타내면 다음과 같은 진행 플로우를 가지게 됩니다.
함수 A가 함수 B를 호출하면서 제어권도 함께 뺏기게 됩니다. 때문에 함수 B의 작업이 모두 끝날 때 까지 함수 A는 어떤 작업도 할 수 없는 상태가 됩니다. 작업 진행이 Block되어서 블로킹이라고 생각하시면 될 것 같습니다. (함수 뿐 아니라 쓰레드 등 다른 개념을 넣어도 같은 의미로 동작합니다.)
그렇다면 논블로킹은 어떨까요? 블로킹과는 반대의 의미일테니, 제어권을 넘겨주지 않는 것으로 생각하면 이해하기 쉬울 것 같습니다. 즉, 다른 함수를 호출하더라도 호출한 쪽 작업을 계속해서 진행할 수 있는 방식입니다.
자 이렇게 동기 - 비동기와 블로킹 - 논블로킹이 다른 개념이라면, 두 개념의 조합이 가능하겠죠? 총 네 가지 경우의 수가 나오게 됩니다.
가장 먼저, 우리에게 친숙한 동기 - 블로킹 조합을 알아보도록 하겠습니다. 그림에서 볼 수 있듯이, 일반적인 읽기/쓰기가 동기 - 블로킹 조합으로 이루어집니다. 특히 제가 사용한느 언어인 자바의 경우 대부분의 코드가 동기 - 블로킹 조합으로 작동하게 됩니다.
마찬가지로 비동기 - 논블로킹 조합도 쉽게 찾아볼 수 있습니다. 자바스크립트를 생각하면 될 것 같은데요, 자바스크립트의 모든 함수가 비동기인 것은 아니지만, fetch, setTimeOut과 같은 비동기 함수를 쉽게 찾아볼 수 있습니다.
그렇다면 동기 - 논블로킹 조합은 어떨까요? 위의 그림에서는 잘 이해가 가지 않을 수 있으니 간단한 예시를 들어보도록 하겠습니다. 어떤 게임들은 실행하면 로딩 창이 뜨고 시간이 지남에 따라 로딩 바가 점점 차오르게 됩니다. 그런데 화면에 로딩 바가 차오르는 것을 그리는 작업과, 실제로 맵이나 유닛 등을 로딩하는 작업은 서로 다른 작업입니다. 이런 경우는 동기 - 논블로킹이라고 할 수 있습니다.
게임을 진행하는 함수는 맵을 로딩하는 함수를 호출합니다. 맵을 로딩하는 동안, 화면이 멈춰 있어서는 안되겠죠? 따라서 얼만큼 로딩되었는지 로딩 바를 그려서 보여줍니다. 위의 함수 A, 함수 B 그림에서 보자면 함수 A는 화면에 로딩 바를 보여주고, 함수 B는 맵 데이터를 가져오는 역할이 됩니다. 그런데 생각해 봅시다. 맵을 불러오는 동안 로딩 바가 변하지 않는다면 굉장히 이상할 것입니다. 때문에 로딩 바를 그리는 함수 A는 계속해서 함수 B에게 로딩이 얼만큼 되었는지 확인합니다. 그리고 로딩 된 만큼 로딩 바를 렌더링 하겠죠. 이 상황이 바로 동기 - 논블로킹 상황이 되는 것입니다. (화면을 구성하는 함수는 로딩 함수와 상관없이 계속해서 본인의 작업을 진행. 하지만 로딩 함수가 맵의 얼만큼 로딩했는지 지속적으로 확인하기 때문.)
그렇다면 비동기 - 블로킹 조합은 어떨까요? 사실 단순히 생각해보면 굉장히 비효율적일 수밖에 없습니다. 호출한 함수의 결과는 관심도 없는데, 제어권은 넘겨줘서 아무 동작도 못하고 있게 되기 때문입니다. 실제 상황에서 발생할 수 있는 경우로는 비동기 처리를 하는 도중에 블로킹 형태로 작동하는 작업을 하는 경우가(예를 들어 데이터베이스에 접근한다든가) 있을 수 있다고 하네요. 그림에는 I/O 멀티플렉싱도 비동기 - 블로킹 조합이라고 하는데, 이 부분은 블로킹인지 논블로킹인지에 대해 논란이 있다고 합니다.
사실 굉장히 어려운 개념이고, 머릿속에 들어있는 내용을 글로 표현하기가 너무 어려운 것 같습니다. 이 글을 통해서는 간단히 동기 - 비동기와 블로킹 - 논블로킹이 다른 개념이라는 점만 인지하고, 좀 더 자세한 설명이 필요하다면 우아한테크코스 테코톡 중 좋은 자료가 있어 해당 영상을 참고하시면 좋을 것 같습니다.
와 진짜 헷갈리는 용어들인데 예시랑 설명해줘서 이해가 잘가네요;
'비동기는 별도의 스레드로 동작한다' 라고 단순히 생각했는데 비동기의 의미를 생각해보면 당연한거였네요