본문 바로가기
모던 C 언어/C언어 객체지향

1-2. 객체지향 C언어? (상속, 캡슐화)

by 커널패닉 2020. 8. 26.
반응형

www.kernelpanic.kr/11

 

1-1. 객체지향 C언어? (클래스, 다형성)

객체지향이란? C언어의 객체지향이라는 주제로 이야기를 하려면, 우선 "객체지향"이란 무엇인지부터 짚고 넘어가야 한다. 나는 개인적으로 "클린 아키텍쳐"에서 말하는 객체지향 개념을 선호한�

www.kernelpanic.kr

상속

상속은 위키백과에 다음과 같이 정의되어 있다. "상속(inheritance)은 객체들 간의 관계를 구축하는 방법이다. 클래스로 객체가 정의되는 고전 상속에서, 클래스는 기반 클래스, 수퍼클래스, 또는 부모 클래스 등의 기존의 클래스로부터 속성과 동작을 상속받을 수 있다. 그 결과로 생기는 클래스를 파생 클래스, 서브클래스, 또는 자식 클래스라고 한다." 간단하게 정리해서 여기서 다루는 상속은 부모 클래스의 속성과 동작를 자식클래스에서 재활용/오버라이딩할 수 있도록 하는 것을 의미한다. C언어에서는 구조체 내부에 부모 객체를 소유하여 상속을 구현할 수 있다.

 

/* shape.h */

struct Shape {
	void (*move)(struct Shape *shape, unsigned int x, unsigned int y);
	void (*draw)(struct Shape *shape);

	char *type;
};

struct Rect {
	/* 부모 구조체는 반드시 첫 번째 변수로 선언 */
	struct Shape parent;

	unsigned int x;
	unsigned int y;
	unsigned int width;
	unsigned int height;
};

struct Rect* rect_init(void);
void rect_move(struct Shape *shape, unsigned int x, unsigned int y);
void rect_draw(struct Shape *shape);
/* shape.c */

#include <stdio.h>
#include <stdlib.h>
#include "shape.h"

/* XXX 편의상 메모리 검사하는 코드는 생략 */

struct Rect* rect_init(void) {
	struct Rect *rect = malloc(sizeof(struct Rect));

	/* rect와 rect->parent의 주소가 동일하기 때문에
	 * 이와 같은 형변환이 가능하다. */
	struct Shape *shape = (struct Shape*)rect;

	shape->move = rect_move;
	shape->draw = rect_draw;
	shape->type = "RECT";

	rect->x = 0;
	rect->y = 0;
	rect->width = 30;
	rect->height = 40;

	return rect;
}

void rect_move(struct Shape *shape, unsigned int x, unsigned int y) {
	/* 위와 마찬가지로 shape(rect->parent)와 rect의 주소가 동일하기 때문에
	 * 이와 같은 형변환이 가능하다. */
	struct Rect *rect = (struct Rect*)shape;

	/* 사각형 이동 */
	rect->x = x;
	rect->y = y;
	printf("사각형을 움직인다.\n");
}


void rect_draw(struct Shape *shape) {
	struct Rect *rect = (struct Rect*)shape;

	/* 사각형 그리는 작업 수행 */
	printf("사각형을 그린다.\n");
	printf("x=%u y=%u w=%u h=%u\n", 
			rect->x, rect->y, rect->width, rect->height);
}

int main() {
	struct Rect *rect = rect_init();

	((struct Shape*)rect)->move((struct Shape*)rect, 1, 2);
	((struct Shape*)rect)->draw((struct Shape*)rect);

	free(rect);
}

출력

사각형을 움직인다.
사각형을 그린다.
x=1 y=2 w=30 h=40

부모 구조체를 구조체의 첫 번째 변수로 선언하면, 아주 간단한 형 변환을 통해 사용할 수 있다. 이는 Rect 구조체의 인스턴스 (위의 예에서는 rect) 주소와 해당 인스턴스의 parent의 주소가 일치하기 때문이다. 위의 그림을 보면 좀 더 이해가 쉽다. rectshape로 형변환하더라도 구조체의 시작 주소는 0x123480으로 동일하다. 또한 rectshape의 멤버들은 주소가 겹치지 않기 때문에 사용하는데 있어 메모리 주소 충돌 등의 문제가 생길 일도 없다.

물론 굳이 부모 구조체를 구조체의 첫 번째 변수로 선언하지 않더라도 rect->parent.draw(rect->parent)처럼 접근하는 것도 가능하다. 하지만 부모 구조체를 첫 번 째 변수로 선언하지 않으면, 부모 구조체에서 다시 자식 구조체로 형을 변환하기 어렵다. 위에서 보이듯이 부모 구조체를 인자로 받는 함수(rect_draw, rect_move)가 있을 때, 자식 구조체의 인자들을 참조하기 어렵다. (번거롭기는 하지만 방법이 있기는 하다. container_of 참조) 반면 부모 구조체를 struct 첫 번째 변수로 선언하여 형 변환하면, 위의 코드와 같이 자식 구조체 -> 부모 구조체, 부모 구조체 -> 자식 구조체로 자유롭게 변환이 가능하다는 장점이 있다.

 

