C++ extern과 링키지: 전역 변수 공유, 내부·외부 연결, C 함수 연동

이 글의 핵심

내부 연결과 외부 연결의 차이, extern으로 전역 변수와 함수를 여러 번역 단위에서 공유하는 방법, extern "C"로 C ABI와 연동할 때의 이름 맹글링 문제를 정리합니다. extern과 static의 차이, 헤더 설계 팁과 중복 정의 링크 에러도 다룹니다.

들어가며

C++의 extern은 여러 파일에서 전역 변수나 함수를 공유할 때 사용하는 키워드입니다. 링키지(linkage) 개념을 이해하는 데 핵심적입니다.

링키지가 필요한 이유는 C++이 파일마다 따로 컴파일되기 때문입니다. 컴파일러는 .cpp 하나(정확히는 헤더까지 펼친 번역 단위 하나)만 보고 목적 파일(.o, .obj)을 만들고, 다른 파일에 있는 변수나 함수는 “이런 이름의 심볼이 어딘가 있다”는 표시만 남겨 둡니다. 그 표시를 실제 정의와 연결하는 것이 링커의 일이고, 링키지는 “이 이름을 다른 번역 단위와 공유할 것인가”를 정하는 속성입니다. 그래서 링키지 관련 실수는 컴파일 에러가 아니라 링크 에러로 나타납니다. 선언은 있는데 정의가 없으면 “undefined reference to counter'"(GCC·Clang) 또는 "LNK2001: unresolved external symbol"(MSVC)이, 정의가 두 곳에 있으면 "multiple definition of counter’” 또는 “LNK2005: already defined”가 납니다.


선언과 정의를 나누는 extern

extern 선언이 하는 일

// file1.cpp
int globalVar = 10;

// file2.cpp
extern int globalVar;
int x = globalVar;

헤더에 선언, 소스에 정의

// globals.h
extern int counter;
extern void increment();

// globals.cpp
#include "globals.h"
#include <iostream>

int counter = 0;

void increment() {
    counter++;
}

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

int main() {
    increment();
    std::cout << counter << std::endl;  // 1
}

내부 연결과 외부 연결

개념의미대표적인 경우
외부 연결(external linkage)다른 번역 단위(.cpp)에서 같은 이름이 하나의 엔티티를 가리킴파일 범위의 비-static 함수, extern 전역 변수 선언/정의
내부 연결(internal linkage)이름이 해당 번역 단위 안에서만 보임static 파일 범위 변수/함수, const 객체의 기본(구버전 규칙과 주의), 익명 네임스페이스 안의 이름
연결 없음지역 변수, 클래스 멤버 등링커 수준이 아닌 스코프 규칙으로만 해석

실무 포인트: “헤더에 넣으면 ODR 위반인가?”는 대개 연결과 inline 규칙 문제입니다. 함수는 inline 정의를 헤더에 둘 수 있지만, 전역 변수의 정의는 보통 한 .cpp에만 둡니다.


extern과 static의 차이

연결 범위 비교

// extern (외부 연결)
extern int globalVar;

// static (내부 연결)
static int localVar;

같은 이름을 두 파일에서 쓸 때

// file1.cpp
int externVar = 10;
static int staticVar = 20;

// file2.cpp
extern int externVar;
// extern int staticVar;  // 에러: 접근 불가

int main() {
    std::cout << externVar << std::endl;  // 10
}

전역 설정·함수 선언·C 함수 연동 예제

여러 파일이 공유하는 전역 설정

// config.h
#ifndef CONFIG_H
#define CONFIG_H

#include <string>

extern int maxConnections;
extern std::string serverName;

#endif

// config.cpp
#include "config.h"

int maxConnections = 100;
std::string serverName = "MyServer";

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

int main() {
    std::cout << "Max: " << maxConnections << std::endl;
    std::cout << "Server: " << serverName << std::endl;
}

다른 파일의 함수 선언

// utils.h
#ifndef UTILS_H
#define UTILS_H

#include <string>

extern void printMessage(const std::string& msg);
extern int calculate(int a, int b);

#endif

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

void printMessage(const std::string& msg) {
    std::cout << msg << std::endl;
}

int calculate(int a, int b) {
    return a + b;
}

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

int main() {
    printMessage("Hello");
    int result = calculate(5, 3);
    printMessage(std::to_string(result));
}

extern “C”로 C 함수 호출

// c_library.h
#ifdef __cplusplus
extern "C" {
#endif

void c_function();
int c_calculate(int a, int b);

#ifdef __cplusplus
}
#endif

// c_library.c
#include "c_library.h"
#include <stdio.h>

void c_function() {
    printf("C function\n");
}

int c_calculate(int a, int b) {
    return a + b;
}

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

int main() {
    c_function();
    int result = c_calculate(5, 3);
    std::cout << result << std::endl;
}

extern "C"와 이름 장식

