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

2. 메모리 누수 없는 C언어 프로그래밍 (3/3) - 레퍼런스 카운트 2부

by 커널패닉 2021. 5. 13.
반응형

이전 포스트들에서 다룬 규칙을 다시 한 번 더 확인해보자.

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

이전 포스트에 이어지는 내용으로 이전 포스트를 읽지 않았다면 먼저 읽기를 강력히 권한다. ("2. 메모리 누수 없는 C언어 프로그래밍 (2/3) - 레퍼런스 카운트 1부" (www.kernelpanic.kr/35)) 이전 포스트에서는 규칙 2를 구현하는 간단한 방법에 대해서 살펴보았다. 이번 포스트에서는 앞에서 살펴본 구현의 취약점을 살펴보고 코드를 좀 더 정교하게 구현하는 방법에 대해서 살펴본다.

 

레퍼런스 카운트가 제대로 해제되지 않는 경우

앞서 우리는 레퍼런스 카운트(이하 rc)를 가지는 struct Object 객체를 정의했다. 새로 만드는 객체는 struct Object를 상속받기 때문에 rc와 관련 함수(ref / unref)를 함께 사용할 수 있다. 아래는 앞에서 살펴본 코드에서 struct Object 객체와 이를 상속받는 struct Employee 객체 코드이다.

// ... < 생략 > ...

// ref / unref 할 때 형 변환을 하지 않으면 컴파일러 경고가 발생한다.
// 동작과는 무관하다.
#define OBJECT(x) ((struct Object*)x)

// 기본적인 객체의 기능을 제공하는 구조체이다. 
// 추후 몇 가지 멤버가 추가될 예정이다.
struct Object {
    unsigned int ref_count;
};

// 레퍼런스 카운트를 1 증가
void ref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count += 1;
}

// 레퍼런스 카운트를 1 감소
// 레퍼런스 카운트가 0이 되면 객체 정리
void unref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count -= 1;
    if (obj->ref_count == 0) 
        free(obj);
}

enum FunctioanlTitle {
    ASSOCIATE,
    MANAGER,
};

struct Team {
    struct Employee *employee[2];
};

struct Employee {
    struct Object base;  // 반드시 첫 번째 멤버로 obj 객체를 할당한다.
    enum FunctioanlTitle functional_title;
    char name[16];
};

// ... < 생략 > ...

struct Employee 객체가 사용되는 방식은 단순하다. struct Team이 struct Employee를 참조하고 있으며, struct Team이 해제되면서 struct Employee의 참조를 해제한다. 이 과정에서 rc의 흐름은 다음과 같다. struct Employee 객체가 생성이 될 때 rc가 1 증가하고, struct Team 객체에 참조될 때 rc가 1이 더 증가한다. 그리고 객체가 생성된 블록(프로시져)에서 빠져나올 때 rc는 1 감소하고, struct Team 객체가 해제되기 전에 rc가 1 감소한다. struct Team 객체가 해제되면서 struct Employee 객체의 rc가 0이 되기 때문에 메모리 누수는 발생하지 않는다.

조금 더 복잡한 경우에는 어떨까? struct Team 객체를 참조하는 struct Company 객체가 있다고 상상해보자. struct Company 객체는 struct Team 객체를 참조하고, struct Team 객체는 struct Employee 객체를 참조한다. 이제 struct Team 객체에도 rc를 적용해야 할 때가 되었다. struct Team에 rc를 적용해보자. 이미 정의된 struct Object 객체를 상속받으면 되기 때문에 수정은 어렵지 않다. 수정된 코드는 아래와 같다. (전체 코드는 맨 하단 참조)

// ... < 생략 > ...

// Company 객체 정의
struct Company {
    struct Team *team;
};

struct Team {
    struct Object base;  // Team 객체에 레퍼런스 카운트 추가
    struct Employee *employee[2];
};

struct Employee {
    struct Object base;
    enum FunctioanlTitle functional_title;
    char name[16];
};

// .. < 생략 > ...

// Company 객체의 생성 / 해제 코드 추가
struct Company* create_company() {
    struct Company *company = NULL;

    company = malloc(sizeof(*company));
    if (company == NULL) goto err;

    company->team = create_team();
    if (company->team == NULL) goto err;
    ref(OBJECT(company->team));

    return company;

err:
    if (company)
        free(company->team);
    free(company);

    return NULL;
}

void free_company(struct Company **company) {
    if (*company == NULL) return;

    // Company 객체가 정리되기 때문에 team객체의 레퍼런스 카운트를 감소시킨다.
    unref(OBJECT((*company)->team));
    free(*company);
    *company = NULL;
}

