얼마 전에 면접에서 이 개념들을 섞어서 대답하는 바람에... ㅠㅠ
이번에 확실히 정리하고자 글을 쓴다.
이 두 개의 개념은 어떤 한 함수가 어떻게 동작하는지에 관한 이야기다.
함수 A와 B가 존재한다고 하자. 그리고 함수 A가 코드 안에서 함수 B를 호출한다. 이 때 함수 A가 어떻게 행동할지에 따라서 blocking과 non-blocking이 결정된다. 함수 B에는 관심이 없다.
여기서 함수 A의 동작에만 초점을 맞춰보자.
blocking에서 함수 A는 다른 행동을 하지 않고 함수 B의 결과를 기다린다.
그러면 함수 A와 함수 B는 싱글 프로세스, 싱글 쓰레드, 멀티 프로세스, 멀티 쓰레드 중에 무엇일까? 모른다. 이것은 구현에 관한 이야기다. 어떤 것으로든 구현하든 함수 A가 아무 것도 못하고 함수 B의 결과를 기다리는 것이 blocking이다.
non-blocking에서 함수 A는 다른 행동을 하며 함수 B의 결과를 기다린다.
그러면 함수 A는 무슨 행동을 할까?
맨 먼저 함수 A는 루프문을 통해 함수 B가 작업을 완료했는지 1초마다 체크할 수 있다.(polling)아니면 작업을 완료했는지 체크하는게 아니라 그 외에 개발자가 만들어둔 다른 일을 하다가 함수 B로부터 작업이 완료됐음을 전달 받을 수도 있다.(callback) 아니면 이 두개를 혼합해서 행동할 수도 있다.
그렇다면 함수 A와 함수 B는 싱글 프로세스, 싱글 쓰레드, 멀티 프로세스, 멀티 쓰레드 중에 무엇일까? 우선 싱글 쓰레드는 아닐 것이다. 함수 A가 함수 B의 결과를 기다리지 않고 다른 행위를 한다는 것은 함수 A와 함수 B가 동시(Parallel 또는 Concurrent)에 작업을 한다는 의미, 즉 실행 흐름을 여러 개 사용한다는 것이다. 그래서 싱글 쓰레드로 구현할 수 없다.
그러나 어쨌든, 이것들은 구현에 관한 이야기다. 현재 우리가 초점을 맞추고 있는 부분이 아니다. 어떤 것으로 구현하든 함수 A가 함수 B의 결과를 기다리지 않고 다른 행위를 할 수 있는 것이 non-blocking이다.
이 두 개념은 두 개 이상의 함수가 어떤 관계를 갖는지에 관한 이야기다.
이 개념은 하나의 함수 동작으로 설명할 수 없다. 왜냐하면 여러 개의 함수가 동작하는 논리적 맥락에 관련된 이야기이기 때문이다. 즉 blocking, non-blocking하고 전혀 다른 이야기다.
함수 Main, B, C가 존재하고 함수 Main에서 함수 B, 함수 C를 호출한다고 하자. 우리는 함수 B와 함수 C의 호출 과정에 초점을 맞춘다.
synchronous에서는 함수 C를 호출하기 위해서 함수 B가 꼭 실행 완료되어야 한다. 그래서 함수 B가 완료되기 전까지는 함수 C를 호출하지 않을 것이다. 이 때 함수 B와 함수 C를 synchronous라고 할 수 있다.
그렇다면 함수 B와 함수 C는 싱글 프로세스, 싱글 쓰레드, 멀티 프로세스, 멀티 쓰레드 중에 무엇일까? 역시 이 부분은 구현에 관한 것으로 우리가 상관할 것이 아니다. 함수들의 순서를 지킬 수 있으면 어느 구현으로 해도 상관 없다.
즉 우리가 초점을 맞춰야할 부분은 함수 C를 실행하기 위해선 함수 B의 결과가 꼭 필요하다는 점이다.(또는 함수 B가 꼭 끝나야한다.) 함수들의 순서가 논리적인 맥락에 맞춰 지켜져야 하는 것이 synchronous다.
asynchronous에서는 함수 C를 호출하기 위해서 함수 B는 실행 완료되지 않아도 된다. 즉 함수 B의 결과가 필요 없다. 따라서 두 개를 동시에 실행시키면 된다. 그리고 두 함수는 언제 끝날지 모른다. 끝날 때 callback을 이용해 처리할 수 있다. 이 때 함수 B와 함수 C를 asynchronous라고 할 수 있다.
그렇다면 함수 B와 함수 C는 싱글 프로세스, 싱글 쓰레드, 멀티 프로세스, 멀티 쓰레드 중에 무엇일까? 우선 싱글 쓰레드는 아닐 것이다. 두 개의 실행 흐름을 가져가려면 싱글 쓰레드로는 불가능하기 때문이다. 그러나 어쨌든 이 부분은 구현에 관한 것으로 우리가 상관할 것이 아니다. 함수들이 서로 기다리지 않고 동시에 실행 흐름을 가져갈 수 있으면 어느 구현으로 해도 상관 없다.
우리가 초점을 맞춰야할 부분은 함수 C가 실행되기 위해서 함수 B의 결과가 어찌됐든 상관 없다는 것이다. 이것이 asynchronous다.
// funcA
while (true){
isDone = recv(...)
if (isDone)
break;
else
sleep(2000)
}
// funcB
err = doSomethingElse()
if (err is not error) {
...
} else {
...
}
funcA
는 recv()
함수를 계속 호출해 작업이 완료됐는지에 대한 flag를 즉시 return 받고 있으며, 작업이 완료됐으면 반복문을 탈출하고 아니라면 다른 행위를 한다.
funcB
는 doSomethingElse()
이 완료될 때까지 기다렸다가 err flag를 받아 flag에 따라 행동을 달리 하고자 한다.
위에서 아래로 순서대로 코드가 실행된다고 가정하자.
위 코드에서 funcA
와 funcB
의 관계는 synchronous라고 할 수 있다. 왜냐하면 funcA
에서 isDone이 true가 되야 while문을 빠져나가funcB
를 실행시킬 수 있기 때문이다. 순서가 지켜지고 있다.
그렇다면 funcA
는 blocking일까 non-blocking일까? funcA
는 recv()
에서 가만히 있는 것이 아니라 계속 recv()
함수로부터 작업이 완료됐는지 flag를 받아와 확인하고, 그 flag에 따라 다른 행동을 하고 있다. 따라서 non-blocking이라고 할 수 있다.
그럼 funcB
는 blocking일까 non-blocking일까? funcB
는 현재 doSomethingElse()
의 결과를 기다리고 있다. 즉 doSomethingElse()
작업이 완료되고 err flag에 따라 행동을 달리할 것이다. doSomethingElse()
를 실행하고 다른 행동을 하지 않고 기다리고 있으므로 blocking이라고 할 수 있다.
synchronous와 asynchronous에 조금 더 살펴보자.
집나가기()
지하철타기()
환승하기()
약속장소로걸어가기()
약속에 나가는 행위를 코드로 구현하려고 한다.
약속 장소에 나가려면 위 행위들을 순차적으로 진행해야 한다. 집나가기()
다음에 지하철을 타지도 않았는데 환승하기()
를 할 수 없다. 이것이 synchronous다.
당근사오기()
김치사오기()
고기사오기()
나물사오기()
비빔밥만들기()
이번엔 비빔밥을 만드는 행위를 코드로 구현하려고 한다.
위 행위들은 어떤가? 비빔밥만들기()
를 제외하고 동시에 진행할 수 있지 않을까?
물론, 한 사람이 재료들을 전부 사온다고 하면 synchronous와 다를 게 없을 것이다. 이것이 asynchronous를 싱글 쓰레드로 구현할 수 없는 이유다. 멀티 쓰레드(또는 멀티 프로세스도 가능할 것이다)로 구현하면 사오는 행위를 동시에 진행할 수 있을 것이다.
정리하자면 함수들이 서로 완료되기를 기다리지 않고 자신의 행위를 하는 것, 이것이 asynchronous다.
그런데 다른 관점에서 이 행위를 한 번 바라보자.
재료사오기()
비빔밥만들기()
재료사오기()
에는 위에 작성한 당근사오기()
, 김치사오기()
등의 동작이 포함되어있다. 비빔밥만들기()
는 재료사오기()
가 완료되기 전까지는 실행할 수 없다.
따라서 재료사오기()
와 비빔밥만들기()
는 synchronous라고 할 수 있다.
즉, 다시 한 번 말하면 synchronous와 asynchronous는 두 개 이상의 함수가 논리적인 맥락에 따라 어떤 관계를 갖는지에 대한 이야기이므로 관점을 어떻게 보느냐, 행위의 참여 주체가 누구냐에 따라 다르게 이야기할 수 있다.