동적 적재

동적 적재(dynamic loading) 또는 동적 링크(dynamic linking)는 컴퓨터 프로그램이 실행 시간에 라이브러리(또는 기타 바이너리)를 메모리에 로드하고, 라이브러리에 포함된 함수와 변수의 주소를 검색하여 해당 함수를 실행하거나 변수에 액세스하고, 메모리에서 라이브러리를 해제할 수 있는 메커니즘이다. 이는 컴퓨터 프로그램이 프로그램 내에서 다른 소프트웨어를 사용할 수 있게 하는 세 가지 메커니즘 중 하나로, 나머지는 정적 링크와 동적 링크이다. 정적 링크 및 동적 링크와 달리 동적 적재를 사용하면 이러한 라이브러리가 없는 상태에서도 컴퓨터 프로그램을 시작하고, 사용 가능한 라이브러리를 탐색하며, 잠재적으로 추가 기능을 얻을 수 있다.

 역사 
동적 적재는 OS/360과 같은 IBM의 System/360용 운영체제에서 흔히 사용되는 기술이었으며, 특히 I/O 서브루틴, 코볼 및 PL/I 런타임 라이브러리에서 많이 사용되었다. 현재도 z/OS와 같은 IBM의 Z/아키텍처용 운영체제에서 계속 사용되고 있다. 애플리케이션 프로그래머 입장에서 적재 과정은 대부분 운영체제(또는 I/O 하위 시스템)에 의해 처리되므로 대체로 투명하게 이루어진다. 주요 장점은 다음과 같다.

 하위 시스템에 대한 수정(패치) 시 프로그램을 다시 링크할 필요 없이 모든 프로그램이 한 번에 수정된다.
 라이브러리를 무단 수정으로부터 보호할 수 있다.

IBM의 전략적 트랜잭션 처리 시스템인 CICS(1970년대 이후)는 커널과 일반 응용 프로그램 적재 모두에 동적 적재를 광범위하게 사용한다. 응용 프로그램의 수정은 오프라인에서 수행될 수 있으며, 변경된 프로그램의 새 복사본은 CICS를 재시작할 필요 없이 동적으로 적재될 수 있다(CICS는 흔히 24/7로 가동된다).

공유 라이브러리는 1980년대에 유닉스에 추가되었으나, 초기에는 프로그램 시작 후에 추가 라이브러리를 로드하는 기능이 없었다.

 용도 
동적 적재는 소프트웨어 플러그인을 구현하는 데 가장 자주 사용된다. 예를 들어, 아파치 웹 서버의 *.dso "동적 공유 객체" 플러그인 파일은 실행 시간에 동적 적재를 통해 로드되는 라이브러리이다. 동적 적재는 여러 다른 라이브러리가 필요한 기능을 제공할 수 있고 사용자가 어떤 라이브러리를 제공할지 선택할 수 있는 옵션이 있는 컴퓨터 프로그램을 구현할 때도 사용된다.

 C/C++에서 
모든 시스템이 동적 적재를 지원하는 것은 아니다. MacOS, 리눅스, 솔라리스와 같은 유닉스 계열 운영체제는 C 언어 "dl" 라이브러리를 통해 동적 적재를 제공한다. 윈도우 운영체제는 윈도우 API를 통해 동적 적재를 제공한다.

 요약 
 이름 표준 POSIX/유닉스 API 마이크로소프트 윈도우 API 헤더 파일 포함 #include <dlfcn.h> #include <windows.h> 헤더 정의 dl
(운영체제에 따라 libdl.so, libdl.dylib 등) kernel32.dll 라이브러리 로드 dlopen LoadLibraryLoadLibraryEx 내용 추출 dlsym GetProcAddress 라이브러리 해제 dlclose FreeLibrary

 라이브러리 로드 
라이브러리 로드는 윈도우에서는 LoadLibrary 또는 LoadLibraryEx를 사용하고, 유닉스 계열 운영체제에서는 dlopen을 사용하여 수행된다. 예시는 다음과 같다.

 대부분의 유닉스 계열 운영체제 (솔라리스, 리눅스, *BSD 등) 