캡슐화

C는 .c 파일과 .h 파일을 가지고 있다. 외부 .c 파일에서는 헤더파일을 인클루드하여 .h파일에 공개되어 있는 인터페이스를 사용할 수 있지만, 헤더파일에 공개되지 않은 .c 내부의 정보는 접근할 수 없다. (extern 키워드를 사용하면, .h 없이도 .c 내부의 정보에 접근할 수 있기는 하다. 하지만 일반적인 사용법은 아니다.) 이 점을 이용해서 공개해야 하는 정보는 .h에 기술하고, 공개를 원치 않는 정보는 .c에 기술하여 C에서도 캡슐화를 구현할 수 있다.

/* rect.h */

/* C에서는 헤더에서 구조체 타입을 선언만 하고, 실제 정의는 .c 파일에서 할 수 있다. */
struct RectPrivate;
struct Rect {
	void (*move)(struct Rect *rect, unsigned int x, unsigned int y);
	void (*draw)(struct Rect *rect);

	/* 현재는 RectPrivate의 크기를 알 수 없기 때문에 구조체 포인터를 사용한다. */
	struct RectPrivate *private;
};

struct Rect* rect_init(void);
void rect_move(struct Rect *rect, unsigned int x, unsigned int y);
void rect_draw(struct Rect *rect);
/* rect.c */

#include <stdio.h>
#include <stdlib.h>
#include "rect.h"

/* 실제 RectPrivate의 정의는 .c 파일에서 이뤄진다. */
struct RectPrivate {
	unsigned int x;
	unsigned int y;
	unsigned int width;
	unsigned int height;
};

/* XXX 편의상 메모리 검사하는 코드는 생략 */

struct Rect* rect_init(void) {
	struct Rect *rect = malloc(sizeof(struct Rect));

	rect->move = rect_move;
	rect->draw = rect_draw;

	rect->private = malloc(sizeof(struct RectPrivate));

	/* 현재 파일에 안에서만 아래 값들에 접근할 수 있다. 
	 * 현재 파일  밖에서는 RectPrivate의 구조를 알 수 없기 때문에, 
	 * 아래 값들에 접근할 수 없다. */
	rect->private->x = 0;
	rect->private->y = 0;
	rect->private->width = 30;
	rect->private->height = 40;

	return rect;
}

void rect_move(struct Rect *rect, unsigned int x, unsigned int y) {
	/* 사각형 이동 */
	rect->private->x = x;
	rect->private->y = y;
	printf("사각형을 움직인다.\n");
}


void rect_draw(struct Rect *rect) {
	/* 사각형 그리는 작업 수행 */
	printf("사각형을 그린다.\n");
	printf("x=%u y=%u w=%u h=%u\n", 
			rect->private->x, rect->private->y, 
			rect->private->width, rect->private->height);
}

int main() {
	struct Rect *rect = rect_init();

	rect->move(rect, 1, 2);
	rect->draw(rect);

	free(rect);
}


헤더파일에는 Rect 구조체의 메서드만 공개하고, 상태는 .c 파일의 RectPrivate에 저장하면 다른 파일에서 RectPrivate의 값에 직접 접근할 수 없게 된다. 이런 방식으로 데이터를 외부에서 직접 접근 하지 못하도록 은닉할 수 있다.

 

C언어를 객체지향적으로 사용하는 것에 대한 개인적인 의견

 나는 C언어를 객체지향적으로 사용하는 데 회의적인 입장이다. 과거에는 C와의 호환성을 지키면서 C 정도 수준의 퍼포먼스를 보이는 객체지향적인 언어가 드물었다. (사실상 Cpp가 유일하였다.) 따라서 리눅스 진영에서는 Gobject를 통해 C로 객체지향을 구현하는 방법을 택했다. 이 방법은 몇 가지 문제*)는 있었지만 그럭저럭 잘 사용되었다. 하지만 오늘날에는 C와 호환성이 좋고, C 수준의 퍼포먼스를 보여주면서 객체지향적 개념을 가진 언어는 여럿 있다. 대표적으로 Rust, Go, Vala 등이 그것이다. 그렇기 때문에 C로 코드를 짜야만 하는 상황이 아니라면, 굳이 C 언어로 객체지향을 구현할 바에는 다른 언어로 구현하는 것이 훨씬 효율적이라고 생각한다.

*) Gobject를 사용하면 코드가 지나치게 장황해지고, 복잡해진다. 또한 가독성을 지키기 위해서 언어 외적으로 코딩 스타일을 빡빡하게 규제하고 있다. 그럼에도 가독성이 최악이 되는 것을 면했을 뿐, 좋다고 말하기는 어렵다.

반응형