ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 헤더 전용 라이브러리는 뭐고 언제 쓸까?
    Programming🧑‍💻/Cpp 2025. 4. 4. 21:38

    헤더 전용 라이브러리란 무엇인가?

    헤더 전용 라이브러리는 단어 그대로 헤더 파일만으로 구성된 라이브러리입니다.

    일반적인 라이브러리들이 헤더 파일(.h, .hpp)과 구현 파일(.c, .cpp)로 나뉘어 있는 것과 달리, 헤더 전용 라이브러리는 모든 코드가 헤더 파일 안에 담겨 있습니다.

     

    전통적인 라이브러리 방식 : 이 방식에서는 선언과 구현이 분리되어 있습니다.

    • calculator.h (선언부)
    #ifndef CALCULATOR_H
    #define CALCULATOR_H
    
    // 함수 선언
    int add(int a, int b);
    int subtract(int a, int b);
    double divide(double a, double b);
    int multiply(int a, int b);
    
    // 클래스 선언
    class Calculator {
    public:
        Calculator();
        ~Calculator();
        
        int add(int a, int b);
        int subtract(int a, int b);
        int multiply(int a, int b);
        double divide(double a, double b);
        
    private:
        int m_lastResult;
    };
    
    #endif // CALCULATOR_H
    
    • calculator.cpp (구현부)
    #include "calculator.h"
    
    // 함수 구현
    int add(int a, int b) {
        return a + b;
    }
    
    int subtract(int a, int b) {
        return a - b;
    }
    
    double divide(double a, double b) {
        return a / b;
    }
    
    int multiply(int a, int b) {
        return a * b;
    }
    
    // 클래스 구현
    Calculator::Calculator() : m_lastResult(0) {}
    
    Calculator::~Calculator() {}
    
    int Calculator::add(int a, int b) {
        m_lastResult = a + b;
        return m_lastResult;
    }
    
    int Calculator::subtract(int a, int b) {
        m_lastResult = a - b;
        return m_lastResult;
    }
    
    int Calculator::multiply(int a, int b) {
        m_lastResult = a * b;
        return m_lastResult;
    }
    
    double Calculator::divide(double a, double b) {
        m_lastResult = static_cast<int>(a / b);
        return a / b;
    }
    

     

    헤더 전용 라이브러리 방식 : 이 방식에서는 선언과 구현이 모두 헤더 파일에 포함됩니다.

    • calculator.h (선언부와 구현부)
    #ifndef CALCULATOR_H
    #define CALCULATOR_H
    
    // 인라인 함수로 구현 (헤더에 구현이 필요)
    inline int add(int a, int b) {
        return a + b;
    }
    
    inline int subtract(int a, int b) {
        return a - b;
    }
    
    inline double divide(double a, double b) {
        return a / b;
    }
    
    inline int multiply(int a, int b) {
        return a * b;
    }
    
    // 클래스 구현도 헤더에 포함
    class Calculator {
    private:
        int m_lastResult;
    
    public:
        // 생성자와 소멸자
        Calculator() : m_lastResult(0) {}
        ~Calculator() {}
        
        // 멤버 함수들 (클래스 내부에서 정의되어 자동으로 인라인 후보가 됨)
        int add(int a, int b) {
            m_lastResult = a + b;
            return m_lastResult;
        }
        
        int subtract(int a, int b) {
            m_lastResult = a - b;
            return m_lastResult;
        }
        
        int multiply(int a, int b) {
            m_lastResult = a * b;
            return m_lastResult;
        }
        
        double divide(double a, double b) {
            m_lastResult = static_cast<int>(a / b);
            return a / b;
        }
    };
    
    #endif // CALCULATOR_H
    

     

    전통적인 라이브러리와 헤더 전용 라이브러리 동작 방식 차이 

    C/C++의 컴파일 과정은 다음과 같습니다. 

    1. 전처리(Preprocessing): #include와 같은 전처리 지시자를 처리합니다.
    2. 컴파일(Compiling): 소스 코드를 오브젝트 파일(.o 또는 .obj)로 변환합니다.
    3. 링킹(Linking): 오브젝트 파일들을 하나의 실행 파일로 합칩니다.
     

    헤더파일은 뭐고 왜 필요한가?

    헤더 파일의 기본 개념 헤더 파일은 간단히 말해서 선언(Declaration)을 포함하는 파일입니다. 여기서 선언이란 "이런 것들이 어딘가에 있을 것이다."라고 컴파일러에게 알려주는 것을 의미합니다

    people-analysis.tistory.com

    전통적인 라이브러리의 동작 방식 

    1. 라이브러리 측면 (라이브러리 제작자)

    1. 헤더 파일 작성: 함수와 클래스의 선언부만 포함된 .h 파일을 만듭니다.
    2. 구현 파일 작성: 실제 코드 구현이 들어있는 .cpp 파일을 만듭니다.
    3. 구현 파일 컴파일: .cpp 파일을 컴파일하여 오브젝트 파일(.o 또는 .obj)을 생성합니다.
    4. 라이브러리 생성:
      • 정적 라이브러리: 오브젝트 파일들을 묶어 .a(Unix) 또는 .lib(Windows) 파일을 만듭니다.
      • 동적 라이브러리: 오브젝트 파일들을 .so(Unix) 또는 .dll(Windows) 파일로 만듭니다.
    5. 배포: 헤더 파일과 컴파일된 라이브러리 파일을 함께 배포합니다.

    2. 사용자 측면 (라이브러리 사용자)

    1. 헤더 포함: 자신의 코드에 헤더 파일을 #include 합니다.
    2. 사용자 코드 컴파일: 사용자의 .cpp 파일을 컴파일하여 오브젝트 파일을 생성합니다.
      • 이 때 헤더 파일은 전처리 과정에서 사용자 코드로 "복사 붙여넣기" 되는 것과 같습니다.
      • 헤더에는 선언만 있기 때문에 컴파일러는 함수와 클래스가 존재한다는 것만 알고, 실제 구현은 나중에 링킹 단계에서 찾을 것으로 예상합니다.
    3. 링킹: 사용자의 오브젝트 파일과 라이브러리를 연결합니다.
      • 이 단계에서 라이브러리의 이미 컴파일된 코드가 사용자 프로그램에 연결됩니다.
      • 함수 호출 등이 실제 구현체와 연결됩니다.

    헤더 전용 라이브러리의 동작 방식

    1.라이브러리 측면 (라이브러리 제작자)

    1. 헤더 파일 작성: 함수와 클래스의 선언부와 구현부를 모두 하나의 .h 파일에 작성합니다.
    2. 구현 방법 선택: 헤더 전용 라이브러리에서 다중 정의 문제를 방지하기 위한 여러 기법을 사용합니다.
      • inline 키워드 사용: 함수 앞에 inline 키워드를 붙여 ODR(One Definition Rule) 예외를 적용
      • 템플릿 사용: 템플릿은 본질적으로 인라인이므로 헤더에 구현 가능
      • 클래스 내부 정의: 클래스 안에서 직접 함수를 정의하면 암묵적으로 인라인 후보가 됨
    3. 헤더 최적화: 컴파일 시간을 줄이기 위한 기법들을 적용합니다.
      • 불필요한 헤더 포함 최소화
      • 전방 선언(forward declaration) 활용
      • 내부 구현 세부사항은 별도 네임스페이스로 분리
    4. 배포: 컴파일된 바이너리 없이 헤더 파일만 배포합니다.
      • 정적/동적 라이브러리 파일(.lib, .dll, .a, .so)이 필요 없음
      • 설치와 사용이 단순해짐

    2. 사용자 측면 (라이브러리 사용자)

    1. 헤더 포함: 사용자는 헤더 파일만 #include 합니다.
    2. 컴파일 과정: 전처리기가 #include 지시자를 처리하면서 헤더 파일의 모든 내용(선언과 구현 모두)이 사용자 코드에 삽입됩니다.
      • 전처리 단계: 헤더 파일의 모든 내용이 소스 파일에 "복사 붙여넣기"됨
      • 컴파일 단계: 모든 코드(사용자 코드 + 헤더 라이브러리 코드)가 함께 컴파일됨
      • 인라인 최적화: 컴파일러는 인라인 함수를 호출 지점에 직접 삽입할 수 있음
    3. 링킹 과정: 별도로 컴파일된 라이브러리 파일이 없으므로, 추가적인 링킹 작업이 필요 없습니다.
      • 사용자 코드가 컴파일될 때 이미 모든 구현이 포함됨
      • 외부 심볼 참조를 해결하기 위한 링킹이 필요 없음
      • 즉, 라이브러리 파일을 찾거나 링크하는 명령(-l 옵션 등)이 필요 없음
    4. 실행 파일 생성: 컴파일 결과로 헤더 라이브러리 코드가 포함된 실행 파일이 바로 생성됩니다.
      • 라이브러리 코드가 실행 파일에 직접 포함됨
      • 동적 라이브러리와 달리 런타임에 외부 의존성이 없음

    헤더 전용 라이브러리의 구현 방법

    1. 인라인 함수 사용

    // mylib.h
    #ifndef MYLIB_H
    #define MYLIB_H
    
    inline int add(int a, int b) {
        return a + b;
    }
    
    #endif
    

    inline 키워드는 컴파일러에게 함수 호출을 함수 본문으로 대체하도록 제안합니다. 또한 ODR(One Definition Rule)을 완화하여, 여러 번역 단위에서 같은 함수가 정의되어도 문제가 없게 해줍니다.

    2. 템플릿 사용

    템플릿은 본질적으로 인라인이므로, 헤더 전용 라이브러리에 적합합니다.

    // mylib.h
    #ifndef MYLIB_H
    #define MYLIB_H
    
    template<typename T>
    T add(T a, T b) {
        return a + b;
    }
    
    #endif
    

    3. 정적 클래스 멤버

    클래스 내부에 정의된 멤버 함수는 자동으로 인라인 후보가 됩니다.

    // mylib.h
    #ifndef MYLIB_H
    #define MYLIB_H
    
    class Calculator {
    public:
        int add(int a, int b) {
            return a + b;
        }
    };
    
    #endif
    

    4. 구현 세부사항 숨기기

    복잡한 구현을 숨기기 위해 Pimpl(Pointer to Implementation) 패턴을 사용할 수도 있지만, 헤더 전용 라이브러리에서는 다른 접근 방식이 필요합니다.

    // mylib.h
    #ifndef MYLIB_H
    #define MYLIB_H
    
    // 공개 인터페이스
    namespace mylib {
        int complex_calculation(int x);
    }
    
    // 내부 구현 - 네임스페이스로 구분
    namespace mylib_detail {
        inline int helper_function(int x) {
            return x * x;
        }
    }
    
    // 공개 함수의 구현
    inline int mylib::complex_calculation(int x) {
        return mylib_detail::helper_function(x) + 1;
    }
    
    #endif
    

    헤더 전용 라이브러리의 장단점

    장점

    1. 간편한 사용성: 헤더 파일만 포함하면 되므로, 라이브러리 설치와 연결이 간단합니다.
    2. 크로스 플랫폼: 플랫폼별 바이너리가 필요 없어 이식성이 뛰어납니다.
    3. 최적화 기회: 인라인 함수를 통해 컴파일러 최적화 기회가 증가합니다.
    4. 버전 관리가 쉬움: 소스 코드만 관리하면 됩니다.

    단점

    1. 컴파일 시간 증가: 모든 코드가 헤더에 있어 컴파일 시간이 길어질 수 있습니다.
    2. 코드 노출: 구현이 모두 헤더에 있어 코드가 사용자에게 노출됩니다.
    3. 바이너리 크기 증가: 인라인 함수가 여러 위치에서 복제될 수 있습니다.
    4. ABI 안정성: 인터페이스가 변경되면 모든 사용자 코드를 다시 컴파일해야 합니다.

    언제 헤더 전용 라이브러리를 사용해야 할까?

    헤더 전용 라이브러리는 다음과 같은 경우에 적합합니다.

    1. 작은 라이브러리: 코드 크기가 작아 컴파일 시간 영향이 적은 경우
    2. 템플릿 기반 라이브러리: 템플릿은 기본적으로 헤더에 구현을 필요로 합니다
    3. 배포 단순화: 플랫폼별 바이너리 배포가 어려운 경우
    4. 접근성 중시: 사용자가 쉽게 설치하고 사용할 수 있어야 하는 경우

    반면, 다음과 같은 경우에는 전통적인 라이브러리가 더 적합할 수 있습니다.

    1. 대규모 라이브러리: 컴파일 시간이 중요한 경우
    2. 구현 보호: 소스 코드를 공개하고 싶지 않은 경우
    3. ABI 안정성: 바이너리 호환성이 중요한 경우
    4. 동적 로딩: 런타임에 라이브러리를 로드해야 하는 경우

    'Programming🧑‍💻 > Cpp' 카테고리의 다른 글

    const 키워드 맥락에 따라 구분하기  (2) 2025.04.06
    inline 키워드 알아보기  (0) 2025.04.05
    상태기계와 코루틴 프레임  (0) 2025.02.14
    서브 루틴과 코루틴  (0) 2025.02.13
    Macro가 뭘까?  (0) 2025.02.12

    댓글

Designed by Tistory.