void* sdl_library = dlopen("libSDL.so", RTLD_LAZY);
if (!sdl_library) {
   // 오류 보고 ...
} else {
   // dlsym 호출 결과 사용
}

 macOS 
유닉스 라이브러리로서:

void* sdl_library = dlopen("libSDL.dylib", RTLD_LAZY);
if (!sdl_library) {
   // 오류 보고 ...
} else {
   // dlsym 호출 결과 사용
}

macOS 프레임워크로서:

void* sdl_library = dlopen("/Library/Frameworks/SDL.framework/SDL", RTLD_LAZY);
if (!sdl_library) {
   // 오류 보고 ...
} else {
   // dlsym 호출 결과 사용
}

또는 프레임워크나 번들에 Objective-C 코드가 포함된 경우:

NSBundle *bundle = [NSBundle bundleWithPath:@"/Library/Plugins/Plugin.bundle"];
NSError *err = nil;
if ([bundle loadAndReturnError:&err])
{
    // 번들의 클래스와 함수 사용
}
else
{
    // 오류 처리
}

 윈도우 
HMODULE sdl_library = LoadLibrary(TEXT("SDL.dll"));
if (!sdl_library) {
   // 오류 보고 ...
} else {
   // GetProcAddress 호출 결과 사용
}

 라이브러리 내용 추출 
동적으로 로드된 라이브러리의 내용을 추출하는 것은 윈도우에서는 GetProcAddress를 사용하고, 유닉스 계열 운영체제에서는 dlsym을 사용하여 수행된다.

 유닉스 계열 운영체제 (솔라리스, 리눅스, *BSD, macOS 등) 
void* initializer = dlsym(sdl_library, "SDL_Init");
if (!initializer) {
   // 오류 보고 ...
} else {
   // initializer를 적절한 유형으로 캐스팅하여 사용
}
macOS에서 Objective-C 번들을 사용할 때는 다음과 같이 할 수도 있다.
Class rootClass = [bundle principalClass]; // 또는 NSClassFromString()을 사용하여 이름으로 클래스를 얻을 수 있음
if (rootClass)
{
    id object = [[rootClass alloc] init]; // 객체 사용
}
else
{
    // 오류 보고
}

 윈도우 
FARPROC initializer = GetProcAddress(sdl_library, "SDL_Init");
if (!initializer) {
   // 오류 보고 ...
} else {
   // initializer를 적절한 유형으로 캐스팅하여 사용
}

 라이브러리 함수 포인터 변환 
dlsym() 또는 GetProcAddress()의 결과는 사용하기 전에 적절한 유형의 포인터로 변환되어야 한다.

 윈도우 
윈도우에서 변환은 간단하다. FARPROC은 본질적으로 이미 함수 포인터이기 때문이다.

typedef INT_PTR (*FARPROC)(void);

함수가 아닌 객체의 주소를 검색해야 할 때는 문제가 될 수 있다. 그러나 보통은 함수를 추출하려고 하므로 일반적으로는 문제가 되지 않는다.

typedef void (*SDLInitFunctionType)(void);
SDLInitFunctionType init_func = (SDLInitFunctionType)initializer;

 유닉스 (POSIX) 
POSIX 사양에 따르면 dlsym()의 결과는 void 포인터이다. 그러나 함수 포인터는 데이터 객체 포인터와 크기가 같을 필요조차 없으므로, void* 유형과 함수 포인터 간의 유효한 변환은 모든 플랫폼에서 구현하기 쉬운 것이 아닐 수 있다.

오늘날 사용되는 대부분의 시스템에서 함수 포인터와 객체 포인터는 사실상 상호 변환 가능하다. 다음 코드 조각은 많은 시스템에서 어쨌든 변환을 수행할 수 있게 하는 한 가지 해결 방법을 보여준다.
typedef void (*SDLInitFunctionType)(void);
SDLInitFunctionType init_func = (SDLInitFunctionType)initializer;

위의 조각은 일부 컴파일러에서 warning: dereferencing type-punned pointer will break strict-aliasing rules와 같은 경고를 발생시킨다. 또 다른 해결 방법은 다음과 같다.
typedef void (*SDLInitFunctionType)(void);

