티스토리 뷰
라이브러리를 만드는 라이브러리 프로그래머
함수 포인터를 열심히 배워도 도대체 어디에 쓰는지 알지 못한다면 배우는 의미가 없겠지요.
지금부터는 함수 포인터가 실제 프로그램에서 어땡게 사용되는지 예시를 들어 차근차근 설명하겠습니다. 조금 어려울 수 있지만 잠 따라오기 바랍니다.
모든 프로그래머가 완제품 형식의 프로그램을 만들지는 않습니다. 예를 들어 음성 데이터를 압축하거나 영상 데이터를 변환하는 작업들은 특별한 지식이 필요하기 때문에 쉽게 구현할 수 없습니다. 그래서 이런 기능을 쉽게 사용할 수 있도록 함수로 만들어서 판매하는 프로그래머들도 있습니다.
이들 프로그래머는 자신의 코드가 노출되면 안 되기 때문에 해당 코드를 컴파일해서 라이브러리(ibrary, *.lib) 형식의 파일로 제공합니다. 그리고 라이브러리 안에 있는 함수들이 어떤 형태로 선언된 함수인지 알아야 코드를 자세히 볼 수 없는 사용자들도 사용할 수 있기 때문에 함수의 원형들을 헤더(header, *.h) 파일에 적어서 함께 제공합니다.
예를 들어 두 개의 정수 값을 넘겨받아서 합산하는 Sum 함수를 라이브러리 형태로 제공한다.
고 생각해 봅시다. 그러면 라이브러리 사용자에게는 파일 내부를 볼 수 없는 라이브러리 파일 sum.lib와 라이브러리 파일을 설명하는 헤더 파일 sum.h를 모두 제공해야 합니다.

라이브러리 프로그래머의 고민
이브러리에 포함된 Sum 함수를 사용하던 사용자(일반 프로그래머)가 Sum 함수에 전달되는 두 숫자 값이 음수인 경우에 양수로 변환해서 합산하는 함수도 추가로 만들어 달라고 요청했다고 합시다. 그러면 라이브러리 프로그래머는 SumA BS라는 새로운 함수를 추가한 후 다시 라이브러리 파일로 만들어서 사용자에게 제공해야 합니다.

하지만 라이브러리 사용자가 첫 번째 매개변수 값만 음수이면 양수로 변환하는 기능이나 두번째 매개변수 값만 음수이면 양수로 변환하는 기능을 또 추가해 달라고 요청하면 어떻게 해야 할까요? 아마 라이브러리 프로그래머는 점점 함수가 늘어나서 관리하기 힘들어질 것입니다.
이것은 단순히 함수가 늘어나는 문제를 떠나서 라이브러리가 사용자의 요구에 점점 종속되는 결과를 가져옵니다.
그래서 라이브러리에 포함된 함수는 본래의 기능을 유지하고 사용자가 원하는 경우에 스스로 함수의 기능을 일부 수정할 수 있도록 제공하는 것이 더 좋습니다. 하지만 그렇다고 해서 라이브러리 소스 코드 전체를 줄 수는 없겠죠. 그러면 라이브러리 사용자는 어떻게 소스 코드없이 라이브러리를 수정해서 사용할 수 있을까요?
함수의 매개변수로 함수 포인터 사용하기
앞의 예시에서 SumABS 함수에는 a 값과 b 값을 수정하는 작업이 추가되어 있습니다. 조건문으로 처리된 두 코드가 비슷하기 때문
에 라이브러리 소스 코드에서 다음과 같이 함수로 만들어서 처리할 수도 있습니다.

