C++ 헤더 파일 작성법: 선언과 정의 분리, 인클루드 가드, ODR 위반과 순환 include

이 글의 핵심

헤더 파일은 여러 번역 단위에 그대로 복사되는 텍스트라서, 헤더에 무엇을 넣느냐가 링크 에러와 빌드 시간을 좌우합니다. 선언, 템플릿, 인라인 함수는 헤더에 두고 일반 함수 정의와 전역 변수 정의는 소스 파일에 두는 원칙을 기준으로, 인클루드 순서 문제와 전방 선언 활용법을 짚고 간단한 로거 라이브러리를 헤더와 소스로 나눠 설계해 봅니다.

들어가며

C++에서 헤더 파일(.h, .hpp)은 선언(declaration)을 담는 파일입니다. 코드를 모듈화하고 재사용성을 높이는 핵심 요소입니다.

헤더를 이해하는 출발점은 #include가 텍스트 복사라는 사실입니다. 전처리기는 #include "math.h"를 만나면 그 파일의 내용을 그 자리에 그대로 붙여 넣고, 컴파일러는 이렇게 만들어진 하나의 큰 파일(번역 단위)을 각 .cpp마다 따로 컴파일합니다. 다른 .cpp에 있는 함수를 호출하려면 컴파일러가 최소한 그 함수의 이름과 시그니처는 알아야 하는데, 이 “알려 주는 역할”을 선언이 맡고, 실제 코드를 연결하는 일은 나중에 링커가 합니다. 그래서 헤더에 정의를 넣으면 그 헤더를 포함한 모든 .cpp에 같은 정의가 복사되어 링크 단계에서 충돌하고, 헤더에 무거운 인클루드를 넣으면 그 헤더를 쓰는 모든 파일의 컴파일 시간이 함께 늘어납니다. 이 글의 규칙은 거의 모두 이 두 가지 결과를 피하기 위한 것입니다.


헤더에는 선언, 소스에는 정의

선언과 정의 코드로 구분하기

// math.h (헤더 파일 - 선언)
#ifndef MATH_H
#define MATH_H

int add(int a, int b);  // 선언 (declaration)
int subtract(int a, int b);

#endif

// math.cpp (소스 파일 - 정의)
#include "math.h"

int add(int a, int b) {  // 정의 (definition)
    return a + b;
}

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

// main.cpp (사용)
#include <iostream>
#include "math.h"

int main() {
    std::cout << add(3, 5) << std::endl;  // 8
    std::cout << subtract(10, 3) << std::endl;  // 7
}

선언과 정의 비교표

구분선언 (Declaration)정의 (Definition)
위치헤더 파일 (.h)소스 파일 (.cpp)
역할존재를 알림실제 구현
중복가능불가능 (ODR 위반)
예시int add(int, int);int add(int a, int b) { return a + b; }

핵심 개념:

  • 선언: 컴파일러에게 “이런 함수가 있다”고 알림
  • 정의: 실제 구현 코드
  • ODR (One Definition Rule): 정의는 프로그램 전체에서 하나만

위 표의 “정의는 소스 파일”은 함수와 변수에 대한 규칙입니다. class MyClass { ... }; 같은 클래스 본문도 엄밀히는 정의지만, 컴파일러가 객체 크기와 멤버 배치를 알아야 하므로 그 클래스를 쓰는 모든 번역 단위에 필요합니다. 그래서 ODR은 클래스 정의, 열거형 정의, 인라인 함수, 템플릿에 대해서는 “각 번역 단위에 토큰 단위로 똑같이 나타나면 여러 번 있어도 된다”는 예외를 둡니다. 이 예외 때문에 생기는 함정도 있습니다. 두 .cpp가 같은 이름의 클래스를 서로 다르게 정의하면(예: 매크로 설정에 따라 멤버가 달라지는 헤더) 링커는 아무 에러도 내지 않고, 실행 중에 한쪽이 다른 쪽의 메모리 배치를 가정해 엉뚱한 값을 읽는 미정의 동작이 됩니다. 빌드 옵션(-DNDEBUG 여부 등)이 다른 라이브러리를 섞어 링크했을 때 원인 모를 크래시가 나는 대표적인 경로입니다.

