Callback이 뭐죠?

JavaScript와 비동기 — 1편

JavaScript를 배우려고 하는 사람은 아마 크게 두 부류가 있을 것이다. 프로그래밍을 이걸로 처음 배우는 사람과 이미 다른 언어를 사용하던 사람. 이제 막 프로그래밍을 시작한 사람이라면 당연히 이게 무슨 말인가 싶겠지만, 사실 다른 언어를 사용하던 입장에서도 JavaScript의 많은 부분이 낯설 것이다. 그리고 이 낯섬의 많은 부분은 다음의 한 문장에서 비롯된다.

JavaScript는 싱글 스레드 이벤트 기반의 언어다.

스레드? 이벤트? 이게 무슨 말일까?


스레드가 뭐죠?

비전문가에게 프로그래밍 언어가 무엇인지 설명할 때 쓰이는 비유 중 하나는 메뉴얼이다. 예를 들자면 라면을 끓이는 프로그램은 아래처럼 만들 수 있다.

라면 메뉴얼
라면 한 봉지를 준비한다.
냄비에 물 3컵을 붓는다.
물에 가루스프를 넣는다.
물이 끓을 때까지 기다린다.
끓는 물에 면발과 건더기스프를 넣는다.
3분을 기다린다.
불을 끄고 냄비를 내린다.

그런데 갑자기 친구가 쳐들어와서 자기는 라면을 먹을때 반드시 김밥이 있어야 한다고 난리를 친다. 그럼 이 메뉴얼을 이렇게 고칠 수 있을 것이다.

라면과 김밥 메뉴얼
라면 한 봉지를 준비한다.
냄비에 물 3컵을 붓는다.
물에 가루스프를 넣는다.
물이 끓을 때까지 기다린다.
끓는 물에 면발과 건더기스프를 넣는다.
3분을 기다린다.
불을 끄고 냄비를 내린다.
밥 한그릇과 김밥김, 김밥 속재료들을 준비한다.
김밥김에 밥을 펴바른다.
그 위에 속재료들을 얹는다.
둘둘 만다.
일정한 간격으로 썬다.
접시에 담는다.

된 걸까? 그럴 리가! 이 메뉴얼 대로라면 김밥을 싸는 동안 라면이 전부 불어버릴것이다. 이제 이 메뉴얼을 좀 더 현실적으로 고쳐 보자.

라면과 김밥 메뉴얼 (최종)
라면 메뉴얼과 김밥 메뉴얼을 준비한다.
친구에게 김밥 메뉴얼을 건네며 김밥을 만들라고 한다.
라면 메뉴얼에 맞춰 라면을 만든다.
친구가 김밥을 다 만들때까지 기다린다.

친구가 놀고 먹는 모습을 지켜볼 필요가 없는 메뉴얼이 완성됐다. 훨씬 낫지 않은가?

첫번째와 두번째 메뉴얼에서는 ‘나’ 혼자서 모든 작업을 순서대로 진행했다. 이런 모델을 프로그래밍에서는 싱글 스레드라고 부른다. 실타래(thread)가 풀리듯이 작업흐름이 한 줄기라는 뜻이다. 반면 세번째 메뉴얼에서는 ‘나’와 친구 두명이서 동시에 작업을 진행했다. 이건 멀티 스레드라고 부른다. 위 예제에서 볼 수 있듯 멀티 스레드는 컴퓨터의 자원을 효율적으로 사용할 수 있으므로 많은 경우에서 싱글 스레드보다 효율적이다.


하지만 친구랑 손발이 안맞으면 더 힘들던데요?

그럴 수 있다! 멀티 스레드를 활용하는 프로그래밍(멀티스레딩)은 공짜가 아니다. 싱글 스레드에서는 고민할 필요도 없지만 멀티스레딩에서는 심심찮게 나타나는 문제들이 많기 때문이다. 가장 간단한 예로 데드락(DeadLock)이 있다. 이해를 위해 라면과 김밥에 깨소금과 고춧가루를 뿌린다고 생각해 보자.

라면 메뉴얼
...
깨소금을 가져온다. 이미 쓰이고 있으면 기다린다.
고춧가루를 가져온다. 이미 쓰이고 있으면 기다린다.
깨소금과 고춧가루를 뿌리고 되돌려 놓는다.
...
김밥 메뉴얼
...
고춧가루를 가져온다. 이미 쓰이고 있으면 기다린다.
깨소금을 가져온다. 이미 쓰이고 있으면 기다린다.
고춧가루와 깨소금을 뿌리고 되돌려 놓는다.
...

그리고 우연히 나와 친구가 각각 깨소금과 고춧가루를 동시에 들고 갔다고 생각해 보자. 나는 그 다음으로 고춧가루를 가져오고 싶지만, 친구가 쓰고 있으므로 기다린다. 문제는 친구도 마찬가지 이유로 기다릴 거란 점이다. 아마 나와 친구는 거대 운석이 떨어져 지구가 멸망하기 전까지 서로를 기다리고 있을 것이다. 이렇게 무한히 기다리게 되는 경우 데드락에 걸렸다 고 한다.