union {
    SDLInitFunctionType func;
    void* obj;
} alias;

alias.obj = initializer;
SDLInitFunctionType init_func = alias.func;

이 방법은 엄격한 앨리어싱(strict aliasing)이 적용되는 경우에도 경고를 비활성화한다. 이는 가장 최근에 기록된 것과 다른 공용체 멤버로부터 읽는 행위(유형 펀닝)가 일반적이며, 메모리가 공용체 유형을 통해 직접 액세스되는 경우 엄격한 앨리어싱이 강제되더라도 명시적으로 허용된다는 사실을 이용한다. 그러나 함수 포인터가 공용체 외부에서 사용되기 위해 복사되므로 이 경우가 엄격히 해당되는 것은 아니다. 데이터 포인터의 크기와 함수 포인터의 크기가 같지 않은 플랫폼에서는 이 트릭이 작동하지 않을 수 있음에 유의하라.

 POSIX 시스템에서 함수 포인터 문제 해결 
함수 포인터와 데이터 객체 포인터 간의 모든 변환은 (본질적으로 이식 불가능한) 구현 확장으로 간주되어야 하며, 이 점에 관해 POSIX와 ISO 표준이 서로 충돌하므로 직접적인 변환을 위한 "올바른" 방법은 존재하지 않는다는 사실은 여전하다.

이 문제 때문에 구버전인 이슈 6의 dlsym()에 관한 POSIX 문서는 "향후 버전에서는 함수 포인터를 반환하는 새 함수를 추가하거나, 데이터 포인터를 반환하는 함수와 함수 포인터를 반환하는 함수의 두 가지 새로운 함수를 위해 현재 인터페이스가 더 이상 사용되지 않을 수 있다"고 명시했다.

이후 버전의 표준(이슈 7, 2008)에서는 이 문제가 논의되었으며, POSIX 준수를 위해 함수 포인터가 void*로 변환 가능해야 한다는 결론을 내렸다. 이를 위해 컴파일러 제작자는 이 경우에 대해 작동하는 캐스트를 구현해야 한다.

라이브러리의 내용을 변경할 수 있는 경우(즉, 맞춤형 라이브러리의 경우), 함수 자체 외에도 그 함수에 대한 포인터를 내보낼 수 있다. 함수 포인터에 대한 포인터는 그 자체로 객체 포인터이므로, 이 포인터는 항상 dlsym() 호출과 후속 변환을 통해 합법적으로 검색될 수 있다. 그러나 이 방법은 외부에서 사용될 모든 함수에 대해 별도의 포인터를 유지 관리해야 하며, 이점은 대개 작다.

 라이브러리 해제 
라이브러리를 로드하면 메모리가 할당되므로, 메모리 누수를 피하기 위해 라이브러리를 할당 해제해야 한다. 또한 라이브러리를 해제하지 않으면 라이브러리가 포함된 파일에 대한 파일 시스템 작업을 방해할 수 있다. 라이브러리 해제는 윈도우에서는 FreeLibrary를 사용하고, 유닉스 계열 운영체제에서는 dlclose를 사용하여 수행된다. 그러나 주 애플리케이션의 객체가 DLL 내에 할당된 메모리를 참조하는 경우 DLL을 해제하면 프로그램 충돌이 발생할 수 있다. 예를 들어, DLL이 새로운 클래스를 도입하고 DLL이 닫힌 경우, 주 애플리케이션에서 해당 클래스의 인스턴스에 대한 추가 작업을 수행하면 메모리 액세스 위반이 발생할 가능성이 높다. 마찬가지로 DLL이 동적으로 로드된 클래스를 인스턴스화하기 위한 팩토리 함수를 도입하는 경우, DLL이 닫힌 후 해당 함수를 호출하거나 역참조하면 정의되지 않은 동작이 발생한다.

 유닉스 계열 운영체제 (솔라리스, 리눅스, *BSD, macOS 등) 
dlclose(sdl_library);

 윈도우 
FreeLibrary(sdl_library);

 특수 라이브러리 
유닉스 계열 운영체제와 윈도우에서의 동적 적재 구현을 통해 프로그래머는 현재 실행 중인 프로세스에서 심볼을 추출할 수 있다.

