[C 시리즈 11편] 파일 분리와 헤더 파일로 코드 정리하기

English version

구조체와 열거형으로 데이터 묶는 감각이 생겼다면, 이제는 코드를 파일로 나눠 관리할 차례입니다. C에서는 함수나 구조체 정의를 여러 파일에 나누고, 선언은 헤더로 모아 공유하는 패턴이 기본입니다. 이렇게 나누면 한 파일에 모든 코드를 몰아넣지 않아도 되고, 어떤 파일이 어떤 역할을 맡는지 더 분명하게 볼 수 있습니다. 이번 글에서는 가장 작은 2파일 프로젝트를 직접 만들어 보며 .h.c가 어떤 역할을 맡는지 확인합니다.

이번 글에서 새로 나오는 용어

  1. 헤더 파일 (Header File): 함수·구조체·상수의 선언을 담아 다른 파일과 공유하는 .h 파일
  2. 선언 (Declaration): 이름과 형식만 알려 주는 구문으로, 컴파일러가 "이 함수가 어딘가에 정의된다"고 믿게 하는 정보
  3. 정의 (Definition): 함수 본문이나 변수 실제 저장 공간을 제공하는 구문
  4. Include Guard: 같은 헤더가 여러 번 포함되어도 중복 선언을 막는 전처리기 패턴

핵심 개념

학습 메모

  • 소요 시간: 70분 내외
  • 준비물: 구조체, 함수 기초, clang 명령 사용 경험
  • 학습 목표: 헤더/소스 분리, 선언과 정의 차이, 다중 파일 컴파일 명령 이해

이번 글에서 집중할 사항은 세 가지입니다.

  1. 헤더에는 선언과 상수, typedef 같은 메타 정보를 넣고, .c에는 실제 구현을 둔다.
  2. #ifndef/#define 패턴으로 include guard를 구성해 중복 포함을 막는다.
  3. 여러 .c 파일을 한 번에 컴파일하거나 목적 파일로 나눠 빌드하는 명령을 익힌다.

코드로 따라하기

프로젝트 구조 잡기

project/
├── main.c
├── stats.c
└── stats.h

stats.h에는 함수 선언과 필요한 구조체를 적고, stats.c에는 구현을 둡니다. main.c헤더 파일만 포함한 뒤 함수를 사용합니다.

  • stats.h: 이름과 형식 소개
  • stats.c: 실제 계산 구현
  • main.c: 기능 사용과 결과 출력

헤더 파일 작성

// stats.h
#ifndef STATS_H
#define STATS_H

#include <stddef.h>

typedef struct {
    double average;
    int max;
    int min;
} Stats;

Stats calculate_stats(const int *numbers, size_t length);

#endif

#ifndef로 시작해 #define을 선언하고, 파일 끝에서 #endif로 닫으면 같은 헤더를 여러 번 포함하더라도 한 번만 처리됩니다. 이런 패턴을 인클루드 가드라고 부르며, 헤더에는 구현이 아닌 선언만 두는 것이 기본 원칙입니다.

구현 파일 작성

// stats.c
#include "stats.h"

Stats calculate_stats(const int *numbers, size_t length) {
    size_t i;
    Stats stats = {0.0, 0, 0};
    int total = 0;

    if (length == 0) {
        return stats;
    }

    stats.max = numbers[0];
    stats.min = numbers[0];

    for (i = 0; i < length; i++) {
        total += numbers[i];
        if (numbers[i] > stats.max) {
            stats.max = numbers[i];
        }
        if (numbers[i] < stats.min) {
            stats.min = numbers[i];
        }
    }

    stats.average = (double)total / (double)length;
    return stats;
}

구현 파일은 헤더를 포함해 선언과 정의가 일치하도록 만듭니다. 이렇게 하면 함수 시그니처가 바뀌었을 때 컴파일 오류로 바로 확인할 수 있습니다.

main.c에서 함수 사용하기

