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

한빛출판네트워크

IT/모바일

Trax, 그만의 코딩이야기(1)

한빛미디어

|

2006-10-12

|

by HANBIT

17,314

저자: 한동훈

가변길이 배열

gcc로 코드를 작성할 때 구조체 안에 비어있는 배열을 선언해서 가변 길이의 데이터를 저장할 수 있습니다. 이런 것을 어디에 쓰냐고 물을지도 모르지만, 쓰이긴 쓰입니다.

예를 들어, TCP/IP에서 모든 패킷의 길이는 전달하는 데이터의 크기에 따라 다릅니다. Ping과 같은 서비스에서 패킷은 수십바이트에 달하지만, 영화를 다운 받을 때 패킷의 크기는 1000 바이트를 넘습니다. 항상 고정된 길이로 전달하는 방법보다는 길이에 따라 데이터 저장 공간의 크기를 동적으로 조정하는 것이 좋습니다. 다음과 같이 선언하면 간단하게 가변길이 배열을 만들 수 있습니다.

struct empty
{
  char fname[0];
};

적절하게 메모리 공간만 할당한다면 구조체안에 포함되는 가변길이 데이터를 만들 수 있습니다. char *fname으로 선언한다면 포인터가 되며, 차지하는 공간은 포인터가 차지하는 4 바이트만 됩니다. char fname[0]로 선언한다면 차지하는 공간은 0바이트가 되며, 실제 데이터를 집어넣으면 그만큼의 공간을 구조체에 담게 됩니다.

포인터로 선언하면 해당 데이터는 파일로 저장되지 않습니다. 포인터를 고려해서 파일로 저장하도록 별도의 저장 함수를 만들어야 합니다. 포인터를 파일로 저장하면 포인터의 주소값만 저장될 뿐, 포인터가 가리키는 데이터가 제장되지는 않습니다.
다음은 empty 구조체의 메모리 공간을 알아보기위해 작성한 간단한 테스트 프로그램입니다.

#include

struct empty
{
  char fname[0];
};

struct one
{
  int one;
  char fname[0];
};

int main(void)
{
  printf( "empty size: %d\n", sizeof( struct empty ) );
  printf( "one size: %d\n", sizeof( struct one ) );

  return 0;
}

소스 코드를 컴파일하고 실행하면 다음과 같은 결과를 볼 수 있습니다.

coffee:~/c# ./size
empty size: 0
one size: 4

구조체 empty의 크기는 0바이트이며, 구조체 one에는 one과 fname이 선언되어 있음에도 불구하고 int 형만 계산한 4바이트가 할당됩니다. 즉, fname에 어떤 데이터가 없으면 0바이트입니다. 구조체안에 파일 이름, 디렉터리 경로명과 같이 가변적으로 변하는 데이터가 있으면 char path_name[256]과 같이 배열을 사용해서 불필요한 공간을 낭비하는 것 보다는 가변길이 배열을 사용해서 실제 데이터를 저장하는 방법을 선호합니다.

C 언어에서는 다른 고급 언어들처럼 문자열의 길이를 알 수 있는 방법이 없습니다. 따라서, 가변 길이 배열을 사용할 때, 데이터의 크기를 저장하기 위해 변수를 하나 사용합니다. empty 구조체의 선언은 다음과 같이 변경되어야 하겠죠.

struct empty
{
  unsigned long fname_size;
  char fname[0];
};

가변 길이 데이터를 다룰 때 주의할 점은 한 가지가 더 있습니다. C 언어에서는 문자열이라는 것이 없으며, 문자들의 연속된 나열로 문자열을 표현하고, 문자열의 끝을 널(NULL)로 표기합니다. 네트워크 패킷 전달과 같은 용도로 가변 길이 배열을 사용한다면, 패킷 데이터의 중간에도 데이터의 일부로 NULL이 들어갈 수 있습니다. 때문에, fname_size 변수로 데이터의 정확한 길이를 유지합니다. 지금 소개할 예제처럼 문자열을 저장하기 위해 사용한다면 해당 데이터의 끝에 널(NULL) 문자를 추가해주는 것을 잊지 말아야 합니다.

#include
#include
#include
#include
#include

struct empty
{
  unsigned long fname_size;
  char fname[0];
};

int main(void)
{
  struct empty emp;
  char *str = "hello";
  int i;

  for( i = 0; i < strlen( str ); i++ )
  {
    emp.fname[ i ] = *(str + i );
  }
  emp.fname[ i ] = 0;
  emp.fname_size = i;


  printf( "fname_size: %lu\n", emp.fname_size );
  printf( "fname     : %s\n", emp.fname );
  printf( "empty size: %d\n", sizeof( emp ) );

  return 0;
}