반대로 선언만 있고 정의가 어디에도 없으면 컴파일은 통과하고 링크에서 실패합니다. GCC/Clang은 undefined reference to 'subtract(int, int)', MSVC는 LNK2019: unresolved external symbol을 냅니다. 헤더는 제대로 포함했는데 이 에러가 난다면, 정의가 들어 있는 .cpp가 빌드 목록(CMake의 add_executable/target_sources)에 빠졌거나 선언과 정의의 시그니처(const, 매개변수 타입)가 미묘하게 다른 경우가 대부분입니다.


인클루드 가드와 #pragma once

같은 헤더를 두 번 포함하면 생기는 일

// myheader.h (가드 없음)
class MyClass {};

// main.cpp
#include "myheader.h"
#include "myheader.h"  // 중복 포함!

// 컴파일 에러: MyClass가 두 번 정의됨

#ifndef 인클루드 가드

// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

class MyClass {
public:
    void doSomething();
};

#endif  // MYHEADER_H

동작 원리:

  1. 첫 번째 포함: MYHEADER_H가 정의되지 않았으므로 내용 포함
  2. 두 번째 포함: MYHEADER_H가 이미 정의되어 있으므로 내용 건너뜀

#pragma once 사용

// myheader.h
#pragma once

class MyClass {
public:
    void doSomething();
};

두 방식 비교

방식장점단점
#ifndef표준, 호환성 좋음코드가 길다, 매크로 이름 충돌 가능
#pragma once간결, 빠름비표준 (대부분 지원)

실전 팁:

  • 개인 프로젝트: #pragma once (간결)
  • 라이브러리: #ifndef (호환성)
  • 둘 다 사용해도 됨 (중복 방지)

#pragma once는 GCC, Clang, MSVC를 포함한 주요 컴파일러가 모두 지원하므로 실무에서 호환성 문제는 거의 없습니다. 다만 “같은 파일”을 파일 경로나 파일 시스템 정보로 판단하기 때문에, 같은 헤더가 심볼릭 링크나 복사본으로 서로 다른 경로에 존재하거나 네트워크 드라이브에 있을 때 중복으로 인식하지 못하는 드문 경우가 있습니다. 반대로 #ifndef 가드는 매크로 이름이 겹치면 문제가 되는데, 서로 다른 라이브러리의 utils.h가 둘 다 UTILS_H를 쓰면 두 번째 헤더가 통째로 무시되어 “선언이 없다”는 엉뚱한 에러가 납니다. 그래서 가드를 쓸 때는 MYPROJECT_NET_UTILS_H처럼 프로젝트와 경로를 포함한 이름을 쓰는 것이 관례입니다.


헤더에 넣어도 되는 것과 안 되는 것

헤더 파일 구성

// mylib.h
#ifndef MYLIB_H
#define MYLIB_H

#include <string>
#include <vector>

// 1. 전역 상수 (inline 또는 constexpr)
inline constexpr int MAX_SIZE = 100;
constexpr double PI = 3.14159;

// 2. 타입 정의
using UserID = int;
using UserList = std::vector<std::string>;

// 3. 열거형
enum class Status {
    Success,
    Error,
    Pending
};

// 4. 클래스 선언
class MyClass {
public:
    void publicMethod();
    
private:
    int data;
};

// 5. 인라인 함수 정의
inline int square(int x) {
    return x * x;
}

// 6. 템플릿 정의
template<typename T>
T max(T a, T b) {
    return a > b ? a : b;
}

// 7. 함수 선언
void globalFunction();

// 8. extern 변수 선언
extern int globalCounter;

#endif

헤더와 소스 파일의 역할 비교

