본문 바로가기
모던 C 언어/C언어 메모리관리

2. 메모리 누수 없는 C언어 프로그래밍 (1/3) - 메모리 할당과 해제는 한 블록에서

by 커널패닉 2021. 4. 28.
반응형

C언어에서 메모리 관리는 실력 여하를 막론하고 꽤나 골치아픈 주제이다. 프로그램 규모가 커지면 동적으로 할당된 메모리 개수가 늘어나게 되는데, 개발자도 사람인 이상 이처럼 할당된 메모리들의 생명주기를 하나도 빠짐없이 관리하는 것은 어렵기 때문이다. 따라서 C개발자들은 메모리 문제를 회피하기 위한 여러가지 테크닉들이 개발하였다.

이번 주제에서는 메모리 누수를 회피할 수 있는 두 가지 규칙과 몇 가지 테크닉들을 소개한다. 이 방법들을 모두 지킨다고 해서 메모리 누수 문제를 100%는 아니더라도 대부분의 메모리 누수 문제들을 피할 수 있다. 소개할 두 가지 규칙은 다음과 같다.

  1. 가능하다면 메모리 할당과 해제는 한 코드 블록 안에서 한 번만
  2. 메모리 할당과 해제가 한 블록 이내에서 이뤄질 수 없다면 레퍼런스 카운터를 활용

우선 이번 포스트에서는 첫 번째 규칙 "가능하다면 메모리 할당과 해제는 한 코드 블록 안에서"가 무엇인지, 어떻게 지킬 수 있는지 알아보자.

 

malloc과 free를 한 블록에서 하기

"메모리 할당과 해제는 한 코드 블록 안에서 한 번만"은 굉장히 단순한 규칙이다. 이하 "메모리 할당과 해제는 한 코드 블록 안에서 한 번만"를 "규칙1"로 부르도록 하겠다. 이 규칙은 단순하지만 지킬수 있다면 기본적인 메모리 누수를 예방할 수 있다.

규칙1을 지키는 가장 간단한 방법은 malloc과 free 함수를 한 블록 안에서 사용하는 일이다. 다음은 이 규칙을 적용한 코드이다.

#include <stdlib.h>

struct Example {
    int a;
    int b;
    char c;
};

void init_example() {
    struct Example *example = malloc(sizeof(*example)); // 메모리 할당
    if (example == NULL) return;
    example->a = 1;
    example->b = 2;
    example->c = 'c';
    free(example); // 해제
}

int main() {
    init_example();

    return 0;
}

메모리가 할당된 블록에서 해제가 수행되고 있다. 명확하게 위 코드에는 메모리 누수가 발생하지 않는다.

 

초기화와 정리를 한 블록에서 하기

물론 규칙1은 직접적인 malloc과 free 사이에서만 성립되는 것은 아니다. 대부분의 라이브러리에서는 객체를 초기화할 때 사용자가 직접 malloc 함수를 사용하도록 하기보다는 init 함수를 제공한다. 또한 free를 직접 호출하는 대신 객체를 정리하는 함수를 제공하곤 한다. 앞서 본 코드를 조금 변경해보자.

#include <stdlib.h>

struct Example {
    int a;
    int b;
    char c;
};

struct Example* init_example() {
    struct Example *example = malloc(sizeof(*example));
    if (example == NULL) return NULL;
    example->a = 1;
    example->b = 2;
    example->c = 'c';
    return example;
}

void free_example(struct Example **example) {
    if (*example == NULL) return;
    free(*example);
    *example = NULL;
}

int main() {
    struct Example *example = init_example(); // 메모리 할당
    free_example(&example); // 메모리 해제

    return 0;
}

main함수에서 메모리 할당과 해제가 발생했다. 이 코드 역시 명확하게 메모리 누수는 발생하지 않는다.

 

goto문을 활용하여 한 군데에서 메모리 정리하기

다익스트라는 "goto문의 해로움(Go To Statement Considered Harmful)"에서 goto문이 구조적 프로그래밍을 해치는 요소로 꼽았다. 난삽하게 사용되는 goto문이 코드를 끔찍하게 만든하는데는 동의한다. 하지만 적절하게 사용되기만 한다면 오히려 코드를 더욱 깔끔하게 유지할 수 있다.

그렇다면 goto문을 적절하게 사용하는 방법은 무엇일까? 많은 오픈소스 프로그램들은 goto문을 에러 처리용으로 사용한다. C에는 try-catch를 사용할 수 없지만 goto를 활용하면 try-catch와 유사한 효과를 볼 수 있다. 따라서 goto를 응용하면 메모리의 할당과 해제를 한 블록에서 할 수 있다.

실제 예를 보자. 아래는 오픈소스인 weston(wayland의 레퍼런스 구현체)에서 goto문을 사용해서 에러처리를 하는 실제 예시이다. 아래 코드에서 display 객체를 유심히 살펴보자.

WL_EXPORT int
wet_main(int argc, char *argv[], const struct weston_testsuite_data *test_data)
{
	// ... 생략 ...

	display = wl_display_create();       // display 객체가 할당됨
	if (display == NULL) {
		weston_log("fatal: failed to create display\n");
		goto out_display;
	}

	// ... 생략 ...

	if (!signals[0] || !signals[1] || !signals[2] || !signals[3])
		goto out_signals;
        
    // ... 생략 ...
    
out:
	wet_compositor_destroy_layout(&wet);

	/* free(NULL) is valid, and it won't be NULL if it's used */
	free(wet.parsed_options);

	if (protologger)
		wl_protocol_logger_destroy(protologger);

	weston_compositor_destroy(wet.compositor);
	weston_log_scope_destroy(protocol_scope);
	protocol_scope = NULL;

out_signals:
	for (i = ARRAY_LENGTH(signals) - 1; i >= 0; i--)
		if (signals[i])
			wl_event_source_remove(signals[i]);

	wl_display_destroy(display);            // display 객체가 해제됨

out_display:
	weston_log_scope_destroy(log_scope);
	log_scope = NULL;
	weston_log_subscriber_destroy(logger);
	weston_log_subscriber_destroy(flight_rec);
	weston_log_ctx_destroy(log_ctx);
	weston_log_file_close();

	if (config)
		weston_config_destroy(config);
	free(config_file);
	free(backend);
	free(shell);
	free(socket_name);
	free(option_modules);
	free(log);
	free(modules);

	return ret;
}

에러 유형에 따라서 out, out_signals, out_display 주소로 점프를 하여 에러 처리를한다. 에러가 발생하면 바로 return을 하는 대신 goto를 사용해서 코드의 특정위치로 이동한다. 아까 언급한대로 display 객체를 보자. display 객체가 할당된 이후에는 out 또는 out_signal로 goto를 해서 항상 display 객체를 해제하고 코드 블록을 종료하고 있다.

 

이번 포스트에서는 아주 간단하고 기본적인 메모리 누수를 예방할 수 있는 규칙에 대해서 알아보았다. 이전에 규칙 1을 몰라서 지키지 않았다면 이를 지키는 것 만으로도 코드의 품질을 크게 개선할 수 있다. 다음 포스트에서는 규칙1이 적용되지 않는 상황이 무엇이 있는지 그리고 이러한 상황에서 어떻게 메모리 누수를 방지할 수 있는지에 대해 살펴본다.

반응형