여기서는 우직하게 for 루프를 돌면서 문자 하나하나를 복사하고, 마지막은 널(NULL) 문자를 추가해서 문자열의 끝을 나타냈습니다. 문자열 "hello" 대신 다른 문자열을 넣어도 제대로 동작한다는 것을 알 수 있습니다.

중첩 함수

함수 안에 함수를 정의하는 기능을 말합니다. 일부 스크립트 언어들과 Java, C# 같은 언어들은 함수 안에서 임의의 함수를 정의하는 기능을 제공합니다. 이 부분은 ANSI C99 표준은 아니며 gcc 확장입니다. gcc 확장 기능이 ANSI C99 표준에 포함된 것도 있기 때문에 gcc 환경에서만 작업한다면 중첩 함수 기능도 고려해 볼만 합니다.

표준화와 TCO(소유총비용)

TCO(Total Cose of Ownership)은 기업에서 PC, 서버 같은 시스템을 도입한 이후에 직원들의 교육 비용, 업그레이드 비용, 유지 보수 비용 등을 모두 합친 개념입니다.

오라클(Oracle)과 같은 상용 데이터베이스 시스템을 사용하면 관련 서비스를 모두 오라클의 것만 사용하게 되어 있고, 나중에 다른 데이터베이스 시스템으로 옮기려해도 이전 비용이 너무나 높기 때문에 이전하지 못하고 해당 벤더에 종속된다 - 이를 벤더 감금 효과(Vendor Lock-in)이라 함 -고 하여 표준 기술만 사용하는 것을 강조했습니다.

이 때문에 표준화된 표준 기술만을 사용하는 것이 다른 S/W 패키지로의 이전도 쉽게 하며, TCO 측면에서도 이득이 될 것이라고 얘기했었습니다.
최근에는 이와 반대의 주장들이 나오고 있어 논란이 되고 있습니다. 이 주장은 어차피 기업에서 특화된 서비스를 제공하려면 표준에서 제공하지 않는 특화된 기능을 제공하는 벤더들의 제품을 써서 정보 시스템의 효율화를 꽤하는 것이 TCO를 낮추는 것이다라는 것입니다.

많은 사람들은 오라클에서 PL-SQL을 사용할 경우 편하게 할 수 있는 확장 기능, MS SQL-Server를 사용할 경우 확장 프로시저를 사용해서 기능을 편하게 확장할 수 있는 기능이나 T-SQL 고유의 함수나 기능을 사용하는 것을 표준이 아니다라는 이유만으로 알러지 반응을 보였습니다. 그러나, 개인적으로는 어떤 일에 대해 최적화된 기능을 제공한다면 표준을 고집하지 말고 쓰면 된다고 생각합니다.

중첩 함수 기능은 현재로서는 gcc에서만 제공하는 확장기능이지만, 먼 훗날에는 C 언어 표준이 될지도 모르는 일입니다. 또한, 리눅스 환경에서 gcc를 주로 사용해서 작업하는 경우엔 리눅스 고유의 헤더 파일들을 많이 사용합니다. 리눅스 환경 고유의 것들을 사용해서 작업하며, 대부분의 경우에 윈도우 같은 다른 시스템에서 사용하는 일들은 없는 것들이었습니다. 마찬가지로, VC++을 사용하고, MFC를 주로 사용해서 개발작업을 한 경우, 윈도우에서만 동작하는 프로그램을 위한 것이지, 리눅스 같은 환경으로 이식할 일이 없었습니다. 해당 OS에 특화된 개발 플랫폼을 사용하는 것이 소프트웨어 개발에 있어 가장 효율적이며, 10년후가 될지, 20년 후가 될지 알지도 못할 포팅 가능성을 염두에 두며 표준만을 고집해서 코딩하는 것이 비효율적인 일이라 생각합니다.

만약, 윈도우와 리눅스에서 함께 실행되어야 하는 프로그램을 개발해야 한다는 과제가 주어진다면 처음부터 이를 고려한 라이브러리와 언어 선정을 할 것입니다.

다음은 main() 함수 안에서 nested() 함수를 정의하고, 이 함수를 호출합니다. 다른 함수들은 nested() 함수를 볼 수 없습니다.

#include

int main( void )
{
  int i = 10;

  int nested( void )
  {
    printf( "i in main: %d\n", i );
    return i + 20;
  }

  i = nested();
  printf( "i: %d\n", i );

  return 0;
}

gcc로 컴파일하고 실행하면 다음과 같은 결과를 볼 수 있습니다.

coffee:~/c# ./nested_func
i in main: 10
i: 30
coffee:~/c#

마치면서

최근에는 C 언어를 많이 사용하고 있기 때문에 C 언어에 대한 이야기를 주로 했습니다. 다음 글에서도 C 언어에 대해서 이야기 하겠지만, 언어에 관계없이 코딩과 관련해서 생각해 볼 점들에 대해서 이야기하고 있다는 점도 알았으면 합니다.
TAG :
댓글 입력
자료실