9 life oriented programming.kr
dalmurii edited this page 2026-07-05 17:11:50 +09:00

C는 작고 쉬운 언어이다.
하지만 RAII는 기능적으로 제공하지 않고, 객체 지향 프로그래밍은 너무 순진하다.
C로 프로젝트를 관리하는 어려움은 아이러니하게도 그 높은 자유도와 단순함에서 나온다.

이런 작은 언어를 위해서는 보다 더 엄격한 규범이 필요하다.
아래 서술되는 생체 지향 프로그래밍은 RAII가 없는 환경에서 프로그램을 작성해야 할 객체 지향 프로그래밍에
익숙한 프로그래머들을 위한 작은 개념서이다.

생체 지향 프로그래밍

완전히 새로운 개념은 아니다. 객체 지향 프로그래밍에 명시적 생명주기 개념을 도입한 것에 가깝다.

정보의 형식은 크게 네 가지로 나뉠 수 있다.

단위

unit (aka primitive)

측정 가능한 가장 순수한 자료형이다.
정수, 실수, 정수벡터, 정수배열, 구조체, 공용체 등등이 있다.
초기화되는 즉시 사용 가능하며, 특별한 기능을 가지지 않는다.

struct u32pos3 {
        unsigned     m_x;
        unsigned     m_y;
        unsigned     m_z;
};

예를 들어 해당 구조체 또한 단위이다.
그 자체로 정보를 대표하며, 대표되는 정보 그대로 해석할 수 있다.

순수객체

object (aka record)

단위의 집합으로써 이루어진다.
그 자체로 정보를 대표할 수 있다는 점에서 단위와 같으나, 목적 및 해석법을 요구한다는 점에서 의미가 있다.
런타임에서 차이는 없을 수 있다.

따라서 대표하는 정보와 해석하는 방법이 다를 수 있다.

각 객체는 해석의 규약을 정할 수 있다.
각 객체는 규약에 따라 구성된 이후 사전에 정의된 규약(Method)에 대해 유효하다.

순수객체의 내용은 단위 및 또 다른 순수객체로써 정의될 수 있다.

이는 예제 u32pos3_a를 통해 보일 수 있다.
위의 단위 예제 u32pos3와 달리 아래의 u32pos3_a는 명시적인 해석법을 가지지 않는다.
그러나 규약을 통해 각 색인자에 x, y, z를 할당할 수 있다.

enum U32POS3_ {
        U32POS3_IDX_X,
        U32POS3_IDX_Y,
        U32POS3_IDX_Z
};

typedef struct { unsigned m_mem[3]; } u32pos3_a;

#define u32pos3_aX(A)   (A).m_mem[U32POS3_IDX_X]
#define u32pos3_aY(A)   (A).m_mem[U32POS3_IDX_Y]
#define u32pos3_aZ(A)   (A).m_mem[U32POS3_IDX_Z]

또 다른 대표적인 순수객체로는 열거형이 있다.
각 열거형의 실제 값은 단위인 정수이나, 열거형으로 쓰임으로써 요일이라는 맥락이 생긴다.

enum WEEK_ {
        WEEK_MON,
        WEEK_TUE,
        WEEK_WED,
        WEEK_THU,
        WEEK_FRI,
        WEEK_SAT,
        WEEK_SUN
};

추가적인 규약을 통해 해석법 또한 제시할 수 있다.

typedef const char* str_lit_t;

enum WEEK_ {
        WEEK_MON,
        WEEK_TUE,
        WEEK_WED,
        WEEK_THU,
        WEEK_FRI,
        WEEK_SAT,
        WEEK_SUN
};

str_lit_t WEEK_to_str(enum WEEK_ c_w) {
        const str_lit_t arr[] = {
                "Monday",
                "Tuesday",
                "Wednesday",
                "Thursday",
                "Friday",
                "Saturday",
                "Sunday"
        };

        return arr[c_w];
}

생체

life (aka class, owner)

정적인 범위로써 표현되지 않는 동적인 생명주기를 가질 수 있다.

생체는 구성과 정리의 규약으로써 동적 생명주기를 표현한다.
따라서, 구성 이후부터 정리 이전까지 생체는 규약에 대해 유효하다.
정보로써는 매 시행마다 다른 값이 들어갈 수 있으므로, 그 자체로는 정보로써 쓸 수 없다.