만약 라이브러리 사용자가 MyAbsolute 같은 기능의 함수를 만들어서 기존 라이브러리에 있 는 Sum 함수를 호출할 때 함께 사용하고 싶다면 어떻게 해야 할까요? 기존 라이브러리에는 라이브러리 사용자가 새로 만들고자 하는 MyAbsolute라는 이름의 함수는 당연히 없기때문 에 함수 이름으로는 호출할 수 없습니다.
우리가 지금까지 배운 함수 포인터를 활용하면 함수 이름이 없어도 함수를 호출할 수 있습니다. 예를 들어 MyAbsolute 함수는 함수의 원형이 void MyAbsolute(int *); 이기 때문에 라이브러리 소스 코드에서 다음과 같이 포인터를 사용하면 이 함수의 주소를 받아서 사용할 수있습니다.
void (*p)(int*);
따라서 SumABS 함수는 함수 포인터를 사용하여 다음과 같이 변경할 수 있습니다.

라이브러리 프로그래머가 함수 포인터를 사용해 Sum시BS 압수물 구성하면 사용자가 원하는 MvAbsolute 함수가 없더라도 정상적으로 컴파일이 완료됩니다. 그리고 라이브러리 사용자가 MyAbsolute 함수를 만들어서 자신이 사용하고 싶은 시점에 다음과 같이 SumABS 함수를 사용할 수도 있습니다.
SumABS(5, -1, &MyAbsolute); // SumABS 함수에서 MyAbsolute 함수를 호출한 것과 같음
SumABS 함수를 이렇게 구성하면 MyAbsolute를 사용하고 싶을 때는 위의 예시처럼 &MyAbsolute 를 세번째 매개변수로 전달하면 됩니다.
SumABS 함수에서 굳이 MyAbsolute 함수를 사용하지 않는 경우도 있겠죠? 그럴 때는 다음처럼 SumABS 함수의 세 번째 매개변수에 NULL을 입력합니다.
SumABS(5, -1, NULL); // SumABS 함수에서 MyAbsolute 함술르 사용하지 않는 경우
그런데 아직 소스 코드에는 SumABS 수의 세 번째 매개변수로 NUIL이 넘어오는 상황을 처리하지 않았습니다. 따라서 NULL이 발생하는 상황을 처리할 수 있도록 다음과 같이 수정해야 합니다.
|
1
2
3
4
5
6
|
int SumABS(int a, int b, void (*fp_abs)(int*))
{
if(fp_abs != NULL) (*fp_abs)(&a);
if(fp_abs != NULL) (+fp_abs)(&b);
return a+b;
}
|
cs |
조건문을 통해 fp_abs에 저장된 주소가 NULL인지 먼저 확인한 후 *(fp_abs)(&a) 또는 •(fp_abs)(&b)라고 사용해서 매개변수로 전달되는 함수를 호출합니다. 그러면 SumABS 함수를 사용할 때 MyAbsolute 함수를 호출할 것인지 선택한 수 있게 됩니다.
이렇게 소스 코드를 수정하고 나면 Sum 함수는 SumABS 함수의 세 번째 매개변수에 NULL 을 전달해서 사용하는 것과 같습니다. 따라서 함수의 이름을 Sum으로 통일하고 기능을 합칠 수 있습니다. 그리고 a 변수 값과 b 변수 값에 적용되는 기준이 다를 수도 있기 때문에 함수 포인터를 a, b 변수마다 다르게 사용할 수 있도록 설정하겠습니다. 즉, 다음과 같이 함수 포인터를 하나 더 추가해서pa 와 pb 로 변경하겠다는 뜻입니다.

