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++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.
목차
- 문제 시나리오
- 기본 개념: 정적·동적 링크 vs 동적 로딩
- 완전한 dlopen 예제 (Linux/macOS)
- 완전한 LoadLibrary 예제 (Windows)
- 크로스 플랫폼 통합 예제
- 자주 발생하는 에러와 해결법
- 모범 사례
- 프로덕션 패턴
- 정리
1. 문제 시나리오
시나리오 1: 플러그인을 빌드 없이 추가하고 싶다
상황: 이미지 에디터에 사용자가 만든 필터를 추가하려 합니다. 앱을 다시 컴파일·배포하지 않고, .so/.dll 파일만 plugins/ 폴더에 넣으면 인식되게 하고 싶습니다.
해결: dlopen/LoadLibrary로 plugins/ 디렉터리를 스캔해 .so/.dll을 로드하고, dlsym/GetProcAddress로 plugin_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/.dll | dlopen/LoadLibrary 호출 시 |
동적 로딩은 프로그램이 이미 실행 중일 때, 코드에서 직접 라이브러리 파일을 열고 함수 포인터를 조회합니다. 링크 시점에 의존성이 없습니다.
플랫폼별 API
| 플랫폼 | 로드 | 심볼 조회 | 해제 | 확장자 |
|---|---|---|---|---|
| Linux | dlopen | dlsym | dlclose | .so |
| macOS | dlopen | dlsym | dlclose | .dylib, .so |
| Windows | LoadLibrary | GetProcAddress | FreeLibrary | .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)를 붙입니다. 그렇지 않으면 GetProcAddress가 nullptr을 반환할 수 있습니다.
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/macOS | Windows |
|---|---|---|
| 로드 | 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) | 불필요 |
핵심 원칙:
- C ABI로 경계를 고정해 name mangling과 ABI 불일치를 피한다.
- RAII로 handle 생명주기를 관리하고,
dlclose/FreeLibrary를 반드시 호출한다. - RTLD_NOW | RTLD_LOCAL을 사용해 로드 시점 검증과 심볼 충돌 방지를 한다.
- 플랫폼 추상화로 한 인터페이스로 Linux/macOS/Windows를 다룬다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 플러그인 시스템, 조건부 기능 로딩, 런타임 확장, 서드파티 모듈 통합 등에 활용합니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
참고 자료
- dlopen(3) - Linux man page
- LoadLibrary - Microsoft Docs
- GetProcAddress - Microsoft Docs
- C++ 시리즈 #55-2: 플러그인 시스템
- C++ 시리즈 #38-3: PIMPL과 ABI
구현 체크리스트
동적 로딩 도입 시 확인할 항목:
-
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]