극단적으로 단순한 생체의 예시로는 표준 라이브러리 함수 malloc을 통해 동적 할당된 주소가 있다.

int main(void) {
        void* ptr = malloc(13);
        free(ptr);
        return 0;
}

위 예시에서 주소 ptr의 정보는 13바이트 길이의 기억공간을 나타내는 주소이다.
이 13바이트 길이의 기억공간은 malloc 이후부터 free 이전까지 유효하며, 이후에는 정보로써 의미가 없다.

생체는 정적인 생명주기를 가진 순수객체와 단위, 동적으로 하위 생명 주기를 가진 생체를 내용으로써 가질 수 있다.

하위 생명 주기를 가진 소유된 생체를 자식이라 한다.
반대로 상위 생명 주기를 가진 소유하는 생체를 부모라 한다.

동적 하위 생명 주기란, 부모가 죽으면 자식도 따라 죽어야 함을 뜻한다.

#include <stdlib.h>

struct plain_dybuf {
        void*           m_mem;
        size_t          m_btsize;
};

/** `make` dybuf */
struct plain_dybuf mk_plain_dybuf(size_t c_size) {
        struct plain_dybuf R;
        R.m_mem = malloc(R.m_btsize = c_size);
        return R;
}

/** `delete` dybuf */
void del_plain_dybuf(struct plain_dybuf d_dybuf) {
        free(d_dybuf.m_mem);
}

int main(void) {
        struct plain_dybuf dybuf = mk_plain_dybuf(sizeof(int));
        *((int*)dybuf.m_mem) = 32;      /** valid */
        del_plain_dybuf(dybuf);
        *((int*)dybuf.m_mem) = 32;      /** ERROR: invalid! */
        return 0;
}

그러나 역으로 부모가 살아 있을 시 자식이 죽어 있는 경우는 허용된다.

/** `make` dybuf */
struct plain_dybuf mk_plain_dybuf(void) {
        struct plain_dybuf R;
        R.m_mem = 0;
        return R;
}

void plain_dybuf_initmem(struct plain_dybuf* dybuf, size_t c_size) {
        if(dybuf->m_btsize < c_size) {
                free(dybuf->m_mem);
                dybuf->m_mem = malloc(dybuf->m_btsize = c_size);
        }
}

struct plain_dybuf mk_plain_dybuf1(size_t c_size) {
        struct plain_dybuf R = mk_plain_dybuf();
        plain_dybuf_initmem(&R, c_size);
        return R;
}

/** `delete` dybuf */
void del_plain_dybuf(struct plain_dybuf d_dybuf) {
        free(d_dybuf.m_mem);
}

int main(void) {
        struct plain_dybuf dybuf = mk_plain_dybuf();

        *((int*)dybuf.m_mem) = 32;      /** ERROR: invalid! */

        /** now dybuf.m_mem is alive */
        plain_dybuf_initmem(&dybuf, sizeof(int));

        *((int*)dybuf.m_mem) = 32;      /** now valid */

        del_plain_dybuf(dybuf);  /** after this `dybuf` is dead. */
        return 0;
}

해당 구조를 통해 생명 주기를 명시적으로 관리함으로써 각 하위 생체의 상태를 예측할 수 있다.

공생체

symbiote (aka view, reference)

공생체는 유형에 상관 없이 어떤 하나 또는 다수의 정보를 참조한다.
공생체 자체는 생명주기를 가지지 않으며, 따라서 참조되는 내용의 생명주기를 따른다.
아래는 생체와 이를 참조하는 예시이다.

int main(void) {
        /** life - must be finilised. */
        char* life = malloc(13);

        /** symbiote - must not be killed explicitly */
        char* symbiote = life;

        /** life is dead explicitly */
        free(life);

        /** now symbiote is implicitly dead too. */
}

아래는 단위와 이를 참조하는 예시이다.

int main(void) {
        /** just a simple unit. has no life cycle. */
        int unit = 3;

        /** symbiote to a simple unit. */
        int* symbiote = &unit;

        /** `unit` and `symbiote` is valid inside the main. */
}

위와 같이 공생체는 유형에 상관 없이 일관적인 해석법을 제공할 수 있기에 유용하다.


