메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

한빛랩스 - 지식에 가능성을 머지하다 / 강의 콘텐츠 무료로 수강하시고 피드백을 남겨주세요. ▶︎

IT/모바일

C로 개발하면 안되나요? (2)

한빛미디어

|

2007-11-06

|

by HANBIT

11,597

제공 : 한빛 네트워크
저자 : 문우식
출처 : Blog2Book, 패턴 그리고 객체지향적 코딩의 법칙

구조적으로 더 나은 방법

C로 작성할 경우 기본적으로 지켜야 할 규칙들을 간단히 정리해보면 아래와 같다.
  • 관련된 구조체와 함수를 쉽게 연관지을 수 있는가?
  • 구조체의 변수들은 어떻게 수정되어야 하는가?
  • 의도되지 않은 수정이 이루어졌을 경우 어떻게 동작해야 하는가?
  • 사용해야 될 함수와 말아야 될 함수들은 어떻게 구분하는가?
복잡한 객체지향의 여러 개념은 일단 생각하지 말기로 하자. 우선 중요한 점은 객체란 무엇인가를 이해하는 것이다. 우리를 혼란스럽게만 만드는 어설픈 자동차, 비행기 예제는 넣어 두기로 하겠다. 어설픈 비유로 혼란을 겪어서는 안된다. 바로 눈 앞에 보이는 객체를 다루어 보자.

C++에서는 객체를 클래스(class)로 표현한다. 결론적으로 위에 언급한 4가지를 모두 클래스를 사용하면 쉽게 만족시킬 수 있다. C는 데이터와 함수가 분리되어 있는 언어이므로 언어적으로 이 둘을 묶을 수 없다. 그래서 이 둘을 묶기 위해서 C에서는 구조체에 함수 포인터를 넣는 방법을 사용할 수 있지만 C++은 그런 복잡한 과정 없이 언어 자체에서 데이터와 코드를 묶어서 사용할 수 있는 기능을 제공한다.

어설픈 객체지향의 혼란은“자동차를 예로 들었을 때 바퀴는 속성(Attribute). 시동을 거는 것 따위를 행위(Behavior)”라고 설명하면서부터 시작한다. 전통적인 프로그래밍의 기본은 데이터를 함수로 조작하는 것이다.

반면 객체지향 언어에서 객체라는 것은 메모리상의 데이터를 직접적으로 다루는 로우 레벨(Low-Level)인 기계적 개념이 아닌 좀 더 하이 레벨(High-Level)의 논리적 개념이 기본이 된다. 이 것을 이해하고 나서 여러 가지 객체지향 언어가 가지는 특성을 이해하는 것이 중요한 것이지 정작 객체에 대한 모호한 개념만 가진 채 상속이니 다형성이니 하는 특성만 이해하는 것은 바람직한 방법이 아니다.

왜 C에서 C++로 발전했는지와 위에서 봤던 GIMP코드의 함수 모양을 연관 시켜보면 이해가 빠를 것이다. 개발자가 일일이 신경써 주면서 GIMP처럼 이해하기 쉽도록 만드는데 드는 비용이 100이라면 객체지향 언어에서 제공하는 클래스라는 도구를 이용하면 그 비용을 50 정도로 줄일 수 있다. 이것은 생산성의 문제이다. 객체지향이라는 도구를 이용하면 데이터와 코드를 쉽게 모을 수 있고 의도되지 않은 행동으로부터 데이터를 보호할 수 있다. 이런 기본 개념을 토대로 구조적 프로그래밍으로 개발하면서 필요한 여러 가지 기법(재사용성이나 확장성)들을 C문법 안에서 테크닉적으로 활용하는 차원이 아닌 언어에서 지원해 주는 기본 기능으로 만들어 모든 개발자들이 더 쉽고 생산성 있게 개발할 수 있는 방법이 언어적 관점의 객체지향 개발 방법인 것이다.

C로 만든 String 예제

