ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • OS 개발일지 1. 코드의 재사용성에 대한 고민
    개발/Operating System 2024. 1. 25. 19:23

     

      OS 개발을 선언한 후 벌써 너무나도 많은 날이 지나갔다. 프로젝트 이외에도 여러 가지 일을 준비하고 있지만, 예상했던 것 만큼 어려움이 많아 계획했던 예정보다 많이 늦어졌다. 어느 정도 자료 구조에 대한 구현은 거의 끝나가기에 이제부터 본격적으로 OS 개발에 들어갈 것 같다.

     

    프로젝트를 진행하면서 몰랐던 것, 고민의 흔적들을 이 탭에 정리해서 주기적으로 정리해보려 한다. 

     

    C 의 한계

      OS와 마이크로 OS 3에 관련한 수업을 저번 학기에 들었고, 그저 블랙박스로만 느껴지던 OS를 면밀하게 살펴보게 되었다. 마이크로 os는 모든 코드가 C로 작성되어 있는데, 이에 영감을 받아 자체 OS는 C로 작성하고 있다. 

     

    C는 빠르고 하드웨어 수준에서의 제어가 가능하다는 장점이 있지만 객체지향 언어에서 제공하는 기능들을 사용할 수 없다는 점에서 한계를 느꼈다. 그 중 하나가 코드의 재사용성이다.

     

    코드의 재사용성

      자료 구조들을 직접 구현하면서 맞닥뜨린 고민이다. C에는 클래스처럼 직접적으로 코드를 재사용할 수 있는 옵션이 없다.. 물론 아예 방법이 없는 것은 아니다. 대표적으로 자식 구조체 내부에 부모 구조체를 선언하는 방법이 있다.

     

    그러나 이 방법으로는 도저히 아래와 같은 문제를 해결할 수 없었다. 예를 들어 해시 테이블을 만들어보자.

     

    그림1. Hash Table

     

      그림1에서는 해시 테이블을 정의하고 있다. 각각의 Key에 대응하는 주소 값에 데이터를 저장하고 있다. 저장되는 데이터들은 불행히도 해시 함수에 따라서 같은 주소로 배정받을 수 있는데, 그림1의 경우 이를 Chaining 기법으로 충돌을 해결하고 있다. 데이터 노드들은 링크드 리스트 형식으로 저장되기 때문에 이전에 구현에 놓았던 DLL 더블 링크드 리스트 라이브러리를 이용해 구현하려 한다.

     

    필자의 생각은 더블 링크드 리스트를 구성하는 노드만 새로 정의하고, 타입 캐스팅만 적절하게 한다면 이전 DLL의 함수를 사용할 수 있을 것으로 보았다.

     

    그래서 새로운 구조체를 DLLHash.h에 정의하고 DLL의 함수들을 사용하려 했다.

    typedef struct tagHashNode
    {
        KeyType Key;
        ValueType Value;
        // Key를 해시 함수에 넣은 값을 주소로 사용
        
        // DLL 노드 추가
        struct tagHashNode* prev;
        struct tagHashNode* next;
    } HashNode;

     

    그에 따른 새로운 구조체 코드이다. 이제 DLLHash.h 파일에서 이 구조체를 사용하는 함수들을 적당히 고쳐 재사용성을 높일 것이다.


     

    ※ DLL 구조체의 재사용?

    DLL 구조체를 HashNode 구조체 내부에 정의해서 사용하는 것은 잘못된 방법이다. 예를 들어 아래와 같은 구조체를 선언해보자. Value의 경우 추가로 정의하는 DLL_node 내부의 data에 저장한다고 가정한다.

    typedef struct tagHashNode
    {
        KeyType Key;
        
        struct tagNode* DLL_node;
    } HashNode;

     

    그러나 next 포인터의 경우 DLL_node 내부의 next 포인터를 사용할 수 없다. 왜냐하면 내부의 포인터의 타입이 HashNode 가 아니기 때문에 다음 해시 노드를 가리킬 수 없기 때문이다. 이는 키 값을 각각 노드마다 저장할 수 없음을 의미한다.

    그렇다고 해서 다음 HashNode를 가리키는 변수를 구조체에 넣는다면 DLL을 사용하는 의미가 없다. 그래서 위의 방법을 다시 고안하게 되었다.


     

    DLLHash.h에 새로 정의되는 함수에서 일부 DLL.h에 정의된 함수를 사용하여 Chaining을 구현할 것이다. 필요한 함수들은 새로운 구조체를 사용한다는 점 이외에는 모든 로직이 동일하다. 

     

    크게 3가지 부분에서 수정을 해야 했다.

    1. 함수 리턴 타입 
    2. 파라미터 타입
    3. 함수 내부에 정의된 타입

    C++의 템플릿 기능을 사용한다면 간단하게 해결할 수 있는 문제지만 C 언어에서는 템플릿을 지원하지 않기 때문에 이와 비슷한 기능을 직접 구현해야 한다.

     

    이런 방법에는 매크로를 사용하거나, 함수포인터와 void* 를 사용해서 템플릿 처럼 만들 수 있다.

     

    매크로

    C에서 지원하는 기능인 매크로를 사용해서 일부 코드를 템플릿 처럼 만들 수 있다.

    #define SWAP(type, a, b) do { type temp = a; a = b; b = temp; } while(0)
    
    int main() {
        int a = 5, b = 10;
        SWAP(int, a, b);
    
        return 0;
    }

     

    type 매개변수를 사용해서 여러 타입에 대해서도 유연하게 대처할 수 있다. 그러나 매크로는 함수가 복잡한 경우 가독성이 좋지 않고 무엇보다도 전처리 단계에서 코드가 생성되기 때문에 특정 클래스에서만 사용되는 맴버 함수처럼 사용하기에는 무리가 있다. 

     

    제네릭 프로그래밍

    제네릭 프로그래밍이란 어떤 함수가 특정 타입에 종속되지 않고 선언한 것을 의미한다. C의 경우 제네릭 프로그래밍을 지원하지 않는다. 

    typedef struct {
        void (*swap)(void*, void*);
    } SwapHelper;
    
    void swap_int(void *a, void *b) {
        int temp = *(int*)a;
        *(int*)a = *(int*)b;
        *(int*)b = temp;
    }
    
    void swap_double(void *a, void *b) {
        double temp = *(double*)a;
        *(double*)a = *(double*)b;
        *(double*)b = temp;
    }
    
    int main() {
        SwapHelper intSwap = { swap_int };
        SwapHelper doubleSwap = { swap_double };
    
        int a = 5, b = 10;
        double x = 3.14, y = 2.71;
    
        intSwap.swap(&a, &b);
        doubleSwap.swap(&x, &y);
    
        return 0;
    }

     

    함수 포인터를 사용해서 swap 함수를 정의한 모습이다. 구현은 다소 흥미롭지만 결국 타입과 함수 이름을 바꾼것과 다름없다. 사용자 입장에서는 타입에 따른 모든 경우의 함수 이름을 외울 필요가 없다는 장점은 있을지 모르지만, 결국 코드를 재사용하지 않고 복붙한 것이라 용량관점에서도, 리펙토링 관점에서도 실패한 코드이다.

     

    코드가 복잡해지고 버그가 많아질수록 이 문제는 두드러지게 나타나는데, 만약 swap의 로직을 잘못 짰다면 swap_int와 swap_double 코드를 일일히 고쳐야 한다.

     

    해결해야 하는 진짜 문제

      코드의 재사용성을 높일 수 있는 방법이 없을까 고민을 많이 했지만 아직 뚜렷한 해결책을 찾지 못했다. 해시 테이블을 DLL을 사용해 구현한다고 해도 더 좋은 성능을 낼 것이라고 기대하지 않는다. 오히려 사용하지 않는 필드가 있으므로 성능이 저하되었을 것이다.

    굳이 DLL을 사용하여 구현하려 했던 진짜 이유는 더 복잡한 레드 블랙 트리를 해시 테이블에 붙이고 싶었고, 그 전에 연습삼아 이 예제를 구현해 보고 싶었기 때문이다. 함수형 프로그래밍의 리펙토링을 공부하면서 좀 더 깔끔한 코드를 작성할 수 있게 노력하겠다.

     

    댓글

Designed by Tistory.