공생체를 일부로 가진 정보 또한 공생체이다.
이를 통해 정보를 조합하여 해석법을 제시하는 객체 형태의 공생체를 만들 수 있다.

struct strview {
        const char*     m_str;
        size_t          m_sz;
};

위 자료형은 문자열이 저장되어 있는 위치와 그 문자열의 크기를 하위 정보로써 가진다.
strview는 공생체로 하위 정보의 생명주기에 제어하지 않으므로, strview가 살아 있을 때 살아 있다면 m_str에는 char의 주소인 무엇이든 넣을 수 있다.

아래는 생체 memblk와 이를 참조하는 공생체 sll을 통한 단방향 연결 리스트의 구현 예시이다.
공생체인 sll의 구현에서 모든 api가 공생 대상인 memblk을 참조하고 있음을 참고해서 보면 좋다.

#include <stdlib.h>

/** memory block */
typedef struct {
        char*   m_mem;
        size_t  m_size;
} memblk;

void memblk_init(memblk* m) {
        m->m_mem        = 0;
        m->m_size       = 0;
}

void memblk_grow(memblk* m, size_t sz) {
        if(m->m_size < sz) {
                char* new = (char*)malloc(sz);
                char* old = m->m_mem;
                char* new_v = new;

                if(new) {
                        for(size_t i = m->m_size; i --> 0; *new_v++ = *old++);
                        m->m_size = sz;
                } else {
                        m->m_size = 0;
                }

                free(m->m_mem);
                m->m_mem = new;
        }
}

void memblk_fini(memblk* m) {
        free(m->m_mem);
}

/** singly linked list */
typedef struct {
        size_t  m_nxt;
        size_t  m_size;
        size_t  m_size_glob;
} sll;

void sll_cfg(sll* r, memblk* m, size_t c_init_size) {
        memblk_grow(m, sizeof(sll) + c_init_size);
        if(!m->m_mem) return;

        *(sll*)m->m_mem = (sll) {
                .m_nxt          = 0,
                .m_size_glob    = sizeof(sll) + c_init_size,
                .m_size         = c_init_size
        };

        *r = *(sll*)m->m_mem;
}

sll* sll_get(memblk* m, const sll* cur) {
        if(cur->m_size_glob > m->m_size)
                return 0;

        if(!m->m_mem)
                return 0;

        return (sll*)(m->m_mem + cur->m_size_glob - cur->m_size - sizeof(sll));
}

sll* sll_nxt(memblk* m, const sll* cur) {
        return m->m_mem && cur->m_nxt ? (sll*)(m->m_mem + cur->m_nxt) : (sll*)0;
}

void* sll_mem(memblk* m, const sll* cur) {
        return sll_get(m, cur) ? sll_get(m, cur) + cur->m_size : 0;
}

sll* sll_grow(memblk* m, const sll* cur, size_t c_size) {
        memblk_grow(m, sizeof(sll) + c_size + cur->m_size_glob);
        sll* cur_loc = sll_get(m, cur);
        if(!cur_loc) return 0;

        cur_loc->m_nxt = cur_loc->m_size_glob;

        sll* nxt = sll_nxt(m, cur_loc);
        *nxt = (sll) {
                .m_nxt = 0,
                .m_size = c_size,
                .m_size_glob = cur_loc->m_size_glob + sizeof(sll) + c_size
        };

        return nxt;
}

아래는 위에서 구현한 sll과 memblk의 사용 예시이다.
생명주기를 유의해서 보면 좋다.

#include <stdio.h>

int main(void) {
        memblk  block;
        sll     list, list1, list2;

        /** now block is alive */
        memblk_init(&block);

        /** now list is head and block is confiugred as list. */
        sll_cfg(&list, &block, 4);

        /** list element 1. still symbiote. */
        list1 = *sll_grow(&block, &list, 13);

        (*(long*)sll_mem(&block, &list1)) = 4;

        /** list element 2. still symbiote. */
        list2 = *sll_grow(&block, &list, 4);

        (*(short*)sll_mem(&block, &list2)) = 3;

        printf("%ld %hd\n"
                        , (*(long*)sll_mem(&block, &list1))
                        , (*(short*)sll_mem(&block, &list2))
                        );

        /** block is dead. */
        memblk_fini(&block);

        /** thus list, list1, list2 is dead. */

        return 0;
}