C를 이용하여 간단한 스트링을 구현해 보자. 우선 스트링을 저장하기 위해 아래와 같은 구조체를 정의하였다. 구체적인 설명은 주석을 참조하자.
typedef struct _ST_STRBUF
{
   intm_nLen; // 스트링 길이
   char* m_pStr; // 스트링 데이터의 힙(Heap)메모리 주소
} ST_STRBUF, * PST_STRBUF;
이 구조체를 다루기 위해 아래와 같은 함수를 추가하였다.
// a_nStrLen만큼 a_pstStr구조체를 초기화한다.
int mystr_new(ST_STRBUF* a_pstStr, int a_nStrLen);

// 문자열 a_pStr을 a_pstStr의 문자열에 할당한다.
int mystr_assign(ST_STRBUF* a_pstStr, char* a_pStr);

// 문자열 a_pStr를 a_pstStr문자열 뒤에 붙인다.
int mystr_append(ST_STRBUF* a_pstStr, char* a_pStr);

// 문자 a_chFind를 a_pstStr에서 찾는다.
int mystr_find(ST_STRBUF* a_pstStr, char a_chFind);

// a_pstStr를 삭제한다.
int mystr_delete(ST_STRBUF* a_pstStr);
작성한 함수는 모두 스트링 구조체(ST_STRBUF)를 다루기 위한 함수들이다. 스트링 구조체와 함수를 이용해서 간단한 샘플 코드를 작성해 보자(리턴값은 무시하겠다).
int main()
{

   ST_STRBUF stStrBuf = { 0, };
   
   mystr_new(&stStrBuf, 256);

   // 초기화를 안한다면 아래 호출되는 모든 함수는 오동작 할 것이다.
   mystr_assign(&stStrBuf, “abc”);
   mystr_append(&stStrBuf, “def”);

   // mystr_assign을 먼저 호출 하지 않았다면 오동작 할 것이다.
   mystr_find(&stStrBuf, ‘d’);
   
   // 2번째 파라미터가 문자가 아닐 경우 오동작 할 수 있다.
   // 문자인지 판단하는 함수가 필요하다.
   mystr_delete(&stStrBuf);

   return 0;
}
위 소스에 혹시나 발생할 수 있는 오동작 가능성에 대해 적어 보았다. 실제 개발을 하다 보면 에러 처리를 위해 코드의 덩치가 커지는 경우가 많다. 이런 경우 모든 예외상황에 맞추어 함수를 하나씩 늘려 가다 보면 어떤 함수를 어떤 경우에 호출해야 하는지를 판단하는 것조차 헷갈리게 된다. 위와 같이 스트링에 관련된 데이터와 함수만을 뽑아 놓고 봤을 때는 복잡하지 않기 때문에 지금 당장 이들을 연관시키는 것이 어렵지 않을 것이다. 하지만 코드가 수만 줄이 넘어가고 프로젝트가 커지면서 점차 ST_STRBUF 구조체를 어떤 식으로 다루어야 하는지 정확히 알 수 없게 될 것이다. 또 커뮤니케이션의 부재로 인해 ST_STRBUF 구조체를 약속된 함수가 아닌 임의의 방식 으로 조작하여 프로그램이 오동작하는 결과를 초래할 수도 있다. ST_STRBUF 구조체를 전혀 연관성 없는 다른 함수에 대입시켜서 데이터가 깨졌다면 그 원인이 mystr 계열 함수 때문인지 아니면 개발자의 단순한 실수인지 파악하기도 쉬운 일은 아니다.

C++로 만든 스트링 클래스 예제

C++을 사용하여 위와 동일한 기능을 하는 스트링을 구현해 보자.
class CMyString
{
public:
   int Assign(char* a_pStr);
   int Append(char* a_pStr);
   int Find(char a_chFind);

public:
   CMyString(int a_nSize);
   ~CMyString();

private:
   int m_nLen;
   char* m_pStr;
};
스트링과 관련된 변수와 함수를 모아서 CMyString 클래스 안에 집어넣었다. 이 클래스를 활용해서 샘플 코드를 작성해 보자.
int main()
{
   CMyString str(256);
   
   str.Assign("abc");
   str.Append("def");
   str.Find("d");

   return 0;
}
우선 호출되는 함수가 어떤 클래스 소속인지 명확하므로 프로젝트가 커진다고 해서 어떤 함수를 호출해야 하는지 헷갈릴 가능성이 훨씬 줄어들었다(없다라는 뜻은 아니다). 그리고 변수를 private 영역에 넣어 클래스 외부에서 접근이 불가능하도록 만들었으므로 변수를 잘못 조작해서 프로그램이 오동작 할 가능성을 없앴다. 또 힙(Heap)메모리의 생성과 소멸도 CMyString 클래스의 생성자, 파괴자 안에서 알아서 동작할 것이므로 직접적으로 메모리를 다루어서 발생할 수 있는 문제점을 제거하였다.

