cheoly's language study blog

📝 C 언어 함수 포인터와 콜백 함수: 유연한 코드 설계를 위한 핵심 패턴

프로그래밍/C언어
반응형
SMALL

C 언어에서 함수를 변수처럼 다루는 '함수 포인터'와 이를 활용한 '콜백 함수'의 모든 것을 파헤칩니다. 코드의 유연성을 극대화하고, 재사용 가능한 모듈을 설계하는 핵심 비법을 실제 예제와 함께 자세히 설명합니다. (키워드: C언어, 함수 포인터, 콜백 함수, 디자인 패턴, 코드 재사용성, 고차 함수)

C 언어 콜백 함수 구조: 함수 포인터를 사용해 메인 로직에서 비교 및 정렬 함수를 동적으로 호출하는 다이어그램

1. 서론: 함수, 그 이상의 활용 - C언어의 유연성을 깨우다

C 언어는 강력하고 효율적인 프로그래밍 언어로 널리 사용되지만, 때로는 그 절차지향적 특성 때문에 코드의 유연성이 부족하다고 느끼는 경우가 있습니다. 하지만 C 언어에도 함수를 마치 변수처럼 다루고, 필요할 때 원하는 함수를 실행시키는 강력한 메커니즘이 존재합니다. 바로 **'함수 포인터(Function Pointer)'**와 이를 활용한 **'콜백 함수(Callback Function)'**입니다.

이번 글에서는 C 언어의 함수 포인터가 무엇인지, 어떻게 선언하고 사용하는지, 그리고 이 함수 포인터가 실제 프로그래밍에서 '콜백 함수'라는 강력한 디자인 패턴으로 어떻게 활용되는지 자세히 살펴보겠습니다. 이를 통해 여러분의 C 언어 코드를 더욱 유연하고 재사용 가능하게 만드는 핵심 비법을 깨우치게 될 것입니다.

2. 함수 포인터: 함수를 가리키는 포인터

우리가 변수의 주소를 저장하기 위해 포인터 변수를 사용하듯이, 함수 포인터는 함수의 메모리 주소를 저장하는 포인터입니다. 이를 통해 우리는 실행 시점에 어떤 함수를 호출할지 동적으로 결정할 수 있게 됩니다.

2.1. 함수 포인터 선언 방법

함수 포인터를 선언하는 문법은 일반 변수 포인터보다 조금 복잡해 보일 수 있습니다. 핵심은 '반환형 (*포인터변수이름)(매개변수 타입, ...)' 형태를 기억하는 것입니다.

// 1. 반환형이 int이고 매개변수가 int 두 개인 함수의 포인터 선언
int (*ptr_to_func)(int, int);

// 예시 함수
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

int main() {
    ptr_to_func = &add; // add 함수의 주소를 함수 포인터에 할당
    // 또는 ptr_to_func = add; (C에서는 &를 생략해도 됩니다)

    int result = ptr_to_func(10, 5); // 함수 포인터를 통해 add 함수 호출
    printf("Add result: %d\n", result); // 출력: Add result: 15

    ptr_to_func = subtract; // subtract 함수의 주소를 할당
    result = ptr_to_func(10, 5); // 함수 포인터를 통해 subtract 함수 호출
    printf("Subtract result: %d\n", result); // 출력: Subtract result: 5

    return 0;
}

2.2. typedef를 이용한 간결한 선언

함수 포인터 선언이 복잡하게 느껴진다면, typedef를 사용하여 별칭을 부여하면 훨씬 간결하고 가독성 높게 코드를 작성할 수 있습니다.

// int를 반환하고 int 두 개를 매개변수로 받는 함수 포인터 타입 정의
typedef int (*OperationFunc)(int, int);

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    OperationFunc op_ptr; // typedef로 정의된 타입 사용

    op_ptr = add;
    printf("Add result: %d\n", op_ptr(20, 10)); // 출력: Add result: 30

    op_ptr = subtract;
    printf("Subtract result: %d\n", op_ptr(20, 10)); // 출력: Subtract result: 10

    return 0;
}

3. 콜백 함수: 유연한 코드 설계를 위한 핵심 패턴

이제 함수 포인터의 진정한 가치를 발휘하는 **'콜백 함수(Callback Function)'**에 대해 알아보겠습니다. 콜백 함수는 특정 이벤트가 발생하거나, 특정 작업이 완료되었을 때 시스템이나 다른 함수가 호출하도록 등록해 둔 함수를 의미합니다. 즉, "나중에 호출해줘!" 하고 맡겨두는 함수입니다.

3.1. 콜백 함수의 개념과 동작 원리

콜백 함수는 다음과 같은 방식으로 작동합니다.

  1. 호출 함수(Caller Function): 어떤 작업을 수행하는 함수가 있습니다. 이 함수는 작업 중간 또는 완료 시점에 특정 기능을 수행해야 하는데, 그 기능이 고정되어 있지 않고 외부에서 주입되기를 기대합니다.
  2. 콜백 함수 (Callback Function): 호출 함수가 나중에 실행할 '특정 기능'을 정의한 함수입니다. 이 함수의 주소를 호출 함수에게 전달합니다.
  3. 함수 포인터 (The Bridge): 호출 함수는 전달받은 콜백 함수의 주소를 함수 포인터에 저장하고 있다가, 필요한 시점에 이 포인터를 통해 콜백 함수를 호출합니다.

