C 프로그램을 C++, C# 프로그램 처럼
하나의 소프트웨어 개발 프로젝트에서 차지하는 프로그램의 크기가 점점 증가하고 시장의 요구에 따른 소프트웨어 개발 기간은 점차 줄어듦에 따라 기존에 만든 소스 코드의 일부만 수정하여 다시 사용할 수 있는 이식성과 기존 코드에 새로운 기능을 쉽게 추가할 수 있는 확장성이 소프트웨어의 품질을 좌우하는 매우 중요한 요소가 되었다.
1. 절차 지향 -> 데이터 지향 소프트웨어 개발
초등학생 시절 (1990년대 초반) 방과 후 수업반으로 컴퓨터 반에서 프로그래밍을 배웠었는데, 선생님께서는 가끔 팩맨 같은 간단한 게임 프로그램 소스 코드를 출력해 오신 뒤에 학생들에게 직접 타이핑해서 실행해 보게 하셨던 기억이 난다 당시에는 빨리 타이핑해서 게임을 플레이하는게 목적이었기 때문에 코드의 흐름이나 구조를 이해하지는 않고 친구들끼리 서로 경쟁하듯이 키보드 자판을 두드려서 2시간에 걸쳐 무작정 코드를 입력했었다.
하지만 한 가지 잊을 수 없는 게 있었는데, 게임 화면에 등장하는 모든 화면 요소 데이터는 특정한 배열에 포함되어 따로 분리되어 있었고 함수 안에서는 해당 배열을 참조만 할 뿐 화면 요소 데이터가 직접 함수 안에 포함되어 있지 않았다는 것이었다.
이렇게 데이터만 따로 분리해 두면 데이터의 변경 및 추가가 쉬워지고 데이터를 다루는 함수 또한 입출력 구조를 파악하기 쉬워지는 장점이 있다. 잘 작성된 코드의 대부분은 데이터와 데이터를 다루는 함수가 잘 분리되어 있는데 프로그램을 작성할 때 데이터와 함수를 가능한 분리하는 것은 좋은 품질의 소프트웨어를 만드는 첫걸음이라고 생각한다.
이렇게 프로그램 흐름을 모니터링 하거나 변경 또는 추가 가능성이 있는 데이터를 구분하여 자료 구조로 만들고 이 데이터를 다루는 함수를 작성하는 방식으로 소프트웨어를 개발하는 방식을 데이터 지향 소프트웨어 개발 방식이라고 들은 것으로 기억한다. 데이터가 많으면 이를 테이블 형태로 저장하고 관리하며, 필요에 따라서는 데이터베이스 시스템을 이용하기도 한다.
2. 데이터 지향 -> 객체 지향 소프트웨어 개발
객체 지향은 소프트웨어의 이식성과 확장성을 향상시키기 위해 데이터 지향에서 한 단계 더 나아간 소프트웨어 개발 방식이라고 생각한다. 객체는 데이터를 객체의 속성으로, 함수를 객체의 행동으로 표현하고 연관된 데이터와 함수를 하나의 객체 안에 넣은 것이다. 이렇게 할 경우 소프트웨어를 객체 단위로 캡슐화하여 관리할 수 있기 때문에 이식성이 향상되고 객체지향 언어에서 제공하는 객체의 상속 기능을 사용해 원본 소스를 수정하지 않고도 기능 확장을 할 수 있게 된다. JAVA, C# 등과 같은 객체지향 언어에서는 여기에 더하여 프로그래머가 하드웨어나 운영체제에 의존적인 부분을 신경쓰지 않을 수 있도록 각 운영체제 위에서 실행되는 가상 머신을 제공하고 또한 멀티 쓰레딩을 위한 객체와 인터페이스를 제공한다.
객체지향 프로그래밍을 통해 규모가 큰 프로그램을 객체 단위로 나누고 생성된 객체들 사이에 함수 호출을 통한 메시지 전달로 전체적인 프로그램의 흐름과 동작을 구현하며 운영체제와 하드웨어 의존적인 부분을 분리함으로써 이식성과 확장성을 향상시킬 수 있다.
3. C 프로그램을 C++, C# 프로그램 처럼
본론으로 들어가서 절차지향 언어인 C 프로그래밍 언어로 객체지향 언어인 C++, C# 에서 제공하는 객체지향 관련 키워드 없이 객체지향 프로그램을 구현할 수 있을까?
사실 가능하다. 물론 객체지향 언어를 사용하여 객체지향 설계 원칙을 따라 프로그램을 구현한 정도에는 미치지 못해겠지만 말이다. 객체지향 설계 및 객체지향 프로그래밍 기법은 객체지향 언어보다 먼저 생겨났으며 C 프로그래밍 언어를 사용하여 객체지향 프로그램을 구현했던 다수 소프트웨어 엔지니어의 필요에 의해 C 언어에 객체 지향 요소가 추가된 C++ 언어가 탄생했다고 보는 것이 맞을 것이다. 초기 운영체제인 UNIX의 영향을 받은 공개 운영체제인 LINUX의 코드를 살펴보면 C 언어로 작성되었음에도 객체지향적인 설계를 지향하고 있음을 볼 수 있다.
그런데 C 언어는 C++, C# 언어에서 제공하는 객체의 타입인 클래스(class)를 선언할 수 있는 키워드인 class를 제공하지 않는다. 대신에 하나의 C 파일을 하나의 class로 간주하여 하나의 C 파일 안에 동일한 목적으로 연관된 데이터와 함수를 구현한다면 객체지향 요소인 캡슐화를 적용한 것과 다름이 없다. 때문에 설계할 때 가급적 기능을 작은 단위로 나누고 하나의 C 파일은 공통 목적을 갖는 데이터 변수와 해당 변수들을 다루는 함수들로만 이루어져 있어야 한다. 여러 목적의 기능을 수행하는 함수들과 데이터가 서로 뒤섞여 있어서는 안된다.
즉, 공통 목적 및 기능 단위로 소프트웨어 구조를 계층적으로 구분한 뒤에 구분된 각 단위마다 하나의 C 파일을 생성하고 코드를 작성한다. 각 C 파일 안에는 기능 단위의 속성 및 상태를 나타내는 변수들과 이 변수들을 다루는 함수들을 선언, 정의한다.
예를 들어, 아래의 코드는 입방체의 특성을 가지는 어떠한 객체의 단면적과 부피를 계산하는 C++ 코드이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#include <iostream>
using namespace std;
typedef unsigned int uint32;
class Geometry {
private:
uint32 length;
uint32 width;
uint32 height;
public:
Geometry(uint32 length, uint32 width, uint32 height) {
this->length = length;
this->width = width;
this->height = height;
}
~Geometry() {
}
uint32 calculateArea() {
return length * width;
}
uint32 calculateVolume() {
return length * width * height;
}
};
int main() {
Geometry *geometry_1 = new Geometry(10, 5, 3);
Geometry *geometry_2 = new Geometry(7, 3, 6);
cout << "Area of geometry_1 = " << geometry_1->calculateArea() << endl;
cout << "Volume of geometry_1 = " << geometry_1->calculateVolume() << endl;
cout << "Area of geometry_2 = " << geometry_2->calculateArea() << endl;
cout << "Volume of geometry_2 = " << geometry_2->calculateVolume() << endl;
delete geometry_1, geometry_2;
return 0;
}
|
cs
|
위의 코드를 객체지향적 요소를 최대한 유지하면서 C 코드로 바꾼다면 아래와 같이 바꿀 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
#include <stdio.h>
#include <stdlib.h>
typedef unsigned int uint32;
typedef struct {
uint32 length;
uint32 width;
uint32 height;
} Geometry;
void setGeometry(Geometry *geometry, uint32 length, uint32 width, uint32 height) {
geometry->length = length;
geometry->width = width;
geometry->height = height;
}
uint32 calculateArea(Geometry *geometry) {
return geometry->length * geometry->width;
}
uint32 calculateVolume(Geometry *geometry) {
return geometry->length * geometry->width * geometry->height;
}
int main() {
Geometry *geometry_1 = (Geometry *)malloc(sizeof(Geometry));
setGeometry(geometry_1, 10, 5, 3);
Geometry *geometry_2 = (Geometry *)malloc(sizeof(Geometry));
setGeometry(geometry_2, 7, 3, 6);
printf("Area of geometry_1 = %d\n", calculateArea(geometry_1));
printf("Volume of geometry_1 = %d\n", calculateVolume(geometry_1));
printf("Area of geometry_2 = %d\n", calculateArea(geometry_2));
printf("Volume of geometry_2 = %d\n", calculateVolume(geometry_2));
free(geometry_1);
free(geometry_2);
return 0;
}
|
cs |
C언어의 경우 구조체 타입 안에 함수를 정의할 수 없기 때문에 C++ 코드와 같이 class 선언 블록 안에 함수 선언을 넣을 수가 없다. 때문에 블록 바깥에 함수를 정의해야 한다. 하지만 함수 포인터를 구조체 타입 안에 선언하고 함수 포인터가 특정 함수를 가리키도록 초기화 할 수는 있다. 함수 포인터를 활용하는 것은 객체지향 요소인 다형성을 구현하기 위해서도 필요한데 이에 대한 설명과 예제는 다음으로 미루려고 한다.
그리고 각 함수의 첫번쨰 인자로 Geomerty 구조체 타입 포인터를 받게 되어 있는데 C++의 경우 class 내부에 선언된 함수들은 같은 class 내부의 변수들에 접근할 수 있지만 C 프로그래밍에서는 그러한 기능을 지원하지 않기 때문에 접근 대상이 되는 변수를 함수 인자로 전달해 주어야 한다.
또한 C++ 에서는 new 키워드를 통해 객체 인스턴스를 생성했지만, C에서는 이를 지원하지 않기 때문에 힙에 메모리 공간을 할당해서 객체 인스턴스를 생성하는 것과 동일한 결과를 수행하는 라이브러리 함수인 malloc 함수를 사용한다.
그런데 Hard Real time OS (실시간 운영체제)에서는 malloc 함수를 사용하여 메모리를 동적 할당할 경우 할당에 실패할 위험이 있어서인지, malloc 함수를 사용한 동적 메모리 할당을 지원하지 않는다. 이 경우에는 아래와 같이 객체에 해당하는 구조체 타입의 전역 변수를 선언하여 힙 영역(동적 메모리 할당 영역)이 아닌 정적 영역(.rwdata 또는 .bss 섹션)에 메모리를 할당 할 수 있다. 이렇게 할 경우 위의 프로그램은 프로그램 실행 과정에서 Geometry 객체의 메모리가 할당되는데 비해 아래 프로그램은 프로그램이 실행되기 전에 Geometry 객체의 메모리가 고정된 상대 주소로 할당되게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
#include <stdio.h>
typedef unsigned int uint32;
typedef struct {
uint32 length;
uint32 width;
uint32 height;
} Geometry;
static Geometry geometry[2];
void setGeometry(uint32 index, uint32 length, uint32 width, uint32 height) {
geometry[index].length = length;
geometry[index].width = width;
geometry[index].height = height;
}
uint32 calculateArea(uint32 index) {
return geometry[index].length * geometry[index].width;
}
uint32 calculateVolume(uint32 index) {
return geometry[index].length * geometry[index].width * geometry[index].height;
}
int main() {
uint32 index = 0;
setGeometry(0, 10, 5, 3);
setGeometry(1, 7, 3, 6);
while(index < 2) {
printf("Area of geometry_%d = %d\n", index + 1, calculateArea(index));
printf("Volume of geometry_%d = %d\n", index + 1, calculateVolume(index));
index++;
}
return 0;
}
|
cs |
위의 코드에서는 객체를 생성하는 대신 static 한정자를 사용해 파일 안에서만 접근 가능한 Geometry 타입의 전역 변수를 선언하고 기존의 객체지향적인 구조는 유지하였다.
또한, 코드상에서 반복된 부분을 제거하기 위해 배열을 선언하고 인덱스를 통해 Geomerty 변수를 참조할 수 있도록 하였다.
** (수정 필요) 추가적으로 C 파일과 동일한 이름을 가진 헤더 파일(.h)을 만들고 기본 자료형이 정의된 헤더 파일 (예: types.h) 을 include 하고 추가적으로 C 파일에 정의한 정적 전역 변수들의 자료형을 선언하고 인터페이스 함수들의 프로토타입을 선언하여 다른 파일 내의 함수에서 호출할 수 있도록 한다.