항목헤더 (.h)소스 (.cpp)
클래스 선언✅❌
함수 선언✅❌
함수 정의❌ (예외: inline, template)✅
전역 변수 선언✅ (extern)❌
전역 변수 정의❌✅
상수✅ (constexpr, inline)✅
인라인 함수✅✅
템플릿✅❌

ODR 위반·순환 인클루드·인클루드 순서 문제

헤더에 함수 정의를 넣어 중복 정의

// ❌ 헤더에 변수 정의
// myheader.h
int globalVar = 10;  // 여러 cpp에서 포함하면 중복 정의!

// ✅ 선언만 (헤더)
// myheader.h
extern int globalVar;

// 정의 (소스)
// myheader.cpp
int globalVar = 10;

// ✅ 또는 inline 사용 (C++17)
// myheader.h
inline int globalVar = 10;

에러 메시지:

error: multiple definition of 'globalVar'

헤더에 정의를 넣었을 때 링크 에러를 피하려고 static int globalVar = 10;이나 익명 네임스페이스로 감싸는 방법을 쓰기도 하는데, 이는 에러만 없앨 뿐 의미가 달라집니다. static은 내부 연결(internal linkage)을 주므로 헤더를 포함한 각 .cpp마다 별개의 변수가 생깁니다. 한 파일에서 값을 바꿔도 다른 파일에서는 원래 값이 보이는, 겉보기에 설명이 안 되는 버그가 됩니다. 프로그램 전체에서 하나의 변수를 공유하려면 extern 선언 + .cpp 정의, 또는 C++17의 inline 변수를 써야 합니다. 반면 네임스페이스 범위의 constexpr/const 변수는 기본이 내부 연결이라 헤더에 둬도 링크 에러가 나지 않는데, 각 번역 단위에 복사본이 생긴다는 점은 같으므로 주소를 비교하는 코드가 있다면 inline constexpr로 하나로 합쳐야 합니다.

두 헤더가 서로를 인클루드

// ❌ 순환 의존성
// a.h
#ifndef A_H
#define A_H
#include "b.h"

class A {
    B* b;  // a.h를 먼저 포함하면 이 시점에 B가 아직 선언되지 않음
};
#endif

// b.h
#ifndef B_H
#define B_H
#include "a.h"

class B {
    A* a;  // b.h를 먼저 포함하면 이 시점에 A가 아직 선언되지 않음
};
#endif

// ✅ 전방 선언으로 해결
// a.h
#ifndef A_H
#define A_H

class B;  // 전방 선언

class A {
    B* b;  // 포인터만 사용
};
#endif

// b.h
#ifndef B_H
#define B_H

class A;  // 전방 선언

class B {
    A* a;
};
#endif

순환 인클루드가 헷갈리는 이유는 인클루드 가드가 “무한 포함”은 막아 주지만 “선언 순서”는 해결하지 못하기 때문입니다. main.cpp가 a.h를 포함하면 A_H가 정의된 뒤 b.h가 포함되고, b.h는 다시 a.h를 포함하려 하지만 가드 때문에 내용이 비어 있는 채로 넘어갑니다. 결국 class B를 처리하는 시점에 A는 아직 한 번도 선언되지 않은 상태라 “‘A’ does not name a type” 에러가 납니다. 어떤 파일을 먼저 포함하느냐에 따라 에러 위치가 바뀌므로, 파일 순서를 바꿨더니 다른 곳에서 에러가 나는 증상이 전형적인 신호입니다. 포인터나 참조만 쓴다면 위처럼 전방 선언으로 끊으면 되고, 멤버를 값으로 가져야 해서 서로의 완전한 정의가 필요하다면 설계 자체에 순환이 있다는 뜻이므로 공통 부분을 제3의 헤더로 분리해야 합니다.

쓰지 않는 헤더를 인클루드

// ❌ 모든 헤더 포함 (컴파일 시간 증가)
// myclass.h
#include <iostream>
#include <vector>
#include <map>
#include <algorithm>
#include <string>
// 실제로는 string만 사용