위의 예시는 함수 표인터가 추가되어 조금 어렵게 보이지만 코드를 이렇게 구성하면 이 함수를 사용하는 사용자가 함수의 기능을 자유롭게 확장할 수 있습니다. 예를 들어 Sum 함수를 사용하는 사용자가 함수의 기능은 번경하지 않고 데이터 합산만 하고 싶다면 다음과 같이 사 용하면 됩니다.
|
1
2
3
4
5
6
7
|
#include "sum.h"
#pragma comment(lib, "sum.lib")
void main()
{
int result = Sum(-3, -2, NULL, NULL); // result에 값 -5가 저장됨
}
|
cs |
이렇게 사용하면 Sum 함수의 셰 번재, 네 번째 매개변수의 함수 포인터에 NULL 값이 대입되어 Sum 수의 if문은 둘 다 처리되지 않습니다. 따라서 -3과 -2는 양수로 변환되지 않고 a+ b결과 값 -5를 반환합니다.
함수의 매개변수로 함수의 주소를 전달하여 호출하기
만약 라이브러리 사용자가 Sum 함수의 첫 번째 매개변수에만 음수가 전달되는 경우에 양수로 바꿔서 더하고 싶다면 다음과 같이 함수를 사용하면 됩니다.
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include "sum.h"
#pragma comment(1 ih- "sum.lib")
// 정수 변수 값의 주소를 넘겨주면 해당 값이 음수이민 양수로 변환하는 함수
void NyAbsolute(int *p)
{
if(*p < 0) +p = (*p) * (-1); // 전달된 주소에 있는 값이 음수이면 -1 을 곱해서 양수로 만듬
}
void main()
{
int result = Sum(-3, -2, MyAbsolute, NULL); // result 에 값 1 이 저장됨
}
|
cs |
초보 프로그래머는 수 포인터를 이해하기 어려울 수 있으니 위 예제의 실행 순서와 의미를 단계별로 설명하겠습니다.
1. main 함수에서 Sum 함수를 호출합니다. Sum 함수의 세 번째 매개변수에 MyAbsolute 함수의 주소 값이 넘어가서 Sum 함수의 함수 포인터 pa 에 저장됩니다.