이러한 구조를 통해 호출 함수는 **'어떤 기능'을 실행할지 몰라도 '기능을 실행해야 할 시점'**만 알면 됩니다. 이는 코드의 모듈화재사용성을 극대화합니다.

3.2. 콜백 함수 실제 예제: 배열 정렬 (버블 정렬)

가장 대표적인 콜백 함수의 예는 정렬 함수입니다. qsort()와 같은 표준 라이브러리 함수가 대표적이지만, 여기서는 직접 간단한 버블 정렬 함수를 만들면서 콜백의 위력을 보여드리겠습니다.

우리는 **'오름차순'**으로 정렬할 수도 있고, **'내림차순'**으로 정렬할 수도 있습니다. 이 **'정렬 기준'**을 콜백 함수로 전달하여 버블 정렬 함수를 유연하게 만들어 봅시다.

#include <stdio.h>

// int 두 개를 비교하여 int 값을 반환하는 함수 포인터 타입 정의
// 반환값이 음수: a < b
// 반환값이 양수: a > b
// 반환값이 0: a == b
typedef int (*CompareFunc)(int a, int b);

// 1. 오름차순 비교 함수 (a가 b보다 작으면 음수 반환)
int compare_asc(int a, int b) {
    return a - b;
}

// 2. 내림차순 비교 함수 (a가 b보다 크면 음수 반환)
int compare_desc(int a, int b) {
    return b - a; // 순서를 바꿔서 빼면 내림차순
}

// 3. 콜백 함수를 사용하는 범용 버블 정렬 함수
// 배열, 배열 크기, 비교 함수 포인터를 매개변수로 받음
void bubble_sort(int arr[], int size, CompareFunc comparer) {
    int i, j, temp;
    for (i = 0; i < size - 1; i++) {
        for (j = 0; j < size - 1 - i; j++) {
            // 전달받은 comparer 함수를 사용하여 두 요소를 비교
            if (comparer(arr[j], arr[j+1]) > 0) { // arr[j]가 arr[j+1]보다 크다면 (정렬 기준에 따라)
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

// 배열 출력 함수
void print_array(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int size = sizeof(arr) / sizeof(arr[0]);

    printf("Original array: ");
    print_array(arr, size);

    // 오름차순으로 정렬
    bubble_sort(arr, size, compare_asc);
    printf("Sorted (Ascending): ");
    print_array(arr, size); // 출력: 11 12 22 25 34 64 90 

    // 배열을 다시 초기화하고 내림차순으로 정렬
    int arr_desc[] = {64, 34, 25, 12, 22, 11, 90};
    bubble_sort(arr_desc, size, compare_desc);
    printf("Sorted (Descending): ");
    print_array(arr_desc, size); // 출력: 90 64 34 25 22 12 11 

    return 0;
}

위 예제에서 bubble_sort 함수는 compare_asc와 compare_desc 함수를 콜백 함수로 받아서, 동일한 정렬 로직으로 오름차순/내림차순 정렬을 모두 수행합니다. bubble_sort는 **'무엇을 비교할지'**에 대해서는 전혀 모르고 **'비교하는 방법(콜백)'**만 전달받아 유연하게 동작합니다.

4. 콜백 함수의 다양한 활용 분야

콜백 함수는 위 예제 외에도 C 언어 프로그래밍의 다양한 곳에서 활용됩니다.

  • 이벤트 처리: GUI 프로그래밍에서 버튼 클릭, 키보드 입력 등 특정 이벤트 발생 시 호출될 함수를 등록할 때 (예: glutDisplayFunc in OpenGL)
  • 비동기 작업: 네트워크 통신이나 파일 입출력처럼 시간이 오래 걸리는 작업이 완료되었을 때 결과를 처리할 함수를 등록할 때
  • 제네릭 라이브러리: 정렬(qsort), 탐색(bsearch)과 같은 범용 라이브러리에서 사용자가 정의한 비교 함수를 콜백으로 받아 다양한 데이터 타입에 적용할 때
  • 하드웨어 제어: 임베디드 시스템에서 특정 인터럽트 발생 시 처리할 함수를 등록할 때

5. 결론: 함수 포인터와 콜백, C언어의 숨겨진 힘

C 언어의 함수 포인터콜백 함수는 언어가 제공하는 강력한 유연성과 확장성을 보여주는 핵심 개념입니다. 이는 단순한 문법을 넘어, 코드를 더욱 모듈화하고 재사용성을 높이며, 다양한 시나리오에 동적으로 대응할 수 있도록 하는 강력한 디자인 패턴입니다.

처음에는 문법이 다소 어렵게 느껴질 수 있지만, 이번 글의 예제를 통해 그 원리와 활용법을 충분히 익히셨기를 바랍니다. 함수 포인터와 콜백 함수를 자유자재로 다루게 된다면, 여러분의 C 언어 프로그래밍 실력은 한 단계 더 도약할 것입니다.


[🔔 다음 글 예고: C언어 데이터 구조의 꽃, 링크드 리스트 완전 정복!] 다음 글에서는 함수 포인터와 더불어 C 언어의 핵심인 데이터 구조 중 가장 기본적이면서도 강력한 **'링크드 리스트(Linked List)'**에 대해 깊이 파고들 예정입니다. 동적인 데이터 관리가 필요한 여러분에게 필수적인 지식이 될 것입니다. 기대해주세요!

반응형
LIST