int main() {
    struct Company *company = create_company();
    if (company == NULL) exit(1);

    printf("Employee A:\n\tname=%s\n\tfunctional_title=%s\n", 
            company->team->employee[0]->name, 
            company->team->employee[0]->functional_title == ASSOCIATE ? "ASSOCIATE" : 
            company->team->employee[0]->functional_title == MANAGER ? "MANAGER" : "Error");

    printf("Employee A:\n\tname=%s\n\tfunctional_title=%s\n", 
            company->team->employee[1]->name, 
            company->team->employee[1]->functional_title == ASSOCIATE ? "ASSOCIATE" : 
            company->team->employee[1]->functional_title == MANAGER ? "MANAGER" : "Error");

    free_company(&company); 

    return 0;
}

컴파일해 보면 잘 동작한다.

$ ./a.out
  Employee A:
      name=employee A
      functional_title=ASSOCIATE
  Employee A:
      name=employee B
      functional_title=MANAGER

하지만 valgrind로 테스트 해보면 메모리 누수가 있음을 발견할 수 있다.

$ valgrind ./a.out
// ... 생략 ...

==4779== HEAP SUMMARY:
==4779==     in use at exit: 48 bytes in 2 blocks
==4779==   total heap usage: 5 allocs, 3 frees, 1,104 bytes allocated
==4779== 
==4779== LEAK SUMMARY:
==4779==    definitely lost: 48 bytes in 2 blocks
==4779==    indirectly lost: 0 bytes in 0 blocks
==4779==      possibly lost: 0 bytes in 0 blocks
==4779==    still reachable: 0 bytes in 0 blocks
==4779==         suppressed: 0 bytes in 0 blocks
==4779== Rerun with --leak-check=full to see details of leaked memory

// ... 하략 ...

해당 코드에서 누수가 발생하는 과정은 아래 그림과 같다. Company 객체가 Team 객체를 참조하고 있고, Team 객체가 Employ 객체를 참조하고 있다. 이전 코드(https://www.kernelpanic.kr/35)에서는 Team 객체가 해제될 때, 명시적으로 unref()를 호출하여 Employ 객체의 rc를 관리했기 때문에 메모리 누수가 발생하지 않았다. 지금 코드에서는 Team 객체의 rc가 0이 될 때 해제되기 때문에 단순히 free 함수만 호출이 된다. 따라서 Employ 객체의 rc가 관리되지 않아 메모리 누수가 발생한다.

 

레퍼런스 카운트를 제대로 해제하는 방법

앞서 살펴보았듯이 rc를 제대로 관리하는 것은 메모리 누수가 없는 프로그래밍을 위해 꼭 필요하다. 그렇다면 어떻게 하는 것이 rc를 제대로 관리하는 것일까? 그 해답은 free() 함수에 있다. 지금까지 살펴본 코드에서는 rc가 0이 되면 free() 함수를 호출해서 객체를 해제한다. 그러나 객체가 다른 객체를 참조하는 경우에는 free() 함수만으로는 참조하는 객체의 rc 관리가 되지 않는다. 따라서 객체별로 자신만의 free() 함수를 정의해야 한다.

자신만의 free() 함수를 만들 때, 범용적이고 편리한 코드를 생산하기 위해서는 아래 조건들을 지켜야 한다.

  1. "rc가 0이 되면 자동으로 객체를 해제한다."는 기본 동작은 유지해야 한다.
  2. 범용적인 해제 인터페이스를 가져야 한다.

두 조건을 지킬 수 있는 방법은 앞서 정의한 Object 객체에 개별적인 코드의 해제를 할 수 있는 함수포인터를 추가하면 된다. 기존 Object 객체와 변경된 Object 객체는 아래와 같다. 기본 레퍼런스 카운팅 방식을 유지하면서 해제가 이뤄질 때는 Object 객체의 free 함수로 개별 객체에 대한 해제를 할 수 있다. 

#define OBJECT(x) ((struct Object*)x)

/* 기존 Object 코드
struct Object {
    unsigned int ref_count;
};

void ref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count += 1;
}

void unref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count -= 1;
    if (obj->ref_count == 0) 
        free(obj);
}
*/

struct Object {
    unsigned int ref_count;
    void (*free)(void* obj);   // 개별 해제 함수 저장을 위한 함수 포인터
};

// Object를 초기화 하는 함수를 만드는 것이 편리하다.
void init_object(struct Object *obj, void (*free)(void*)) {
    obj->ref_count = 0;
    obj->free = free;
}

void ref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count += 1;
}

void unref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count -= 1;
    if (obj->ref_count == 0) {
        if (obj->free) obj->free(obj); // 개별 해제 함수가 있으면 사용
        else free(obj);                // 없으면 free() 함수 사용
    }
}

아래 변경사항을 반영해서 Team 객체를 생성하는 부분에 대해서만 수정을 적용해 보자.