유닉스 계열 운영체제는 프로그래머가 주 실행 파일과 이후에 로드된 동적 라이브러리를 모두 포함하는 전역 심볼 테이블에 액세스할 수 있도록 허용한다.

윈도우는 프로그래머가 주 실행 파일에서 내보낸 심볼에 액세스할 수 있도록 허용한다. 윈도우는 전역 심볼 테이블을 사용하지 않으며 이름으로 심볼을 찾기 위해 여러 모듈을 검색하는 API도 없다.

 유닉스 계열 운영체제 (솔라리스, 리눅스, *BSD, macOS 등) 
void* this_process = dlopen(NULL, 0);

 윈도우 
HMODULE this_process = GetModuleHandle(NULL);

HMODULE this_process_again;
GetModuleHandleEx(0, 0, &this_process_again);

 자바에서 

자바 프로그래밍 언어에서 클래스는  객체를 사용하여 동적으로 로드될 수 있다. 예시는 다음과 같다.

Class type = ClassLoader.getSystemClassLoader().loadClass(name);
Object obj = type.newInstance();

리플렉션(Reflection) 메커니즘도 클래스가 아직 로드되지 않은 경우 클래스를 로드하는 수단을 제공한다. 이는 현재 클래스의 클래스로더를 사용한다.

Class type = Class.forName(name);
Object obj = type.newInstance();

그러나 제어된 방식으로 클래스를 해제하는 간단한 방법은 없다. 로드된 클래스는 클래스를 로드하는 데 사용된 클래스로더가 시스템 클래스로더가 아니고 클래스로더 자체가 해제될 때만 프로그래머가 원하는 시점에 제어된 방식으로 해제될 수 있다. 이 과정에서 클래스가 실제로 해제되었는지 확인하기 위해 다양한 세부 사항을 준수해야 한다. 이 때문에 클래스 해제는 번거로운 작업이다.

가비지 컬렉터에 의한 제어되지 않는 방식의 암시적 클래스 해제는 자바에서 몇 번 변경되었다. 자바 1.2까지 가비지 컬렉터는 클래스를 로드하는 데 어떤 클래스로더가 사용되었는지에 관계없이 공간이 필요하다고 판단되면 언제든지 클래스를 해제할 수 있었다. 자바 1.2부터 시스템 클래스로더를 통해 로드된 클래스는 절대 해제되지 않았으며, 다른 클래스로더를 통해 로드된 클래스는 해당 클래스로더가 해제될 때만 해제되었다. 자바 6부터 클래스는 가비지 컬렉터가 원할 경우 로드에 사용된 클래스로더와 관계없이 해제될 수 있음을 나타내는 내부 마커를 포함할 수 있게 되었다. 가비지 컬렉터는 이 힌트를 무시할 수 있다.

마찬가지로 네이티브 메서드를 구현하는 라이브러리는 System.loadLibrary 메서드를 사용하여 동적으로 로드된다. System.unloadLibrary 메서드는 존재하지 않는다.

 동적 적재가 없는 플랫폼 
1980년대 유닉스와 윈도우를 통해 널리 보급되었음에도 불구하고, 일부 시스템은 여전히 동적 적재를 추가하지 않거나 심지어 제거하기도 한다. 예를 들어, 벨 연구소의 플랜 9와 그 후속작인 9front는 동적 링크가 "해롭다"고 간주하여 의도적으로 피한다. 플랜 9의 일부 개발자들이 만든 Go 프로그래밍 언어 역시 동적 링크를 지원하지 않았으나, Go 1.8(2017년 2월)부터 플러그인 로딩이 가능해졌다. Go 런타임과 모든 라이브러리 함수는 컴파일된 바이너리에 정적으로 링크된다.

 같이 보기 
 DLL 지옥
 동적 디스패치
 동적 링커
 동적 링크 라이브러리
 GNU 링커
 지연된 로딩
 라이브러리 (컴퓨팅)
 링커 (컴퓨팅)
 로더 (컴퓨팅)
 정적 라이브러리
 종료 후 상주 프로그램

 각주 

분류:라이브러리
분류:운영체제 기술