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

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

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

앞선 포스트에서 C언어에서 메모리 누수를 피할 수 있는 두 가지 규칙을 소개하고, 규칙1 "가능하다면 메모리 할당과 해제는 한 코드 블록 안에서 한 번만"에 대해서 다뤘다. (www.kernelpanic.kr/34)

규칙1은 간단하고 강력하지만 규칙1을 적용할 수 없는 예외적인 상황들이 있다. 이 경우에 사용 가능한 방법이 규칙 2 "메모리 할당과 해제가 한 블록 이내에서 이뤄질 수 없다면 레퍼런스 카운터를 활용"이다. 이번 포스트에서는 어떤 경우에 규칙1을 적용할 수 없는지, 그리고 레퍼런스 카운터를 활용하는 방법에 대해서 다루도록 한다.

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

 

규칙1을 사용할 수 없는 상황

동적으로 할당한 객체/데이터를 같은 블록에서 해제할 수 있다면 좋겠지만 그렇게 할 수 없는 경우가 있다. 동적 할당된 객체/데이터를 다른 객체에 전달하는 경우이다. 예를 들어서 회사원이라는 객체를 SW팀이라는 객체에 저장한다고 생각해보자. SW팀 객체와 회사원 객체가 한 블록에서 모두 할당과 해제가 이뤄진다면 큰 문제는 없겠지만 일반적으로는 SW팀과 회사원 객체는 다른 블록에서 할당이 이뤄질 가능성이 크다. 이런 경우에 규칙1을 적용하면 어떻게 될까? 아래는 위 사례에 규칙1을 적용한 예시이다.

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

// 직급
enum FunctioanlTitle {
    ASSOCIATE,
    MANAGER,
};

struct Team {
    // 구현을 간단히 하기 위해 팀에는 두명의 팀원만 있다고 가정...
    struct Employee *employee[2];
};

struct Employee {
    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));
    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;  // Team에 Employee 할당

    employee_b = malloc(sizeof(*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;  // Team에 Employee 할당

    // 규칙1을 적용하여 employee 객체를 해제
    free(employee_a);
    free(employee_b);

    return team;

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

    return NULL;
}

void free_team(struct Team **team) {
    if (*team == NULL) return;
    free(*team);
    *team = NULL;
}

int main() {
    struct Team *team = create_team();
    if (team == NULL) exit(1);

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

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

    free_team(&team); 

    return 0;
}

위 코드를 실행한 결과는 다음과 같다. (실행 환경에 따라 값은 다소 달라질 수 있다.)

$ gcc example.c
$ ./a.out
Employee A:
	name=
	functional_title=Error
Employee A:
	name=�U
	functional_title=Error

일방적으로 회사원 객체에 규칙1을 적용해 보니 dangling pointer 에러가 발생했다. 문자열로 구성된 name은 값이 깨졌고, enum에도 쓰레기 값이 들어갔다.Team 객체에 있는 포인터가 이미 해제된 Employee 객체를 가르키기 때문에 발생한 문제이다. 따라서 이런 경우에는 규칙1을 적용할 수 없다.

 

레퍼런스 카운트란?

이처럼 규칙1을 적용할 수 없는 상황에서는 규칙2 "메모리 할당과 해제가 한 블록 이내에서 이뤄질 수 없다면 레퍼런스 카운트를 활용"를 사용해야 한다. 규칙2를 적용하는 방법에 대해 살펴보기전에 레퍼런스 카운터가 무엇인지 개념을 잡고 가보자.

레퍼런스 카운트는 동적으로 할당된 메모리를 참조하는 객체의 수를 의미한다. 동적으로 할당된 메모리를 특정 객체가 참조하게 되면 레퍼런스 카운트 값을 증가시키고 해당 메모리를 참조하는 객체가 종료되면 레퍼런스 카운트를 감소시킨다. 레퍼런스 카운트 값이 0이 되면 해당 메모리를 해제한다.

레퍼런스 카운트를 활용한 메모리 관리는 원리가 어렵지 않고 구현하기 간단하며 프로세스에 연산 / 메모리 부하를 많이 주지 않는다. 따라서 초기버전의 가비지 컬렉터나 Rust의 Rc/Arc 객체등에 의해서 사용되고 있다. (최신 가비지 컬렉터에서는 레퍼런스 카운팅 방식을 잘 사용하지 않는다. 이유는 레퍼런스 카운트 방식은 순환 참조 문제를 해결할 수 없기 때문이다.) C에서도 레퍼런스 카운터를 쉽게 구현할 수 있다.

 

C에서 레퍼런스 카운트 사용하기 1

본 내용은 C언어의 구조체 형변환을 사용한다. "객체지향 C언어? (클래스, 다형성)" www.kernelpanic.kr/11 포스트를 먼저 읽고 오는 것을 강력히 권장한다.

 

레퍼런스 카운팅 방식을 지원하는 최신 언어들은 컴파일러 혹은 가비지 컬렉터가 자동으로 레퍼런스 카운트를 관리한다. 하지만 C언어에서는 개발자가 레퍼런스 카운트를 직접 관리해야 한다.

원칙은 아주 간단하다. 규칙1을 적용할 수 없는 객체에 레퍼런스 카운트를 부여한다. 임의의 포인터가 해당 객체를 가리키면 레퍼런스 카운트를 1 증가시킨다. 임의의 포인터가 더 이상 객체를 가리키지 못할 때 레퍼런스 카운트를 1 감소시킨다. 만약 레퍼런스 카운트가 0이 되면 객체를 정리한다. 아래는 C에서 레퍼런스 카운트를 적용하는 예제이다.

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

// 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 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));
    // employee_a가 Employee 인스턴스를 가리키기 때문에 레퍼런스 카운트를 증가시킨다.
    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;
    // team->employee[0]가 Employee 인스턴스를 가리키기 때문에 레퍼런스 카운트를 증가시킨다.
    ref(OBJECT(employee_a));

    employee_b = malloc(sizeof(*employee_b));
    // employee_b가 Employee 인스턴스를 가리키기 때문에 레퍼런스 카운트를 증가시킨다.
    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;
    // team->employee[1]가 Employee 인스턴스를 가리키기 때문에 레퍼런스 카운트를 증가시킨다.
    ref(OBJECT(employee_b));

    // employee_a와 employee_b 포인터가 스택에서 정리되기 때문에 레퍼런스 카운트를 감소시킨다.
    unref(OBJECT(employee_a));
    unref(OBJECT(employee_b));

    return team;

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

    return NULL;
}

void free_team(struct Team **team) {
    if (*team == NULL) return;

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

int main() {
    struct Team *team = create_team();
    if (team == NULL) exit(1);

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

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

    free_team(&team); 

    return 0;
}

해당 코드를 실행하면 정상적으로 동작한다. valgrind를 실행해 보아도 메모리 누수가 없다고 나온다.

$ ./a.out                                                                                                                                                                                                                                                       at 23:46:18
Employee A:
	name=employee A
	functional_title=ASSOCIATE
Employee A:
	name=employee B
	functional_title=MANAGER
$ valgrind ./a.out                                                                                                                                                                                                                                              at 23:46:24

... < 생략 > ...

==4616== 
==4616== HEAP SUMMARY:
==4616==     in use at exit: 0 bytes in 0 blocks
==4616==   total heap usage: 4 allocs, 4 frees, 1,088 bytes allocated
==4616== 
==4616== All heap blocks were freed -- no leaks are possible

 

 
 

 

 

반응형