C API 리소스 관리 패턴

C 언어에서 자주 볼 수 있는 리소스 관리 패턴은 다음과 같다.

  1. 초기화 함수: 리소스를 할당하고 초기화한다.
  2. 정리 함수: 할당된 리소스를 해제한다.
  3. 불투명 포인터(Opaque Pointer): 리소스를 나타내는 포인터로, 사용자는 내부 구조를 알 필요가 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <stdlib.h>

// 불투명 포인터를 위한 구조체 선언
typedef struct Resource Resource;

// 실제 구조체 정의 (보통 .c 파일에 숨겨져 있음)
struct Resource {
int some_data;
char* more_data;
};

// 초기화 함수
Resource* resource_init(int initial_value) {
Resource* res = malloc(sizeof(Resource));
if (res == NULL) return NULL;

res->some_data = initial_value;
res->more_data = NULL;
return res;
}

// 정리 함수
void resource_cleanup(Resource* res) {
if (res) {
free(res->more_data);
free(res);
}
}

// 리소스 사용 예시
int main() {
Resource* my_resource = resource_init(42);
if (my_resource == NULL) {
// 에러 처리
return 1;
}

// 리소스 사용...

// 사용 완료 후 정리
resource_cleanup(my_resource);

return 0;
}

이 패턴에서 확연히 보이는 문제점은 리소스 관리의 책임이 사용자에게 있다는 것이다. 사용자가 resource_cleanup() 을 호출하지 않거나 예외가 발생하는 경우 리소스 누수가 발생한다.

이런 패턴을 보이는 C API들은 꽤 많다. 주로 시스템 프로그래밍, 그래픽스 라이브러리, 데이터베이스 인터페이스 등에서 볼 수 있다. libcurl의 curl_easy_init()/curl_easy_cleanup()이나 SDL의 SDL_CreateWindow()/SDL_DestroyWindow() 같이 짝을 이루는 함수들이 대표적이다.

RAII을 위해 std::unique_ptr 도입

앞에서와 같은 경우에 std::unique_ptr을 사용하면 리소스 관리를 더 안전하고 편리하게 할 수 있다. 커스텀 deleter를 사용해 cleanup 함수를 자동으로 호출할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <memory>
#include <utility>

template<typename T, typename Deleter>
auto make_resource(T* ptr, Deleter&& deleter) {
return std::unique_ptr<T, Deleter>(ptr, std::forward<Deleter>(deleter));
}

// Usage example
#include <cstdio>

FILE* open_file(const char* filename, const char* mode) {
return std::fopen(filename, mode);
}

int main() {
auto file = make_resource(open_file("example.txt", "r"), [](FILE* f) {
if (f) std::fclose(f);
});

if (file) {
// Use the file...
}

// File will be automatically closed when 'file' goes out of scope
return 0;
}

예제에서 make_resource 와 같이 구현하게 되면 편리하다.

  1. 포인터와 delete만 전달하게 되어 인터페이스를 간단하게 유지할 수 있다.
  2. 컴파일러가 T와 Deleter 타입을 자동으로 추론하게 된다.
  3. Perfect Forwarding을 통해서 deleter를 효율적으로 전달하게 된다.

위의 예제에서는 FILE* 포인터를 관리할 때 편리하다. 파일은 스코프를 벗어날 때 자동으로 닫히게 된다. 이 패턴을 사용해서 RAII(Resource Acquisition Is Initialization) 원칙도 쉽게 따를 수 있다. 리소스 누수를 방지하고 예외 상황 발생에 대한 안정성도 높일 수 있다.