Computer Science
탄탄한 기반 실력을 위한
전공과 이론 지식 모음
Today I Learned!
배웠으면 기록을 해야지
TIL 사진
Flutter 사진
Flutter로 모바일까지
거꾸로캠퍼스 코딩랩 Flutter 앱개발 강사
스파르타코딩클럽 즉문즉답 튜터
카카오테크캠퍼스 3기 학습코치
프로필 사진
박성민
임베디드 세계에
발을 들인 박치기 공룡
임베디드 사진
EMBEDDED SYSTEM
임베디드 SW와 HW, 이론부터 실전까지
ALGORITHM
알고리즘 해결 전략 기록
🎓
중앙대학교 소프트웨어학부
텔레칩스 차량용 임베디드 스쿨 3기
애플 개발자 아카데미 1기
깃허브 사진
GitHub
프로젝트 모아보기
Instagram
인스타그램 사진

TIL

[250901] Day 22 - C언어도 캡슐화가 가능하다고

sm_amoled 2025. 9. 1. 21:53

들어가며

지난주 대가리를 박아가며 개발했던 프로젝트의 발표를 마무리했다. 사실 지금 생각해보면 결과물이 좀 짜치는 느낌이고, 방향키가 아니라 커서를 이용해서 화면에다가 입력하는 시스템을 내가 맡아서 한 번 도전해봤다면 조금 더 ‘와우’ 모먼트를 이끌어낼 수 있었을 것 같은데. 오늘 아래에 작성한 딥다이브 같은 조금 더 재미있는 것들을 많이 사용해볼걸 이라는 생각이 든다.

약간 후회가 되는 것이겠찌.

그치만 우리 팀원들에게 열심히 교육을 한 것에 대해서는 보람을 많이 느낀다.

지난 달에는 너무 보조강사같은 느낌으로 다른 사람들을 도와주는 포지션으로 있으면서 복습에 철저히 하는 한 달이였다면, 이번 9월부터는 차라리 스터디에 조금 더 적극적으로 참여하면서 선행학습들을 진행하는게 더 나을까? 라는 고민도 있다. 흠쓰따리 뭐가 좋을까.

오늘의 키워드

C언어로 하는 데이터 캡슐화

프로젝트를 진행할 때, 게임 실행에 있어서 필요한 다양한 전역변수 값들을 캡슐화하여 보호하는 방식을 설계하고 코드로 구현하였다.

처음에는 필요한 대부분의 데이터를 단순 전역변수로 관리하였는데, 이 경우 언제 어디에서 이 값이 참조되고있는지 파악하기도 어렵고, 의도치않게 변경이 되어버리는 상황이 발생할 수 있겠다고 생각이 들었다.

그래서 전역 변수처럼 사용할 수 있으면서 조금 더 안전하게 데이터를 관리하는 방법을 찾아봤고, 연구 및 적용을 할 수 있었다.

우선 내가 전역변수처럼 사용하고자 하는 값(또는 구조체)을 static 키워드를 붙여 정적 변수로 만들어준다. 이제 이 변수는 해당 파일에서만 접근할 수 있는 변수가 되었다.

// cat.c
static cat_t g_cat;

const cat_t* get_cat()
{
    return &g_cat;
}

다른 파일에서 이 데이터에 접근하기 위해서는 변수명으로 접근할 수는 없으니 메모리 주소를 통해 접근할 수 있도록 해야한다. &g_cat 을 반환하는 함수를 만들어, *get_cat() 의 방식으로 g_cat의 값을 가져가 사용할 수 있도록 했다. 이제 g_cat 값에 접근하기 위해서는 반드시 위 함수를 이용해야만 한다.

그런데, *get_cat().pos_x = 10 같은 방식으로 현재 코드에서는 구조체의 멤버 값을 변경할 수 있다는 단점이 있다. 캡슐화를 했지만 값을 지키지 못하는 문제…

그래서 함수의 반환 타입을 const로 만들어서 여기 값에 접근하면 수정하지 못하게 만드는 방법을 적용해주었다.

// cat.h
typedef struct _cat {
    double pos_x;
} cat_t;

extern inline const cat_t* get_cat();

---
// 포인터 상수를 변경하려 했으니 컴파일 에러가 발생함!
get_cat()->pos_x = 10;

대신에 이렇게 cat.c 파일 내에 static 변수값을 변경하는 함수를 만들어서 값 변경을 적용할 수 있다.

// cat.c
void change_cat_state()
{
        g_cat.pos_x= 10;
}

그런데, 맙소사. 형변환을 통해서 const 로 값을 지정한 것을 회피할 수 있다.

// get_cat()의 결과는 const T* 이지만, 이를 T* 으로 형변환하여 접근금지를 해제할 수 있음.
((cat_t*) get_cat())->pos_x = 10;

이걸 또 한 번 회피하기 위한 방법이 또 있다. 바로 struct 의 구현을 헤더파일이 아닌 c 파일에서 하는 것.

// cat.h
typedef struct _cat cat_t

extern inline const cat_t* get_cat();

---
// cat .c
#include "cat.h"

struct _cat {
    double pos_x;
}

static cat_t g_cat;

만약 이렇게 작성을 한다면, cat.h 를 import 하는 다른 파일들에서는 cat_t 라는 구조체 타입이 있다는건 아는데, 그 내부가 어떻게 생겼는지에 대해서는 알 수 있는 방법이 없다. 따라서 (cat_t*) 으로 형변환을 하더라도 내부 멤버 변수가 무엇이 있는지 모르기 때문에 아무것도 할 수 있는게 없다.