// ✅ 필요한 것만 포함
// myclass.h
#include <string>

class MyClass {
    std::string name;
};

실전 팁:

  • 헤더에서 사용하는 타입만 포함
  • 소스 파일에서 추가 헤더 포함
  • 전방 선언 활용

인클루드 순서에 따라 컴파일이 깨짐

// ✅ 권장 순서
// myclass.cpp
#include "myclass.h"      // 1. 자신의 헤더 (의존성 확인)
#include <iostream>       // 2. C++ 표준 라이브러리
#include <vector>
#include <sys/types.h>    // 3. 시스템 헤더
#include "other.h"        // 4. 프로젝트 헤더

// ❌ 잘못된 순서
#include <iostream>
#include "other.h"
#include "myclass.h"  // 자신의 헤더가 마지막

자신의 헤더를 먼저 포함하는 이유:

  • 헤더가 독립적인지 확인 (누락된 include 발견)
  • 의존성 문제를 조기에 발견

클래스·템플릿·전방 선언·인라인 함수 헤더 작성

클래스 선언 헤더

// calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H

class Calculator {
public:
    int add(int a, int b);
    int subtract(int a, int b);
    int multiply(int a, int b);
    int divide(int a, int b);
    
private:
    int lastResult;
};

#endif

// calculator.cpp
#include "calculator.h"
#include <stdexcept>

int Calculator::add(int a, int b) {
    lastResult = a + b;
    return lastResult;
}

int Calculator::subtract(int a, int b) {
    lastResult = a - b;
    return lastResult;
}

int Calculator::multiply(int a, int b) {
    lastResult = a * b;
    return lastResult;
}

int Calculator::divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("0으로 나눌 수 없습니다");
    }
    lastResult = a / b;
    return lastResult;
}

// main.cpp
#include <iostream>
#include "calculator.h"

int main() {
    Calculator calc;
    
    std::cout << calc.add(10, 5) << std::endl;      // 15
    std::cout << calc.subtract(10, 5) << std::endl; // 5
    std::cout << calc.multiply(10, 5) << std::endl; // 50
    std::cout << calc.divide(10, 5) << std::endl;   // 2
}

템플릿은 헤더에 정의까지

// stack.h
#ifndef STACK_H
#define STACK_H

#include <vector>
#include <stdexcept>

template<typename T>
class Stack {
private:
    std::vector<T> data;
    
public:
    void push(const T& value) {
        data.push_back(value);
    }
    
    T pop() {
        if (data.empty()) {
            throw std::runtime_error("스택이 비어있습니다");
        }
        T value = data.back();
        data.pop_back();
        return value;
    }
    
    const T& top() const {
        if (data.empty()) {
            throw std::runtime_error("스택이 비어있습니다");
        }
        return data.back();
    }
    
    bool empty() const {
        return data.empty();
    }
    
    size_t size() const {
        return data.size();
    }
};

#endif

// main.cpp
#include <iostream>
#include "stack.h"

int main() {
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    
    std::cout << "Top: " << intStack.top() << std::endl;  // 3
    std::cout << "Pop: " << intStack.pop() << std::endl;  // 3
    std::cout << "Size: " << intStack.size() << std::endl; // 2
}

템플릿 헤더 규칙:

  • 템플릿은 헤더에 전체 구현을 넣어야 함
  • 컴파일러가 인스턴스화할 때 정의가 필요
  • 소스 파일로 분리하면 링크 에러 발생

템플릿을 .h와 .cpp로 나누면 컴파일은 되지만 링크 단계에서 “undefined reference to Stack<int>::push(int const&)'" 같은 에러가 납니다. main.cpp를 컴파일할 때 컴파일러는 Stack의 선언만 보고 "어딘가에 구현이 있겠지"라고 넘어가는데, stack.cpp를 컴파일할 때는 누가 Stack를 쓰는지 모르므로 아무 코드도 만들지 않기 때문입니다. 사용할 타입이 정해져 있다면 .cpp끝에template class Stack;`처럼 명시적 인스턴스화를 적어 구현을 숨길 수도 있지만, 목록에 없는 타입을 쓰는 순간 같은 링크 에러가 다시 나므로 범용 템플릿에는 헤더 구현이 기본입니다.

