1. 개요
다른 함수의 인자로써 넘겨진 후 특정 이벤트에 의해 호출되는 함수. 이 때 함수는 포인터나 람다식 등으로 전달된다. 흔히 시스템에 의해 호출 시점이 결정되는 함수라고 정의되기도 하나 여기[1]에서도 볼 수 있듯 이제 이러한 문장은 굉장히 낡고 협소한 정의가 되어버렸다.2. 예시
Windows API를 예로 들자면#!syntax cpp
#include <windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
LPSTR lpszClass = "First";
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpszCmdParam, int nCmdShow)
{
HWND hWnd;
MSG Message;
WNDCLASS WndClass;
g_hInst = hInstance;
WndClass.cbClsExtra = 0;
WndClass.cbWndExtra = 0;
WndClass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
WndClass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
WndClass.hInstance = hInstance;
WndClass.lpfnWndProc = (WNDPROC)WndProc;
WndClass.lpszClassName = lpszClass;
WndClass.lpszMenuName = NULL;
WndClass.style = CS_HREDRAW | CS_VREDRAW;
RegisterClass(&WndClass);
hWnd=CreateWindow(lpszClass,lpszClass, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, (HMENU)NULL, hInstance, NULL);
ShowWindow(hWnd, nCmdShow);
while(GetMessage(&Message, 0, 0, 0)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
return Message.wParam;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
{
switch(iMessage) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
이때 WndProc 함수는 프로그래머가 직접 호출하는 것이 아닌 프로세스에 이벤트가 들어왔을 때 DispatchMessage 함수를 호출하여 이벤트를 처리하며 시스템에서 WndProc를 호출한다.
이런 콜백함수가 널리 쓰이는 대표적인 예시는 Node.js 런타임에서 구동되는 JavaScript 코드이다. Node.js의 특성상 비동기처리가 많기 때문에 처리의 결과를 프로그래머가 받기 위해서는 콜백함수를 사용하여 시스템이 결과값을 콜백함수를 호출하여 넘겨주기 때문이다. Node.js는 이게 숱하게 중첩되는 경우가 많아 콜백 지옥이라는 별명까지 붙었던 사례가 있다.
예를 들어 pbkdf2는 문자열의 해시를 계산하는 함수이며 그 처리시간이 오래 걸린다. 그 처리시간을 그저 기다리지 말고 다른 작업를 하여 전체 처리 시간을 줄일 수 있다.
#!syntax javascript
const crypto = require('crypto');
crypto.pbkdf2('secret', 'salt', 100000, 512, 'sha512', (err, key) => {
if (err) throw err;
console.log(key.toString('hex')); // 'c5e478d...1469e50'
});
console.log("main ended.");
콜백은 어디까지나 비동기 구현을 위한 방법 중 하나일 뿐이고, 현대의 JavaScript에는 Promise나 async/await 등의 문법이 추가됨에 따라 콜백의 사용을 크게 줄일 수 있다. JavaScript를 포함하여 어지간한 고생산성 언어에서는 비동기 프로그램에서의 흐름 제어를 편리하게 하기 위해 비슷한 방식의 API나 문법들이 제공된다. 예를 들어 위 코드를 아래와 같이 한 번만 wrapping해 놓으면 이후에는 코드가 대각선으로 자라나는 일 없이 암호화 작업을 수행할 수 있게 될 것이다.
#!syntax javascript
const crypto = require('crypto');
function pbkdf2(password, salt, iterations, len, hashType) {
return new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, iterations, len, hashType, (err, key) => {
err ? reject(err) : resolve(key.toString('hex'));
});
});
}
async function doCrypto() {
const result = await pbkdf2('secret', 'salt', 100000, 512, 'sha512');
console.log(result); // 'c5e478d...1469e50'
}
최신 JavaScript 문법을 반영한 라이브러리 패키지들은 처음부터 비동기 형태의 API가 제공되는 경우가 대부분이므로 굳이 wrapping하는 번거로움을 겪을 일도 적다.
3. 콜백 지옥(callback Hell)
비동기 작업을 위해 사용되는 콜백의 특성상 비동기 이후에 처리될 작업들을 콜백 내부에 작성해주어야 한다.만일 숙련된 프로그래머라면 이런 콜백 지옥이 발생하지 않도록 세심하게 고려해서 코드를 작성하겠지만[2], 콜백이란 개념을 처음 접하는 프로그래머들은 그렇지 않고 콜백 안에 콜백을 무는 식으로 작성하게 되는 경우가 많다
그러면 아래와 같은 끔찍한 코드가 탄생하게 된다
#!syntax javascript
obj.callback(parameter1, () => {
obj.callback(parameter2, () => {
obj.callback(parameter3, () => {
obj.callback(parameter4, () => {
obj.callback(parameter5, () => {
obj.callback(parameter6, () => {
console.log('Hello, World!')
})
})
})
})
})
})
이런식으로 계속해서 콜백안에 콜백을 물어서 오른쪽 아래로 코드가 내려가는 형태를 콜백 지옥이라고 한다. 보기에도 어지럽고 읽기도 불편한데다, 코드의 흐름을 추적하기도 쉽지 않고, 만약 수정할 일이 생기면 콜백의 구조 특성상 수정도 쉽지 않다.
간단한 코드는 콜백으로 작성하면 깔끔하게 보여서 나쁘지 않지만 만일 조금이라도 복잡해지기 시작하면 답이 없기 때문에 각 언어에서 제공하는 콜백 지옥을 방지할만한 기능을 알아보고 적극적으로 활용하도록 하자.
[1] MS 공식 가이드. C#의 delegate 개념을 설명하면서 callback이라는 용어를 사용하며, 이 callback의 호출은 사용자 코드 내부에서 이루어진다.[2] 예를 들어 JavaScript의 Promise, async/await 를 사용하는 등