데드락 외에도 멀티스레딩에서 흔히 나타나는 문제들은 한 두 가지가 아니다. 여러 실행흐름이 동시에 이어진다는 개념 자체가 인간이 이해하고 적응하기 쉽지 않은 것이다. 양쪽 눈으로 서로 다른 영화 두 편을 동시에 감상한다고 생각해 보자. 집중할 수 있겠는가?


라면 끓이고 김밥 싸는 정도는 그냥 혼자 해도 되는거 아니예요?

그렇다! 멀티스레딩으로 해결하면 될 것처럼 보이는 작업 중 대부분은 사실 굳이 멀티스레딩까지 들고 올 필요가 없는 경우가 많다. 벽돌을 날라 건물을 짓는 작업이라면 두명이서 하면 두배로 빨리 할 수 있을 것이다. 하지만 평일 오전 치킨집 전화응대를 두명이서 한다고 손님이 기다리는 시간이 줄어들 것 같지는 않다.

그렇다면 메뉴얼은 어떻게 짜야 할까? 우린 이미 위에서 혼자 라면과 김밥을 만들 수 있는 메뉴얼을 작성해 보았다. 그런데 이걸 잘 보면 좀 이상한 점을 찾을 수 있을 것이다.

라면과 김밥 메뉴얼
...
물이 끓을 때까지 기다린다.
...
3분을 기다린다.
...

김밥도 싸야 하는데 고작 물이나 쳐다보며 기다린다니, 대체 누가 이딴걸 메뉴얼이라고 만든 걸까? 당신이 직접 짠다면 아래처럼 만들 수도 있을텐데 말이다.

라면과 김밥 메뉴얼 (개선)
라면 한 봉지를 준비한다.
냄비에 물 3컵을 붓는다.
물에 가루스프를 넣는다.
물이 끓으면...
끓는 물에 면발과 건더기스프를 넣는다.
3분이 지나면...
불을 끄고 냄비를 내린다.
밥 한그릇과 김밥김, 김밥 속재료들을 준비한다.
김밥김에 밥을 펴바른다.
그 위에 속재료들을 얹는다.
둘둘 만다.
일정한 간격으로 썬다.
접시에 담는다.
라면이 다 되면 함께 상을 차린다.

이제 물이 끓는 동안 김밥을 쌀 수 있다. 좀 더 엄밀히 말하자면 물이 끓는 이벤트가 발생하면 라면을 끓이자고 메모해 두고, 그 전까지는 김밥을 싸는 것이다. 이게 바로 이벤트 기반이다!


그래서 Callback이 뭐예요?

우선 Callback이라는 단어의 뜻부터 생각해 보면, call back, 다시 부른다는 의미다. 위의 개선된 메뉴얼을 보자. 물이 끓는 이벤트가 발생하면, 이 이벤트는 김밥을 싸고 있는 당신을 라면 냄비 앞으로 다시 부를 것이다. 간단한 JavaScript 코드로 살펴보자.

var button = document.getElementById('my-button')
button.onclick = function onButtonClick () {
console.log('버튼 눌림')
}
... // 잡다한 작업들

이 코드는 웹 페이지가 잡다한 작업들을 하고 있는 와중에도 버튼이 클릭되면 onButtonClick 함수를 다시 불러서 실행할 것이다. 이게 바로 이런 함수를 Callback 함수라고 부르는 이유다.


그래서 무슨 말이 하고 싶으신거죠?

JavaScript는 싱글 스레드 이벤트 기반의 언어로 main() 함수가 끝난다고 프로세스를 종료시켜버리지 않는다. 본래 이는 클라이언트 사이드 스크립트 언어라는 독특한 위치에서 나온 결정이었지만, C10K 등의 문제로 이벤트 모델이 각광받음에 따라 NodeJS라는 서버사이드 런타임이 등장하는 계기가 되기도 하였다.

하지만 Thread.wait() 대신 setTimeout()을 사용하는 등의 JavaScript만의 독특한 흐름제어는 익숙하지 않은 사람에게 혼란을 주기 쉽다. 그러나 Callback 등의 구문이 실제로 어떻게 작동하는지 이해한다면 혼란을 줄이고 보다 좋은 코드를 짤 수 있을 것이다.

물론 이러한 방식이 앞서 말한 여러 문제를 해결한다고 해도 실제 코딩하는 사람에게 불편해서 제대로 쓰지 못한다면 결국 아무 의미 없을 것이다. 그래서 ES2015(ES6이라고도 한다)에서는 이를 해결하기 위한 새로운 방법을 내놓았다. 여기에 대해서는 다음 글에서 살펴보기로 하자.