본문으로 건너뛰기 C++ Inline Namespace | '인라인 네임스페이스' 가이드

C++ Inline Namespace | '인라인 네임스페이스' 가이드

C++ Inline Namespace | '인라인 네임스페이스' 가이드

이 글의 핵심

C++ Inline Namespace: "인라인 네임스페이스" 가이드. inline namespace란?·버전 관리.

inline namespace란?

inline namespace는 안에 선언된 이름들을 마치 바깥 네임스페이스에 직접 선언된 것처럼 보이게 만드는 C++11 기능입니다. 일반 네임스페이스와 달리 안쪽 이름을 감싸는 껍데기를 하나 더 붙여도, 사용자는 그 껍데기의 존재를 몰라도 바깥 이름만으로 접근할 수 있다는 점이 핵심입니다. 이 성질 덕분에 라이브러리 작성자는 내부적으로 버전이나 구현을 구분해두면서도, 사용자에게는 항상 하나의 안정적인 진입점만 노출할 수 있습니다. 아래 예제에서 MyLib::func()MyLib::v2::func()가 완전히 동일하게 동작하는 것이 이 기능의 가장 기본적인 효과입니다.

namespace MyLib {
    inline namespace v2 {
        void func() {
            std::cout << "v2" << std::endl;
        }
    }
}

// 두 방법 모두 가능
MyLib::func();     // v2 호출
MyLib::v2::func(); // v2 호출

버전 관리

inline namespace가 가장 자주 쓰이는 곳이 바로 이 지점입니다. 같은 이름의 클래스나 함수를 버전별로 다른 네임스페이스에 두고, 그중 현재 기본으로 삼고 싶은 버전에만 inline 키워드를 붙이면 됩니다. 아래 예제에서 v1::Widgetv2::Widget은 데이터 멤버 구성이 다른 별개의 타입이지만, v2inline으로 선언되어 있어 MyLib::Widget이라고만 써도 자동으로 v2::Widget을 가리킵니다. 기존 코드가 굳이 옛 버전을 써야 한다면 MyLib::v1::Widget처럼 전체 경로를 명시해 명확히 선택할 수 있어, 하위 호환성과 최신 기본값 제공이라는 두 요구를 동시에 만족시킬 수 있습니다.

namespace MyLib {
    namespace v1 {
        class Widget {
            int data;
        };
    }
    
    inline namespace v2 {
        class Widget {
            int data;
            std::string name;
        };
    }
}

// 기본적으로 v2 사용
MyLib::Widget w;  // v2::Widget

// 명시적으로 v1 사용
MyLib::v1::Widget w1;  // v1::Widget

실전 예시

이론만으로는 감이 잘 오지 않는 개념이므로, 실무에서 inline namespace를 도입하는 네 가지 대표적인 상황을 코드로 살펴보겠습니다. API 버전 관리, ABI 호환성 유지, 실험적 기능 분리, 플랫폼별 구현 전환이 그것입니다.

예시 1: API 버전 관리

라이브러리가 성숙해지면서 함수 시그니처를 바꿔야 하는 상황이 자주 생깁니다. 아래 예제는 v1::process(int)v2::process(int, int = 0)를 함께 유지하면서 v2inline으로 선언해, 새 코드는 자연스럽게 개선된 시그니처를 기본으로 쓰게 하고 예전 코드는 MyAPI::v1::process로 명시적으로 접근해 기존 동작을 그대로 유지하게 합니다. 이렇게 하면 라이브러리 사용자가 한꺼번에 마이그레이션하지 않아도 새 버전과 구버전이 한 바이너리 안에서 공존할 수 있습니다.

namespace MyAPI {
    namespace v1 {
        void process(int x) {
            std::cout << "v1: " << x << std::endl;
        }
    }
    
    inline namespace v2 {
        void process(int x, int y = 0) {
            std::cout << "v2: " << x << ", " << y << std::endl;
        }
    }
}

int main() {
    MyAPI::process(10);     // v2
    MyAPI::v1::process(10); // v1
}

예시 2: ABI 호환성