전방 선언으로 의존성 줄이기

// window.h
#ifndef WINDOW_H
#define WINDOW_H

#include <string>

class Window {
public:
    Window(const std::string& title);
    void show();
    void hide();
};

#endif

// widget.h
#ifndef WIDGET_H
#define WIDGET_H

class Window;  // 전방 선언 (window.h 포함 불필요)

class Widget {
private:
    Window* window;  // 포인터만 사용
    
public:
    void setWindow(Window* w);
    Window* getWindow() const;
};

#endif

// widget.cpp
#include "widget.h"
#include "window.h"  // 여기서 포함

void Widget::setWindow(Window* w) {
    window = w;
}

Window* Widget::getWindow() const {
    return window;
}

전방 선언 장점:

  • 컴파일 시간 단축
  • 헤더 의존성 감소
  • 순환 의존성 해결

전방 선언만으로는 불완전 타입이라서 할 수 있는 일이 제한됩니다. 포인터·참조 멤버, 함수 매개변수·반환 타입의 선언에는 쓸 수 있지만, 값 멤버(Window window;), sizeof(Window), 멤버 함수 호출(window->show())에는 완전한 정의가 필요해 field 'window' has incomplete type 'Window' 또는 invalid use of incomplete type 'class Window' 에러가 납니다. 그래서 위 예제는 헤더에서는 전방 선언만 하고, 실제로 Window의 멤버를 호출하는 widget.cpp에서 window.h를 포함합니다. std::unique_ptr<Window> 멤버도 전방 선언으로 쓸 수 있지만, 이때는 Widget의 소멸자를 헤더가 아닌 .cpp에 정의해야 합니다. 헤더에서 컴파일러가 소멸자를 암묵적으로 만들면 그 자리에서 Window의 소멸자가 필요해져 can't delete an incomplete type 류의 에러가 나기 때문입니다(pImpl 패턴에서 가장 흔히 겪는 에러입니다).

inline 함수를 헤더에 정의하기

// utils.h
#ifndef UTILS_H
#define UTILS_H

#include <algorithm>

// 인라인 함수는 헤더에 정의 가능
inline int max(int a, int b) {
    return a > b ? a : b;
}

inline int min(int a, int b) {
    return a < b ? a : b;
}

inline int clamp(int value, int minVal, int maxVal) {
    return std::min(std::max(value, minVal), maxVal);
}

// 템플릿 인라인 함수
template<typename T>
inline T square(T value) {
    return value * value;
}

#endif

// main.cpp
#include <iostream>
#include "utils.h"

int main() {
    std::cout << max(10, 20) << std::endl;      // 20
    std::cout << min(10, 20) << std::endl;      // 10
    std::cout << clamp(15, 0, 10) << std::endl; // 10
    std::cout << square(5) << std::endl;        // 25
    std::cout << square(3.5) << std::endl;      // 12.25
}

여기서 inline의 의미는 “호출 자리에 코드를 펼쳐 넣어라”라는 최적화 지시가 아니라, “이 정의가 여러 번역 단위에 나타나도 모두 같은 것이니 하나로 합쳐라”라는 링크 규칙입니다. 실제로 펼쳐 넣을지는 컴파일러가 inline 여부와 상관없이 스스로 판단합니다. 클래스 본문 안에서 정의한 멤버 함수와 템플릿은 자동으로 이 성질을 가지므로 따로 inline을 붙이지 않아도 됩니다. 한 가지 주의할 점은 이 예제처럼 전역에 max, min 같은 흔한 이름을 정의하면 다른 헤더와 충돌하기 쉽다는 것입니다. 특히 Windows에서 <windows.h>는 max와 min을 매크로로 정의하므로, 그 뒤에 이 헤더를 포함하면 함수 정의가 매크로로 치환되어 이해하기 어려운 문법 에러가 납니다. 자체 유틸리티는 네임스페이스로 감싸고, Windows에서는 <windows.h> 전에 NOMINMAX를 정의하는 것이 일반적인 대처입니다.


