많은 사람들이 구조체를 단순히 데이터 묶음 정도로만 사용한다. 실제 개발시에는 구조체를 통해 책임분리, 인터페이스 설계, 캡슐화 까지 하게 된다. 필자는 이번 글에서 SRP를 기준으로 구조체를 어떻게 설계해야 하는지, 그리고 C언어 특유의 Opaque Pointer 패턴을 이용해 객체지향적인 모듈화를 어떻게 달성하는지 설명하도록 하겠다.
SRP는 Single Responsibility Principle 으로 단일 책임 원칙이다. 쉽게 설명하면 하나의 구조체는 하나의 목적만 가지도록 하는것이다. 예시를 통해 알아보도록 하겠다.
typedef struct {
char name[32];
int score;
int level;
int socket_fd;
time_t last_login;
} Player;
해당 예시는 데이터(이름, 점수)와 네트워크 상태(socket_fd)가 섞여있다. 이러한 구조체는 수정할 때 의존성 문제가 생기므로 유지보수성이 악화된다.
그럼 좋은 예시를 설명하도록 하겠다.
typedef struct {
char name[32];
int score;
int level;
} PlayerProfile;
typedef struct {
int socket_fd;
time_t last_login;
} PlayerSession;
이런식으로 분리해서 코드를 작성하면 각각의 변경이 서로 영항을 주지 않고, 컴파일 의존성도 분리되므로 유지보수성이 좋다.
C에서는 클래스가 존재하지 않는다 그러나 구조체 + 함수 집합 을 하나의 모듈로 묶어서 비슷한 효과를 얻을 수 있다.
모듈화의 기본 패턴은 구조체 선언은 헤더파일(.h) 파일에서 작성하고 구조체 조작 함수는 C언어 파일(.c) 파일에서 작성한다. 또한 캡슐화를 통해 외부에서 내부를 모르게 한다.
Opaque Pointer는 불투명 포인터 이다.
불투명 포인터란 구조체 내부를 헤더에 숨기고, 포인터만 노출하는 설계 방식이다. 기존 전통적인 공개 구조체 방식은 라이르러리 내부 구조 변경 시 모든 의존 코드도 재컴파일 해야하는 문제가 발생한다.
예제코드를 통해 불투명 포인터 방식을 살펴보도록 하곘다.
// player.h
typedef struct Player Player; //선언만 하여 구현은 노출되지 않음
Player* player_create(const char* name);
void player_set_score(Player* p, int score);
void player_destroy(Player* p);
// player.c
struct Player {
char name[32];
int score;
};
Player *player_create(const char *name) { ... }
void player_set_score(Player *p, int score) { ... }
void player_destroy(Player *p) { ... }
이런식으로 구현된 불투명 포인터 방식은 헤더파일에는 구조체의 선언만 있고, 구조체의 정의는 .player.c 파일 안에만 있기 때문에 외부 코드(외부모듈, 다른 소스 파일)는 구조체의 내부 구성을 절대 알 수 없다.
typedef struct Player Player
이건 Player 라는 구조체 타입이 존재한다. 하지만 구체적으로 어떻게 생겼는지는 모른다 라는 의미이다.
이를 Incomplete Type(불완전 타입)이라고 부른다.
불완전 타입은 포인터 연산(포인터 선언, 포인터 전달)은 가능하지만 구조체 멤버 접근이나 크기는 불가는 하다. 그렇기 때문에 외부 소스 파일에서는 이 구조체 Playe의 구성을 알 수 없다. 예제 코드로 설명하겠다.
include "player.h"
int main(){
player* p = player_create("name");
p->score = 100; // 컴파일 에러
}
다음 코드에서는 컴파일 에러가 발생한다. 왜냐하면 Player 구조체 내부에 score 라는 맴버가 있다는걸 모르기 때문에 그렇다. 그러므로 외부에서는 포인터를 생성, 피괴, 포인터를 함수의 인자로 넘기기만 할 수 있다. 내부 구현은 전혀 모른 채 오직 함수 인터페이스만 사용할 수 있는 것이다.
컴파일러의 관점에서 구조체가 완전히 정의된 곳(palyer.c)만 sizeof(Player)를 알 수 있다. 외부에서는 sizeof(Player)계신이 안되므로 메모리를 직접 할당하거나 구조체를 직접 접근할 수 없다. 링커의 관점에서는 함수의 심볼만 연결한다. 구조체와 레이아웃에는 전혀 관여하지 않는것이다. 따라서 불투명 포인터를 사용하면 링커 수준에서도 완벽하게 숨겨지는 것이다. 따라서 불투명 포인터 방식은 C언어에서 가능한 가장 강력한 캡슐화 기법이다.
이번 글은 여기까지이다. 다음 글에서는 SRP + Opaque Pointer로 모듈 설계하기에 대해서 다루도록 하겠다.