콜백

내위키
Dennis (토론 | 기여)님의 2020년 12월 25일 (금) 02:26 판

Callback.

콜백함수(callback function)라고도 부른다. 프로그래밍 언어에서 함수의 매개변수(인수)로 전달되는 함수로, 콜백함수를 매개변수로 받은 함수 또는 객체는 이를 적절한 시기에 실행시킬 수 있다.

Dart 언어에서 콜백함수를 쓰는 간단한 예를 보면,

void count(int number){
  for (int i = 0; i < 5; i++) {
    print('hello ${i + 1}');
  }
}

void main() {
  count(5);
}

콜백함수를 쓰지 않을 때에는 for 루프로 변수의 값을 1씩 증가시키는 부분과, 변수의 값을 출력하는 부분이 모두 count() 함수 안에 들어가 있다.

void hello(int i) {
  print('hello ${i + 1}');
}

void goodbye(int i) {
  print('goodbye ${i + 1}');
}

void count(int number, void Function(int) callback){
  for (int i = 0; i < 5; i++) {
    callback(i);
  }
}

void main() {
  count(5, hello);
  count(5, goodbye);
}

count() 함수가 두 번째 매개변수로 콜백함수를 받도록 하고 숫자를 출력하는 부분을 hello(), goodbye() 콜백함수로 분리했다. 그리고 main() 함수에서는 처음에는 hello() 함수를 콜백으로 전달해서 count() 함수를 실행시키고 다음에는 goodbye() 함수를 콜백으로 count() 함수를 실행시킨다. 이제 count()는 직접 출력을 처리하지 않고 숫자가 1 증가할 때마다 전달 받은 콜백함수만 부른다. 콜백함수를 어떻게 만드느냐에 따라 출력 형식을 바꿀 수도 있고 출력이 아닌 다른 일을 할 수도 있지만 count() 함수 내부에는 영향을 미치지 않는다.

C에서는 함수 포인터 방식으로 콜백을 구현한다.

#include <stdio.h>

void hello(int i) {
  printf("hello %d\n", i + 1);
}

void goodbye(int i) {
  printf("goodbye %d\n", i + 1);
}

void count(int number, void(*callback)(int)){
  for (int i = 0; i < 5; i++) {
    callback(i);
  }
}

int main() {
  count(5, hello);
  count(5, goodbye);

  return 0;
}

콜백이 주로 쓰이는 경우는 함수 또는 컴포넌트 간의 통신이다. 예를 들어, 프로그램에서 + 버튼을 누를 때마다 숫자가 1씩 올라가도록 표시하는 컴포넌트가 있다고 가정해 보자. 버튼이 눌리는 이벤트가 발생할 때 버튼이 직접 해당 컴포넌트를 조작하도록 할 수도 있지만, 같은 버튼을 눌렀을 때 내용이 업데이트 되어어 할 컴포넌트가 한 개 이상일 수도 있고, 상황에 따라 그런 컴포넌트의 갯수가 바뀔 수도 있다. 버튼 쪽에서 이걸 다 감안해서 코드를 넣으면 상호 의존성이 커지고[1] 효율이 떨어진다. 그보다는 이러한 컴포넌트들이 버튼이 눌렸을 때 해야 할 동작을 콜백함수 안에 넣고, 이를 버튼에 넘겨주면 버튼이 눌렸을 때 그냥 콜백함수만 불러주면 된다.

비동기처리를 할 때에도 많이 쓰이는데, CPU와 메모리 사이의 작업에 비해서 보조기억장치, 네트워크와 통신하는 작업은 훨씬 시간이 오래 걸리는데 예를 들어 네트워크로 요청을 보내서 답을 받을 때까지 컴퓨터 자원은 그냥 멍때리고 있어야 한다. 이 때문에 요청을 보내놓고 컴퓨터는 다른 일을 하다가 답이 왔을 때 이를 처리하는 비동기 프로그래밍을 많이 사용하는데, 프로그래밍 언어에서 비동기 프로그래밍 지원을 하지 않는 경우에는 콜백함수로 비슷한 효과를 내는 경우가 많으며, 프로그래밍 언어에서 비동기 지원을 하는 경우라도 리스너 패턴을 비롯한 다양한 방법으로 콜백함수를 쓰게 된다.

콜백 지옥

콜백은 여러모로 쓸모가 많지만 프로그래머의 뒷목을 잡게 만드는 원흉일 때도 있다. 그 대표 사례가 자바스크립트의 콜백 지옥. 자바스크립트는 비동기 처리를 위해 콜백을 주로 사용하는데, 비동기처리로 해야 할 일이 많아지다 보면 콜백함수 안에서 다시 콜백함수를 호출하고, 그 콜백함수 안에서 또 콜백함수를 호출하고 ... 이러다 보면 들여쓰기가 하염없이 이어지고 뭐가 뭔지 모르게 되는 콜백 지옥에 빠지기 십상이다.

$.get('url', function(response) {             // 지정한 URL로부터 RESPONSE를 받아온다.
	parseValue(response, function(id) {       // 받아온 값을 파싱해서 ID를 얻는다.
		auth(id, function(result) {           // 받아온 ID로 인증을 진행한다.
			display(result, function(text) {  // 인증 결과를 표시한다.
				console.log(text);
			});
		});
	});
});

익명함수를 쓰지 않고 전부 함수에 이름을 붙여서 쓰면 콜백 지옥은 피할 수 있는데, 저기서 한 번만 쓸 함수를 그렇게 하는 것은 좀 낭비이긴 하다. 그래서 ES6에는 promise라는 개념이 도입되었고, ES7에서는 await, async 같은 비동기 지원 키워드가 생겼다. 현대 프로그래밍 언어들은 비동기 처리가 중요하기 때문에 대부분 await, async 키워드 또는 비슷한 형태의 키워드로 비동기 처리를 지원한다.

각주

  1. 의존성이 커지면 버그가 있을 때 문제 해결이 복잡해진다. 프로그래머들은 상호 의존성을 줄이기 위해 별별 짓을 다 한다.