C++는 이름 장식(name mangling)으로 오버로드를 구분합니다. C ABI는 그렇지 않으므로, C 라이브러리 심볼과 링크하려면 extern "C" 블록으로 “C 링키지”를 명시합니다.

  • 포인터 타입: extern "C" 함수에 C++에서 만든 콜백을 넘길 때는 콜백도 extern "C"로 맞추거나, 바인딩 규칙을 문서화해야 합니다.
  • 오버로드 불가: C 링키지 함수는 이름만으로 심볼이 만들어지므로, 같은 이름의 extern "C" 함수는 프로그램 전체에서 하나만 존재할 수 있습니다.
  • 예외 전파 주의: C 코드는 C++ 예외를 모르므로, C 라이브러리가 호출하는 콜백에서 예외가 빠져나가면 정의되지 않은 동작이 됩니다. 콜백 경계에서는 try/catch로 예외를 잡아 에러 코드로 바꿔야 합니다.
  • 헤더 관용: #ifdef __cplusplus로 감싸 공통 헤더를 C와 C++에서 같이 씁니다(앞의 extern “C” 예제와 같은 패턴).
extern "C" int plugin_init(void* ctx);  // 심볼명이 C 규칙으로 노출

이름 장식이 실제로 어떻게 보이는지 확인하면 개념이 분명해집니다. GCC에서 int add(int, int)는 목적 파일 안에 _Z3addii 같은 이름으로 기록되고, extern "C"를 붙이면 그냥 add로 기록됩니다. C 컴파일러로 빌드한 라이브러리는 add라는 심볼만 제공하므로, C++ 쪽 헤더에 extern "C"가 빠져 있으면 링커는 _Z3addii를 찾다가 “undefined reference to add(int, int)'"를 냅니다. 에러 메시지에 매개변수 목록이 붙어 있다면 C++ 이름으로 찾고 있다는 뜻이므로 extern “C”누락을 가장 먼저 의심하면 됩니다. Linux에서는nm -C lib.a | grep add로 라이브러리가 실제로 어떤 이름을 내보내는지 확인할 수 있습니다. dlsym이나 GetProcAddress로 플러그인 함수를 이름으로 찾는 경우에도 같은 이유로 extern “C”`가 필요합니다.


extern template으로 인스턴스화 줄이기

// header.h
template<typename T>
class MyClass {
public:
    void print(T value);
};

extern template class MyClass<int>;

// source.cpp
template class MyClass<int>;

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

int main() {
    MyClass<int> obj;
}

extern template은 “이 인스턴스는 다른 파일에서 만들어 두었으니 여기서는 만들지 말라”는 선언입니다. 템플릿은 보통 사용하는 모든 번역 단위에서 각자 인스턴스화되고 링커가 중복을 버리는데, 수백 개 파일이 같은 MyClass<int>를 쓰면 같은 코드를 수백 번 컴파일하는 셈이라 빌드 시간이 늘어납니다. extern template으로 인스턴스화를 한 파일(source.cpp)에 몰아 두면 이 중복 컴파일을 줄일 수 있습니다. 대신 source.cpp의 명시적 인스턴스화를 빠뜨리면 “undefined reference to MyClass<int>::print(int)'" 링크 에러가 나고, 명시적으로 인스턴스화하지 않은 다른 타입(MyClass`)은 평소처럼 사용하는 쪽에서 인스턴스화되므로 헤더에 템플릿 정의 전체가 있어야 한다는 점은 그대로입니다.


중복 정의·초기화 순서·const 연결 문제

헤더에 정의를 넣어 multiple definition

// ❌ 헤더에 정의
// header.h
int globalVar = 10;

// ✅ 헤더에 선언, 소스에 정의
// header.h
extern int globalVar;

// source.cpp
int globalVar = 10;

파일 간 전역 변수 초기화 순서

// file1.cpp
int a = 10;

// file2.cpp
extern int a;
int b = a * 2;

// ✅ 함수 내 static
int& getA() {
    static int a = 10;
    return a;
}

int b = getA() * 2;

이 문제는 “정적 초기화 순서 문제(static initialization order fiasco)“로 알려져 있습니다. 서로 다른 번역 단위에 있는 전역 변수들이 동적 초기화되는 순서는 표준이 정하지 않으므로, file2.cpp의 b가 초기화될 때 file1.cpp의 a가 아직 초기화되지 않았을 수 있습니다. 엄밀히 말하면 위의 int a = 10;처럼 상수로 초기화되는 변수는 프로그램 시작 전에 값이 채워지는 상수 초기화 대상이라 실제로는 안전합니다. 진짜 문제는 std::string serverName = "MyServer";나 생성자가 있는 객체처럼 실행 시점에 초기화 코드가 돌아야 하는 전역 변수입니다. 이 경우 링크 순서나 빌드 설정만 바뀌어도 결과가 달라져, 어떤 빌드에서는 멀쩡하다가 다른 빌드에서만 시작하자마자 크래시가 납니다. 함수 안의 static 지역 변수는 처음 호출될 때 초기화되므로 이 순서 문제를 피할 수 있고, C++11부터는 초기화가 스레드 안전하게 한 번만 일어나도록 보장됩니다. C++20의 constinit을 붙이면 전역 변수가 반드시 상수 초기화되는지를 컴파일러가 검사해 줍니다.