전처리기 매크로와 inline namespace를 조합하면 컴파일 시점에 어떤 버전이 기본이 될지 선택할 수 있습니다. 아래 예제는 MYLIB_VERSION 매크로 값에 따라 v1 또는 v2 중 하나만 inline으로 컴파일되도록 만들어, 같은 소스 코드베이스에서 여러 배포 버전의 바이너리를 만들어낼 수 있게 합니다. 이런 패턴은 특히 여러 프로젝트가 서로 다른 시점에 라이브러리를 링크하는 대규모 코드베이스에서, 소스 호환성은 유지하면서 바이너리 레이아웃 변경을 안전하게 관리하는 데 사용됩니다.

namespace MyLib {
    #if MYLIB_VERSION >= 2
        inline namespace v2 {
    #else
        inline namespace v1 {
    #endif
            class Data {
                // 버전별 구현
            };
    #if MYLIB_VERSION >= 2
        }
    #else
        }
    #endif
}

예시 3: 실험적 기능

새로 추가하는 실험적 기능이 안정 버전 API와 섞이지 않도록 구분하고 싶을 때도 inline namespace가 유용합니다. 아래 예제에서 검증된 기능이 담긴 stable 네임스페이스만 inline으로 선언해 기본 경로로 노출하고, 아직 API가 바뀔 수 있는 experimental 네임스페이스는 명시적으로 경로를 적어야만 접근할 수 있게 합니다. 이렇게 하면 실험적 기능을 실제 프로젝트에 통합해 테스트해볼 수 있으면서도, 사용자가 실수로 불안정한 API에 의존하게 되는 상황을 방지할 수 있습니다.

namespace MyLib {
    inline namespace stable {
        void reliableFunc() {
            std::cout << "안정 버전" << std::endl;
        }
    }
    
    namespace experimental {
        void newFunc() {
            std::cout << "실험 버전" << std::endl;
        }
    }
}

int main() {
    MyLib::reliableFunc();  // 기본
    MyLib::experimental::newFunc();  // 명시적
}

예시 4: 플랫폼별 구현

크로스 플랫폼 라이브러리를 작성하다 보면 같은 함수라도 Windows와 POSIX 시스템에서 완전히 다른 구현이 필요한 경우가 많습니다. 아래 예제는 #ifdef _WIN32로 컴파일 대상 플랫폼을 구분한 뒤, 해당 플랫폼의 네임스페이스만 inline으로 선언해 호출부 코드는 MyLib::platformFunc()라는 동일한 이름만 알면 되도록 만듭니다. 플랫폼 차이를 네임스페이스 뒤로 숨기는 이 패턴은 조건부 컴파일 매크로가 코드 전반에 흩어지는 것을 막고, 플랫폼별 구현을 한 곳에 모아 유지보수하기 쉽게 해줍니다.

namespace MyLib {
    #ifdef _WIN32
        inline namespace windows {
            void platformFunc() {
                std::cout << "Windows" << std::endl;
            }
        }
    #else
        inline namespace posix {
            void platformFunc() {
                std::cout << "POSIX" << std::endl;
            }
        }
    #endif
}

int main() {
    MyLib::platformFunc();  // 플랫폼별 자동 선택
}

중첩 inline namespace

inline namespace는 여러 겹으로 중첩할 수도 있습니다. 각 계층이 모두 inline으로 선언되어 있으면, 그 효과가 계단식으로 전파되어 가장 안쪽 이름까지 최상위 네임스페이스에서 직접 접근할 수 있게 됩니다. 아래 예제에서 Outer::func(), Outer::Middle::func(), Outer::Middle::Inner::func()가 모두 같은 함수를 가리키는 이유가 바로 이것입니다. 다만 중첩 단계가 늘어날수록 컴파일러 에러 메시지와 심볼 탐색이 복잡해지므로, 실무에서는 버전이나 실험적 기능처럼 명확한 목적이 있을 때만 한두 단계로 제한해서 쓰는 것이 좋습니다.

namespace Outer {
    inline namespace Middle {
        inline namespace Inner {
            void func() {
                std::cout << "Inner" << std::endl;
            }
        }
    }
}

// 모두 가능
Outer::func();
Outer::Middle::func();
Outer::Middle::Inner::func();

자주 발생하는 문제

