C언어에서 함수 포인터는 단순 문법이 아니다.
코드 구조를 바꾸는 핵심 도구다.
특히 임베디드 / AUTOSAR에서는 이걸 이해 못 하면 아래 구조가 전부 막힌다.
- Callback 구조
- Driver ↔ Application 분리
- 상태 머신
- 인터페이스 설계
1. 왜 함수 포인터가 필요한가
일반적인 코드에서는 상태에 따라 다른 함수를 실행하기 위해 if문이나 switch문을 사용한다.
예를 들어 상태값에 따라 Idle, Run, Stop 함수를 호출한다고 가정하면 대부분 다음과 같은 구조를 사용한다.
if (state == IDLE)
{
Idle();
}
else if (state == RUN)
{
Run();
}
else if (state == STOP)
{
Stop();
}
또는
switch(state)
{
case IDLE: Idle(); break;
case RUN: Run(); break;
case STOP: Stop(); break;
}
이 방식은 초기에는 문제가 없다.
하지만 실무에서는 상태의 개수가 계속 증가하게 된다.
기능이 추가될 때마다 조건문이 길어지고,
기존 코드를 계속 수정해야 하며,
결국 유지보수가 어려운 구조가 된다.
즉, 문제는 “문법”이 아니라 코드 구조 자체가 확장에 불리하다는 것이다.
2. 함수 포인터의 핵심 개념
함수 포인터는 이름 그대로 “함수를 가리키는 포인터”이다.
조금 더 본질적으로 보면 함수를 변수처럼 다룰 수 있게 해주는 개념이다.
void (*funcPtr)(void);
이건 변수다. 단지 값이 “함수 주소”일 뿐이다.
동작 흐름
funcPtr = Run;
funcPtr(); // Run 실행
일반 변수에는 값이 들어가지만, 함수 포인터에는 함수의 주소가 들어간다.
이 구조를 사용하면 코드가 직접 함수를 선택하는 것이 아니라
데이터에 의해 실행할 함수가 결정되는 구조로 바뀐다.
이 차이가 매우 중요하다.
즉, 함수 포인터는 “실행 흐름을 코드가 아니라 데이터로 제어하는 방식”이다.
3. 실무 패턴 ① : 상태 테이블 기반 설계
함수 포인터가 가장 많이 사용되는 패턴 중 하나는 상태별 함수 테이블 구조이다.
각 상태에 대응되는 함수를 배열 형태로 구성해두고 현재 상태값을 인덱스로 사용하여 바로 실행하는 방식이다.
이 구조를 사용하면 기존의 switch문이 완전히 사라진다.
typedef enum
{
STATE_IDLE,
STATE_RUN,
STATE_STOP,
STATE_MAX
} StateType;
void State_Idle(void) {}
void State_Run(void) {}
void State_Stop(void) {}
void (*StateTable[STATE_MAX])(void) = {State_Idle, State_Run, State_Stop};
이 방식의 가장 큰 장점은 확장성이다.
새로운 상태가 추가되더라도 기존 로직을 수정할 필요 없이 함수 하나와 테이블 항목 하나만 추가하면 된다.
또한 상태와 동작의 매핑 관계가 명확하게 드러나기 때문에 코드 가독성도 크게 개선된다.
4. 실무 패턴 ② : 콜백 구조 (Callback)
함수 포인터의 또 다른 핵심 사용처는 콜백(callback)이다.
임베디드 시스템에서는 드라이버와 어플리케이션이 분리되어 동작한다.
예를 들어 드라이버에서 어떤 이벤트가 발생했다고 가정해보자.
void Driver_Event(void)
{
// 이벤트 발생
}
이때 드라이버가 특정 이벤트를 감지했을 때 어플리케이션에게 이를 알려줘야 한다.
잘못된 구조 (직접 호출)
가장 단순한 방법은 드라이버에서 어플리케이션 함수를 직접 호출하는 것이다.
void Driver_Event(void)
{
App_Handle();
}
겉보기에는 문제 없어 보이지만, 구조적으로 큰 문제가 있다.
드라이버가 App_Handle()라는 함수를 알고 있어야 하고, 드라이버가 어플리케이션에 의존하게 된다.
또한, 다른 프로젝트에서는 재사용이 어렵고, 테스트 및 확장이 매우 힘들어진다.
해결 방법 : 함수 포인터 기반 콜백
이 문제를 해결하기 위해 함수 포인터를 사용한다.
핵심은 “호출”이 아니라 “등록” 구조로 바꾸는 것이다.
typedef void (*CallbackFunc)(void); // 콜백 타입 정의
static CallbackFunc g_Callback = NULL; // 드라이버 내부에 함수 포인터 변수 선언
void RegisterCallback(CallbackFunc cb) // 어플리케이션이 함수 등록
{
g_Callback = cb;
}
void Driver_Event(void) // 이벤트 발생 시 호출
{
if (g_Callback != NULL)
{
g_Callback();
}
}
어플리케이션 코드
void App_Handler(void)
{
// 처리
}
즉, 호출 주체(Driver)와 실행 주체(Application)를 분리하는 구조다.
코드 흐름 정리 (중요)
전체 흐름을 순서로 보면 이렇게 된다.
1. 어플리케이션이 자신의 처리 함수(App_Handler)를 준비한다.
2. RegisterCallback()을 통해 드라이버에 함수 주소를 전달한다.
3. 드라이버는 해당 함수를 g_Callback에 저장한다.
4. 이후 Driver_Event()에서 이벤트 발생.
5. 드라이버는 g_Callback()을 호출.
결과적으로 등록된 App_Handler()가 실행된다.
5. 실무 패턴 ③ : 인터페이스 설계 (구조체 + 함수 포인터)
함수 포인터는 구조체와 함께 사용할 때 더 강력해진다.
여러 개의 함수 포인터를 하나의 구조체로 묶으면 특정 기능 집합을 하나의 인터페이스처럼 정의할 수 있다.
typedef struct
{
void (*Init)(void);
void (*Start)(void);
void (*Stop)(void);
} MotorIf;
예를 들어 Init, Start, Stop과 같은 동작을 하나의 구조체로 구성하면 이 구조체 자체가 하나의 “기능 단위”가 된다.
void MotorA_Init(void) {}
void MotorA_Start(void) {}
void MotorA_Stop(void) {}
MotorIf MotorA = {MotorA_Init, MotorA_Start, MotorA_Stop};
이 방식의 장점은 구현 교체가 매우 쉽다는 점이다.
같은 인터페이스를 유지한 채 내부 구현만 바꿀 수 있기 때문에 드라이버 변경이나 테스트 코드 작성이 훨씬 수월해진다.
이 구조는 C언어에서 객체지향 개념을 흉내 내는 대표적인 방법이며,
AUTOSAR에서도 유사한 형태로 널리 사용된다.
정리
함수 포인터는 단순한 문법 기능이 아니라 코드의 구조를 유연하게 만드는 설계 도구다.
조건문 중심의 구조를 데이터 중심 구조로 바꾸고,
모듈 간 의존성을 줄이며,
확장성과 유지보수성을 크게 향상시킨다.
임베디드나 AUTOSAR 환경에서는 사실상 필수적으로 사용되는 개념이다.
'C 언어 > 실무' 카테고리의 다른 글
| C언어 switch문 실무 사용법 (if문보다 switch문이 강력한 이유) (0) | 2026.04.21 |
|---|---|
| C언어 내부 변수 / 전역 변수 / static 변수 사용 기준 (0) | 2026.04.16 |
| C언어 구조체 실무 예제 (왜 구조체를 쓰는가?) (0) | 2026.04.15 |
| C언어 비트 연산자 쉽게 설명하기 (임베디드 실무 관점에서 제대로 이해하기) (0) | 2026.04.14 |
| C언어 extern 변수 사용법 (파일 간 변수 공유 방법) (0) | 2026.04.13 |