공개 래퍼(Wrapper)와 내부 코어(Core) 분리 패턴 — 언더스코어(__) 관행과 C 표준 예약어

NOTE

C 언어 시스템 프로그래밍과 대규모 코드베이스에서 흔히 볼 수 있는 func()와 __func()의 2단계 함수 분리 패턴을 살펴본다. 유효성 검증과 코어 로직의 분리, 인자 추상화, 리눅스 커널의 더블 언더스코어(__) 관행, 그리고 ISO C 표준의 예약어 규칙을 정리한다.


0. 왜 함수를 func()와 __func() 둘로 나누는가?

대형 C 프로젝트나 시스템 라이브러리를 열어보면 동일한 이름 앞에 언더스코어가 2개 붙은 함수 쌍을 자주 마주치게 됩니다:

/* 1. 외부 공개 래퍼 함수 (Public Wrapper) */
int resource_queue_push(int mem_type, int index)
{
    // [검사] 파라미터 유효 범위 체크
    if (mem_type < 0 || mem_type >= MAX_MEM_TYPES) {
        LOG_ERR("잘못된 메모리 타입: %d\n", mem_type);
        return FAIL;
    }
 
    // [로깅 및 모니터링]
    LOG_DBG("Pushing resource into type %d\n", mem_type);
 
    // [위임] 주소를 꺼내 내부 코어 함수로 넘김
    return __resource_queue_push(&g_resource_queues[mem_type], index);
}
 
/* 2. 내부 전용 코어 함수 (Internal Core Worker) */
static int __resource_queue_push(resource_queue_t *queue, int index)
{
    // 순수 큐 삽입 알고리즘만 초고속으로 수행 (중복 검사 없음)
    return queue_put(queue->mpq, (void *)(long)index);
}

이 패턴은 **“호출자의 편의와 안전을 보장하는 문지기(Wrapper)“**와 **“검증이 끝난 데이터를 받아 순수 알고리즘만 수행하는 일꾼(Core)“**을 명확히 분리하는 객체지향의 캡슐화 기법을 C 언어로 구현한 것입니다.


1. 문지기(Wrapper) vs 진짜 일꾼(Core)의 역할 분담

구분외형 래퍼 함수 (func)내부 코어 함수 (__func)
스코프헤더 파일(.h)에 노출되는 공개 API파일 내부(.c)에 한정된 static 함수
주요 책임• 인자 유효성 검사 (Sanity Check)
• 경계값(Bounds) 및 NULL 체크
• 디버그 로깅 및 모니터링 통계 수집
• 전역 배열/컨텍스트에서 실제 구조체 주소 추출
• 순수 알고리즘 수행 (Enqueue, Dequeue, Memory Copy 등)
• 불필요한 검증 오버헤드 배제 (초고속 연산)
호출 가정”호출자가 넘긴 인자는 언제든 쓰레기값일 수 있다""이미 상위에서 검증되었으므로 포인터는 100% 유효하다”

2. 인자(Signature)의 변화: 관심사의 분리 (Separation of Concerns)

두 함수의 인자 형태가 바뀌는 점이 이 패턴의 백미입니다:

  1. 외부 호출자 관점:
    • resource_queue_push(int mem_type, int index)
    • 호출자는 내부 전역 배열 구조(g_resource_queues)나 복잡한 포인터 체계를 알 필요가 없습니다.
    • 단지 번호(enum이나 int)만 던지면 되므로 모듈 간 결합도(Coupling)가 낮아집니다.
  2. 내부 코어 관점:
    • __resource_queue_push(resource_queue_t *queue, int index)
    • 코어 함수는 자신이 어떤 전역 배열의 몇 번째 타입인지 몰라도 됩니다.
    • 단지 넘겨받은 구조체 포인터(queue)에 index를 집어넣는 단일 책임(Single Responsibility)만 갖습니다.

3. 리눅스 커널(Linux Kernel)의 고전적 __ 네이밍 관행