같은 기능을 C와 C++코드로 예를 들었지만 진정한 객체지향의 힘은 위에 보여지는 코드 몇 줄을 비교한다고 얻어지는 것이 아니다. 개발을 해 나가면서 코드를 추가하고 기능을 확장할 때 객체지향 언어가 지원해 주는 막강한 기능을 활용한다면 더 안정적이고 쉽게 덩치를 키워 나갈 수 있다.

정리

백 줄짜리 코드를 디버깅하고 확장하는데 백 원이 든다면 천 줄짜리 코드를 디버깅하고 확장하는 데는 천원이 아닌 만원이 든다. 그 이상은 코드의 줄 수에 따른 단순 비례가 아닌 기하급수적으로 늘어나는 비용을 지불하게 될 것이다. 사실 천줄 단위의 프로그램은 구조적 프로그래밍을 통해 작성하나 객체지향 프로그래밍을 통해 작성하나 그 효과는 별반 차이가 없다. 많은 오해가 이런 간단한 예제를 통해서 형성되는데 현실적으로 최소한 수 만줄 이상의 코드를 작성할 때 진정한 개발 방법론의 가치를 알수 있다.

C가 처음 세상에 나왔을 때 C는 기존 프로그래밍의 단점을 혁신적으로 보강해서 나온 언어였다. 그 당시의 프로그램은 현재의 프로그램과 그 형태나 크기가 많이 달라 구조적 프로그래밍만으로도 개발하는데 문제가 없었다. 하지만 프로그램이 점점 커지자 구조적 프로그래밍만으로는 너무 많은 비용이 소모되었다. 이를 극복하기 위해 더 좋은 방법에 대한 탐구가 모색되어 왔고 그 결과 객체지향 프로그래밍이 등장한 것이다. 객체지향 프로그래밍은 구조적 프로그래밍이 가지는 단점을 보완하면서 더 큰 프로그램을 개발하는데 적합한 개념과 여러 가지 언어적 지원이 덧붙여진 연구와 경험의 산물이다.

본 장에서는 C와 C++을 사용하여 아주 기초적인 부분에서의 비교만을 해보았을 뿐이다. 여러분들이 실전에서 큰 프로그램을 작성할 때 느끼는 근본적인 개발 방법론의 차이는 훨씬 더 크다고 할 수 있다. 하지만 오해하지 말아야 할 점은 두 방법 중 어느 한 방법에 도저히 극복할 수 없는 치명적 결함이 있다는 것은 아니라는 것이다. 객체지향이 더 나중에 나온 방법이긴 하지만 구조적 프로그래밍 역시 당당히 오랫동안 쌓아 온 위치를 지켜 왔으며 그 오랜 시간 만큼 단점들을 극복할 수 있는 여러 가지 기법들이 개발되어 왔다. 반면 객체지향은 더 나중에 나온 만큼 기존의 어렵고 높은 레벨의 개발자들만이 영유했던 개념이나 테크닉들을 언어적으로 지원해서 생산성을 높인 방법이라고 할 수 있다. 막연히 서로 다른 방법이라고 보지 말고 언어가 발전해 온 역사를 이해하라. 또 현재 진행형으로 발전해 가는 모습을 통해 어떤 부분들이 추가, 삭제되는지를 살펴 본다면 언어에 대해 근본적으로 이해하는데 많은 도움이 될 것이다.
TAG :
댓글 입력
자료실

최근 본 상품0