잘 만든 헤더의 모습

좋은 헤더 파일 예제

// user.h
#ifndef USER_H
#define USER_H

#include <string>
#include <vector>

// 전방 선언
class Database;
class Logger;

// 상수
constexpr int MAX_USERNAME_LENGTH = 50;

// 클래스 선언
class User {
public:
    // 생성자
    User(const std::string& name, int age);
    
    // Getter
    const std::string& getName() const;
    int getAge() const;
    
    // Setter
    void setName(const std::string& name);
    void setAge(int age);
    
    // 비즈니스 로직
    bool isAdult() const;
    void save(Database* db);
    
private:
    std::string name;
    int age;
    
    // 헬퍼 함수 선언
    bool validateName(const std::string& name) const;
};

// 인라인 함수 (간단한 getter)
inline const std::string& User::getName() const {
    return name;
}

inline int User::getAge() const {
    return age;
}

// 유틸리티 함수
inline bool isValidAge(int age) {
    return age >= 0 && age <= 150;
}

#endif  // USER_H

헤더를 리뷰할 때 보는 항목

// ✅ 좋은 헤더 파일
#pragma once  // 또는 #ifndef

#include <필요한_헤더>

// 전방 선언
class ForwardDeclared;

// 상수
constexpr int CONSTANT = 100;

// 클래스 선언
class MyClass {
    // public → protected → private 순서
};

// 인라인 함수
inline int helper() { return 0; }

// (#ifndef 가드를 썼다면 파일 끝에 #endif)

모범 사례:

  1. 최소 의존성: 필요한 헤더만 포함
  2. 전방 선언: 포인터/참조는 전방 선언 활용
  3. 인클루드 가드: 중복 포함 방지
  4. 인라인 함수: 간단한 함수는 헤더에
  5. 템플릿: 전체 구현을 헤더에

작은 로거 라이브러리로 헤더 설계하기

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

#include <string>
#include <fstream>

// 로그 레벨
enum class LogLevel {
    Debug,
    Info,
    Warning,
    Error
};

// Logger 클래스
class Logger {
public:
    static Logger& getInstance();
    
    void setLogLevel(LogLevel level);
    void setOutputFile(const std::string& filename);
    
    void debug(const std::string& message);
    void info(const std::string& message);
    void warning(const std::string& message);
    void error(const std::string& message);
    
private:
    Logger();
    ~Logger();
    
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;
    
    void log(LogLevel level, const std::string& message);
    std::string levelToString(LogLevel level) const;
    
    LogLevel currentLevel;
    std::ofstream outputFile;
};

// 편의 매크로
#define LOG_DEBUG(msg) Logger::getInstance().debug(msg)
#define LOG_INFO(msg) Logger::getInstance().info(msg)
#define LOG_WARNING(msg) Logger::getInstance().warning(msg)
#define LOG_ERROR(msg) Logger::getInstance().error(msg)

#endif  // LOGGER_H

// logger.cpp
#include "logger.h"
#include <iostream>
#include <ctime>

Logger& Logger::getInstance() {
    static Logger instance;
    return instance;
}

Logger::Logger() : currentLevel(LogLevel::Info) {}

Logger::~Logger() {
    if (outputFile.is_open()) {
        outputFile.close();
    }
}

void Logger::setLogLevel(LogLevel level) {
    currentLevel = level;
}

void Logger::setOutputFile(const std::string& filename) {
    outputFile.open(filename, std::ios::app);
}