더블 언더스코어(__)를 내부 코어 함수에 붙이는 관례는 리눅스 커널 소스코드에서 유래하여 시스템 C 개발자들에게 표준 관용구로 정착되었습니다.

안전장치 포함 함수내부 코어 / Unchecked 함수리눅스 커널에서의 실제 차이
kmalloc(...)__kmalloc(...)kmalloc은 플래그 검증 및 캐시 선택 후 __kmalloc 코어 호출
copy_to_user(...)__copy_to_user(...)일반 함수는 유저 주소 검증(access_ok) 수행, __는 검증 생략
spin_lock(...)__spin_lock(...)인터럽트 상태 플래그 처리 유무의 차이

시스템 프로그래머들 사이에서 __ 접두어는 다음과 같은 강력한 경고 신호입니다:

“이 함수(__xxx)는 안전장치(파라미터 검사, 주소 검증, 락 획득)가 생략된 원시(Raw) 함수이므로, 외부에서 직접 부르지 말고 반드시 상위 래퍼 함수(xxx)를 통해 부르시오!“


4. C 표준(ISO C) 상식 팁: 예약어(Reserved Identifier) 규칙

이 패턴을 작성할 때 반드시 알아두어야 할 C 언어 표준 규약이 있습니다.

ISO C 표준 (ISO/IEC 9899, 7.1.3절)

C 표준 규격에 따르면 다음 식별자들은 컴파일러와 표준 라이브러리(glibc 등)를 위해 영구 예약되어 있습니다:

  1. __ (더블 언더스코어)로 시작하는 모든 식별자:
    • 예: __func_name, __aligned__, __THROW, __attribute__
  2. _ (단일 언더스코어) 뒤에 대문자가 오는 식별자:
    • 예: _FILE_OFFSET_BITS, _IO_
  3. 파일 스코프에서 _ (단일 언더스코어)로 시작하는 식별자:
    • 예: _sys_errlist

C 표준 관점에서의 엄격한 해석

사용자가 __nrf_util_index_queue_push처럼 __로 시작하는 이름을 직접 정의하면, 이론적으로는 컴파일러의 내장 함수(Built-in)나 표준 라이브러리의 매크로와 이름이 충돌(Name Collision)할 위험이 있습니다.

실무 관행 vs 현대적 대안

구분형태장단점 및 권장 환경
전통적 리눅스 커널 스타일__func_name()• 직관적으로 “내부/Raw 함수”임을 누구나 알아봄
• 통신/임베디드/OS 개발 실무에서 압도적으로 널리 쓰임
• C 표준 예약어 규칙과는 형식상 상충
모던 C 표준 준수 스타일func_name_internal() 또는 do_func_name()• C 표준 100% 준수 (예약어 충돌 위험 제로)
• 함수 목적이 영문으로 명확히 드러남
접미사 언더스코어func_name_()• 표준과 충돌하지 않으면서 C++ 스타일의 내부 식별자 관례 유지

5. 실무 적용 시 핵심 체크리스트

  1. 내부 코어 함수는 반드시 static으로 선언할 것:
    • __func는 파일 외부로 심볼이 노출되면 안 됩니다. 컴파일 단위(.c) 내부로 스코프를 제한하여 외부 직접 호출을 원천 봉쇄합니다.
  2. 동일 모듈 내부에서의 재사용 이점:
    • 모듈 내부의 다른 함수가 이미 pst_queue 포인터를 확보하고 있다면, 굳이 mem_type으로 다시 배열 조회를 거치는 래퍼를 부르지 않고 __func(pst_queue, index)를 직접 호출하여 불필요한 조회를 생략할 수 있습니다.
  3. 관심사의 분리 준수:
    • 래퍼 함수에는 비즈니스 핵심 알고리즘을 넣지 말고 검증/로깅만 담당하게 합니다.
    • 코어 함수에는 if (param == NULL) 같은 방어 코드를 중복 작성하지 않아 코드 가독성과 성능을 극대화합니다.

관련 문서