inline namespace는 개념 자체는 단순하지만, 실제로 적용하다 보면 컴파일러가 잡아주지 않는 미묘한 실수들이 있습니다. 아래 네 가지는 실무에서 가장 흔히 마주치는 함정입니다.

문제 1: 이름 충돌

inline namespace가 여러 개 있으면 그 안의 이름들이 모두 바깥 스코프로 노출되기 때문에, 서로 다른 inline namespace에 같은 이름의 함수가 있으면 어느 쪽을 가리키는지 컴파일러가 판단할 수 없어 모호성 에러가 발생합니다. 아래 예제처럼 v1v2가 둘 다 inline이고 둘 다 func()를 정의하고 있으면, MyLib::func()를 호출하는 순간 컴파일이 실패합니다. 이 문제는 다음 절에서 볼 “하나의 네임스페이스만 inline으로 유지한다”는 규칙을 지키면 애초에 발생하지 않습니다.

namespace MyLib {
    inline namespace v1 {
        void func() {}
    }
    
    inline namespace v2 {
        void func() {}  // 에러: 모호함
    }
}

문제 2: 버전 전환

새 버전을 추가할 때 실수로 이전 버전의 inline 키워드를 지우지 않고 새 버전에도 inline을 붙이면, 앞서 본 이름 충돌 문제가 그대로 재현됩니다. 버전을 전환하는 올바른 절차는 항상 “새 버전에 inline을 추가하는 동시에 이전 버전에서는 반드시 제거한다”는 원칙을 지키는 것입니다. 아래 대비되는 두 코드처럼, 한 네임스페이스 아래에는 어떤 시점이든 inline으로 선언된 자식이 정확히 하나만 존재해야 합니다.

// ❌ 여러 inline
namespace MyLib {
    inline namespace v1 {}
    inline namespace v2 {}  // 충돌
}

// ✅ 하나만 inline
namespace MyLib {
    namespace v1 {}
    inline namespace v2 {}
}

문제 3: ADL 영향

inline namespace 안에 정의된 타입과 그 타입을 인자로 받는 함수는, 인자 의존 탐색(ADL, Argument-Dependent Lookup)에서 마치 바깥 네임스페이스에 있는 것처럼 취급됩니다. 아래 예제에서 DataMyLib::v2 안에 정의되어 있지만, func(d)를 호출할 때 using namespace 선언 없이도 ADL이 MyLib::v2::func를 자동으로 찾아냅니다. 이 자체는 편리한 동작이지만, 라이브러리에 연산자 오버로드나 swap 같은 ADL 후보 함수를 설계할 때는 어느 네임스페이스의 무엇이 검색 범위에 들어오는지 명확히 인지하고 있어야 의도치 않은 오버로드 선택 문제를 피할 수 있습니다.

namespace MyLib {
    inline namespace v2 {
        struct Data {};
        void func(Data) {}
    }
}

MyLib::Data d;
func(d);  // ADL로 MyLib::v2::func 찾음

문제 4: 문서화

inline namespace의 가장 큰 함정은 사람이 코드만 보고는 “지금 기본으로 쓰이는 버전이 무엇인지”를 파악하기 어렵다는 점입니다. 특히 여러 버전이 파일 곳곳에 흩어져 있으면, 어느 것이 inline으로 선언되어 있는지 확인하려고 헤더 전체를 뒤져야 하는 상황이 생깁니다. 아래 예제처럼 각 버전 위에 Doxygen 스타일 주석으로 “최신 안정 버전”인지 “레거시 버전”인지 명시해두면, 코드를 읽는 사람과 IDE의 문서 툴팁 모두가 현재 기본 경로를 즉시 파악할 수 있습니다.

// ✅ 버전 명시
namespace MyLib {
    /// @brief 최신 안정 버전
    inline namespace v2 {
        void func();
    }
    
    /// @brief 레거시 버전
    namespace v1 {
        void func();
    }
}

표준 라이브러리 사용

C++ 표준 라이브러리 자체도 inline namespace를 사용자 정의 리터럴(user-defined literal)을 조직화하는 데 활용합니다. std::literalsstd::string_literals, std::chrono_literals 등 여러 하위 리터럴 네임스페이스를 묶은 inline namespace들의 집합으로 되어 있어, using namespace std::literals;처럼 최상위 네임스페이스만 가져와도 그 안의 모든 리터럴 연산자가 자연스럽게 사용 가능해집니다. 아래 예제의 "hello"s5s가 각각 std::stringstd::chrono::seconds로 해석되는 것은, 이 리터럴 연산자들이 inline namespace를 통해 눈에 보이지 않게 최상위로 끌어올려져 있기 때문입니다.