void free_team(void *obj) {
    struct Team * team = (struct Team*)obj;
    if (team == NULL) return;
    unref(OBJECT(team->employee[0]));
    unref(OBJECT(team->employee[1]));
    free(team);
}

struct Team* create_team() {
    struct Team *team = NULL;
    struct Employee *employee_a = NULL;
    struct Employee *employee_b = NULL;

    team = malloc(sizeof(*team));
    if (team == NULL) goto err;
    init_object(&team->base, free_team);

	// ... 하략 ...
}

Team객체를 해제하는 free_team 함수가 Object 객체에 등록되었다. free_team 함수는 Team 객체의 rc가 0이 되면 호출이 된다. 실제로 본 변경사항을 적용하고 valgrind로 테스트해 보면 메모리 누수가 없다는 결과가 나온다.

 

여기까지 총 3회의 포스트에 걸쳐서 메모리 누수를 막는 레퍼런스 카운트를 활용한 C 프로그래밍에 대해서 살펴보았다. 본 방법을 올바르게 사용하면 C 프로그램에서 발생하는 대부분의 메모리 누수 버그를 회피할 수 있다. 다음포스트에서는 레퍼런스 카운트를 활용한 방법의 한계점과 레퍼런스 카운트 방법을 더 잘 사용하기 위한 몇 가지 팁들을 살펴보도록 하겠다.

 

 

 

 

"레퍼런스 카운트가 제대로 해제되지 않는 경우"의 전체 코드

#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define OBJECT(x) ((struct Object*)x)

struct Object {
    unsigned int ref_count;
};

void ref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count += 1;
}

void unref(struct Object *obj) {
    if (obj == NULL) return;
    obj->ref_count -= 1;
    if (obj->ref_count == 0) 
        free(obj);
}

enum FunctioanlTitle {
    ASSOCIATE,
    MANAGER,
};

// Company 객체 정의
struct Company {
    struct Team *team;
};

struct Team {
    struct Object base;  // Team 객체에 레퍼런스 카운트 추가
    struct Employee *employee[2];
};

struct Employee {
    struct Object base;
    enum FunctioanlTitle functional_title;
    char name[16];
};

struct Team* create_team() {
    struct Team *team = NULL;
    struct Employee *employee_a = NULL;
    struct Employee *employee_b = NULL;

    team = malloc(sizeof(*team));
    if (team == NULL) goto err;

    employee_a = malloc(sizeof(*employee_a));
    ref(OBJECT(employee_a));
    if (employee_a == NULL) goto err; 
    employee_a->functional_title = ASSOCIATE;
    strncpy(employee_a->name, "employee A", sizeof(employee_a->name));
    team->employee[0] = employee_a;
    ref(OBJECT(employee_a));

    employee_b = malloc(sizeof(*employee_b));
    ref(OBJECT(employee_b));
    if (employee_b == NULL) goto err;
    employee_b->functional_title = MANAGER;
    strncpy(employee_b->name, "employee B", sizeof(employee_b->name));
    team->employee[1] = employee_b;
    ref(OBJECT(employee_b));

    unref(OBJECT(employee_a));
    unref(OBJECT(employee_b));

    return team;

err:
    free(employee_a);
    free(employee_b);
    free(team);

    return NULL;
}

// Company 객체의 생성 / 해제 코드 추가
struct Company* create_company() {
    struct Company *company = NULL;

    company = malloc(sizeof(*company));
    if (company == NULL) goto err;

    company->team = create_team();
    if (company->team == NULL) goto err;
    ref(OBJECT(company->team)); // 레퍼런스 카운터 할당

    return company;

err:
    if (company)
        free(company->team);
    free(company);

    return NULL;
}

void free_company(struct Company **company) {
    if (*company == NULL) return;

    // Company 객체가 정리되기 때문에 team객체의 레퍼런스 카운트를 감소시킨다.
    unref(OBJECT((*company)->team));
    free(*company);
    *company = NULL;
}

int main() {
    struct Company *company = create_company();
    if (company == NULL) exit(1);

    printf("Employee A:\n\tname=%s\n\tfunctional_title=%s\n", 
            company->team->employee[0]->name, 
            company->team->employee[0]->functional_title == ASSOCIATE ? "ASSOCIATE" : 
            company->team->employee[0]->functional_title == MANAGER ? "MANAGER" : "Error");

    printf("Employee A:\n\tname=%s\n\tfunctional_title=%s\n", 
            company->team->employee[1]->name, 
            company->team->employee[1]->functional_title == ASSOCIATE ? "ASSOCIATE" : 
            company->team->employee[1]->functional_title == MANAGER ? "MANAGER" : "Error");

    free_company(&company); 

    return 0;
}
반응형