void Logger::debug(const std::string& message) {
    if (currentLevel <= LogLevel::Debug) {
        log(LogLevel::Debug, message);
    }
}

void Logger::info(const std::string& message) {
    if (currentLevel <= LogLevel::Info) {
        log(LogLevel::Info, message);
    }
}

void Logger::warning(const std::string& message) {
    if (currentLevel <= LogLevel::Warning) {
        log(LogLevel::Warning, message);
    }
}

void Logger::error(const std::string& message) {
    log(LogLevel::Error, message);
}

void Logger::log(LogLevel level, const std::string& message) {
    std::string levelStr = levelToString(level);
    std::string output = "[" + levelStr + "] " + message;
    
    std::cout << output << std::endl;
    
    if (outputFile.is_open()) {
        outputFile << output << std::endl;
    }
}

std::string Logger::levelToString(LogLevel level) const {
    switch (level) {
        case LogLevel::Debug: return "DEBUG";
        case LogLevel::Info: return "INFO";
        case LogLevel::Warning: return "WARNING";
        case LogLevel::Error: return "ERROR";
        default: return "UNKNOWN";
    }
}

// main.cpp
#include "logger.h"

int main() {
    Logger& logger = Logger::getInstance();
    logger.setLogLevel(LogLevel::Debug);
    logger.setOutputFile("app.log");
    
    LOG_DEBUG("디버그 메시지");
    LOG_INFO("정보 메시지");
    LOG_WARNING("경고 메시지");
    LOG_ERROR("에러 메시지");
}

헤더 파일 정리

핵심 요약

  1. 헤더 파일: 선언을 담는 파일 (.h, .hpp)
  2. 인클루드 가드: 중복 포함 방지 (#ifndef, #pragma once)
  3. 전방 선언: 컴파일 시간 단축, 순환 의존성 해결
  4. 템플릿: 헤더에 전체 구현
  5. 인라인 함수: 헤더에 정의 가능

헤더 파일 설계 원칙

원칙설명
최소 의존성필요한 헤더만 포함
자기 완결성헤더만으로 컴파일 가능
인클루드 가드중복 포함 방지
전방 선언 활용컴파일 시간 단축
ODR 준수정의는 소스 파일에

빌드 시간이 문제가 될 때

헤더가 빌드 시간에 미치는 영향은 포함 횟수 × 헤더 크기로 커집니다. <iostream>이나 <windows.h>처럼 무거운 헤더를 자주 쓰이는 헤더에 넣으면, 그 헤더를 포함하는 수백 개의 .cpp가 모두 그 내용을 다시 파싱합니다. 어느 헤더가 비싼지 모를 때는 Clang의 -ftime-trace로 번역 단위별 시간 분포를 보거나, include-what-you-use(IWYU) 도구로 쓰지 않는 인클루드를 찾을 수 있습니다. 표준 라이브러리처럼 자주 바뀌지 않는 헤더는 프리컴파일드 헤더(PCH, CMake의 target_precompile_headers)로 묶으면 효과가 크고, C++20 모듈(import std;)은 텍스트 복사 자체를 없애는 근본적인 대안이지만 빌드 시스템과 컴파일러 지원 상황을 먼저 확인해야 합니다.

같이 보면 좋은 글


자주 묻는 질문 (FAQ)

Q. include 순서를 바꾸면 헤더에서 갑자기 컴파일 에러가 나는 이유는 무엇인가요?

A. 헤더가 자신에게 필요한 다른 헤더를 직접 include하지 않고, 앞에서 먼저 포함된 헤더 덕분에 우연히 컴파일되고 있었기 때문입니다. 순서가 바뀌거나 앞의 헤더가 정리되면 std::string이나 사용하는 타입의 선언이 사라져 에러가 납니다. 모든 헤더가 단독으로 컴파일되도록 필요한 헤더를 직접 포함하고, 각 .cpp 파일에서 짝이 되는 자기 헤더를 가장 먼저 include하면 이런 누락을 바로 발견할 수 있습니다.