// main.c
#include <stdio.h>
#include "stats.h"

int main(void) {
    int numbers[5] = {72, 85, 90, 68, 77};
    Stats stats = calculate_stats(numbers, 5);

    printf("avg = %.1f\n", stats.average);
    printf("max = %d\n", stats.max);
    printf("min = %d\n", stats.min);

    return 0;
}

헤더는 쌍따옴표로 포함해 같은 프로젝트 안의 파일을 찾도록 합니다. 표준 라이브러리처럼 설치된 헤더는 꺾쇠 괄호(<...>)를 사용합니다.

다중 파일 컴파일하기

clang main.c stats.c -o stats-app

또는 목적 파일을 분리해 더 세밀하게 빌드할 수도 있습니다.

clang -c main.c -o main.o
clang -c stats.c -o stats.o
clang main.o stats.o -o stats-app

프로젝트가 커질수록 두 번째 방식이 더 유리합니다. 파일 하나를 고친 뒤에는 해당 .c만 다시 컴파일하면 되고, 나머지 목적 파일은 재사용할 수 있기 때문입니다. 첫 번째 명령에서 main.cstats.c를 함께 적는 이유는, main.c가 호출하는 calculate_stats의 실제 구현이 stats.c 안에 있기 때문입니다.

외부 변수 선언하기

// config.h
#ifndef CONFIG_H
#define CONFIG_H

extern int MAX_USERS;

#endif

// config.c
int MAX_USERS = 100;

전역 변수를 여러 파일에서 공유해야 할 때는 헤더에 extern 선언을 두고, .c 파일 중 하나에서만 실제 정의를 제공합니다. extern은 "메모리를 새로 만드는 것"이 아니라 "다른 파일에 이미 있는 변수를 여기서 쓰겠다"고 알리는 문장에 가깝습니다. 이렇게 하면 중복 정의 오류를 피할 수 있습니다.

왜 중요한가

프로그램이 커지면 한 파일에 모든 코드를 넣는 방식은 금방 한계에 부딪힙니다. 헤더와 소스를 구분하면 팀원이 서로 다른 부분을 동시에 작업할 수 있고, 컴파일러도 각 단위를 독립적으로 빌드해 시간을 절약할 수 있습니다. 또한 선언과 정의가 따로 있는 구조 덕분에 인터페이스(헤더)만 보고도 함수를 사용할 수 있어 테스트와 문서화가 쉬워집니다.

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. starter를 fork한 뒤 hello.c 같은 실습 파일을 만든다
  2. 본문 코드를 붙여 넣고 clang 또는 gcc가 있으면 직접 컴파일한다
  3. 코드를 고친 뒤 다시 빌드하고 실행 결과를 비교한다

C는 브라우저 미리보기보다 터미널 빌드 흐름이 핵심입니다. Universal starter의 컴파일러 구성이 환경에 따라 다를 수 있으니, 먼저 clang이나 gcc 사용 가능 여부부터 확인하세요.

실습

  • 따라 하기: stats.h, stats.c, main.c 구조를 그대로 만들고 두 가지 방식으로 컴파일해 봅니다.
  • 확장하기: Stats에 표준편차 필드를 추가하고, 함수 선언과 정의를 동시에 수정해 일관성을 확인합니다.
  • 디버깅: include guard를 일부러 빼고 같은 헤더를 두 번 포함해 본 뒤, 어떤 오류가 발생하는지 관찰합니다.
  • 완료 기준: 선언과 정의의 차이, extern의 역할, 다중 파일 컴파일 명령을 각각 한 문장으로 설명할 수 있으면 됩니다.

마무리

이번 글에서는 헤더와 구현 파일을 나누어 관리하는 기본 패턴을 익혔습니다. 다음 글에서는 이렇게 나뉜 코드를 활용해 stdio.h 기반 표준 입출력과 파일 처리를 통해 실제 데이터를 읽고 쓰는 흐름을 실습하겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요