콜백: 두 판 사이의 차이

내위키
편집 요약 없음
편집 요약 없음
 
(같은 사용자의 중간 판 15개는 보이지 않습니다)
5번째 줄: 5번째 줄:
[[Dart]] 언어에서 콜백함수를 쓰는 간단한 예를 보면,
[[Dart]] 언어에서 콜백함수를 쓰는 간단한 예를 보면,


<source lang="dart">
<syntaxhighlight lang="dart">
void count(int number){
void count(int number){
   for (int i = 0; i < 5; i++) {
   for (int i = 0; i < 5; i++) {
15번째 줄: 15번째 줄:
   count(5);
   count(5);
}
}
</source>
</syntaxhighlight>


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


<source lang="dart">
<syntaxhighlight lang="dart">
void hello(int i) {
void hello(int i) {
   print('hello ${i + 1}');
   print('hello ${i + 1}');
38번째 줄: 38번째 줄:
   count(5, goodbye);
   count(5, goodbye);
}
}
</source>
</syntaxhighlight>


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


<source lang="c">
<syntaxhighlight lang="c">
#include <stdio.h>
#include <stdio.h>


67번째 줄: 67번째 줄:
   return 0;
   return 0;
}
}
</source>
</syntaxhighlight>


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


비동기처리를 할 때에도 많이 쓰이는데, CPU와 메모리 사이의 작업에 비해서 보조기억장치, 네트워크와 통신하는 작업은 훨씬 시간이 오래 걸리는데 예를 들어 네트워크로 요청을 보내서 답을 받을 때까지 컴퓨터 자원은 그냥 멍때리고 있어야 한다. 이 때문에 요청을 보내놓고 컴퓨터는 다른 일을 하다가 답이 왔을 때 이를 처리하는 비동기 프로그래밍을 많이 사용하는데, 프로그래밍 언어에서 비동기 프로그래밍 지원을 하지 않는 경우에는콜백함수로 비슷한 효과를 내는 경우가 많으며, 프로그래밍 언어에서 비동기 지원을 하는 경우라도 리스너 패턴을 비롯한 다양한 방법으로 콜백함수를 쓰게 된다.
비동기처리를 할 때에도 많이 쓰이는데, CPU와 메모리 사이의 작업에 비해서 보조기억장치, 네트워크와 통신하는 작업은 훨씬 시간이 오래 걸리는데 예를 들어 네트워크로 요청을 보내서 답을 받을 때까지 컴퓨터 자원은 그냥 멍때리고 있어야 한다. 이 때문에 요청을 보내놓고 컴퓨터는 다른 일을 하다가 답이 왔을 때 이를 처리하는 비동기 프로그래밍을 많이 사용하는데, 프로그래밍 언어에서 비동기 프로그래밍 지원을 하지 않는 경우에는 콜백함수로 비슷한 효과를 내는 경우가 많으며, 프로그래밍 언어에서 비동기 지원을 하는 경우라도 리스너 패턴을 비롯한 다양한 방법으로 콜백함수를 쓰게 된다.
 
특히 비동기 함수를 사용해서 값을 바꾸고 그 값을 다른 명령어에서 사용할 때에는 그 '다른 명령어'가 값을 참조할 때 비동기 함수가 값을 바꿨는지 여부가 불확실하다. 예를 들어 자바스크립트에서,
 
<syntaxhighlight lang="javascript">
var foo = 0
 
setTimeout(function() {
  foo++
}, 1000)
 
console.log('Foo is ' + foo)
</syntaxhighlight>
 
addOne() 함수가 먼저 실행되면 그 안에 있는 setTimeout() 문이 먼저 실행되고, 그 안에 있는 foo++ 문으로 foo 변수를 0에서 1로 증가시켰으므로 뒤이어 나오는 console.log() 명령에 따라 콘솔에는 {{InlineCode|lang="console"|Foo is 1}}이 나와야 하지만 실제로는 {{InlineCode|lang="console"|Foo is 0}}이 나온다. setTimeOut()은 1,000 밀리초, 즉 1초를 지연시킨 후에 foo++ 문을 실행시키는 비동기 함수다 보니 뒤에 나오는 console.log()가 먼저 실행되어 버린다.
 
<syntaxhighlight lang="javascript">
var foo = 0
 
function addOne(callback) {
  setTimeout(function() {
    foo++
    callback()
    }, 1000)
}
 
addOne(function() {console.log('Foo is ' + foo)})
</syntaxhighlight>
 
addOne() 함수를 콜백함수를 매개변수로 받도록 한 다음, setTimeout() 안의 foo++ 다음에 실행시키도록 바꾸고, 콘솔 출력 명령을 콜백함수 형식으로 넘겨주면 원래 기대했던 대로 {{InlineCode|lang="console"|Foo is 1}}이 콘솔에 출력된다. 또한 콜백을 사용하지 않았을 때에는 콘솔 출력이 바로 나오지만 콜백을 사용하면 setTimeout()에 지정된 대로 1초 후에 출력될 것이다.


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