2. Sum 함수의 pa 변수가 NULL이 아니기 때문에 if(NULL != pa) (*pa)(&a); 문장이 수행됩니다.
3. Sum 함수의 pa 변수가 MyAbsolute 함수의 주소 값을 가지고 있기 때문에 ( *pa)(&a);는 MyAbsolute(&a);라고 사용한 것과 같습니다. 따라서 Sum 함수의 매개변수 a의 주소값이 MyAbsolute 함수의 포인터 변수 p에 저장됩니다.
4. MyAbsolute 함수의 포인터 변수 p)는 변수 a의 주소 값을 저장하고 있습니다. 포인터 변수 p가 가리키는 값은 음수인 -3이라서 if(*p < 0) *p= (*p) * (-1); 조건문이 실행됩니다. 따라서 -3에 -1 이 곱해져서 양수로 변환된 결과 값 3이 Sum 함수의 변수 a에 저장됩니다.
5. 함수의 포인터 pb에는 NULL 값이 저장되어 있기 때문에 :(NULL != pb) (*pb)(&b); 조전문은 실행되지 않습니다.
6. Sum 함수의 변수 값이 -3에서 3으로 변경되었기 때문에 3 + (-2)가 수햄되어 1이 반환됩니다.
결과적으로 라이브러리 안에 있는 Sum 함수는 수정되지 않았지만, 함수 포인터를 사용해서 MyAbsolute 함수를 호출했기 때문에 다음과 같이 사용한 것과 같습니다.
|
1
2
3
4
5
|
int Sum(int a, int b)
{
MyAbsolute(&a); // if(NULL != pa) (*pa)(&a);
return a + b;
}
|
cs |
함수의 암시적 호출 : 콜백
이렇게 함수 포인터를 사용하면 Sum 함수는 a, b 변수에 대한 어떤 요구 조건이 생겨도 다 처리할 수 있습니다. 라이브러리에 포함된 Sum 함수에 새로운 기능이 필요하더라도 사용자가 라이브러리 프로그래머에게 기능을 추가해 달라는 요구를 하지 않아도 되는 것 입니다. 왜냐하면 자신이 직접 MyAbsolute 함수를 만들어서 이 함수의 주소 값을 Sum 함수에 매개변수로 전달하면 되기 때문이죠.
하지만 위에서 설명한 구조는 초보 프로그래머들에게 어려율 수밖에 없습니다. sum.lib를사용하는 사용자 입장에서는 당연히 Sum 함수 내부의 소스 코드를 볼 수 없습니다. 그렇기 때 문에 라이브러리를 사용하는 프로그래머의 소스 코드에는 MvAbsolute 함수가 구현만 되어있지 실제로 언제, 어떻게 호출되는지 알 수 없습니다. 즉 Sum 함수가 적절한 시점에 매개변수로 전달된 MyAbsolute 함수를 호출해서 결과 값을 만들어 준다고 예상만 할 뿐입니다.
이런 식으로 자신이 사용할 함수가 명시적으로 호출되지 않고 함수 포인터에 의해서 호출되는 방식을 암시적 호출,
즉 '콜백' (Callback) 이라고 합니다.
(암시적 호출이란 구체적으로 함수를 명시 하지는 않지만 주어진 상황을 통해 판단해서 함수를 사용(호출)하는 방식입니다.)
그리고 이때 암시적으로 호출되는 MyAbsolute 함수를 '콜백 함수'라고 합니다.
C 언어로 프로그래밍에 처음 입문한 여러분들이 지금 당장 만들 수 있는 프로그램에서 함수 포인터나 콜백 함수를 사용할 일은 없을 수도 있습니다. 따라서 당장 공부를 하지 않아도 괜찮다고 생각할 수도 있겠죠. 하지만 하나의 프로그래밍 언어를 배운다는 것은 그언어를 통해 실제로 유용한 프로그램을 만들 수 있는 기반을 닦는 것입니다. 즉 배운다는 것은 사용한다는 것을 전제로 두게 되죠. C 언어로 만들 수 있는 여러 분야의 프로그램 중 윈도우 응용프로그램을 개발할 때 콜백 함수를 사용하는 경우가 종중 있습니다. 특히
원도우 운영제제에서 모니터에 어떤 화면이 출력되는 프로그램을 개발한다면 반드시 콜백을 사용해야 합니다. 그런데 콜백이나 함수 포인터의 개념을 제대로 익히지 않은 채 원도우 웅용 프로그랩 개발을 시작하는 경우도 많습니다. 이렇게 무작정 시작하면 함수를 어떻게 호출하는 것이 효과적인지, 어떻게 코드를 구성해야 문제없이 잘 동작하는지 이해하지 못한 상태로 사용하다가 버그에 시달리게 됩니다.
당장 이 장의 내용 전부를 이해하지는 못하더라도 펜찮습니다. 이후에 원도우 응용 프로그램을 개발할 때, 이 책에서 콜백이나 함수 포인터에 대한 내용을 본 것을 기억하는 것만으로도 큰 도읍이 될 것입니다.
참고>
콜백 함수는 미래를 준비하는 함수입니다.
라이브러리 프로그래머들은 자신의 라이브러리를 사용할 많은 사용자들을 위해 다양한 조건을 미리 예측해야 합니다. 그런데 예측을 많이 한다고 다 좋은 것은 아닙니다. 왜냐하면 예측한 내용을 모두 함수에 반영하다 보면 조건문이 많아져서 해당 함수는 비효율적인 함수가 될 수밖에 없기 때문이죠.
그래서 정말 필요한 기능만 함수에 구현하고 나머지 예상되는 조건은 수 포인터를 사용하여 이 함수를 사용할 사용자들에게 도움을 요청하는 것이 바로 콜백 구조입니다. 콜백 구조가 어렵다고 할지라도 함수를 사용하는 프로 그래머가 이 함수에 추가적으로 필요한 기능을 스스로 추가해서 사용할 수 있도록 콜백 구조를 만들어 놓는 것이 좋습니다. 즉 예측할 수 없는 여러 가지 조건을 미리 조건문으로 처리하는 것은 현실적으로 어렵지만 콜백 구조를 사용하면 이후에 어떤 문제가 생기든 콜백 함수를 사용해서 대처할 수 있습니다. 이것이 운영체제가 콜백 구조를 많이 사용하는 이유입니다. 운영체제는 한번 만들면 10년 이상 사용하게 되는데 운영체제를 만드는 시점에 10년 뒤 추가월 기술을 예축한다는 것은 불가능에 가깝기 때문입니다.