const 전역 변수는 내부 연결

// ❌ const는 기본 internal linkage
// header.h
const int MAX = 100;

// ✅ extern 명시
// header.h
extern const int MAX;

// source.cpp
const int MAX = 100;

const 전역 변수가 기본적으로 내부 연결인 이유는 헤더에 const int MAX = 100;을 두고 여러 파일에서 포함해도 중복 정의 에러가 나지 않게 하려는 설계입니다. 즉 위의 ”❌” 코드도 실제로는 각 파일이 자기만의 MAX 사본을 갖는 방식으로 정상 동작하며, 값이 작은 상수라면 컴파일러가 대부분 상수로 치환해 사본 비용도 거의 없습니다. 문제가 되는 것은 모든 파일이 정확히 하나의 객체를 공유해야 할 때(주소 비교, 큰 테이블)입니다. 이때 주의할 점은 source.cpp의 정의에도 외부 연결이 적용되어야 한다는 것입니다. 위 코드처럼 source.cpp가 extern 선언이 있는 헤더를 먼저 포함하면 그 선언을 따라 외부 연결이 되지만, 헤더를 포함하지 않은 채 const int MAX = 100;만 쓰면 내부 연결이 되어 다른 파일에서 “undefined reference to MAX'"가 납니다. 이런 실수를 피하려면 정의에도 extern const int MAX = 100;처럼 명시하거나, C++17 이상에서는 헤더에 inline constexpr int MAX = 100;`을 두는 것이 가장 간단합니다.

전역 extern 변수의 네임스페이스 오염

// ❌ 전역 네임스페이스 오염
extern int counter;

// ✅ 네임스페이스 사용
namespace MyApp {
    extern int counter;
}

헤더 가드·#pragma once와의 관계

헤더 가드(#ifndef / #define)나 #pragma once는 전처리 단계에서 같은 헤더가 같은 번역 단위에 두 번 포함되는 것을 막습니다. 링키지와는 층이 다릅니다: 가드는 “중복 파싱 방지”, 링키지는 “링커가 심볼을 어떻게 합치나”입니다.

다만 실무에서는 다음을 한 세트로 생각합니다.

  1. 선언은 헤더에 여러 번 포함돼도 안전해야 함 → #pragma once + 선언만.
  2. 정의는 한 번만 → .cpp에 두거나 inline/constexpr 등 규칙 준수.
  3. extern 전역 변수: 헤더에는 extern int x;, 소스 한 곳에 int x = 0;.

즉 “가드가 링키지를 바꾼다”가 아니라, 가드 덕분에 헤더를 안전하게 여러 곳에서 include할 수 있어 ODR을 지키기 쉬워집니다.


여러 파일에서 쓰는 로거 만들기

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

#include <string>

extern int logLevel;
extern void log(const std::string& message);

#endif

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

int logLevel = 1;

void log(const std::string& message) {
    if (logLevel > 0) {
        std::cout << "[LOG] " << message << std::endl;
    }
}

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

int main() {
    logLevel = 2;
    log("Application started");
}

라이브러리를 설계할 때의 연결 규칙

  1. 공개 API는 네임스페이스로 감싸고, 전역 extern 남발을 피합니다.
  2. 구현 세부는 내부 연결(static, 익명 네임스페이스) 또는 detail 네임스페이스에 둡니다.
  3. C API 노출이 필요하면 extern "C" + 안정적인 ABI 문서(호출 규약, 스레드 안전성)를 제공합니다.
  4. 모듈(C++20) 또는 명확한 include 계층으로 “헤더에서 무엇이 노출되는지”를 줄입니다.

extern과 링키지 정리

핵심 요약

  1. extern: 외부 연결
  2. 선언 vs 정의: 헤더에 선언, 소스에 정의
  3. extern “C”: C 링키지
  4. const: extern 명시 필요
  5. static: 내부 연결

extern과 static 비교표

특성externstatic
연결외부내부
다른 파일 접근OX
중복 정의XO
용도전역 공유파일 내부

이어서 볼 글


같이 보면 좋은 글


자주 묻는 질문 (FAQ)

Q. const 전역 변수를 다른 파일에서 extern으로 참조하면 왜 undefined reference가 나나요?

A. C++에서 네임스페이스 범위의 const 변수는 따로 지정하지 않으면 내부 연결을 가지므로, 정의한 .cpp 파일 밖에서는 보이지 않습니다. 그래서 다른 파일에서 extern const int kMax;로 선언해도 링커가 심볼을 찾지 못합니다. 정의하는 쪽에도 extern const int kMax = 100;처럼 extern을 붙여 외부 연결로 만들거나, C++17 이상이라면 헤더에 inline constexpr int kMax = 100;을 두는 방법이 더 간단합니다.