<source lang="javascript">
<syntaxhighlight lang="javascript">
$.get('url', function(response) { // 지정된 URL로부터 RESPONSE를 받아온다.
$.get('url', function(response) {             // 지정한 URL로부터 RESPONSE를 받아온다.
parseValue(response, function(id) { // 받아온 값을 파싱해서 ID를 얻는다.
  parseValue(response, function(id) {       // 받아온 값을 파싱해서 ID를 얻는다.
auth(id, function(result) { // 받아온 ID로 인증을 진행한다.
    auth(id, function(result) {           // 받아온 ID로 인증을 진행한다.
display(result, function(text) {  // 인증 결과를 표시한다.
      display(result, function(text) {  // 인증 결과를 표시한다.
console.log(text);
        console.log(text);
});
      });
});
    });
});
  });
});
});
</source>
</syntaxhighlight>
 
이 정도는 지옥 축에도 못 들어간다. 10 단계 이상은 흔하고 작정하면 수십 단계도 못 만들 게 없다. 콜백 지옥이 계속 이어지면 결국 함수 스택이 꽉 차서 오류가 터지기 때문에 무한대의 지옥은 불가능한지만 그 정도까지 가려면 만 단위까지 가야 하므로 얼마든지 끝없는 지옥을 맛볼 수 있다. [[Node.js]] 64x 환경에서 테스트해 본 바로는 지역 변수가 없는 함수라면 20961 단계, 지역 변수가 하나 있는 함수라면 17967 단계까지 재귀호출을 할 수 있다.<ref>[https://glebbahmutov.com/blog/javascript-stack-size/ "JavaScript stack size"], Better world by better software, 16 May 2014.</ref>
 
익명함수를 쓰지 않고 전부 함수에 이름을 붙여서 쓰면 콜백 지옥은 피할 수 있는데, 저기서 한 번만 쓸 함수를 그렇게 하는 것은 코드 낭비이긴 하다. 그래서 ES6에는 promise라는 개념이 도입되었고, ES7에서는 await, async 같은 비동기 지원 키워드가 생겼다. 현대 프로그래밍 언어들은 비동기 처리가 중요하기 때문에 대부분 await, async 키워드 또는 비슷한 형태의 키워드로 비동기 처리를 지원한다.


{{각주}}
{{각주}}

2021년 10월 21일 (목) 10:16 기준 최신판

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

특히 비동기 함수를 사용해서 값을 바꾸고 그 값을 다른 명령어에서 사용할 때에는 그 '다른 명령어'가 값을 참조할 때 비동기 함수가 값을 바꿨는지 여부가 불확실하다. 예를 들어 자바스크립트에서,

var foo = 0

setTimeout(function() {
  foo++
}, 1000)

console.log('Foo is ' + foo)

addOne() 함수가 먼저 실행되면 그 안에 있는 setTimeout() 문이 먼저 실행되고, 그 안에 있는 foo++ 문으로 foo 변수를 0에서 1로 증가시켰으므로 뒤이어 나오는 console.log() 명령에 따라 콘솔에는 Foo is 1이 나와야 하지만 실제로는 Foo is 0이 나온다. setTimeOut()은 1,000 밀리초, 즉 1초를 지연시킨 후에 foo++ 문을 실행시키는 비동기 함수다 보니 뒤에 나오는 console.log()가 먼저 실행되어 버린다.

var foo = 0

function addOne(callback) {
  setTimeout(function() {
    foo++
    callback()
    }, 1000)
}

addOne(function() {console.log('Foo is ' + foo)})

addOne() 함수를 콜백함수를 매개변수로 받도록 한 다음, setTimeout() 안의 foo++ 다음에 실행시키도록 바꾸고, 콘솔 출력 명령을 콜백함수 형식으로 넘겨주면 원래 기대했던 대로 Foo is 1이 콘솔에 출력된다. 또한 콜백을 사용하지 않았을 때에는 콘솔 출력이 바로 나오지만 콜백을 사용하면 setTimeout()에 지정된 대로 1초 후에 출력될 것이다.

콜백 지옥

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

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

이 정도는 지옥 축에도 못 들어간다. 10 단계 이상은 흔하고 작정하면 수십 단계도 못 만들 게 없다. 콜백 지옥이 계속 이어지면 결국 함수 스택이 꽉 차서 오류가 터지기 때문에 무한대의 지옥은 불가능한지만 그 정도까지 가려면 만 단위까지 가야 하므로 얼마든지 끝없는 지옥을 맛볼 수 있다. Node.js 64x 환경에서 테스트해 본 바로는 지역 변수가 없는 함수라면 20961 단계, 지역 변수가 하나 있는 함수라면 17967 단계까지 재귀호출을 할 수 있다.[2]

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

각주

  1. 의존성이 커지면 버그가 있을 때 문제 해결이 복잡해진다. 프로그래머들은 상호 의존성을 줄이기 위해 별별 짓을 다 한다.
  2. "JavaScript stack size", Better world by better software, 16 May 2014.