C++ 동적 로딩 완벽 가이드 | dlopen·LoadLibrary·실전 패턴 [#55-2]

C++ 동적 로딩 완벽 가이드 | dlopen·LoadLibrary·실전 패턴 [#55-2]

이 글의 핵심

C++ 동적 라이브러리 로딩: dlopen/LoadLibrary 완전 예제, 문제 시나리오, 흔한 에러, 모범 사례, 프로덕션 패턴. Linux·macOS·Windows 크로스 플랫폼 구현.

들어가며: “라이브러리를 실행 시점에 골라서 로드하고 싶어요"

"빌드 시점이 아니라 런타임에 .so/.dll을 선택해서 쓰고 싶다”

정적 링크는 컴파일 시점에 모든 코드가 실행 파일에 박혀 들어갑니다. 반면 동적 로딩은 실행 중에 .so(Linux), .dylib(macOS), .dll(Windows)을 열고, 그 안의 함수를 이름으로 조회해 호출합니다. 플러그인, 조건부 기능, 서드파티 모듈 통합 등에서 필수입니다.

문제의 핵심:

  • 플랫폼마다 API가 다릅니다: Linux/macOS는 dlopen/dlsym/dlclose, Windows는 LoadLibrary/GetProcAddress/FreeLibrary
  • 경로·확장자·의존성 해석이 OS마다 다릅니다
  • 잘못 사용하면 “symbol not found”, “module not found” 등 런타임 에러가 납니다

이 글에서 다루는 것:

  • 문제 시나리오: 동적 로딩이 필요한 실제 상황
  • 완전한 예제: dlopen(Linux/macOS), LoadLibrary(Windows) 각각 동작하는 코드
  • 자주 발생하는 에러와 해결법
  • 모범 사례프로덕션 패턴

요구 환경: C++17 이상

이 글을 읽으면:

  • 동적 로딩의 개념과 플랫폼 차이를 이해할 수 있습니다
  • Linux/macOS/Windows에서 동작하는 로더를 직접 구현할 수 있습니다
  • 흔한 에러를 피하고 프로덕션 수준 코드를 작성할 수 있습니다

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. 문제 시나리오
  2. 기본 개념: 정적·동적 링크 vs 동적 로딩
  3. 완전한 dlopen 예제 (Linux/macOS)
  4. 완전한 LoadLibrary 예제 (Windows)
  5. 크로스 플랫폼 통합 예제
  6. 자주 발생하는 에러와 해결법
  7. 모범 사례
  8. 프로덕션 패턴
  9. 정리

1. 문제 시나리오

시나리오 1: 플러그인을 빌드 없이 추가하고 싶다

상황: 이미지 에디터에 사용자가 만든 필터를 추가하려 합니다. 앱을 다시 컴파일·배포하지 않고, .so/.dll 파일만 plugins/ 폴더에 넣으면 인식되게 하고 싶습니다.

해결: dlopen/LoadLibraryplugins/ 디렉터리를 스캔해 .so/.dll을 로드하고, dlsym/GetProcAddressplugin_init 같은 심볼을 조회해 호출합니다.

시나리오 2: GPU가 있는 환경에서만 CUDA 라이브러리 로드

상황: 앱은 CPU 모드와 GPU 모드 모두 지원합니다. GPU가 없거나 드라이버가 없으면 libcudart.so를 로드하면 앱이 시작 시점에 크래시합니다.

해결: dlopen으로 조건부 로딩합니다. GPU 감지 후에만 libcudart.so를 열고, 실패하면 CPU 모드로 폴백합니다. 정적 링크였다면 GPU 없이도 시작 시점에 cudaInit이 링크되어 크래시할 수 있습니다.

시나리오 3: 라이선스에 따라 기능 모듈 로드

상황: 기본 버전은 무료, 프리미엄 기능은 유료 라이선스로 제공합니다. 유료 사용자에게만 premium_features.so를 로드해 기능을 활성화하고 싶습니다.

해결: 라이선스 검증 후 dlopen("premium_features.so")를 호출합니다. 실패하면 해당 기능을 비활성화합니다. 정적 링크였다면 바이너리에 유료 코드가 포함되어 역공학 위험이 있습니다.

시나리오 4: A/B 테스트용 실험 모듈

상황: 새 알고리즘을 일부 사용자에게만 노출하는 A/B 테스트를 합니다. 실험 모듈을 별도 .so로 빌드하고, 설정에 따라 로드 여부를 결정하고 싶습니다.

해결: 설정 파일에서 experiment_module.so 경로를 읽고, 해당 경로가 있으면 dlopen으로 로드합니다. 없으면 기존 로직만 사용합니다.

시나리오 5: “cannot open shared object file” 에러

상황: 개발 PC에서는 잘 되는데, 배포 환경에서 ./myapp 실행 시 error while loading shared libraries: libfoo.so: cannot open shared object file가 발생합니다.

해결: LD_LIBRARY_PATH 설정, RPATH/RUNPATH 빌드 시 지정, 또는 상대 경로로 dlopen("./lib/libfoo.so")처럼 명시적으로 열어 줍니다. 이 글의 자주 발생하는 에러 섹션에서 상세히 다룹니다.

동적 로딩 의사결정 흐름

flowchart TB
    subgraph input[입력]
        Q1[런타임에 모듈 선택?]
        Q2[조건부 기능?]
        Q3[플러그인 확장?]
    end
    subgraph decision[의사결정]
        D1{필요?}
    end
    subgraph result[선택]
        R1[동적 로딩 사용]
        R2[정적/동적 링크]
    end
    Q1 --> D1
    Q2 --> D1
    Q3 --> D1
    D1 -->|Yes| R1
    D1 -->|No| R2

2. 기본 개념: 정적·동적 링크 vs 동적 로딩

세 가지 링크 방식 비교

방식시점코드 위치심볼 해석
정적 링크링크 시실행 파일에 포함링크 시
동적 링크로드 시별도 .so/.dll프로그램 시작 시
동적 로딩런타임별도 .so/.dlldlopen/LoadLibrary 호출 시

동적 로딩은 프로그램이 이미 실행 중일 때, 코드에서 직접 라이브러리 파일을 열고 함수 포인터를 조회합니다. 링크 시점에 의존성이 없습니다.

플랫폼별 API

플랫폼로드심볼 조회해제확장자
Linuxdlopendlsymdlclose.so
macOSdlopendlsymdlclose.dylib, .so
WindowsLoadLibraryGetProcAddressFreeLibrary.dll

dlopen 플래그 (Linux/macOS)

// RTLD_NOW: 로드 시점에 모든 심볼 해석 (권장)
// - 누락된 심볼이 있으면 바로 dlopen 실패
// - 첫 호출 시 지연 없음

// RTLD_LAZY: 첫 사용 시 심볼 해석
// - 로드가 빠름
// - 첫 호출 시 dlsym 실패 가능

// RTLD_LOCAL: 로드한 심볼을 다른 모듈에 노출하지 않음 (권장)
// - 플러그인 간 심볼 충돌 방지

// RTLD_GLOBAL: 로드한 심볼을 전역에 노출
// - 다른 dlopen된 모듈이 이 심볼을 사용 가능
// - 같은 이름의 심볼이 있으면 덮어쓰기 주의

아키텍처 다이어그램

flowchart TB
    subgraph host[호스트 애플리케이션]
        H1[main]
        H2[dlopen/LoadLibrary]
        H3[dlsym/GetProcAddress]
    end
    subgraph lib[동적 라이브러리]
        L1[libplugin.so / plugin.dll]
        L2[exported_func]
    end
    H1 --> H2
    H2 --> L1
    H2 --> H3
    H3 -.->|함수 포인터| L2

3. 완전한 dlopen 예제 (Linux/macOS)

3.1 공유 라이브러리 작성 (export할 함수)

먼저 로드할 대상 라이브러리를 만듭니다. C 링크(extern "C")를 사용하면 name mangling 없이 심볼 이름이 고정됩니다.

// mylib.cpp — 공유 라이브러리 소스
#include <cstdio>

extern "C" {

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

void greet(const char* name) {
    printf("Hello, %s!\n", name);
}

}  // extern "C"

빌드 (Linux):

g++ -std=c++17 -shared -fPIC -o libmylib.so mylib.cpp

빌드 (macOS):

clang++ -std=c++17 -shared -fPIC -o libmylib.dylib mylib.cpp
  • -shared: 공유 라이브러리 생성
  • -fPIC: Position Independent Code (공유 라이브러리 필수)

3.2 dlopen으로 로드하고 dlsym으로 호출

// main_dlopen.cpp — Linux/macOS 전용
#include <dlfcn.h>
#include <iostream>
#include <string>
#include <cstring>

int main() {
    const char* lib_path =
#ifdef __APPLE__
        "./libmylib.dylib"
#else
        "./libmylib.so"
#endif
        ;

    void* handle = dlopen(lib_path, RTLD_NOW | RTLD_LOCAL);
    if (!handle) {
        std::cerr << "dlopen failed: " << dlerror() << "\n";
        return 1;
    }

    // dlsym: 심볼 조회 (함수 포인터)
    using AddFunc = int (*)(int, int);
    using GreetFunc = void (*)(const char*);

    AddFunc add = reinterpret_cast<AddFunc>(dlsym(handle, "add"));
    GreetFunc greet = reinterpret_cast<GreetFunc>(dlsym(handle, "greet"));

    if (!add || !greet) {
        std::cerr << "dlsym failed: " << dlerror() << "\n";
        dlclose(handle);
        return 1;
    }

    // 호출
    std::cout << "add(3, 5) = " << add(3, 5) << "\n";
    greet("World");

    dlclose(handle);
    return 0;
}

빌드 및 실행 (Linux):

g++ -std=c++17 -o main_dlopen main_dlopen.cpp -ldl
./main_dlopen

빌드 및 실행 (macOS):

clang++ -std=c++17 -o main_dlopen main_dlopen.cpp
./main_dlopen

macOS에서는 libdl이 시스템 라이브러리에 포함되어 있어 -ldl이 필요 없을 수 있습니다.

3.3 에러 처리 강화 버전

// main_dlopen_safe.cpp — 에러 처리 포함
#include <dlfcn.h>
#include <iostream>
#include <string>
#include <stdexcept>

class DynamicLibrary {
public:
    explicit DynamicLibrary(const std::string& path) {
        handle_ = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
        if (!handle_) {
            throw std::runtime_error(std::string("dlopen failed: ") + dlerror());
        }
    }

    ~DynamicLibrary() {
        if (handle_) {
            dlclose(handle_);
        }
    }

    DynamicLibrary(const DynamicLibrary&) = delete;
    DynamicLibrary& operator=(const DynamicLibrary&) = delete;

    template <typename Func>
    Func getSymbol(const char* name) const {
        dlerror();  // 이전 에러 초기화
        void* sym = dlsym(handle_, name);
        const char* err = dlerror();
        if (err) {
            throw std::runtime_error(std::string("dlsym failed: ") + err);
        }
        return reinterpret_cast<Func>(sym);
    }

    bool isLoaded() const { return handle_ != nullptr; }

private:
    void* handle_ = nullptr;
};

int main() {
    try {
        DynamicLibrary lib(
#ifdef __APPLE__
            "./libmylib.dylib"
#else
            "./libmylib.so"
#endif
        );

        auto add = lib.getSymbol<int (*)(int, int)>("add");
        auto greet = lib.getSymbol<void (*)(const char*)>("greet");

        std::cout << "add(3, 5) = " << add(3, 5) << "\n";
        greet("World");
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

주의: dlerror()는 스레드당 하나의 에러 문자열만 반환합니다. dlsym 전에 dlerror()를 호출해 이전 에러를 초기화하는 것이 좋습니다.


4. 완전한 LoadLibrary 예제 (Windows)

4.1 공유 라이브러리 작성 (DLL)

// mylib_win.cpp — Windows DLL 소스
#ifdef _WIN32

#include <windows.h>
#include <string>

// DLL 진입점 (선택)
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
    (void)hModule;
    (void)lpReserved;
    switch (reason) {
        case DLL_PROCESS_ATTACH:
            break;
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

extern "C" {

__declspec(dllexport) int add(int a, int b) {
    return a + b;
}

__declspec(dllexport) void greet(const char* name) {
    MessageBoxA(NULL, ("Hello, " + std::string(name) + "!").c_str(),
                "Greeting", MB_OK);
}

}  // extern "C"

#endif

단순화 버전 (MessageBox 대신 콘솔):

// mylib_win.cpp — Windows DLL (콘솔 출력)
#ifdef _WIN32

#include <cstdio>

extern "C" {

__declspec(dllexport) int add(int a, int b) {
    return a + b;
}

__declspec(dllexport) void greet(const char* name) {
    printf("Hello, %s!\n", name);
}

}  // extern "C"

#endif

빌드 (MSVC):

cl /LD /EHsc mylib_win.cpp /Fe:mylib.dll

빌드 (MinGW):

g++ -std=c++17 -shared -o mylib.dll mylib_win.cpp

4.2 LoadLibrary로 로드하고 GetProcAddress로 호출

// main_win.cpp — Windows 전용
#ifdef _WIN32

#include <windows.h>
#include <iostream>
#include <string>

int main() {
    HMODULE handle = LoadLibraryA("mylib.dll");
    if (!handle) {
        DWORD err = GetLastError();
        std::cerr << "LoadLibrary failed, error: " << err << "\n";
        return 1;
    }

    using AddFunc = int (*)(int, int);
    using GreetFunc = void (*)(const char*);

    AddFunc add = reinterpret_cast<AddFunc>(
        GetProcAddress(handle, "add"));
    GreetFunc greet = reinterpret_cast<GreetFunc>(
        GetProcAddress(handle, "greet"));

    if (!add || !greet) {
        std::cerr << "GetProcAddress failed, error: " << GetLastError() << "\n";
        FreeLibrary(handle);
        return 1;
    }

    std::cout << "add(3, 5) = " << add(3, 5) << "\n";
    greet("World");

    FreeLibrary(handle);
    return 0;
}

#endif

4.3 Windows 에러 메시지 개선

// Windows: GetLastError를 문자열로 변환
#include <windows.h>
#include <iostream>
#include <string>

std::string getLastErrorString() {
    DWORD err = GetLastError();
    if (err == 0) return "No error";

    char* msg = nullptr;
    size_t len = FormatMessageA(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
        nullptr, err, 0, reinterpret_cast<LPSTR>(&msg), 0, nullptr);
    std::string result(msg, len);
    LocalFree(msg);
    return result;
}

int main() {
    HMODULE h = LoadLibraryA("mylib.dll");
    if (!h) {
        std::cerr << "LoadLibrary failed: " << getLastErrorString() << "\n";
        return 1;
    }
    // ...
}

5. 크로스 플랫폼 통합 예제

5.1 플랫폼 추상화 헤더

// dynamic_loader.hpp — 크로스 플랫폼 동적 로더
#pragma once

#include <string>
#include <stdexcept>

#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif

class DynamicLoader {
public:
    using Handle = void*;

    explicit DynamicLoader(const std::string& path) {
#ifdef _WIN32
        handle_ = LoadLibraryA(path.c_str());
        if (!handle_) {
            throw std::runtime_error("LoadLibrary failed: " + path +
                " (error " + std::to_string(GetLastError()) + ")");
        }
#else
        handle_ = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL);
        if (!handle_) {
            throw std::runtime_error(std::string("dlopen failed: ") + dlerror());
        }
#endif
    }

    ~DynamicLoader() {
        if (handle_) {
#ifdef _WIN32
            FreeLibrary(static_cast<HMODULE>(handle_));
#else
            dlclose(handle_);
#endif
        }
    }

    DynamicLoader(const DynamicLoader&) = delete;
    DynamicLoader& operator=(const DynamicLoader&) = delete;

    void* getSymbol(const char* name) const {
        if (!handle_) return nullptr;
#ifdef _WIN32
        return reinterpret_cast<void*>(
            GetProcAddress(static_cast<HMODULE>(handle_), name));
#else
        return dlsym(handle_, name);
#endif
    }

    template <typename Func>
    Func get(const char* name) const {
        void* sym = getSymbol(name);
        if (!sym) {
            throw std::runtime_error(std::string("Symbol not found: ") + name);
        }
        return reinterpret_cast<Func>(sym);
    }

    bool isLoaded() const { return handle_ != nullptr; }

    static std::string getPlatformExtension() {
#ifdef _WIN32
        return ".dll";
#elif defined(__APPLE__)
        return ".dylib";
#else
        return ".so";
#endif
    }

    static std::string getPlatformPrefix() {
#ifdef _WIN32
        return "";
#else
        return "lib";
#endif
    }

private:
    Handle handle_ = nullptr;
};

5.2 라이브러리 경로 자동 구성

// 플랫폼별 라이브러리 경로 생성
std::string makeLibraryPath(const std::string& base_name) {
    std::string prefix = DynamicLoader::getPlatformPrefix();
    std::string ext = DynamicLoader::getPlatformExtension();
    return prefix + base_name + ext;
}

// 사용 예
int main() {
    std::string path = makeLibraryPath("mylib");  // libmylib.so, mylib.dll 등
    try {
        DynamicLoader loader(path);
        auto add = loader.get<int (*)(int, int)>("add");
        std::cout << "add(3, 5) = " << add(3, 5) << "\n";
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

5.3 CMake 크로스 플랫폼 빌드

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(DynamicLoadDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

# 공유 라이브러리
add_library(mylib SHARED mylib.cpp)
set_target_properties(mylib PROPERTIES
    PREFIX ""
    OUTPUT_NAME "mylib"
)
if(WIN32)
    target_compile_definitions(mylib PRIVATE _WIN32)
else()
    set_target_properties(mylib PROPERTIES
        PREFIX "lib"
        SUFFIX ".so"
    )
    if(APPLE)
        set_target_properties(mylib PROPERTIES SUFFIX ".dylib")
    endif()
endif()

# 호스트 실행 파일
add_executable(host main.cpp)
target_link_libraries(host PRIVATE)
if(UNIX AND NOT APPLE)
    target_link_libraries(host PRIVATE dl)
endif()

6. 자주 발생하는 에러와 해결법

에러 1: “cannot open shared object file” (Linux)

증상: dlopen("./libfoo.so") 또는 프로그램 시작 시 error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

원인:

  • libfoo.so가 검색 경로에 없음
  • libfoo.so가 의존하는 다른 라이브러리를 찾지 못함

해결:

# 방법 1: LD_LIBRARY_PATH 설정
export LD_LIBRARY_PATH=/path/to/libs:$LD_LIBRARY_PATH
./myapp

# 방법 2: 실행 시 한 줄로
LD_LIBRARY_PATH=./plugins:$LD_LIBRARY_PATH ./myapp
// 방법 3: 절대 경로 또는 실행 파일 기준 상대 경로 사용
std::string plugin_dir = "./plugins/";
std::string path = plugin_dir + "libmylib.so";
void* h = dlopen(path.c_str(), RTLD_NOW);
# 방법 4: 빌드 시 RPATH 설정 (실행 파일 기준)
g++ -o myapp main.cpp -Wl,-rpath,'$ORIGIN/plugins' -L./plugins -lmylib

에러 2: “symbol not found” / “undefined symbol” (dlsym 실패)

증상: dlsym(handle, "add")nullptr 반환, dlerror()에 “undefined symbol: add”

원인:

  • C++ name mangling: add_Z3addii 등으로 맹글링됨
  • extern "C" 없이 선언/정의

해결:

// ❌ 잘못된 예 — C++ 링크
int add(int a, int b) { return a + b; }  // _Z3addii

// ✅ 올바른 예 — extern "C"
extern "C" {
int add(int a, int b) { return a + b; }  // add
}

확인: nm -D libmylib.so | grep add로 심볼 이름 확인

nm -D libmylib.so | grep add
# add (C 링크) vs _Z3addii (C++ 링크)

에러 3: Windows “The specified module could not be found”

증상: LoadLibrary("mylib.dll") 실패, GetLastError() == 126

원인: mylib.dll이 의존하는 다른 DLL(예: MSVC 런타임, Visual C++ Redistributable)을 찾지 못함

해결:

  • 호스트와 DLL을 같은 런타임으로 빌드 (/MD 또는 /MT 일치)
  • Dependencies로 누락된 DLL 확인
  • mylib.dll을 호스트 실행 파일과 같은 디렉터리에 배치
  • 또는 SetDllDirectory로 검색 경로 추가
// DLL 검색 경로 추가 (Windows)
SetDllDirectoryA("C:\\path\\to\\plugins");
HMODULE h = LoadLibraryA("mylib.dll");

에러 4: “undefined reference to dlopen” (Linux 링크 에러)

증상: 링크 시 undefined reference to 'dlopen'

원인: libdl을 링크하지 않음

해결:

g++ -o myapp main.cpp -ldl

에러 5: dlclose 후 크래시

증상: dlclose(handle) 호출 후, 해당 라이브러리의 코드를 호출하면 세그폴트

원인: dlclose 후에는 로드된 코드가 유효하지 않음. 함수 포인터를 보관해 두고 나중에 호출하면 안 됩니다.

해결:

  • dlclose 전에 해당 라이브러리의 함수를 더 이상 호출하지 않도록 보장
  • 함수 포인터와 handle 생명주기를 함께 관리
// ❌ 잘못된 예
void* h = dlopen("libfoo.so", RTLD_NOW);
auto func = (void(*)())dlsym(h, "foo");
dlclose(h);
func();  // 크래시! 이미 언로드됨

// ✅ 올바른 예
void* h = dlopen("libfoo.so", RTLD_NOW);
auto func = (void(*)())dlsym(h, "foo");
func();   // 사용
dlclose(h);  // 사용 완료 후 해제

에러 6: RTLD_GLOBAL 사용 시 심볼 충돌

증상: 두 플러그인이 같은 이름의 helper 함수를 export할 때, 나중에 로드된 쪽이 덮어씀

해결: RTLD_LOCAL 사용 (기본 권장). 플러그인 간 통신이 필요하면 호스트가 중개 API를 제공합니다.

에러 7: macOS에서 “dyld: Library not loaded”

증상: dyld: Library not loaded: @rpath/libfoo.dylib 또는 Reason: image not found

원인: @rpath, @executable_path 등이 올바르게 설정되지 않음

해결:

# 빌드 시 rpath 설정
clang++ -shared -o libfoo.dylib foo.cpp \
    -install_name @rpath/libfoo.dylib

# 실행 파일에 rpath 추가
clang++ -o myapp main.cpp -Wl,-rpath,@loader_path

7. 모범 사례

1. C ABI 경계 유지

호스트와 동적 라이브러리 경계에서는 C 타입만 사용합니다. std::string, std::vector 등은 ABI가 컴파일러·버전마다 달라 위험합니다.

// ✅ C 타입
extern "C" int process(const char* input, size_t len, void* output);

// ❌ STL 타입 (ABI 불안정)
extern "C" std::string process(std::vector<int>& input);  // 위험

2. RTLD_NOW 사용 (Linux/macOS)

RTLD_LAZY는 첫 호출 시점에 심볼을 해석하므로, 그때서야 에러가 납니다. RTLD_NOW로 로드 시점에 모든 심볼을 검증하는 것이 안전합니다.

3. RTLD_LOCAL 사용

플러그인/모듈 간 심볼 충돌을 피하려면 RTLD_LOCAL을 사용합니다.

4. RAII로 리소스 관리

dlopen/LoadLibrary로 연 handle은 반드시 dlclose/FreeLibrary로 해제합니다. 예외 안전을 위해 RAII 클래스로 감쌉니다.

class ScopedLibrary {
public:
    explicit ScopedLibrary(const std::string& path) { /* dlopen */ }
    ~ScopedLibrary() { /* dlclose */ }
    // 복사 금지, 이동 가능
};

5. dlerror() 호출 타이밍 (Linux/macOS)

dlsym 직전에 dlerror()를 한 번 호출해 이전 에러를 초기화합니다. 그렇지 않으면 이전 실패의 에러 메시지가 남아 있을 수 있습니다.

6. Windows: __declspec(dllexport)

DLL에서 export할 함수에는 __declspec(dllexport)를 붙입니다. 그렇지 않으면 GetProcAddressnullptr을 반환할 수 있습니다.

extern "C" __declspec(dllexport) int add(int a, int b);

7. 버전 검사

플러그인/모듈에 버전 필드를 두고, 호스트가 호환되는지 확인합니다.

extern "C" int plugin_version = 2;
// 호스트: if (plugin_version < 2) { /* 거부 */ }

8. 프로덕션 패턴

패턴 1: 지연 로딩 (Lazy Load)

필요할 때만 로드해 시작 시간을 줄입니다.

class LazyPlugin {
public:
    void* getSymbol(const char* name) {
        if (!handle_) {
            handle_ = dlopen(path_.c_str(), RTLD_NOW | RTLD_LOCAL);
            if (!handle_) throw std::runtime_error("dlopen failed");
        }
        return dlsym(handle_, name);
    }
private:
    std::string path_;
    void* handle_ = nullptr;
};

패턴 2: 플러그인 디렉터리 스캔

#include <filesystem>

void loadPluginsFromDir(const std::string& dir) {
    namespace fs = std::filesystem;
    for (const auto& entry : fs::directory_iterator(dir)) {
        if (!entry.is_regular_file()) continue;
        auto ext = entry.path().extension().string();
        if (ext != ".so" && ext != ".dll" && ext != ".dylib") continue;
        try {
            DynamicLoader loader(entry.path().string());
            // ...
        } catch (const std::exception& e) {
            std::cerr << "Skip " << entry.path() << ": " << e.what() << "\n";
        }
    }
}

패턴 3: 조건부 로딩 (폴백)

GPU 라이브러리가 없으면 CPU 모드로 폴백합니다.

void* handle = dlopen("libcuda.so", RTLD_NOW | RTLD_LOCAL);
if (handle) {
    // GPU 모드
    auto init = (void(*)())dlsym(handle, "cudaInit");
    if (init) init();
} else {
    // CPU 모드
    useCpuBackend();
}

패턴 4: 심볼 캐싱

dlsym/GetProcAddress는 상대적으로 비용이 있습니다. 한 번 조회한 함수 포인터를 캐시합니다.

std::unordered_map<std::string, void*> symbol_cache;

void* getCachedSymbol(void* handle, const char* name) {
    auto it = symbol_cache.find(name);
    if (it != symbol_cache.end()) return it->second;
    void* sym = dlsym(handle, name);
    if (sym) symbol_cache[name] = sym;
    return sym;
}

패턴 5: 샌드박스 / 격리

플러그인이 크래시해도 호스트가 죽지 않게 하려면:

  • 별도 프로세스에서 플러그인 실행 (IPC로 통신)
  • 또는 시그널 핸들러로 플러그인 호출 구간을 보호 (복잡함)

패턴 6: 로깅

로드 실패 시 경로, 에러 메시지, GetLastError()/dlerror()를 로그에 남깁니다.

if (!handle) {
    log_error("dlopen failed", "path", path, "error", dlerror());
    return nullptr;
}

9. 정리

항목Linux/macOSWindows
로드dlopen(path, RTLD_NOW | RTLD_LOCAL)LoadLibraryA(path)
심볼 조회dlsym(handle, name)GetProcAddress(handle, name)
해제dlclose(handle)FreeLibrary(handle)
확장자.so / .dylib.dll
export기본 노출 (visibility 조절 가능)__declspec(dllexport)
링크-ldl (Linux)불필요

핵심 원칙:

  1. C ABI로 경계를 고정해 name mangling과 ABI 불일치를 피한다.
  2. RAII로 handle 생명주기를 관리하고, dlclose/FreeLibrary를 반드시 호출한다.
  3. RTLD_NOW | RTLD_LOCAL을 사용해 로드 시점 검증과 심볼 충돌 방지를 한다.
  4. 플랫폼 추상화로 한 인터페이스로 Linux/macOS/Windows를 다룬다.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. 플러그인 시스템, 조건부 기능 로딩, 런타임 확장, 서드파티 모듈 통합 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


참고 자료


구현 체크리스트

동적 로딩 도입 시 확인할 항목:

  • extern "C"로 export할 함수 선언
  • Windows: __declspec(dllexport) 적용
  • RTLD_NOW | RTLD_LOCAL 사용 (Linux/macOS)
  • RAII로 handle 해제 (dlclose/FreeLibrary)
  • dlerror() 호출 타이밍 (dlsym 전 초기화)
  • C ABI 경계 유지 (STL 타입 경계에 사용 금지)
  • 플랫폼별 확장자·경로 처리
  • 에러 로깅 (경로, dlerror, GetLastError)
  • Linux: -ldl 링크
  • 테스트: 각 플랫폼에서 로드·심볼 조회·해제 확인

한 줄 요약: dlopen/LoadLibrary로 런타임에 동적 라이브러리를 로드하고, C ABI와 플랫폼 추상화로 안정적인 확장 시스템을 구축할 수 있습니다.


관련 글

  • C++ 플러그인 시스템 완벽 가이드 | dlopen·LoadLibrary·인터페이스·핫 리로드 [실전]
  • C++ 플러그인 시스템 | 동적 로딩·인터페이스·버전 관리 [#55-2]
  • C++ ABI 호환성 완벽 가이드 | PIMPL·C 인터페이스·버전 관리·프로덕션 패턴 [#55-4]
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3