((cat_t*) get_cat())-> (여기에서 참조할 멤버에 대한 정보가 없음)

다만, get_cat()→ 으로 나도 값 접근을 하지 못하기 때문에, cat.c에서 멤버 값을 반환하는 함수들을 열심히 만들어둬야 한다는 단점이 있다. (== 사실상 이건 get_cat() 도 필요 없는 상황 아니냐??)

아마 이 정도로 캡슐화를 할 일은 거의 없을 것 같긴 하다. (진짜 진짜 캡슐화가 중요한 경우에는 이렇게 작성할 수는 있겠지만, 굳이굳이 느낌?)

C언어로 하는 뷰-모델 아키텍처 짜기

C언어에서도 모듈간의 의존성을 관리해 계층 단위의 개발을 할 수 있다는 것을 이번에 프로젝트 덕분에 알게되었다. 물론 객체지향적인 특징을 지원하는 키워드는 없기 때문에 의존성 역전이나 하드한 추상화를 진행하기에는 어려웠지만, 어느정도의 아키텍처 정도는 충분히 코드로 구현할 수 있을 것 같았다.

특히나 #include 와 header 파일을 통해서 원하는 모듈만 가져와 사용할 수 있기 때문에, 잘못된 모듈을 참조하고 있는지에 대해서 분명하게 시각적으로 알 수 있다는 점은 유리했던 것 같다.

프로젝트에서는 주로 화면을 그리는 코드에서 추상화와 계층 분리 (책임에 대한 분리)를 진행했는데, 나름 깔끔하게 분리가 되었던 것 같음!

딥다이브

C에서 상속을 사용할 수 있을까?

이번에 게임 프로젝트를 하면서 여러가지 구조체 타입에서 동일하게 pos_x, pos_y, size_w, size_h 4가지 데이터를 필요로 하는 경우가 많았다. 만약 유니티나 다른 언어로 구현하고 있었다면 바로 공통 부모를 만들고 자식들로 깔쌈하게 만들면 아주 부드러운 코드가 될 것이라 생각했는데.

typedef struct _go {
    int pos_x;
    int pos_y;
} game_object_t;

typedef struct _cat {
    int pos_x;
    int pos_y;
    int delay;
} cat_t;

typedef struct _enemy {
    int pos_x;
    int pos_y;
    char type;
    char code;
} enemy_t;

흠 그러다가, 구조체의 변수들이 메모리에 들어가는 순서는 선언한 순서대로라는 것이 생각났다.

(오호?)

그리하여 나온 아이디어.

요렇게 cat_t 와 enemy_t 두 가지 구조체에서 중복되는 부분에 대해, 순서까지 똑같이 맞춰서 game_object_t 구조체 타입을 하나 만들어준다.

그리고, cat_t, enemy_t 두 구조체에서 겹치는 부분은 game_object_t 구조체로 대신 넣어준다. 그러면 메모리 상에는 위 이미지처럼 가장 앞에 pos_x, pos_y 값이 들어있을 것이다.

typedef struct _go {
    int pos_x;
    int pos_y;
} game_object_t;

typedef struct _cat {
    game_object_t location;
    int delay;
} cat_t;

typedef struct _enemy {
    game_object_t location;
    char type;
    char code;
} enemy_t;

그렇다면, cat_t*enemy_t*game_object_t* 로 형변환 했을 때, 메모리 상 해당 주소에는 game_object_t의 데이터가 있기 때문에, pos_x, pos_y의 값을 정상적으로 변경할 수 있지 않을까? 라는 아이디어.

void move_game_object(game_object_t* ptr, int x, int y)
{
    ptr->pos_x += x;
    ptr->pos_y += y;
}

그리고 이 로직을 실행한 결과는 아래와 같다.

int main(void)
{
    cat_t cat = { {0, 0}, 3 };
    enemy_t enemy = { {0, 0}, 0, 'A' };

    printf("cat:   %d %d %d\n", cat.location.pos_x, cat.location.pos_y, cat.delay);
    printf("enemy: %d %d %d %d\n", enemy.location.pos_x, enemy.location.pos_y, enemy.type, enemy.code);

    move_game_object(&cat, 3, 5);
    move_game_object(&enemy, 7, 8);

    printf("\n MOVE ====== \n");

    printf("cat:   %d %d %d\n", cat.location.pos_x, cat.location.pos_y, cat.delay);
    printf("enemy: %d %d %d %d\n", enemy.location.pos_x, enemy.location.pos_y, enemy.type, enemy.code);

    printf("%d\n", ((int(*)()) foo)());

    return 0;
}

>>> 실행 결과
cat:   0 0 3
enemy: 0 0 0 65

 MOVE ======

cat:   3 5 3
enemy: 7 8 0 65

와 진짜 되네?

아마 내부에다가 game_object_t 구조체를 멤버로 담지 않고 그냥 pos_x, pos_y 의 타입과 순서를 지켜서 넣어주기만 하면 문제가 없겠지만, 언제 누가 이걸 변경해버릴 지 모름 + game_object_t 구조체의 값 변경으로 상속받는(이라 말해도 되나) 타입들에게 일괄 변경사항을 전파할 수 있음 의 장점이 있기 때문에 요렇게 하는게 좋을 것 같다.

실전에서 사용할 일이 있는지는 모르겠지만, 재미있는 성질인 것 같다. ㅋㅋ

개고생

팀원 3명을 가르치며 프로젝트로 5일만에 C언어로 게임 하나 만들어내기.

320x100