// std::literals
using namespace std::literals;
auto s = "hello"s;  // std::string

// std::chrono_literals
using namespace std::chrono_literals;
auto duration = 5s;  // 5 seconds

버전 관리 전략 (API 진화)

inline namespace“기본 진입점”을 한 번에 옮기되, 구버전 심볼은 이름으로 고정해 두는 데 사용됩니다.

  • 신규 메이저: v2inline으로 바꾸으며, v1MyLib::v1:: 접두로만 쓰이게 문서화합니다.
  • 호환 깨짐이 있는 변경: 타입 레이아웃·함수 시그니처가 바뀌면 새 네임스페이스에 두며, 마이그레이션 가이드를 제공합니다.
  • 점진적 폐기(deprecated): 구버전에 [[deprecated]]를 붙이며, 기본 경로는 새 구현으로 연결합니다.

한 부모 아래 동시에 둘 이상을 inline으로 두면 안 됩니다. 한 시점에 “기본”은 하나만 있어야 합니다.

ABI와의 관계

ABI(Application Binary Interface)는 이름 장식·호출 규약·객체 레이아웃 등이 맞물린 결과입니다.

  • 네임스페이스 이름은 링커가 보는 심볼에 영향을 줄 수 있어, 라이브러리 소비자가 어떤 v1/v2 타입을 링크했는지가 바이너리 호환과 연결됩니다.
  • 헤더만 바꾸고 바이너리는 옛날 같은 상황에서, inline namespace로 기본 심볼이 바뀌면 링크 오류나 ODR 위반이 드러나기도 합니다. 배포 시 헤더·바이너리 세트를 함께 관리해야 합니다.
  • 플랫폼별 inline namespace: Windows vs POSIX 구현을 갈라 넣으면, 같은 API 이름으로 소스 호환을 유지하면서 플랫폼 ABI 차이를 구현 쪽에 가둘 수 있습니다.

실전 라이브러리 설계 체크리스트

  • 공개 최상위 네임스페이스 하나 아래에 inline namespace detail보다는 inline namespace vN처럼 의미 있는 버전 태그를 권장합니다(detail은 보통 inline이 아님).
  • ADL: inline namespace 안의 타입은 바깥 이름으로도 보이므로, 연산자·swap 등 ADL 후보가 의도대로 찾아지는지 확인합니다.
  • 문서: “MyLib::Widget은 현재 v2::Widget이다”를 릴리스 노트에 명시합니다.
  • 테스트: v1 고정 경로와 기본 경로 둘 다 빌드 테스트합니다.

C++17 inline 변수와의 구분

  • inline namespace: 이름 공간을 겹쳐 보이게 하는 기능(버전·플랫폼별 별칭).
  • inline 변수(C++17): 여러 번역 단위에 동일 정의를 헤더에 둘 수 있게 하는 ODR 규칙. inline constexpr과 함께 헤더 상수·std::atomic 전역 등에 사용됩니다.

둘 다 “inline”이지만 해결하는 문제가 다릅니다. 네임스페이스 설계와 변수 ODR은 문맥에 맞게 따로 선택합니다.

FAQ

Q1: inline namespace는 언제?

A:

  • API 버전 관리
  • ABI 호환성
  • 실험적 기능

Q2: 여러 inline 가능?

A: 불가. 하나만 inline.

Q3: 성능 영향?

A: 없음. 컴파일 타임 기능.

Q4: 레거시 코드?

A: 명시적 네임스페이스로 접근.

Q5: 표준 라이브러리?

A: literals 네임스페이스가 inline.

Q6: inline namespace 학습 리소스는?

A:

  • “Effective Modern C++”
  • C++11 표준
  • “API Design for C++“

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

관련 글


이 글에서 다루는 키워드 (관련 검색어)

C++, inline-namespace, namespace, C++11, versioning 등으로 검색하시면 이 글이 도움이 됩니다.