engineering/System Eng.2007. 2. 26. 10:41


1회 | C 복습! 컴파일, 그리고 소스 분리하기
2회 | 포인터는 우스워
3회 | 에러없는 프로그램에 도전한다
4회 | 소스 분석하기
5회 | 디버깅하기
6회 | C로 임베디드 시스템 맛보기
7회 | 임베디드 리눅스 애플리케이션 개발하기




포인터는 C 프로그래머가 사용할 수 있는 가장 강력한 무기입니다. 포인터가 없는 C는 C라고 할 수 없고 마찬가지로 포인터를 제대로 사용할 수 없는 C 프로그래머라면 C를 반만 사용할 줄 아는 것이라고 이야기해도 좋습니다. 또 C가 임베디드 시스템에 적합한 이유 중의 하나는 바로 이 포인터가 있어서라고 감히 이야기할 수 있습니다. 그만큼 포인터는 C에서 중요한 요소입니다.
C를 처음 학습하면서 포인터의 예제로 가장 많이 등장하는 함수는 아마도 swap()이라는 함수일 것입니다. 왜 포인터를 사용해야만 하는지에 대해서 그리고 함수에서 인자를 전달하는 방식을 설명하기 위해 가장 짧고 명확한 예제라고도 할 수 있겠지요.

void swap(int *a, int *b)
{
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
}
하지만 이 swap이라는 예제는 여전히 포인터의 수많은 얼굴을 설명해 주기에는 부족합니다. 당장 필자의 오래 전 기억을 되살려 봐도 swap이라는 예제를 보고 포인터를 떠올리면서 대체 ‘&’는 왜 등장하는 것인지, int *a와 &a와 *a는 대체 어떻게 다른 것인지를 이해할 수 없었습니다(게다가 C도 제대로 모르는 주제에 배웠던 C++ 덕분에 &에 대한 혼란은 대단했었지요).
이번 호에서는 설명보다는 코드를 많이 적어두려고 합니다. 프로그래밍 학습은 백 마디의 말보다는 한 줄의 코드가 모든 것을 설명해준다는 것이 진리이지요. 예제 코드에는 가능한 많은 내용을 한 번에 담아두려 노력했습니다. 꼭 한 번씩 실행해보고 값을 바꾸며 실험하면서 미묘한 차이점을 직접 확인해 보기 바랍니다.


포인터는 대체 무엇일까
포인터는 단어 그대로 ‘가리키는 것’에 불과합니다. 한번 더 풀어서 이야기하면 ‘포인터는 메모리의 번지를 값으로 가지는 정수형 변수’입니다. 다시 한번 강조해 봅니다. 포인터는 32비트 시스템에서 32비트(= 4바이트)의 크기를 가지는 정수입니다. 물론 64비트 시스템에서라면 포인터는 당연히 64비트가 되는 것이 맞습니다. 왜일까요? 포인터의 값은 ‘메모리의 주소’이기 때문입니다. 이 시점에서 C의 변수형에 대해 복습도 할 겸 <리스트 1>의 코드를 한번 실행시켜 보세요.
<리스트 1>의 실행 결과를 보면 포인터는 모두 4바이트의 크기라는 사실이 증명되었습니다. 여기서 포인터의 속성을 조금 더 파악해보기 위해 <리스트 2>도 한번 실행시켜 보기 바랍니다. 물론 표시되는 주소값은 실행시키는 환경마다 다르겠지요. 현재 필자가 글을 쓰고 있는 환경에서는 이런 결과가 되었습니다.
<리스트 2>의 결과값을 제대로 이해할 수 있다면 독자는 기본적인 포인터의 개념을 확실히 알고 있는 것입니다. 이 실행 결과에서 pa+1의 값을 잘 살펴보기 바랍니다. 분명히 pa=2293484였는데, pa+1은 2293488이 되었습니다. 포인터를 이해하기 위해서는 이 점을 꼭 짚고 넘어가야 합니다. 왜 pa+1은 1이 아니라 4가 늘어난 것일까요?
포인터는 정수형 변수이지만 메모리의 위치를 값으로 가집니다. 그렇다면 대체 int *p와 long *p는 무엇이 다른 것일까요? 어차피 똑같은 정수값을 가진다면 값을 구분할 필요가 없을텐데…. 포인터의 변수형은 바로 포인터의 값을 증가시키는 시점에 의미를 가지게 됩니다. <리스트 2>의 예제에서 pa+1을 했을 때 pa는 정수형 포인터이므로 ‘메모리 상에서 다음 정수가 있어야 할 위치’를 가리키게 되는 것입니다. 물론 문자형 포인터였다면 1만큼만 증가하는 것이 맞겠지요. 그렇다면 만일 int a를 short a로 바꾸고 int *pa를 short *pa로 바꾸면 실행 결과는 어떻게 달라질까요? 직접 확인해 보세요.
포인터를 사용하기 위해서는 꼭 숙지하고 있어야 할 두 가지 기호가 있습니다. 바로 &와 *입니다. 아직 몰랐던 독자라면 꼭 기억하기 바랍니다. &는 주소, *는 주소가 가리키는 값입니다. 다시 말하면 a == *(&a)입니다. 포인터는 주소를 가리키는 값이므로 int *pa = a라고 하면 옳지 않은 연산이 됩니다. 반드시 int *pa = &a라고 해야 맞습니다.




배열과 포인터
결론부터 이야기하겠습니다. 배열과 포인터는 ‘같습니다’. 배열로 선언한 변수는 그대로 포인터로 사용해도 무방합니다. 공식을 적어볼까요. a = &a[0]이라고 암기하고 있어도 좋습니다. 배열과 포인터를 동일하게 취급할 수 있다는 이 사실은 실제로는 char의 배열인 문자열을 char의 포인터로 그대로 다룰 수 있다는 것과도 같은 말이 됩니다.

  문자열과 포인터
C에서의 문자열은 배열과 포인터의 개념이 없이는 이해할 수 없습니다. 물론 이 글을 읽고 있는 독자들은 C에서의 문자열이 ‘\0’으로 끝나는 문자의 배열이라는 사실 정도는 이미 알고 있으리라 생각합니다. 지금 이야기하려고 하는 내용은 문자열을 처리하는 함수를 작성하면서 포인터를 이용하면 배열을 이용하는 것보다 아주 간결하게 작성할 수 있는 경우가 많다는 것입니다. 문자열의 길이를 구하는 함수를 배열과 포인터의 두 가지 개념으로 작성해 보았습니다. 포인터를 이용해 strtok() 같은 함수를 구현한다면 포인터를 이용한 스트링의 처리에 대해 많은 것을 생각해 볼 수 있을 것입니다.



<리스트 3>에서 주의할 것은 문자열을 얻어내는 함수는 예제를 위해 아주 간단하게 구성한 것입니다. 실제로 제품이 될 코드를 작성하면서 이런 식의 ‘엄밀하지 못한 코드’를 작성하는 것은 자살행위(?)와도 같습니다. 꼭 명심하세요. 에러를 줄일 수 있는 엄밀한 코드의 작성에 관해서는 다음 호에서 다룰 예정입니다. 일단 궁금해 할 독자들을 위해 간단히 설명하면 앞의 함수에서는 포인터가 잘못된 값이 아닌지 미리 검사해줘야 합니다.


동적 메모리 할당
포인터와 동적 메모리의 할당은 정말 떼려고 해도 뗄 수가 없는 밀접한 관계에 있습니다. 포인터와 동적 메모리 할당이라는 것이 없으면 아마 프로그램을 작성하는 것이 불가능할지도 모르겠습니다. 하지만 C가 어려운 이유는 바로 이 부분 때문입니다. C는 지극히 간단한 언어이므로 결코 배울 내용이 많지 않은데도 C로 제대로 프로그램을 작성할 수 있는 프로그래머를 찾기 힘든 이유는 바로 포인터와 메모리 때문입니다. 훌륭한 C 프로그래머라면 바로 이곳에서 발생하는 문제들을 비교적 쉽게 해결하거나 예방할 수 있는 자신만의 노하우나 원칙을 틀림없이 가지고 있습니다.
초심자들이 포인터를 이용하면서 범하는 가장 큰 실수는 바로 배열에 메모리를 할당하거나 또는 포인터에 메모리를 할당하지 않고 사용하는 것입니다. 두 가지는 포인터와 ‘실제 사용하는 공간’을 혼동하기 때문에 생겨나는 일이라고 해도 좋습니다. 가장 큰 원칙은 다음과 같습니다.

① 포인터는 동적 할당, 또는 변수의 주소 할당을 통해 가리킬 공간을 지정해 주기 전에는 사용할 수 없다.
② 변수는 선언하는 것과 동시에 자동으로 공간이 할당된다. 물론 배열은 변수의 모음이므로 역시 공간이 이미 할당되어 있다.

다음 함수들을 살펴봅시다.

int wrong_malloc_to_array()
{
  int array[10];
  array = malloc(10);
}

이 함수는 문법조차 잘못된 함수입니다. 배열에 메모리를 할당할 수는 없습니다. 하지만 필자는 a = &a[0]라는 사실에 얽매여 왜 저런 식의 코드가 잘못된 것인지 한참 궁금해 했던 적이 있었습니다. 하지만 &a[0]은 값을 대입할 수 있는 변수가 아니므로 이 방법은 완전히 잘못된 것입니다.

int wrong_free_array()
{
  int array[10];
  free(array);
}

이런 코드를 과연 만들게 될까 생각하지만 의외로 malloc과 free에 대해 이해하지 못한 상태에서 이런 코드를 작성하는 경우를 필자는 무척 많이 보았습니다. 바로 앞의 예와는 달리 &array[0]에 값을 대입하는 코드가 아니므로 이 예제는 논리적으로는 완전히 잘못되었지만 문법적으로는 틀린 점이 없습니다.

int wrong_invalid_pointer()
{
  int *pa=NULL;
  *pa = 0;
}

다음은 포인터만을 할당해 둔 상태에서 가리킬 장소는 정해두지 않았지만 그곳에 값을 대입하려고 한 경우입니다. 실제로 이런 실수를 저지르는 경우는 아주 많습니다. 특히 코드가 복잡해지면서 이런 실수를 저지르는 경우는 무척 많습니다. 같은 예를 하나 더 들어보겠습니다.

int wrong_invalid_pointer()
{
  int *pa=NULL;
  pa = (int *)malloc(10);
  /* 코드 30줄 정도 */
  free(pa);
  /* 다시 코드 30줄 정도 */
  *pa = 0;
}


여기서 보인 것과 완전히 동일한 경우이지만 이런 실수는 의외로 찾아내기 힘이 드는 경우가 많습니다.


int wrong_no_free()
{
  int *pa = NULL;
  pa = (int *)malloc(10);
  *pa = 0;
  pa = (int *)malloc(5);
}

int wrong_two_free()
{
  int *pa = NULL;
  pa = (int *)malloc(10);
  free(pa);
  free(pa);
}


역시 잘못된 경우를 두 가지 보였습니다. 왜 잘못되었는지 찾는 것은 어렵지 않겠지요. 배열과 포인터를 설명하면서 배열과 포인터는 ‘같은 것’이라고 단정지었지만 이제는 다시 ‘다른 것’이라고 이야기를 할 때가 된 것 같습니다. 값을 사용하기 위해서는 배열과 포인터는 완전히 같은 방법으로 접근할 수 있습니다. 하지만 포인터에는 값을 대입할 수 있지만 배열에 포인터와 같은 방식으로 값을 대입하는 것은 잘못된 것입니다. 다음 예를 한번 살펴보기 바랍니다.


int wrong_string_assignment(const char *str)
{
  int array[10];
  int *pa;

  pa = str; /* Good */
  array = str; /* Bad : &array[0]에는 값을 대입할 수 없음 */

  strncpy(pa, str, 10); /* Bad : pa가 가리킬 공간이 할당되지 않음 */
  strncpy(array, str, 10); /* Good */

  pa = (char *)malloc(10);
  strncpy(pa, str, 10); /* Good : pa에 공간이 할당된 상태 */
  pa = str; /* Bad : 할당한 공간을 free하지 않고 다시 다른 값을 할당 */
}


이번에 보인 예제는 문자열의 처리와 관련하여 자주 범하게 되는 실수를 보여주고 있습니다. 특히나 생각하기 어려운 것 중의 하나는 문자열 배열( == char **, char [][], char *[] )의 경우인데, 특히 이런 경우에는 메모리를 할당하는 것을 잊고 코드가 작동하지 않아 고민하는 경우가 무척 많습니다.


int wrong_pchar_array()
{
  char *array[10];
  strcpy(array[0], “hello, jiny!”);
  /* Bad! : array[0]는 char *이며 할당되지 않은 공간 */
}


같은 내용의 코드는 strdup()를 이용하면 간편하게 작성할 수 있습니다. strdup()는 malloc()과 strcpy()를 합친 함수라고 할 수 있습니다.


int wrong_pchar_array()
{
  char *array[10];
  array[0] = strdup(“hello, jiny1!”);
  array[1] = strdup(“hello, jiny2!”);
  /* 함수를 종료하기 전에 반드시 free()해 줘야 한다 */
}


가장 고민을 많이 하는 경우는 배열을 사용하지 않고 두 개의 포인터로만 구성하는 경우입니다. 포인터 두 개를 이용한 문자열 배열을 만드는 코드를 적어두겠습니다.


#include

int main()
{
  char **ppchar=NULL;

  ppchar = (char **)malloc( sizeof(char *)* 10);
  memset(ppchar, 0, sizeof(char *) * 10 );

  ppchar[0] = (char *)malloc( sizeof(char) * 15);
  strncpy( ppchar[0], “hello, jiny!”, 14);

  ppchar[1] = strdup(“hello, jiny2!”);
  *(ppchar+2) = strdup(“hello, jiny3!”);

  free(ppchar[0]);
  free(ppchar[1]);

  printf(“ppchar[2] = %s\n”, ppchar[2]);
  free(ppchar[2]);
}


ppchar에는 포인터 타입의 변수를 동적 할당하는 것을 유심히 보아두기 바랍니다. 그리고 다시 ppchar[1]에 strdup()로 문자열을 할당한 것, 그리고 ppchar[2]를 포인터 변수를 이용해 접근한 부분을 유심히 보아 두기 바랍니다. 가장 이해가 되지 않을만한 부분은 *(ppchar+2) = strdup(“hello, jiny3!”)라는 부분이 될텐데, ppchar는 char **형이므로 *ppchar는 char *형이 됩니다. 따라서 문자열을 대입할 수 있는 공간인 것이지요. 여기에 보인 샘플만 이해한다면(아마 이해하기 쉽지 않으리라 생각합니다) 포인터와 관련된 대부분의 문제는 간단히 해결할 수 있습니다.
지금까지 몇 가지의 오류를 통하여 포인터를 사용하면서 범하기 쉬운 실수들에 대해 살펴봤습니다. 소개한 코드는 일부러 눈에 보이기 쉽게 만든 샘플이라는 것을 명심하세요. 실제로 부딪치는 오류들은 적은 것보다 훨씬 복잡한 모양이 됩니다. 하지만 독자 여러분들이 실제로 프로그램을 작성하면서 만나게 되는 대부분의 오류는 필자가 적어둔 잘못된 코드 어딘가에 있습니다. 앞에서 적어둔 잘못된 코드들을 완전히 이해한다면 막막하게만 느껴지던 포인터가 정리되어 가는 것을 느낄 수 있을 것입니다.  함수와 포인터
지금까지 포인터에 대해 간단하게 알아봤습니다. 하지만 포인터의 진가는 바로 함수 포인터를 자유자재로 사용할 때에야 알 수 있습니다. 함수 포인터는 ‘함수에 대한 포인터’입니다. 포인터는 주소를 가리키는 정수형 변수라고 앞에서 이야기하였지요. 보통 생각하는 포인터와 함수 포인터가 다른 점은 변수에 대한 포인터는 ‘데이터가 저장되어 있는 주소의 값’이지만, 함수 포인터는 ‘함수가 실행될 프로그램의 기계어 코드가 시작되는 주소의 값’이라고 생각할 수 있습니다. 어쨌든 주소의 값이라는 점에서는 다른 점이 없지요. 이 함수 포인터를 사용하면 아주 아름다운 코드를 많이 만들 수 있습니다. 역시 결론부터 말하면 ‘함수를 하나의 이름으로 호출하지만 다른 함수를 실행하기 위해’ 함수 포인터를 사용합니다.
필자는 꽤 오래 전 gnuplot이라는 프로그램의 소스코드를 읽어 본 적이 있습니다. 이 프로그램은 각종 함수에 대해 그래프를 그려주는 프로그램인데 필자에게 함수 포인터의 아름다움을 실감하게 해준 코드였습니다. 그래프를 화면에 출력하기 위해서는 무수히 많은 터미널들을 지원해야 하는데 gnuplot에서는 이 부분을 드라이버로 만들고 함수 포인터를 이용하여 중요한 코드는 모두 한 가지로만 구현하고 있습니다. 지금 생각해보면 지극히 당연한 일인데도, 당시 이 코드를 본 필자는 너무나도 아름다운 구조에 흥분해 있었던 기억이 납니다.
함수 포인터를 가장 간편하게 사용해 볼 수 있는 기회는 아마도 표준 C 라이브러리에 포함된 qsort()나 bsearch()와 같은 함수가 될 것입니다. 이 함수들은 정렬이나 검색에서 가장 중요한 역할을 하는 비교 함수를 함수 포인터로 전달할 수 있도록 해 놓았기 때문에 단순히 숫자나 문자열의 정렬이나 검색 이외에도 자신이 원하는 자료형을 자신이 원하는 방식대로 정렬하고 검색할 수 있도록 구성되어 있습니다.
함수 포인터가 많이 사용되는 또 한 가지의 경우라면 콜백함수를 들 수 있습니다. 콜백 함수란 ‘이러이러한 조건이 만족되면 그때쯤 이 함수를 실행하렴’이라는 의미로 미리 등록해두는 함수를 의미합니다. 꽤 어려워 보이지만 사실은 다음 qsort 예제에 사용된 compar_int 함수 등도 일종의 콜백에 속하는 것이라 생각해도 무방합니다. 실제로는 ‘타이머를 두고 5분 후에 이 함수를 실행시킬 것’과 같은 경우가 많습니다.
표준 C 라이브러리의 qsort 함수를 이용하여 배열의 원소를 정렬하는 예제를 간단히 적어봅니다. compar_int와 compar_int2만을 바꿔 주는 것으로 qsort라는 함수의 결과가 완전히 달라진다는 사실은 함수 포인터를 마음먹기에 따라 얼마나 효율적으로 사용할 수 있는지를 잘 보여줍니다.

#include

static
int compar_int2(const void *a, const void *b)
{
int x = *( (int *)a);
int y = *( (int *)b);

if ( x> y) return -1;
if ( x==y) return 0;
return 1;
}


static
int compar_int(const void *a, const void *b)
{
int x = *( (int *)a);
int y = *( (int *)b);

if ( x> y) return 1;
if ( x==y) return 0;
return -1;
}


int main()
{
int array[] = {10,3,5,2,1,6,51};
int i=0;

qsort(array, 7, sizeof(int), compar_int );

for (i=0; i< 7; i++)
printf(“array[%d] = %d\n”, i, array[i]);

qsort(array, 7, sizeof(int), compar_int2 );
for (i=0; i< 7; i++)
printf(“array[%d] = %d\n”, i, array[i]);

}


실행 결과
array[0] = 1
array[1] = 2
array[2] = 3
array[3] = 5
array[4] = 6
array[5] = 10
array[6] = 51
array[0] = 51
array[1] = 10
array[2] = 6
array[3] = 5
array[4] = 3
array[5] = 2
array[6] = 1
프로그래머는 코드로 이야기합니다. 필자가 즐겨 사용하는 함수 포인터를 이용한 테크닉 하나를 적어둡니다. 커맨드에 따라 적당한 함수를 호출하는 간단한 예제인데, do_cmd()에서와 같은 코드를 do_cmd_using_function_pointer()와 같은 코드로 고쳐서 사용하면 나중에 커맨드가 추가되더라도 테이블에 한 줄을 추가해 넣기만 하면 됩니다. if-else가 반복되는 것보다는 훨씬 깔끔한 코드가 되겠지요. 하지만 함수 포인터를 너무 복잡하게 사용하면 디버깅이 힘들어진다는 단점이 있기도 합니다.

#include

void _do_hello()
{
  printf(“command was HELLO\n”);
}


void _do_jiny()
{
  printf(“command was JINY\n”);
}


void _do_none()
{
  printf(“command was NONE\n”);
}


void do_cmd(const char *cmd)
{
  if ( strcmp(cmd, “hello”)==0 )
    _do_hello();
  else if ( strcmp(cmd, “jiny”)==0 )
    _do_jiny();
  else
    _do_none();
}


struct {
  const char *cmd;
  void (*cmd_handler)(void);
}

cmdtbl[] = {
  { “hello”, _do_hello},
  { “jiny”, _do_jiny},
  { NULL, _do_none},

};


void do_cmd_using_function_pointer(const char *cmd)
{
  int i=0;
  for (i=0; cmdtbl[i].cmd!=NULL; i++)
  {
    if ( strcmp(cmdtbl[i].cmd, cmd)==0 )
    {
      (*cmdtbl[i].cmd_handler)();
      return;
    }
  }
  (*cmdtbl[i].cmd_handler)();
  return;
}

main()
{
  const char *cmd=”hello”;


  printf(“Using IF-ELSE... \n”);
  do_cmd(cmd);


  printf(“Using Function Pointer Table\n”);
  do_cmd_using_function_pointer(cmd);
}  


  플러그인을 만들자
함수 포인터는 실제로 프로그램을 작성하면서 정말 많은 부분에서 유용하게 사용할 수 있습니다. 하지만 지금까지 본 내용에서는 실행되는 프로그램의 내부에 있는 함수만을 함수 포인터를 이용해 실행할 수 있었지요. 이제 프로그램을 수정하지 않고 외부의 모듈을 추가해 프로그램의 기능을 확장시키는 방법을 알아보겠습니다.
많은 독자들은 프로그래밍을 하면서 MP3 음악 파일을 즐겨 듣겠지요. 이 MP3 재생 프로그램 중 많이 사용하는 플레이어로 윈앰프(winamp)를 들 수 있습니다. 그리고 윈앰프의 강력한 기능은 대부분 플러그인 덕분이라고 해도 과언이 아닙니다. 윈앰프라는 프로그램 자체는 단지 플러그인을 처리해주기 위한 본체에 불과합니다. MP3 파일을 재생하고 음악 CD를 재생하고 재생하는 음악을 3차원의 멋진 동영상으로 표시해 주고, 듣고 있는 음악을 3차원 음향으로 바꿔 출력하는 모든 기능은 각각 별도의 플러그인이 담당해주는 것이지요. 플러그인을 추가할 때마다 winamp.exe라는 기본 프로그램은 전혀 변하지 않지만 우리가 사용하는 윈앰프의 기능은 점점 더 많아집니다. 바로 이것이 플러그인의 위력이라고 할 수 있습니다.
플러그인은 사실 앞에서 적어둔 함수 포인터를 이용해 커맨드 핸들러를 추가하는 방법과 원리에서 크게 다르지 않습니다. 단 몇 가지의 다른 점이 있습니다. 플러그인은 보통 별도의 파일인 것이 대부분입니다. 동적으로 로딩을 위해 dll이나 shared library로 구현되며 플러그인을 로딩하는 쪽에서는 Win32와 유닉스에서 각각 LoadLibrary()나 dlopen()을 사용하여 라이브러리를 로드하고 로드한 라이브러리에서 이름으로 함수 포인터를 얻어내기 위해 GetProcAddess()나 dlsym()을 이용합니다. 함수 포인터를 얻어내면 적당한 함수 포인터형 변수에 이 포인터를 대입하고 실행해주기만 하면 됩니다. 복잡해 보이는 플러그인을 구현하기 위한 내용은 이것이 전부입니다. 믿어지지 않는다고요? 믿으면 됩니다. 윈앰프도 이 방법으로 만든 프로그램이 확실하니까요.
필자는 리눅스 PDA 요피(Yopy)에서 작동하는 애플리케이션 요피투데이(YopyToday)를 개발한 적이 있습니다. 포켓PC(PocketPC)에서 흔히 볼 수 있는 오늘의 일정을 요약해 주는 첫 화면에 해당하는 프로그램인데 바로 이 요피투데이는 여러 가지의 플러그인을 이용해 기능을 확장시키도록 구성되어 있습니다. 요피투데이의 소스코드는 http://kldp.net/ projects/yopytoday/에서 언제든지 다운받을 수 있습니다. 실제로 GUI 프로그램을 어떻게 플러그인을 이용해 구현할 수 있는지 궁금한 독자라면 요피투데이의 소스코드를 읽어보면 많은 도움이 되리라 믿습니다.

다음은 자주 범하는 오류에 대해서
이번 호에서는 포인터에 대해 알아보았습니다. 무척 편리하기도 하지만 포인터는 프로그램이 크래시하는 가장 큰 원인의 하나입니다. 다음 시간에는 포인터를 사용한 프로그램을 작성하면서 C 프로그래머라면 반드시 지켜야 할 일들을 알아보도록 하겠습니다. 물론 C 프로그래머가 자주 범하는 오류에 대해서도 알아보도록 하지요. 바로 ‘에러없는 프로그램 작성하기’입니다.


Posted by theYoungman
engineering/System Eng.2007. 2. 26. 09:48

1회 | C 복습! 컴파일, 그리고 소스 분리하기
2회 | 포인터는 우스워
3회 | 에러없는 프로그램에 도전한다
4회 | 소스 분석하기
5회 | 디버깅하기
6회 | C로 임베디드 시스템 맛보기
7회 | 임베디드 리눅스 애플리케이션 개발하기



많은 프로그래머 지망생들이 꼭 배워야 하는 프로그래밍의 관문이라면 C와 C++를 꼽을 수 있습니다. 요즘은 훌륭한 툴 덕분에 아주 간편하게 프로그래밍을 할 수 있는 방법이 많지만 제대로 컴퓨터를 이해하기 위해서는 C와 C++는 한번은 꼭 거쳐야 할 관문입니다. 임베디드 프로그래밍 시대를 살아갈 독자들을 위해 준비한 이번 연재가 실제 프로그래밍을 하면서 사용하는 많은 테크닉을 배우는 ‘결정적’ 기회가 되기를 바랍니다.

언어처리에 관심이 많으며 맹목적으로 유닉스와 PDA를 추종하는 동물적 감각을 지녔다는 필자는 PDA는 비싼 장난감의 일종이라고 굳게 믿고 있다. 기계에 묻혀 살다보니 얼리 어댑터가 아니냐는 이야기를 자주 듣지만 결코 아니다! 필자는 유닉스 관련 동호회와 PDA 관련 동호회 몇 군데에 자주 출몰한다고 한다.

많은 사람들이 C를 배우기는 했지만 제대로 사용하지 못하는 경우를 많이 접하게 됩니다. 무언가 해보고는 싶지만 무엇을 해야 할 것인가, 또 어떻게 시작해야 할지, 하고는 싶지만 그저 막막한 기분에 멍하게 앉아 있다가 웹 서핑으로 시간을 보내는 경우가 대부분일 것입니다. 필자는 PHP를 이용해 웹 프로그래밍을 하기 위해서는 꼭 C를 이용해 처음부터 CGI를 만들어 본 경험이 있어야 한다고 생각합니다. 훌륭한 GUI 프로그램을 만들기 위해서는 MFC나 GTK+, Qt와 같은 툴킷을 사용해 보기 전에 먼저 WIN32 API나 Xlib와 같은 저수준의 라이브러리를 이용해 처음부터 C로 작성해 볼 필요가 있습니다. 그리고 훌륭한 IDE(Intergrated Development Environment)가 갖춰져 있는 개발툴을 이용해 프로그램을 작성하려면 반드시 에디터와 컴파일러만을 이용해 프로그램을 개발해 본 경험이 꼭 필요합니다. C 언어로 직접 작업을 하게 되는 시스템 프로그래머가 아니라도 이런 의미에서 C는 프로그래밍을 하려면 알아야 할 언어인 것입니다.


다시 C 언어에 도전하기
이번 연재는 C를 배우고 싶었지만 중도에 포기해 버렸던 독자들, C를 배우기는 했지만 아직 무언가 부족하다고 생각하는 독자들, 실제로 프로그래밍을 하면서 사용하는 많은 테크닉을 배우고 싶다고 생각하는 독자들을 위해 마련되었습니다. C에서 비교적 이해하기 힘든 내용들을 간단히 다시 살펴보고, 실제로 프로그래밍을 하면서 접하게 되는 상황에서 자주 사용하게 되는 테크닉을 적어보려고 합니다. 그리고 관심이 있을 독자들을 위해 임베디드 시스템에서의 프로그래밍에 대해서도 간략히 소개하겠습니다.
대부분의 기사는 리눅스 환경에서 vi, gcc, make를 이용하는 것을 전제로 합니다. 윈도우를 이용하는 사용자라면 cygwin을 이용하면 비슷한 환경을 이용할 수 있겠지요. 물론 비주얼 C++를 사용하는 독자도 cl.exe와 nmake.exe만을 이용한 콘솔 환경에 익숙해질 수 있도록 연습해보면 많은 도움이 되지만 지면 관계상 cl.exe와 nmake.exe의 사용에 대해서는 따로 다루지 않겠습니다. IDE라는 것은 이런 콘솔 환경을 뒷받침해주기 위한 도구에 불과하다는 것을 항상 기억하기 바랍니다. cygwin의 설치에 대해서는 cygwin 홈페이지(http://www.cygwin.com)를 참조하세요. 물론 필자도 윈도우 XP에서 MS 워드를 이용해 글을 쓰면서 cygwin을 이용해 샘플을 작성하고 있습니다.
이번 호에서는 C로 작성한 프로그램이 실행 파일이 되는 과정과 소스를 분리해 프로그램을 작성하는 방법에 대해 간략히 다루고자 합니다.

[ 정말 고급 개발자가 되고 싶다면! ]
gcc의 매뉴얼 페이지를 숙지하는 것이 좋습니다. 매뉴얼 페이지는 방대한 양도 양이지만, 컴파일러와 바이너리 구조에 대한 상세한 지식 없이는 이해가 불가능한 내용들이 대부분이죠. gcc의 매뉴얼 페이지를 제대로 이해할 수 있다는 생각이 든다면 그때는 자신의 프로그래밍 실력에 대해 어느 정도의 확신을 가져도 좋습니다. 물론 cl.exe의 MSDN 문서도 꼭 한번 읽어볼 것을 권합니다.



Gcc로 만든 C 프로그램이 실행되기까지
C를 언급하면 가장 먼저 시작하는 프로그램은 그 유명한 ‘hello world’라는 간단한 프로그램이지요(hello world 대신 ‘hello jiny’로 변경했습니다). 하지만 오늘은 이 프로그램을 조금 다른 용도로 사용하려고 합니다. 콘솔 환경에서 소스 코드를 컴파일하고 프로그램을 여러 개의 파일로 분리하는 방법, 그리고 여러 개의 파일로 되어 있는 프로젝트를 make를 이용해 관리하는 방법, 프로그램이 컴파일되어 실행되는 과정에 대해 간략히 알아보겠습니다.


자주 사용하게 되는 또 아주 가장 기본적인 gcc의 옵션 몇 가지를 적어 보았습니다. 보통은 <리스트 1>의 옵션들을 여러 가지로 조합하여 사용하지만 <리스트 1>에 적어둔 옵션들은 가장 ‘기본적’이고 많이 사용하는 옵션들이니 항상 숙지하고 있어야 합니다. 물론 I, -L, -l과 같은 옵션도 꼭 암기하고 있어야 합니다.
프로그램을 컴파일한다는 것은 대체 무슨 뜻일까요? 컴파일러는 어떤 과정을 거쳐서 실행 파일을 생성해주는 것일까요? 만일 처음 보는 환경에서 gcc를 이용해 실행 파일을 생성했는데 무언가 에러를 하나 보여주면서 실행이 되지 않는다면 이때 개발자들은 대체 무얼 해야 할까요? 이런 일은 ‘실제로도’ 자주 일어납니다. 이제 gcc의 옵션들을 무기로 이 hellojiny.c라는 프로그램이 컴파일되는 과정을 한번 살펴보도록 하겠습니다.
C 또는 C++는 기본적으로 Perl이나 파이썬, LISP, 베이직 등의 언어와 같이 소스 상태에서 바로 실행되는 언어가 아니라, 컴퓨터가 알아들을 수 있는 기계어로 변환된 이후에만 실행되는 언어입니다(물론 C 인터프리터도 없는 것은 아닙니다). C로 되어 있는 소스 형태를 어셈블리 언어로 바꾸는 프로그램을 컴파일러, 어셈블리 언어를 기계어로 바꾸는 프로그램을 어셈블러라고 합니다. 하지만 어셈블리 언어를 기계어로 변경했다고 바로 프로그램이 실행될 수 있는 것은 아닙니다. 자, 바로 여기에서 퀴즈 두 개를 내겠습니다.

① Hello world 프로그램에서 우리는 printf라는 함수를 사용했습니다. 이 함수는 어디에 있는 것일까요?
짿 C 프로그램은 main()이라는 함수에서 시작됩니다. 하지만 함수는 다른 함수가 호출해주기 전에는 실행되지 않습니다. 그러면 main 함수는 어떻게 시작되는 것일까요?

이 퀴즈의 답을 적기 전에 답을 이해하기 위해 필요한 내용을 좀더 적어보려고 합니다. 조금만 더 읽어 보세요. 소스 코드는 가장 먼저 전처리기(preprocessor)라는 프로그램을 이용해 다른 형태의 소스 코드로 변환됩니다. 헤더 파일과 소스 코드를 합쳐 새로운 소스 코드를 만들어 내는 것이 전처리기의 역할이지요. 프로그래머가 사용한 매크로나 매크로 함수 등도 전처리기를 통해 숫자나 코드로 치환됩니다. C 컴파일러는 전처리기를 통해 처리된 소스 코드만을 컴파일 할 수 있습니다. 그리고 C 컴파일러는 입력된 소스 코드를 몇 가지 마법(?)을 사용해 어셈블리 소스 코드로 변신시킵니다. 컴파일러가 프로그램을 최적화해 주는 것도 바로 이 컴파일러의 역할입니다.
어셈블리로 변신한 C 소스 코드는 다시 어셈블러를 통해 기계어 모음으로 변신합니다. 이렇게 태어난 기계어 파일을 우리는 오브젝트 파일이라고 부릅니다. 하지만 이 오브젝트 파일은 아직도 실행되지 않습니다. 물론 이 오브젝트 파일에는 00101110 따위의 기계어 실행 코드 말고도 아주 여러 가지의 암호가 함께 적혀 있습니다. 프로그램이 실행되려면 다시 몇 가지 정보가 더 필요합니다.
프로그램이 실행되기 위해서는 프로그램에서 어떤 라이브러리를 사용하는지, 프로그램에서 사용하는 함수들은 라이브러리에 다 있는지 확인해 이 정보들을 실행 파일에 포함시키는 과정이 필요합니다. 이 부분을 담당하는 것이 링커(linker)라는 프로그램입니다. 그리고 이런 정보를 써 넣는 형식을 실행 파일의 포맷이라고 부릅니다. 흔히 이야기하는 윈도우에서의 PE나 리눅스에서의 elf, a.out과 같은 포맷들이라고 생각하면 됩니다.
gcc는 이런 일을 하기 위한 프로그램의 모음이라고 생각할 수 있습니다. gcc 그 자체는 실제로는 소스 코드를 분석해 내부 정보로 변환하고(프론트엔드라고 합니다), 이 내부 정보를 어셈블리로 변환시키는 부분(백엔드라고 합니다)을 모아놓은 프로그램의 집합이고, 이후의 부분은(GNU의 경우) as, ld와 같은 별도의 프로그램이 담당합니다. 바이너리를 생성해 내기 위한 이런 툴들의 모음이 바로 GNU binutil이라는 것입니다. gcc에서 설명한 각 단계를 처리해주는 옵션들이 E, -S, -c 등이며 gcc 자체는 이 단계를 자동으로 처리해주는 스크립트라고 볼 수 있습니다. 즉 앞에서 설명한 E, -S, -c 옵션들을 이용하면 각 단계에서의 결과물을 얻어낼 수 있습니다. 자. 이제 퀴즈의 답을 적어 보겠습니다.

짾 printf 함수는 C의 기본 라이브러리입니다. C에서 기본적으로 제공되어야 하는 라이브러리들을 standard C library라고 하는데, 보통 리눅스나 freebsd 등의 운영체제에서는 GNU libc의 형태로 제공됩니다.
짿 윈도우에서 API를 이용해 프로그램을 해본 경험이 있는 독자라면 프로그램은 반드시 main에서 시작해야 하는 것이 아니라는 사실을 알고 있을 것입니다. C 프로그램이 main에서 시작해야 하는 이유는 그렇게 정해져 있기 때문입니다. 프로그램이 실행되기 위한 몇몇 환경을 맞추어 주는 코드를 startup 코드라고 보통 CRT라고 되어 있습니다. gcc든 VC++이든 모두 CRT 코드가 없으면 프로그램이 실행될 수 없습니다! 메인 함수는 이 CRT 코드에서 호출해 주게 되어 있습니다. 확인하고 싶은 독자가 있다면 약간의 어셈블리를 배워보는 것이 좋습니다. 임베디드 시스템에 관심이 있다면 CRT라는 이름을 접하게 되는 일이 종종 생길지도 모릅니다.

프로그램을 컴파일하는 과정에서 사용하게 되는 일은 거의 없다고 해도 좋지만 gcc는 정말 유용한 옵션을 하나 가지고 있습니다. ‘gcc의 실행과정 하나하나를 살펴볼 수 있게 해주는 v라는 옵션인데…’까지만 이야기했지만 호기심이 많은 독자들은 이미 실행을 해보고 있겠지요. 자, 필자가 지금의 글을 작성하고 있는 환경(윈도우 XP, cygwin) 에서 gcc v를 실행한 결과를 여기에 옮겨 적습니다. 자세한 옵션들은 환경에 따라 달라질 수도 있지만 중요한 내용과는 관련이 없습니다.

gcc -v hellojiny.c

Reading specs from /usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/specs
Configured with: /GCC/gcc-3.3.1-3/configure --with-gcc (생략) --
build=i686-pc-cygwin
Thread model: posix
gcc version 3.3.1 (cygming special)
여기에서부터 컴파일이 시작됩니다. 결과물로 cckh80cG.s라는 어셈블리 파일이 생성되는 것을 알 수 있을 것입니다. 물론 include 파일에 대한 검색을 하고 있는 것도 확인할 수 있습니다.

/usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/cc1.exe -quiet -v -D__GNUC__=3 -D__GNUC_MINOR__=3 -D__GNUC_PATCHLEVEL__=1 -D__CYGWIN32__ -D__CYGWIN__ -Dunix -D__unix__ -D__unix -idirafter /usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/../../../../include/w32api -idirafter /usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/../../../../i686-pc-cygwin/lib/../../include/w32api hellojiny.c -quiet -dumpbase hellojiny.c -auxbase hellojiny -version -o /cygdrive/c/DOCUME~1/saxboy/LOCALS~1/Temp/cckh80cG.s
GNU C version 3.3.1 (cygming special) (i686-pc-cygwin)
compiled by GNU C version 3.3.1 (cygming special).
GGC heuristics: --param ggc-min-expand=62 --param ggc-min-heapsize=61374
ignoring nonexistent directory “/usr/local/include”
ignoring nonexistent directory “/usr/i686-pc-cygwin/include”
ignoring duplicate directory “/usr/i686-pc-cygwin/lib/../../include/w32api”
#include “...” search starts here:
#include <...> search starts here:
/usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/include
/usr/include
/usr/include/w32api
End of search list.
여기에서부터 어셈블러가 작동합니다. 앞에서 생성한 cckh80cG.s라는 어셈블리 파일을 ccaKWInj.o라는 오브젝트 파일로 변환했습니다.

/usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/../../../../i686-pc-cygwin/bin/as.exe --traditional-format -o /cygdrive/c/DOCUME~1/saxboy/LOCALS~1/Temp/ccaKWInj.o /cygdrive/c/DOCUME~1/saxboy/
LOCALS~1/Temp/cckh80cG.s

이곳이 링커로 오브젝트 파일과 각종 라이브러리들을 이용해 최종 결과물인 실행 파일을 생성해내는 부분입니다. ‘Crt 어쩌고’ 라고 적혀 있는 부분에서 필자가 거짓말을 하고 있는 것이 아니라는 것은 증명됐습니다. Collect2라는 프로그램은 주어진 옵션으로 링커를 호출해 주는 프로그램입니다.

/usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/collect2.exe -Bdynamic --dll-search-prefix=cyg /usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/../../../crt0.o /usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/crtbegin.o -L/usr/lib/gcc-lib/i686-pc-cygwin/3.3.1 -L/usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/../../.. /cygdrive/c/DOCUME~1/saxboy/LOCALS~1/Temp/ccaKWInj.o -lgcc -lcygwin -luser32 -lkernel32 -ladvapi32 -lshell32 -lgcc /usr/lib/gcc-lib/i686-pc-cygwin/3.3.1/crtend.o

아주 간단하게 프로그래머가 만든 소스 코드가 바이너리 파일로 바뀌는 과정을 살펴보았지만 실제로 이 내용을 자세히 이해하기 위해서는 어셈블리 언어와 컴파일러의 구조, OS의 바이너리 로딩 등에 대해 약간의 지식이 더 필요합니다. 관심이 있는 독자들은 운영체제에서의 바이너리 포맷이나 바이너리 로딩에 관한 부분에 대해 좀더 깊게 공부해 보면 도움이 될 것입니다.

[ '관심이 있는' 독자에게 던지는 제언 ]
gcc hellojiny.c를 위해 gcc를 바로 사용하지 않고 직접 하나하나 명령을 실행해서 a.out으로 변환시켜 볼 것을 권하고 싶습니다. 특히 gcc -S를 이용해 생성된 어셈블리 결과물을 as와 ld를 이용해 직접 바이너리로 변환하는 과정을 직접 해 보면 프로그램이 만들어지는 과정과 실행되는 과정에 대해 많은 것을 얻을 수 있게 됩니다. 또 이렇게 얻어진 경험에 대해서 혹시라도 임베디드 시스템에서 바이너리를 만들어야 할 필요가 있다면 필자에게 감사하게 될 것입니다. 혹시 좀더 시간이 남는다면 gcc가 아닌 다른 컴파일러와 GNU as가 아닌 다른 어셈블러 등을 이용해 여러 가지를 실험해보는 것은 어떨까요. 미리 경고해두는데, 절대 간단하지 않습니다.



함수 사용, 파일 분리, 그리고 linkage
시작부터 조금 당혹스러운 내용이 되었을지도 모르겠습니다. 다시 눈높이를 약간만 낮춰 함수와 분할 컴파일, 그리고 분할 컴파일을 위한 make에 대해서 조금만 알아보도록 하겠습니다.
아주 당연한 질문이지만 왜 소스 코드를 여러 파일에 분리해서 작성해야 할까요? 꼭 이렇게 해야 할 필요는 없습니다. 프로그램을 작성하는 것은 결국 실행 파일을 만들어내기 위해서이고 따라서 1만 라인 정도 되는 프로그램이라 하더라도 소스 파일 하나에 프로그래밍해도 별로 관계는 없습니다. 실행 파일만 제대로 생성되면 상관없다는 의미입니다. 문제는 이것이 간단한 프로그램 몇 개를 만드는 정도라면 상驩平嗤?조금 프로젝트의 규모가 커지기 시작하면 관리가 불가능해진다는 데에 있습니다. 1만 라인 정도 되는 소스 코드를 한 파일에 작성해놓고 찾아다니면서 프로그램을 수정하는 것은 거의 불가능에 가깝습니다.
하지만 또 하나 중요한 이유가 있습니다. 컴파일에 걸리는 시간 때문입니다. 프로그램을 개발하면서 소스 코드를 컴파일하기 위해 사용하는 시간은 결코 무시할 수 없습니다. 컴파일에 걸리는 시간을 줄이기 위해서 빠른 컴파일러를 개발하는 것은 물론이고, 분산 컴파일, 헤더만 미리 컴파일하기(precom piled header) 등의 갖가지 방법이 개발됩니다. 그만큼 컴파일 시간은 프로그램의 개발에 중요한 요소입니다. 당장 필자가 문서를 작성하고 있는 워드프로그램을 컴파일하려면 시간이 얼마나 필요할까요? 한두 라인만을 수정하기 위해서 워드의 소스 코드를 전부 컴파일해야 한다면 얼마나 비효율적인 일이 될까요!
이제부터 독자들은 가능한 한 프로그램을 분할해서 작성하도록 노력해야 합니다. 이런 작업을 하기 가장 좋은 툴 중의 하나가 바로 make라는 것이다. 비주얼 C++에는 nmake. exe는 툴이 있습니다. Make와 문법은 약간 다르지만 사용하는 방법이 크게 다르지는 않습니다.
먼저 hellojiny 프로그램(<리스트 2>)을 함수를 이용해 바꿔 보았습니다. 아주 간단한 코드이니 독자들에게 전혀 무리가 없으리라 생각합니다. 하지만 다시 퀴즈입니다. 왜 print_hello() 함수의 앞에 static을 사용했을까요? 답은 파일을 분리한 hellojiny에서 설명하려고 합니다. 이제 두 함수를 각각의 파일로 분리해 보았습니다(<리스트 3>).
자, 이제 hellojiny 프로그램을 두 개의 파일로 나눠 보았다. 하지만 이 파일들은 몇 가지 심각한 문제가 있습니다. 이 문제를 발견하기 위해서 두 파일을 컴파일합니다.





gcc -Wall main.c s2b.c
main.c: In function `main’:
main.c:5: warning: implicit declaration of function `print_hello’
main.c:6: warning: control reaches end of non-void function

gcc의 -Wall 옵션은 컴파일하는 과정에서 발견된 문법적인 오류를 검사해 줍니다(더 자세한 문법 오류를 검사하기 위해서는 lint라는 별도의 프로그램을 사용할 수도 있습니다). 컴파일하면서 오류 메시지를 두 개 발견했는데 첫번째는 main.c에서 print_hello()라는 함수에 대해 알고 있지 못하다는 내용이고, 두번째는 main() 함수에서 리턴값을 돌려주지 않는다는 내용입니다. 두 가지 다 중요한 내용이지만 지금은 첫번째 문제에 관해서 좀더 생각해보겠습니다.
C는 선언(declaration)이 꼭 필요한 언어입니다. 함수를 호출하는 부분은 컴파일러에게는 함수 자체가 변수와 마찬가지로 생각한다고 해도 과언이 아닙니다. 함수의 반환형(return type)이 제대로 선언되지 않은 경우 컴파일러는 무조건 int를 반환한다고 생각하기 때문에 심각한 오류의 원인이 되기도 합니다.
프로그램의 실행이 완전히 틀린 결과를 내놓을 수 있는 소스 코드를 첨부합니다. 실제로 복잡한 프로그램을 작성하면서 헤더를 제대로 작성해 주지 않고 컴파일러의 경고 메시지를 무시하다 보면 이런 사소한 실수에서 잘못된 결과를 얻어 디버깅에 어마어마한 시간을 소비하게 되는 경험을 여러 번 하게 됩니다. 선임자가 만들던 몇 만 라인짜리 프로그램이 이렇게 작성되어 있다면 이 프로그램은 언제 터질지 모르는 폭탄이라고 생각해도 좋습니다.


<리스트 4>는 정상적으로 컴파일되고 실행도 가능합니다. 왜 그런 것일까요? 앞에서 선언되지 않는 함수에 대해서는 int를 가정한다고 설명한 바 있습니다. 따라서 이 프로그램은 경고 메시지를 출력하기는 하지만 문법적으로 전혀 하자가 없습니다. 하지만 출력되는 결과는 어떤 값이 될지 예측할 수 없을 뿐만 아니라 이 값을 직접 이용하는 루틴이 있다면 심각한 오류의 원인이 된다는 것을 다시 강조합니다.
다시 원래의 소스 코드에 함수의 선언을 추가했습니다(<리스트 5>). 이제 컴파일러의 경고 메시지가 하나 없어졌다는 것을 알 수 있을 것입니다. 앞으로도 분리한 모듈에 대해서는 꼭 이렇게 함수의 선언을 추가해야 합니다. 하지만, 만일 sub.c를 다른 프로젝트에서도 계속 사용하고 싶은데 이렇게 함수를 선언해 줘야 한다면 아주 귀찮은 일이 될 것입니다. 이런 귀찮은 역할을 대신 해주는 것이 헤더라는 사실은 이미 잘 알고 있으리라 생각합니다. 헤더를 이용해서 hello jiny 프로그램을 다시 수정해보겠습니다(<리스트 6>).
이렇게 헤더가 완성되었습니다. 하지만 함수의 선언은 그렇다고 해도 시작과 끝에 적혀 있는 #ifndef는 왜 필요한 것일까요? sub.h에 다음 한 줄을 추가하고 #ifndef 등을 삭제해 컴파일해 보면 쉽게 답을 얻을 수 있을 것입니다.










sub.h
//#ifndef __SUB_H__
//#define __SUB_H__

#include “sub.h”
void print_hello(void);
//#endif /* __SUB_H__ */
#ifndef는 같은 헤더 파일이 프로그래머가 알지 못하는 사이에 재귀적으로 포함되어 문제가 생기는 것을 방지해 주는 역할을 합니다. 필자가 매우 놀랐던 것 중 하나는 비교적 경험이 많은 프로그래머들도 헤더의 시작에 ‘recursive inclusion’을 방지하기 위한 장치를 사용하는 것을 간과하는 경우가 많다는 것입니다. 독자들은 헤더를 작성하는 경우에 이렇게 쓰는 것을 공식이라고 생각하고 잊지 않았으면 합니다.
헤더 파일은 보통 파일로 분리된 모듈에 대해서 인터페이스를 제공해주기 위해 사용합니다. 하지만 헤더는 컴파일러의 입장에서는 C 소스 코드와 전혀 다르지 않습니다. #include에는 헤더만 포함될 수 있는 것이 아니므로 #include “sub.c”와 같은 문장을 사용해도 문법적으로는 전혀 문제가 없습니다. 하지만 이렇게 사용하는 것은 적어도 필자의 관점에서는 바람직하지 않습니다.
많은 초심자들이 자주 범하는 실수 중 하나가 헤더에 함수를 정의(definition)해 사용하는 것입니다. 이러한 방식은 코드를 작성하면서 자신이 그 내용에 대해 이미 알고 있다면, 그리고 편리한 프로그래밍을 위해 일부러 이렇게 작성한 것이라면 전혀 문제가 되지 않습니다. 하지만 작성자가 이 사실을 인지하지 못한 상태라면 심각한 문제가 될 가능성이 많습니다. 좋은 헤더를 만들기 위해서는 많은 경험이 필요합니다. 헤더 파일과 소스 파일에 대해 명확한 기준을 세워 놓지 않고 프로그래밍을 하게 되면 어느 한 순간 “symbol multiply defined”나 “include nested too deeply” 같은 피곤한 에러들을 만나게 될 것입니다. 헤더가 열 개가 넘고 서로의 포함 관계가 복잡한 경우 이런 문제를 해결하려면 소스 코드를 여러 곳 수정해야 하는 경우가 많습니다.
그러면 다시 여기에서 퀴즈를 내겠습니다. 이번에는 sub.c의 print_hello()에서 static을 사용하지 않았습니다. 만일 print_hello() 함수를 static void print_hello()로 바꾼다면 이 프로그램은 작동하지 않습니다. 정확하게 말하면 컴파일을 할 수 없습니다. 좀더 정확하게 말하면 컴파일된 sub.o와 main.o를 링크해 실행 파일을 생성하지 못합니다. 왜 그런 것일까요?
Static은 두 가지의 조금 다른 의미를 지닙니다. 함수에 사용되는 경우에는 “이 함수가 적혀 있는 이 파일 이외의 파일에서는 이 함수를 사용할 수 없다”는 의미를 가지게 됩니다. 아울러 C로 작성한 대형 프로젝트에서는 보통 C를 이용해 객체지향의 개념을 도입하게 되는데, 이때 C++에서의 private을 비슷하게 구현하기 위해 사용되기도 합니다.
Static을 변수에 사용하는 경우에는 “이 변수는 이 프로그램이 끝날 때까지 다른 변수와는 다른 특별한 곳에 저장되어 함수가 끝나도 값이 변형되지 않는다”라는 의미를 가지게 됩니다. Sub.c를 static을 사용해 약간 변형시켜 보았습니다. Print_text() 함수는 main.c에서는 직접 사용할 수 없는 함수로 선언되어 있으며 동시에 print_text() 내에서 사용하는 print_cout라는 변수는 실행될 때마다 값이 변하지 않고 계속 유지되는 것을 확인할 수 있습니다. 이 프로그램의 실행 결과는 다음과 같습니다.

./a.exe
[1] hello, world
[2] hello, world
[3] hello, world

[ 'The C Programming Language'를 권하는 이유 ]
먼저 고백해두자면 필자가 C를 배운 것은 아주 오래 전의 일이고, C에 관한 책을 마지막으로 읽어 본 것은 컴파일러를 공부하면서였습니다. 그렇다보니 필자는 최근에 출간되고 있는 C에 관한 서적을 접해본 적이 없을 뿐만 아니라 어떤 내용이 담겨 나오는지도 사실은 잘 모르고 있습니다. 하지만, 그럼에도 불구하고 좋은 책이란 변하지 않는 법입니다. 처음 C를 배우던 시절 몇 권의 C 관련 서적을 접해본 기억이 있고, 그 후에도 여러 가지의 다른 컴퓨터 관련 서적을 읽고 있지만 가장 기억에 남는 책 중의 하나라면 그 중에서도 C에 한정지어서 이야기한다면 단연코 Brian Kernigan과 Dennis Ritchie의 「The C Programming Language」를 꼽아야 하겠습니다. 잘 알고 있는 독자들도 많이 있겠지요. 이 책을 이해하지 않고 C에 대해 이야기하는 것은 말도 안되는 이야기입니다. 이 책이 좋은 이유를 필자가 느낀 대로 간단히 적어보겠습니다.

① 이 책의 저자들은 바로 C를 개발한 주인공들입니다. 저자들은 유닉스를 개발한 사람들이고, C는 유닉스를 개발하기 위해 개발된 언어입니다.

② 이 책에는 간결하면서도 명확하고, C의 철학에 잘 맞는 주옥같은 코드가 많이 실려 있습니다.

③ 이 책에는 처음 접하는 개발자에게는 결코 해결하기 쉽지 않은, 그러나 해결하고 나면 많은 것을 생각하게 해주는 연습문제가 많이 실려 있습니다.

④ 짧고 간결하게 모든 것을 설명해주고 있습니다. 필자가 포인터를 여전히 어렵게 느끼고 있던 시절, 겨우 열 페이지에 불과한 이 책의 포인터 설명 부분은 모든 것을 명확하게 정리해 주었습니다. 다음 기사에서 꼭 소개하겠지만 단 한 줄의 코드가 복잡한 머릿속을 깔끔하게 정리해 주었던 그 때의 흥분을 필자는 항상 기억하고 있습니다. 하지만 이 책은 참 어렵습니다. 200페이지 남짓한 아주 짧은 책이지만 이 책의 모든 내용을 이해하기 위해서는 전산과학의 중요한 내용을 대부분 이해하고 있어야 한다 해도 과언이 아닙니다. 우스갯 소리지만 이 책을 완전히 이해하려면 전산과 4학년은 되어야 한다고 말하기도 합니다. C를 좀더 공부해 보려 한다면, 그리고 좋은 책을 찾고 있다면 이 책을 꼭 정독해 보기 바랍니다. 이해하기 어려운 내용도 있겠지만 아주 좋은 경험이 될 것입니다.


자, 그러면 이때 static int print_count에서 static을 없애면 어떻게 될까요? 답은 ‘보장할 수 없다’입니다. Print_count라는 변수가 초기화되어 있지 않기 때문입니다. static으로 선언되는 변수는 선언되는 시점에서 자동으로 0으로 초기화가 되지만 다른 변수들은 그렇지 못합니다.
또 하나 소스 코드를 파일로 분리하면서 꼭 알고 있어야 하는 것은 extern과 global의 개념입니다. 자세한 설명은 따로 하지 않으려고 합니다. 다만 독자들에게 숙제로 남겨 놓고 해답은 다음 기사의 첫 부분에 싣도록 하겠습니다.
Static을 사용하지 않고 전역 변수를 이용해 프린트 함수를 호출한 횟수를 기억하기 위해서는 어떻게 하면 될까요? 그리고 main.c에서도 이 횟수를 알기 위해서는 sub.h에 어떤 내용이 추가되어야 할까요?
이 문제를 스스로 해결할 수 있다면 그 시점에서 static, extern, global에 대해 다시 한번 깊게 생각해 보기 바랍니다. Variable의 scope와 linkage라는 단어로 이 내용을 기술하게 되는데 프로그래밍 경험이 많아도 정확한 개념을 알지 못하고 대강 넘어가는 경우가 많기 때문입니다. 또 초심자들에게 가장 어렵게 느껴지는 개념 중 하나이기도 합니다.
이제 마지막으로 Makefile을 만들어 보겠습니다. Makefile을 이용하면 여러 개의 파일로 구성되어 있는 프로젝트를 손쉽게 관리할 수 있습니다.
BIN = hellojiny
OBJS = main.o sub.o

all: $(OBJS)
gcc -o ${BIN} ${OBJS}


clean:
rm ${OBJS}

.c.o:
gcc -c $<
Makefile은 개념과 작성하는 방법만으로도 소책자가 될 정도이므로 간단히라도 설명하기 위해서는 꽤 많은 지면을 필요로 합니다. 다만 makefile을 쓰면 몇 가지 편리한 점이 있습니다. 가장 좋은 점은 make라고 네 글자만 타이핑하면 된다는 것이고, 모든 작업을 target을 정해 기술해 둘 수 있으므로 rm과 같은 민감한 작업을 하는 경우 실수를 줄일 수 있습니다. 더 좋은 점은 아직 컴파일이 되지 않았거나 소스 코드가 변경된 파일에 대해 선택적으로 알아서 해당되는 작업을 수행해주기 때문에 전체 소스 코드를 컴파일하는 시간도 훨씬 짧아집니다. 또한 파일의 변경에 대해 의존성을 기술할 수 있기 때문에 sub.h가 변경된 경우 자동으로 sub.c를(sub.o가 존재하더라도) 새로 컴파일할 수 있습니다.
makefile은 단순히 소스를 컴파일하기 위해 사용할 수 있는 것만은 아닙니다. Makefile의 문법을 보며 눈치가 빠른 독자들은 느낄 수 있으리라 생각하지만 의존성을 가지고 하나의 파일을 다른 파일로 변경해야 하는 대부분의 작업에 makefile을 사용할 수 있습니다. 아주 좋은 예로는 latex 파일을 dvi로 컴파일하거나 오픈소스 프로젝트에서 문서를 생성하기 위해 makefile에 의존한다는 사실을 들 수 있겠지요.
좀 더 오픈소스 프로젝트에 가까운 방법은 GNU autoconf/ automake를 이용하는 것입니다. 많은 오픈소스 프로그램은 ./configure; make; make install만으로 컴파일과 설치를 마칠 수 있습니다. 이것을 가능하게 해주는 것이 autoconf와 automake이며 간단하게는 makefile을 간단한(?) 설정만으로 자동으로 생성할 수 있도록 해주는 프로그램입니다. 이 패키지의 사용에 대해서는 여기서는 생략할 수밖에 없지만 오픈소스와 GNU에 관심이 있는 독자라면 꼭 한번 관심을 가지고 배워보면 좋습니다. 물론 오픈소스 개발툴인 anjuta, kdevelop 등의 IDE 환경에서는 자동으로 automake, autoconf에 필요한 파일들을 생성해 줍니다.

자신감은 첫 프로그램으로부터!
이제 막 C 언어를 배웠지만 더 고급 프로그래밍을 하고 싶은 독자를 위해 첫 기사를 마쳤습니다. 필자는 어린 시절 터보 C라는 좋은 프로그램으로 처음 C를 배웠습니다. 무작정 책을 사 놓고 페이지를 뒤적이고 나서 문법은 이해가 되었다고 생각했던 바로 그 때쯤의 당혹스러움을 아직도 잊지 못하고 있다. 이곳저곳에서 몇 가지 언어로 여러 프로그램을 때로는 일로 때로는 재미삼아 만들고 있는 지금까지도 어느 순간 프로그래밍에 대해 두려움이 없어졌는지 잘 기억해 낼 수가 없嗤?확실한 것은 첫번째 프로그램 하나가 엄청난 자신감을 안겨준다는 사실입니다. 스스로 동기를 부여해 쓸모없어 보이는 간단한 프로그램이라도 많은 것을 만들어 보기를 권하고 싶습니다.
가능하면 C에 관한 내용보다는 C를 이용해 프로그래밍하면서 만나는 여러 가지 문제나 일부러 신경을 쓰지 않으면 간과하기 쉬운 문제들에 대해 다루려고 생각합니다. 특히 어느 정도 막막한 느낌을 떨쳐내고 나면 고민하게 되는 코딩 스타일의 문제나 버그를 적게 만들어 낼 수 있는 코드의 작성 등에 관해서도 다뤄보려 합니다.
다음 호에서는 C에서 가장 어려운 부분이라고 누구나 느끼게 되는 포인터에 대해 다루고자 합니다. 기본적인 포인터의 개념보다는 구조체의 포인터, 함수 포인터, 함수 포인터를 이용한 플러그인의 구현 방법 등에 대해 다룰 것입니다. 기사의 내용에 관한 궁금한 점이 있으면 언제든지 마소 사이트(www.imaso.co.kr)를 이용해 질문해 주기 바랍니다.

[ 프로그래밍,그 막막함을 없애자]
제대로 된 첫 프로그램을 만들어 보는 것은 굉장히 어려운 일입니다. 특히 프로그램을 만들어 본 경험이 없는데, 이제 막 첫 언어를 배운 후라면 그 답답함이란 이루 말할 수 없습니다. 혹시라도 도움이 될 지도 모를 이야기 몇 가지를 두서없이 풀어보려고 합니다.
프로그래밍이란 무엇일까요? 어렵고 거창할 필요는 없습니다. 사람이 하는 말을 기계가 알아듣게 번역해 주는 일에 불과합니다. 컴퓨터가 사람이 하는 말을 배우는 것은 아직 어려운 일이니 사람이 컴퓨터가 할 줄 아는 말을 배워서 일하는 법을 가르쳐 주는 것이지요. 한국어는 너무 익숙하니 영어를 처음 배우던 시절을 한 번 생각해 보면 어떨까요? 관사, 정관사에 부정사 따위의 문법은 어느 정도 알고 있다고 생각하지만 영어로 말을 하려면 막막하기만 한 느낌, 바로 그 느낌이 프로그래밍을 시작하지 못하고 있는 독자의 느낌과 같은 것입니다.

‘자료의 구조’ 표현에 익숙해지기
아주 간단한 프로그램을 제외하면 대부분의 프로그램은 자료구조로 이뤄집니다. 여기에서 트리, 리스트 같은 자료구조를 떠올리면 아직은 조금 성급한 판단입니다. 필자가 이야기하고 싶은 것은 단어 그대로 순수한 ‘자료의 구조’입니다. 트리나 리스트, 그래프와 같은 자료구조는 컴퓨터에게 알려주어야 할 자료의 구조를 컴퓨터에게 알려주는 가장 기본적인 방법입니다. 자료구조에 대해 배운다는 것은 사람이 생각하는 방식을 컴퓨터로 생각하는 방식으로 바꾸는 기초적인 방법을 배운다는 것과 비슷한 이야기가 됩니다. 프로그래밍은 이 자료의 구조를 표현하는 방식과 표현한 여러 가지의 구조를 조합하여 원하는 일을 하는 방법을 적는 방법에만 익숙해지면 생각보다 그렇게 어려운 일은 아닙니다.

결정해야 할 의문들에 스스로 답하기
프로그래밍이 어렵게 느껴지는 이유 중의 또 하나는 아무리 간단해 보이는 것이라도 결정해야 할 것이 매우 많은데, 자신이 만든 프로그램이 작동하는 것을 본 경험이 적은 프로그래머의 경우에는 너무 깊게(또는 너무 단순하게) 이 문제를 생각하기 때문입니다.
간단한 주소록 관리 프로그램을 예로 들어보겠습니다. 주소록의 용도는 아주 단순합니다. 친구들의 이름과 전화번호를 기록해 찾을 수만 있다면 이미 주소록은 제 역할을 다 하게 되는 것이지요. 이름과 전화번호만을 기록하고 찾을 수 있는 주소록을 만들고 싶다면 어떻게 하면 좋을까요? 먼저 이름과 전화번호는 어떻게 표현하면 좋을까요? 이름은 char name[10]이 될 수도 있고, 전화번호는 char phone[10]이 될 수도 있습니다.
하지만 성과 이름을 분리하려고 한다면 이름을 표현하기 위해 char firstname[10]과 char lastname[10]이 필요할 수도 있습니다. 전화번호의 국가번호와 지역번호, 국번을 표현하기 위해 각기 다른 변수를 사용할 수도 있습니다. 이름과 전화번호를 연결해 저장하기 위해서는 어떻게 표현하는 것이 좋을까요? 저장할 수 있는 인원의 수에 한계를 두지 않으려면 어떤 방법으로 만드는 것이 좋을까요? 저장은 파일에 직접 하는 것이 좋을까요? 데이터베이스를 사용하는 것이 좋을까요? 파일에 저장한다면 어떤 방법으로 파일에 저장하는 것이 좋을까요? 이 방법은 저장하기와 읽어오기의 효율이 얼마나 좋을까요? 저장한 정보에서 이름으로 전화번호를 검색하려면 어떤 방법을 사용하면 좋을까요? 이 검색을 더 빠르게 하고 싶다면 저장하는 구조를 어떻게 바꾸는 것이 좋을까요? 내가 사용하는 검색방법은 얼마나 메모리를 사용할까요? 메모리가 아주 작은 컴퓨터에서도 작동하게 하기 위해서 메모리 사용량을 줄이려면 어떻게 하면 좋을까요? 프로그래머는 이런 많은 의문에 스스로 확신할만한 답을 얻어내야 합니다. 그리고 그 과정에서 컴퓨터의 사고방식을 조금 더 잘 이해할 수 있게 되고, 이 과정에 대해 답을 얻기 위해서 많은 것을 스스로 공부하게 됩니다.
처음 프로그램을 시작하려고 할 때 가장 좋은 예로 들게 되는 것은 바로 앞에서 이야기한 주소록이 됩니다. 자료구조의 표현과 검색, 정렬 등 프로그래밍에서 자주 사용하는 대부분의 방법들이 포함되기 때문이지요. 아직 주소록을 C로 직접 구현해 본 경험이 없는 독자라면 꼭 한번 시도해 볼 것을 권합니다.


Posted by theYoungman
engineering/System Eng.2007. 2. 22. 15:11

CVS는 각종 파일의 버젼을 쉽게 관리할 수 있도록 도와주는 도구이다. 독자들 중에는 아직 버젼 관리의 필요성을 크게 느끼지 못하는 사람도 많이 있을 것이다. 하지만 CVS가 제공하는 기능들을 직접 보고 그 편리함을 체험해 본다면, 앞으로는 CVS 없이 프로젝트를 진행한다는 것은 상상할 수도 없는 일이 될 것이다.

예를 들어 간단한 프로그램을 하나 작성한다고 하자. 비교적 간단해 보이는 문제여서 곧바로 코딩을 시작하여 하루만에 작업을 마쳤다. 하지만 실행해보니 의도한 바대로 결과가 나오지 않는다. 원인이 무엇인지 알아내기 위해 이부분 저부분을 뜯어고치다 보면 처음의 모습은 거의 찾아볼 수 없는 복잡한 코드로 변해버린다. 하지만 결국 발견하는 것은 사소한 실수, 이제 그 동안의 작업을 모두 원래대로 돌려놓으려 하지만 이것 역시 버그를 잡는 일 못지 않게 어려운 일이다. 나름대로 바뀌었다고 생각되는 부분을 복구해봐도 제대로 되지 않고, 결국에는 처음부터 다시 코딩해 버린다. 만일 이 때 처음의 코드를 백업해 놓았다면 찾아낸 버그만을 고치고 작업을 계속 진행할 수 있을 것이다. 하지만 그렇다고 해서 매 작업 단계마다 코드를 백업해 놓는다면 조금만 지나도 수없이 많은 파일들이 생겨날 것이다. 이런 파일들은 공간도 많이 차지할 뿐더러 그 많은 파일들을 관리하는 것 자체가 또다른 문제가 될 것이다. CVS는 바로 이러한 문제를 해결해 준다. CVS를 이용하면 매 작업 단계마다 코드를 저장할 수 있음은 물론, 원하는 단계의 코드를 언제라도 꺼내 볼 수 있다. 게다가 CVS는 각 단계에서 변경된 부분만을 저장하기 때문에 저장 공간도 많이 필요로 하지 않는다. 또한 각 단계마다 작업한 내용을 글로 적어 함께 저장할 수 있으므로 개발 내용을 한눈에 알아볼 수 있게 해 준다.

CVS의 또다른 장점은 여럿이 공동으로 진행하는 프로젝트에서 나타난다. 사실 여러명이 함께 작업하다 보면 각자 자기의 작업 파일을 갖게 되기 때문에 아무리 주의를 하여도 다른 사람이 변경해 놓은 것을 덮어 쓰는 일이 종종 발생하게 된다. 그렇다고 작업을 잘 나누어 각각이 서로 다른 파일만을 갖고 작업하도록 하는 것도 쉽지 않다. 처음에는 각기 다른 파일을 다루도록 일을 나누지만 조금 지나다보면 서로가 상대방의 파일을 고쳐야만 하는 경우가 발생하게 된다. 쉬운 대안은 한 파일을 작업하는 동안 다른 사람은 그 파일을 건드리지 못하게 하는 것이지만, 이럴 경우 파일을 수정하고 싶어도 다른 개발자가 그 파일을 수정하고 있다면 그 작업이 끝날 때까지 기다려야 하기 때문에 전체 작업 효율이 떨어지게 된다. 반면 CVS는 심지어 같은 파일도 여러 사람이 함께 작업할 수 있게 해 준다. 각자 한 파일 내의 다른 부분을 수정한다면 CVS가 그 내용을 하나로 합쳐 줄 것이다. 물론 두 사람이 동시에 같은 줄을 고친다면 CVS가 그것을 합쳐줄 도리는 없다. 하지만 그럴 경우라도 서로 같은 줄을 고쳤음을 개발자에게 알려주고 그 내용도 보여주어서 개발자가 적절한 조치를 취할 수 있도록 해 준다. 뿐만아니라 CVS는 Unix 환경은 물론, Windows나 Macintosh에서도 사용할 수 있어 각 개발자들이 원하는 환경에서 작업할 수 있다.

CVS는 이미 대다수의 공개 프로젝트에서 사용되어 그 효능을 입증하였다. 실제로 우리가 알고 있는 대부분의 공개 프로젝트가 CVS를 사용한다. Apache HTTP server, Mozilla 등이 대표적인 예이다. 이러한 공개 프로젝트들은 대부분 현재 개발 중인 내용을 CVS를 통해 모든 사람들이 받아 볼 수 있도록 하고 있다. 그렇기 때문에 CVS를 프로젝트에 도입하는 경우가 아니라 하더라도 이런 개발 버젼을 받아 보고 싶다면 CVS의 사용법을 익혀 둘 필요가 있다.

동작 방식

저장소

여러 명이 함께 작업할 수 있는 환경을 제공하려면 어떠한 요소가 필요할까? 가장 먼저 떠오르는 것은 같이 작업하는 파일을 보관할 장소일 것이다. CVS는 이러한 역할을 하는 곳을 저장소(repository)라 부른다. 그렇다고 특별한 데이터베이스나 다른 저장 매체를 사용하는 것이 아니라 그저 하나의 디렉토리에 불과하다. 이 디렉토리에는 CVS의 전반적인 설정 사항과 각 프로젝트의 파일들(문서, 프로그램 등)은 물론, 각 파일의 버젼 관리에 필요한 정보, 파일별 작업 기록들을 저장하게 된다. 여기에 있는 파일들은 모두 CVS가 관리하므로 사용자는 이 파일들을 직접 건드릴 필요가 전혀 없다.

프로젝트 시작

일단 저장소를 만들고 나면 이후로는 여러 프로젝트들이 그 저장소를 이용할 수 있다. CVS를 이용하려는 사용자는 처음 프로젝트를 시작하는데에 필요한 파일 및 기본 디렉토리 구조를 자신의 작업 디렉토리에 만들고, 이를 저장소로 옮길 수 있다. 그러면 저장소에는 새로운 디렉토리가 만들어지고 이 곳은 앞으로 프로젝트 진행을 위한 공간으로 사용된다. 여기까지가 완료되면 프로젝트 시작을 위한 모든 준비가 끝난 것이다. 이후로는 프로젝트에 참여하는 사람들 각각의 몫이다.

개발자 각자의 프로젝트 진행

저장소에 프로젝트를 위한 공간이 마련되고 나면 실제로 프로젝트에 참여하는 개발자는 어떻게 CVS를 이용하면 될까? 저장소의 내용을 가지고 직접 작업할 수는 없으므로 우선 저장소에 있는 내용을 복사하여 자신의 작업 디렉토리를 만들어야 한다. 이 과정을 CVS에서는 checkout이라고 한다. checkout으로 만들어진 작업 디렉토리는 자신만의 작업 공간이 되며, 개발자는 그 파일들을 마음대로 변경할 수 있다. 이후로는 계속 작업을 진행하면서 CVS의 명령을 이용하여 자신의 작업 결과를 저장소로 옮기고, 다른 사람이 저장소에 올려 놓은 작업 결과를 받아오는 일만을 반복하면 된다. 공동 작업으로 인해 생길 수 있는 문제들의 대부분은 CVS가 해결해 줄 것이다.

요약

CVS를 이용하여 프로젝트를 수행하는 절차는 다음과 같다.

  1. 저장소를 초기화한다. (CVS 관리자)

  2. 프로젝트를 초기화한다. (프로젝트 관리자)

  3. 작업 공간을 마련한다. (개발자)

  4. 실제 작업 (개발자)

  5. 자신의 작업 내용을 저장소에 저장하고 다른 개발자의 작업 내용을 받아온다. (개발자)

1번은 처음 CVS를 설치할 때 한 번만 해 주면 되고, 2번은 새로운 프로젝트를 시작할 때마다 필요한 작업이다. 각 개발자는 3번을 수행한 후 4번과 5번 과정을 되풀이하며 작업을 진행하게 된다.

기본적인 사용 방법

CVS가 어떤 것이고 어떻게 동작하는지를 알았으니 각 단계별로 CVS를 사용하는 법을 알아보겠다. 이 절에서는 CVS의 모든 기능을 설명하기보다는 자주 사용되는 기능을 예와 함께 설명한다. 여기에서 설명하는 내용 정도만 가지고도 실제 프로젝트에서 CVS를 사용하는 데에는 무리가 없을 것이다.

CVS는 대부분의 Linux 배포본에 포함되어 있으므로 Linux를 사용하고 있다면 이미 CVS가 설치 되어 있을 것이다. 만일 CVS 명령이 없다면 http://www.cyclic.com/에서 적당한 파일을 받아 설치하면 된다.

CVS를 이용하는 모든 과정은 cvs 명령을 통해 이루어진다. cvs 명령의 기본 형식은 다음과 같다.

$ cvs [cvs 옵션] 명령 [명령 옵션과 인자]
명령 부분에는 실제로 cvs에게 지시할 명령이 오게 되며, 각 명령마다 요구하는 인자가 있을 수 있다. 실제로 사용되는 명령의 종류와 기능에 대해서는 새로운 명령이 소개될 때마다 언급하도록 하겠다.

저장소 초기화

CVS를 사용하기 위해서 가장 먼저 할 일은 각 프로젝트의 파일들을 저장할 저장소(repository)의 위치를 정하는 것이다. 저장소를 이용할 프로젝트들의 규모를 고려하여 충분한 공간을 가진 디렉토리를 저장소로 쓰도록 한다. 저장소를 /home/CVS에 만들기로 했다면 다음과 같이 초기화할 수 있다.

# cvs -d /home/CVS init
여기서 -d는 저장소의 위치를 나타내며, 마지막의 init가 CVS 명령이다. 물론 이 명령을 사용하기 위해서는 /home에 쓰기 권한을 갖고 있어야 한다. 그러므로 대개의 경우 저장소의 초기화는 root 권한을 가진 씨스템 관리자가 하게 된다. 명령이 실행되고 나면 /home에 CVS란 디렉토리가 생기고, 그 안에는 CVSROOT란 디렉토리가 있을 것이다. 이 CVSROOT 디렉토리는 CVS의 각종 설정 내용을 담고 있는 디렉토리이다. 다시 말하지만 저장소(/home/CVS)의 파일들을 직접 건드리는 것은 절대로 피해야 한다. 이 파일들을 변경하고 싶다면 cvs 명령을 이용해야 한다.

저장소를 만들었으면 CVS를 이용할 개발자들에게 저장소가 있는 디렉토리에 쓰기 권한을 주어야 한다. 그러기 위해 개발자들을 모두 같은 그룹(예: cvs)의 구성원으로 만든다. 이는 씨스템별로 존재하는 그룹 관리 도구를 이용하거나 groupadd와 같은 명령으로 cvs라는 그룹을 만들고, 그룹 관리 도구를 쓰거나 /etc/group 파일을 직접 수정하여 개발자들을 그 그룹에 추가하면 된다. 예를 들어 minskim과 sehkone이라는 사용자를 cvs 그룹에 추가한다면 다음과 같은 줄이 /etc/group에 생길 것이다.

cvs:*:510:minskim,sehkone
이제 다음 명령으로 디렉토리의 권한을 열어 주면 된다.
# chgrp -R cvs /home/CVS
# chmod -R g+w /home/CVS
이후로는 cvs 그룹에 속한 개발자는 이 저장소를 마음대로 이용할 수 있다.

저장소 이용

모든 CVS 명령은 저장소의 위치를 알아야 수행될 수 있다. 앞에서 본 저장소 초기화의 예에서도 init 명령을 사용하면서 -d 옵션으로 저장소의 위치를 알려 주고 있다. CVS의 다른 명령들도 같은 옵션으로 저장소의 위치를 지정할 수 있다. 또다른 방법은 CVSROOT라는 환경변수의 값으로 저장소의 위치를 주는 것이다. 예를 들어 sh이나 bash를 쓴다면 다음의 내용을 초기화 파일(.bash-profile 등)에 넣어 놓으면 편리할 것이다.

CVSROOT=/home/CVS
export CVSROOT
csh이나 tcsh을 쓴다면 다음과 같이 한다.
setenv CVSROOT /home/CVS
이렇게 설정을 하고 나면 -d 옵션과 저장소의 위치를 매번 줄 필요 없이 바로 cvs 뒤에 원하는 명령을 주면 된다.

개발자가 CVS 명령을 이용하려 하는 경우 고려해야 할 사항이 하나 더 있다. 개발자가 어떤 방식으로 저장소가 위치한 기계에 연결하는가 하는 것이다. CVS는 다양한 연결 방식을 지원하는데, 여기에서는 주로 사용되는 세 가지 방식에 대해 저장소의 위치를 지정하는 방법을 알아보도록 하자.

   저장소와 개발자가 같은 기계를 쓸 경우

앞에서 저장소를 초기화한 것과 같은 경우이다. 같은 기계를 쓰기 때문에 저장소가 있는 디렉토리 이름을 주는 것으로 충분하다. 앞에서 만든 저장소에 CVS 명령을 내리려면 다음과 같이 한다.

$ cvs -d /home/CVS 명령

   개발자가 rsh을 이용하는 경우

개발자가 저장소와 다른 기계를 사용할 경우 일반적으로 쓰이는 방법이다. 여럿이 진행하는 프로젝트는 대부분 개발자들이 자신의 기계에서 작업을 하게 되는데, 이 방식을 이용하면 그러한 경우에도 쉽게 저장소를 공유할 수 있게 해 준다. 이 방법을 쓰려면 먼저 rsh을 사용할 수 있어야 한다. 예를 들어 저장소는 cvs.webdox.co.kr이란 기계에 있고, 개발자의 기계는 birch.webdox.co.kr이라고 하자. 개발자는 cvs.webdox.co.kr에 minskim이란 계정을 갖고 있고, birch.webdox.co.kr에 min이란 계정을 갖고 있다. 그러면 다음과 같은 방법으로 rsh을 쓸 수 있는지를 시험해 볼 수 있다.

rsh -l minskim cvs.webdox.co.kr 'echo $PATH'
뒤의 명령이 수행된다면 rsh을 쓸 수 있는 것이다. 만일 그렇지 않다면 cvs.webdox.co.kr에 로그인하여 .rhosts란 파일을 만들자. .rhosts에는 다음과 같은 내용이 있어야 한다.
birch.webdox.co.kr min
이제 다시 한 번 위의 rsh 명령을 시험해보자. 이번에도 안 된다면 cvs.webdox.co.kr에서 rsh을 통한 접속을 막아 놓았을 가능성이 크다. 이는 cvs.webdox.co.kr의 관리자에게 문의하여 해결하여야 한다. rsh을 쓸 수 있다면 CVS 명령을 다음과 같이 사용하면 된다.
$ cvs -d :ext:minskim@cvs.webdox.co.kr:/home/CVS 명령
만일 rsh과 같은 방식으로 동작하는 다른 명령이 있다면 그 명령을 사용할 수도 있다. 대표적인 것이 ssh이다. 특히 누군가 네트웍을 통해 전달되는 계정의 암호를 가로챌 위험이 있다면 rsh 대신 ssh을 사용하여 그 위험을 줄일 수 있다. 그러기 위해서는 CVS_RSH라는 환경변수를 ssh로 선언해주면 된다. 이 경우 저장소가 있는 기계에서는 .rhosts 파일을 만들 필요는 없는 대신 sshd 등이 돌고 있어야 한다.

   암호 인증 방식 이용

프로젝트에 직접 이용하지 않더라도 CVS가 필요한 경우는 CVS를 사용하는 여러 공개 프로젝트들의 최신 프로그램들을 받아 오는 경우일 것이다. 개발이 빨리 진행되는 프로젝트들은 대부분 CVS를 통해 프로그램을 받아 갈 수 있도록 하고 있다. 이런 프로젝트들의 홈페이지에서는 'CVSROOT를 ...로 설정하십시오'라는 문구를 볼 수 있는데, 이것이 바로 저장소의 위치이다. Mozilla의 최신 source를 받아 보고 싶다면 다음과 같이 하면 된다.

$ cvs -d :pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot login
그러면 암호를 물어오는데 암호는 anonymous이다. 일단 로그인을 하고 나면 인증 정보가 .cvspass란 파일에 저장이 되며, 이후로는 암호 없이 CVS 명령을 이용할 수 있다. 방법은 다음과 같다.
$ cvs -d :pserver:anonymous@cvs-mirror.mozilla.org:/cvsroot 명령

프로젝트 초기화

사용 가능한 CVS 저장소가 있고, 어떻게 저장소를 이용하는지 알고 있다면 프로젝트를 시작할 모든 준비가 끝난 셈이다. 더군다나 기존의 프로젝트에 CVS를 도입하려는 경우라면 이미 디렉토리와 파일들이 있을테니 이를 저장소에 저장하는 방법만 배우면 된다. 하지만 처음 시작하는 프로젝트를 CVS로 관리하려 한다면 먼저 디렉토리 구조를 잘 설계해야 한다. 물론 언제라도 CVS를 이용해서 파일의 위치를 옮기고 새로운 디렉토리를 만드는 등의 일은 가능하다. 하지만 처음부터 디렉토리 구조와 각 디렉토리에 들어갈 파일들을 잘 정해 놓는 것은 전체 프로젝트 관리에도 도움을 주므로 되도록이면 나중에 파일이나 디렉토리의 위치 변경이 생기지 않도록 설계를 하고 프로젝트를 시작하도록 하자.

먼저 할 일은 프로젝트의 이름을 정하는 것이다. 이 이름은 저장소에서 이 프로젝트 관련 파일들을 저장하는 디렉토리의 이름으로 쓰인다. 여기에서는 myprj란 이름으로 프로젝트를 만들어 보기로 하겠다. 이후에 나올 모든 예는 환경변수 CVSROOT가 제대로 설정되어 있다고 가정한다. CVSROOT를 어떻게 설정해야 하는가에 대해서는 앞 절의 내용을 참고하기 바란다.

myprj란 디렉토리가 다음과 같이 되어 있다고 하자.

$ cd myprj
$ ls
Hello.java     README
저장소에 새 프로젝트를 만들고 이 두 파일을 저장하기 위해서는 import 명령을 사용한다. import 명령의 사용법은 다음과 같다.
$ cvs import -m "메시지" 프로젝트이름 vender_tag release_tag
'메시지'는 프로젝트를 시작하면서 저장소에 기록하고 싶은 내용을 적어주면 된다. CVS는 파일을 저장할 때마다 메시지를 적도록 하고 있다. 이 내용을 원하는 때에 다시 볼 수 있으므로 개발자가 어떤 작업을 했는지를 적어 놓으면 다른 개발자들이 파일의 변경 내용, 작업 진척 상황등을 파악하는데에 도움을 줄 수 있다. 뒤의 두 태그는 지금 단계에서는 별 의미가 없으므로 적당한 말을 써 주면 된다. myprj란 프로젝트를 저장소에 만들기 위해서는 다음 명령을 사용한다.
$ cvs import -m "프로젝트 시작" myprj webdox start
N myprj/Hello.java
N myprj/README

No conflicts created by this import
앞에 말했듯이 뒤의 두 단어는 별 의미가 없으므로 크게 신경쓰지 말기 바란다. 이 명령을 실행하면 CVS는 저장소에 myprj란 디렉토리를 만들고, 거기에 README와 Hello.java를 저장한다. 이 외에도 옵션으로 준 메시지와 각 파일의 부가적인 정보를 기록하게 된다. 출력되는 메시지를 보면 각 파일 이름 앞에 N이란 글자가 있는 것을 알 수 있다. CVS는 특정 명령 수행시에 파일별로 수행 결과를 나타낸다. N은 새로운 파일이 추가된 것을 의미한다. 이후의 예에서 다른 경우들도 보게 될 것이다. 마지막 줄에서 conflict란 현재 디렉토리에 있는 파일을 저장소에 저장하게 되면 어떤 이유로 이미 저장소에 있는 파일과 충돌을 일으키는 경우를 뜻한다. 다른 사람이 작업해서 저장소에 저장해 놓은 것을 덮어쓰려 한다거나 하는 경우인데, 자세한 것은 뒤에서 conflict가 발생하는 경우를 예로 들어 설명하겠다. 여기에서는 저장소에 아무 파일도 없으므로 충돌(conflict)이 생기지 않는 것이 당연하다.

import 명령으로 프로젝트를 초기화하고 나면 더 이상 현재 디렉토리에 갖고 있는 것들을 사용하면 안 된다. 현재 디렉토리에는 CVS에 관련된 정보가 아무 것도 없기 때문이다. 대신 저장소에 모든 내용이 들어 있으므로 myprj 디렉토리 자체를 완전히 지워버려도 괜찮다. 다음 절에서 소개할 명령들을 이용해서 언제라도 다시 불러올 수가 있다. 이상으로 프로젝트 초기화는 모두 끝난 것이며, 이후로는 각 개발자들이 CVS를 이용하여 프로젝트를 진행시키는 일만이 남아 있다.

프로젝트 진행

이제 프로젝트에 참여하는 개발자의 입장에서 프로젝트를 바라보자. 모든 필요한 파일들은 저장소에 갖추어져 있고, 프로젝트 관리자로부터 저장소의 위치와 프로젝트 이름도 통보받았다. 그러면 개발자는 앞으로의 작업을 위해 저장소의 위치를 환경변수 CVSROOT에 저장하고, 작업을 시작하면 된다. 단, CVS는 파일의 변경 시각을 참조하는 경우가 많으므로 각 개발자들이 사용하는 기계의 시간을 서로 맞추어 놓을 필요가 있다.

   작업 공간 생성 (checkout)

작업을 시작하기 위해서는 파일을 마음대로 변경하고 저장하여 테스트해 볼 수 있는 작업 공간이 필요하다. 저장소에 있는 파일들을 불러와 나만의 작업 공간을 만드는 명령이 checkout이다. 앞에서 만든 myprj를 checkout 명령으로 불러 오자.

$ cvs checkout myprj
cvs checkout: Updating myprj
U myprj/Hello.java
U myprj/README
checkout 대신 co만 써 주어도 된다. 앞에 붙은 U는 파일이 갱신(update)되었음을 의미한다. 명령을 수행하고 나면 현재 디렉토리에 myprj란 디렉토리가 생긴다. 이 디렉토리가 바로 작업 공간이다. 이 안에서 필요에 따라 파일들을 수정하고, 이를 다시 저장소로 저장하면 되는 것이다. 그러면 디렉토리 안에는 어떤 파일들이 들어 있을까?
$ cd myprj
$ ls
CVS    Hello.java    README
프로젝트를 초기화할 때 있던 파일들이 그대로 들어 있는 것을 볼 수 있다. 하지만 하나 달라진 것이 있다. CVS란 디렉토리가 바로 그것이다. 바로 이 디렉토리에 CVS가 파일들을 관리하는데에 필요한 정보가 기록된다. 예를 들면 각 파일들의 버젼, 최종 수정 시각, 저장소의 위치 등이 이에 속한다. 앞으로 여러 CVS 명령들을 사용하게 되는데, 그때마다 CVS는 이 디렉토리의 정보를 참고하여 각 명령을 수행한다. 하지만 개발자가 이 디렉토리의 내용에 신경쓸 필요는 전혀 없다. 관심이 있다면 살펴보는 것은 좋지만, 내용을 임의로 변경한다거나 해서는 안 된다.

   작업 내용의 저장 (commit)

만들어진 작업 공간에서 작업을 시작해보자. Hello.java의 내용이 다음과 같다고 하자.

public class Hello {
    public static void main(String args[]) {
    }
}
여기에 인사말을 출력하는 부분을 추가해보자. 아무 편집기로나 파일을 열어 작업하면 된다. CVS를 사용한다고 해도 특별한 에디터나 통합 개발 환경을 필요로 하지는 않는다. Hello.java에 '안녕하세요?'라고 출력하는 문장을 추가했다고 하자.
public class Hello {
    public static void main(String args[]) {
        System.out.println("안녕하세요?");
    }
}
컴파일을 해서 실행을 해 보고 예상대로 실행된다면 이제 작업 내용을 저장소에 저장해야 한다. 그래야 다른 개발자들도 인사말이 추가되었다는 것을 알게 될 것이다. 파일의 변동 사항을 저장소에 저장하는 명령은 commit이다.
$ cvs commit -m "인사말 추가" Hello.java
Checking in Hello.java;
/home/CVS/myprj/Hello.java,v  
<-- Hello.javanew revision: 1.2; previous revision: 1.1done
어떤 명령인지 쉽게 이해할 수 있을 것이다. -m은 앞의 예와 마찬가지로 메시지를 의미한다. 이 명령으로 Hello.java는 '인사말 추가'란 메시지와 함께 저장소에 저장된다. 그렇다고 이전의 Hello.java가 없어지는 것은 아니다. CVS는 각 버젼의 변경 내용을 파악하여 언제라도 개발자가 원하는 버젼을 꺼내 줄 수 있도록 파일들을 저장한다. 출력 결과를 보면 원래 있던 Hello.java는 1.1이고, 새로 저장된 Hello.java는 1.2임을 알 수 있다. 이 번호는 CVS가 자동으로 붙이는 것이며 변경 사항이 저장될 때마다 올라가므로 각 파일마다 다를 수 있다. 나중에 특정 버젼이 필요하다면 이 번호를 이용해서 불러 오면 된다.

맨 끝의 파일 이름은 생략할 수 있다. 파일 이름을 주지 않으면 CVS가 변경된 파일을 모두 찾아 저장소에 저장한다. 이때 저장되는 모든 파일에는 같은 메시지가 붙게 된다. 하지만 파일마다 다른 메시지를 붙이고 싶다거나 특정 파일의 변경 내용만을 저장하고 싶다면 위의 예처럼 파일 이름을 명시해 주어야 한다.

commit을 할 때 주의할 점은 반드시 제대로 동작하는가를 확인하고 commit을 해야 한다는 것이다. 일단 commit을 하면 다른 개발자들도 나의 작업 결과를 받아갈 수 있다. 그러므로 내가 제대로 컴파일조차 되지 않는 파일을 commit으로 저장소에 저장하면 이 파일을 받아간 다른 개발자들도 컴파일이 안 되는 결과를 가져올 것이다. 특히나 여러 파일을 변경하고 일부 파일만을 commit하는 경우라면 더욱 주의해야 한다. 공동 작업을 하는 경우라면 이렇게 자신의 부주의로 다른 개발자들에게 피해를 입히는 일은 절대로 삼가야 한다. 대개의 경우 변경 내용 전체가 서로 관련이 있는 경우가 많으므로 특정한 인자를 주지 않고 commit 명령을 사용하여 작업 공간 전체의 변경 내용이 한꺼번에 저장소에 저장되도록 하는 것이 좋다.

   저장소의 파일 받아오기 (update)

공동 작업에서 자신의 작업 결과를 저장하는 일 못지 않게 중요한 일은 다른 사람의 작업 결과를 받아오는 일이다. 자신이 작업한 내용을 모두 저장한 상태라면 작업 디렉토리를 지워버리고 checkout 명령으로 새로운 작업 공간을 만드는 것도 한 방법이 될 것이다. 하지만 이는 모든 파일을 새로 받아오므로 저장소와의 연결이 느릴 경우에는 상당한 시간을 기다려야 할 수도 있다. 보다 좋은 방법은 update 명령을 사용하는 것이다. 작업중이던 디렉토리에서 이 명령을 쓰면 CVS에 저장된 파일들 중 내가 받아온 이후로 변경된 것들만을 다시 받아 온다.

$ cvs update
cvs update: Updating .
현재 작업 공간에 있는 파일들이 저장소에 있는 파일과 모두 동일하면 위와 같이 별다른 출력 없이 끝날 것이다. 그렇다면 작업 공간에 있는 파일을 수정하고 commit을 하지 않은 상태라면 어떤 결과가 나오는지 보기 위해 Hello.java를 다음과 같이 수정해보자.
public class Hello {
    public static void main(String args[]) {
        System.out.println("안녕하세요?");
	System.out.println("반갑습니다.");
    }
}
원래의 파일에 "반갑습니다."를 출력하는 문장을 추가하였다. 이제 update 명령을 실행하면 다음과 같이 나올 것이다.
$ cvs update
cvs update: Updating .
M Hello.java
파일 이름 앞에 있는 영문자(이 경우는 M)가 각 파일별 상태를 알려준다. M은 파일이 변경된(modified) 상태임을 의미한다. 즉, 파일의 내용이 원래 저장소에 있는 것과 달라졌을 때 M을 써서 표시하게 된다.

저장소의 파일과 작업 공간의 파일이 달라질 수 있는 또 다른 경우는 다른 사람이 파일을 변경하여 commit 명령으로 저장소에 저장한 경우이다. 예를 들어 원래의 README 파일이 다음과 같았다고 하자.

$ cat README
이 파일은 매우 중요한 내용을 담고 있습니다.
그런데 다른 개발자가 README 파일을 다음과 같이 변경하여 저장소에 저장하였다.
$ cat README
이 파일은 매우 중요한 내용을 담고 있습니다.
1999년 12월 11일
$ cvs commit -m "날짜 추가" README
Checking in README;
/home/CVS/myprj/README,v  
<-- READMEnew revision: 1.2; previous revision: 1.1done
이제 내가 update를 하면 다음과 같은 결과를 볼 수 있다.
$ cat README
이 파일은 매우 중요한 내용을 담고 있습니다.
$ cvs update
cvs update: Updating .
M Hello.java
U README
README 앞에 붙은 U는 파일이 갱신된(updated) 것을 의미한다. 이제 내 작업 공간의 README가 저장소에 저장된 내용과 같게 갱신된 것을 볼 수 있다.

$ cat README
이 파일은 매우 중요한 내용을 담고 있습니다.
1999년 12월 11일

그렇다면 이 두 경우가 혼합된 경우는 어떤 일이 발생할까? 즉, 다른 개발자가 수정하여 저장소에 저장한 파일을 나 역시 수정한 후 update를 하는 경우이다. 다른 개발자가 Hello.java를 다음과 같이 main 함수 앞에 주석을 한 줄 추가하여 commit을 했다고 생각해보자.

$ cat Hello.java
public class Hello {
    // main 함수
    public static void main(String args[]) {
        System.out.println("안녕하세요?");
    }
}
$ cvs commit -m "주석 추가" Hello.java
Checking in Hello.java;
/home/CVS/myprj/Hello.java,v  
<-- Hello.javanew revision: 1.3; previous revision: 1.2done
그런 다음 내가 update를 하면 다음과 같이 된다.
$ cat Hello.java
public class Hello {
    public static void main(String args[]) {
        System.out.println("안녕하세요?");
	System.out.println("반갑습니다.");
    }
}
$ cvs update
cvs update: Updating .
RCS file: /home/CVS/myprj/Hello.java,v
retrieving revision 1.2
retrieving revision 1.3
Merging differences between 1.2 and 1.3 into Hello.java
M Hello.java
여기서 update 명령은 현재 디렉토리의 Hello.java는 1.2에서 수정된 상태이고, 저장소에 있는 Hello.java는 1.3이기 때문에 저장소의 내용을 가지고 1.2와 1.3의 차이를 파악하게 된다. 그 차이가 한 줄이 추가된 것임을 발견하고 나면 그 차이를 현재 디렉토리의 Hello.java에 반영한다. 하지만 Hello.java에는 "반갑습니다."를 출력하는 줄도 들어 있기 때문에 여전히 저장소에 있는 파일과는 다른 상태이다. 따라서 저장소에 있는 내용에서 변경되었다는 의미의 M을 출력하는 것이다. 실제로 Hello.java의 내용을 보면 다음과 같이 바뀐 것을 볼 수 있다.
$ cat Hello.java
public class Hello {
   // main 함수
   public static void main(String args[]) {
      System.out.println("안녕하세요?");
      System.out.println("반갑습니다.");
   }
}
다른 개발자가 추가한 주석이 들어 있으면서 내가 추가한 줄도 그대로 있음을 볼 수 있다. 이것이 CVS의 강력한 장점이다. 즉, 서로 다른 두 개발자가 같은 파일을 수정하였는데도, 서로의 작업을 하나로 합쳐주는 것이다. 한가지 주의할 점은, 만일 update를 수행할 때 Hello.java를 에디터에서 불러 작업중이었다면, 계속 에디터로 작업을 한 후 저장할 때 update에 의해 변경된 내용을 덮어 쓸 수가 있다는 것이다. 대부분의 에디터는 작업하고 있는 파일이 다른 프로그램에 의해 변경되면 이 사실을 사용자에게 알리고, 변경된 파일을 다시 불러올 수 있도록 하지만, 간혹 그렇지 못한 에디터도 있을 수 있으므로 조심하기 바란다.

   충돌의 해결

비록 CVS가 두 개발자의 작업 내용을 합쳐 주기는 하지만 여기에도 한계는 있다. 두 개발자가 같은 부분을 수정한다면 CVS는 누구의 변경 내용을 택해야 하는지 알 도리가 없다. 이런 경우를 충돌(conflict)라 하며, CVS는 이 사실을 개발자에게 알려 개발자가 적절한 조치를 취할 수 있도록 한다.

위의 예에서 다른 개발자가 작업을 계속하여 "환영합니다."를 출력하도록 만들고, 이를 저장소에 저장했다고 하자. 그러면 저장소에는 다음과 같은 내용이 들어 있을 것이다.

public class Hello {
   // main 함수
   public static void main(String args[]) {
      System.out.println("안녕하세요?");
      System.out.println("환영합니다.");
   }
}
이 때 내가 update를 하면 충돌이 발생한다.
$ cvs update
cvs update: Updating .
RCS file: /home/CVS/myprj/Hello.java,v
retrieving revision 1.3
retrieving revision 1.4
Merging differences between 1.3 and 1.4 into Hello.java
rcsmerge: warning: conflicts during merge
cvs update: conflicts found in Hello.java
C Hello.java
현재 디렉토리의 Hello.java는 1.3에서 변경된 상태("반갑습니다." 출력 추가)이다. 저장소에 저장되어 있는 것은 1.4이므로 CVS는 저장소에 있는 1.3과 1.4의 차이를 살핀다. 둘의 차이는 다른 개발자에 의해 "환영합니다."를 출력하는 줄이 추가된 것이다. 이를 현재디렉토리의 Hello.java에 추가하려고 시도하지만, 그 위치에 이미 다른 내용("반갑습니다." 출력)이 들어가 있음을 알게 된다. 결국 CVS는 충돌(C로 표시)이 생겼음을 알리고 수행을 마친다. Hello.java의 내용을 보면 어디에서 어떤 충돌이 발생했는지 CVS가 표시해 놓을 것을 발견할 수 있다.
$ cat Hello.java
public class Hello {
   // main 함수
   public static void main(String args[]) {
      System.out.println("안녕하세요?");
<<<<<<< Hello.java      
System.out.println("반갑습니다.");=======
System.out.println("환영합니다.");>>>>>>> 1.4 } }
<<<<<<<와 >>>>>>> 사이가 충돌이 일어난 부분이다. 그 부분은 다시 두 부분으로 나뉘는데, ======= 이전까지가 현재 디렉토리에 있는 파일의 내용이고, 그 이후가 저장소에 있는 파일의 내용이다. 개발자는 이걸 보고 어느 한 쪽을 없애거나 두 내용을 적절히 합친 후 다시 commit을 해 주면 된다. 이 경우 "반갑습니다."가 더 마음에 들어 "환영합니다."를 삭제하기로 했다면 다음과 같이 고치고 commit을 한다.
$ cat Hello.java
public class Hello {
   // main 함수
   public static void main(String args[]) {
      System.out.println("안녕하세요?");
      System.out.println("반갑습니다.");
   }
}
$ cvs commit -m "인사말 변경" Hello.java
Checking in Hello.java;
/home/CVS/myprj/Hello.java,v  
<-- Hello.javanew revision: 1.5; previous revision: 1.4done
하지만 다른 개발자가 수정한 내용을 그대로 놔 두고 내가 고친 부분을 없애기로 했다면 따로 commit을 할 필요가 없다. 이미 저장소에 그 내용이 들어 있기 때문이다.

충돌은 commit 시에도 발생할 수 있다. 앞의 예에서 다른 개발자가 "환영합니다." 출력 문장을 추가하여 commit을 한 후, 내가 update 대신 commit을 하려 했다면, CVS는 다음과 같이 충돌이 발생했음을 알려 줄 것이다.

$ cvs commit -m "반갑습니다 추가" Hello.java
cvs commit: Up-to-date check failed for `Hello.java'
cvs [commit aborted]: correct above errors first!
이런 메시지를 보게 되면 update 명령으로 어디에서 충돌이 발생했는지를 확인하면 된다. 그 결과와 수정 방법은 앞에서 나온 것과 동일하다.

   파일의 추가/삭제 (add/delete)

지금까지는 저장소에 있는 파일을 수정하고, 그 결과를 다시 저장하는 방법을 알아보았다. 하지만 작업을 하다 보면 새로운 파일을 만들거나 기존의 파일을 지워야 할 경우가 생긴다. 이런 경우에 사용하는 명령이 add와 delete이다. 사용 방법은 매우 간단하다. Test.java란 파일을 새로 만들려면 먼저 현재 디렉토리에 Test.java를 생성한 후 다음과 같이 하면 된다.

$ cvs add Test.java
cvs add: scheduling file `Test.java' for addition
cvs add: use 'cvs commit' to add this file permanently
메시지에서 알 수 있듯이 add 명령만으로는 저장소에 Test.java가 생기지 않는다. 저장소를 변경하는 것은 commit 명령뿐이다. add 명령은 단지 commit 명령시에 Test.java를 추가해야 한다는 것을 기록해 놓을 뿐이다. 이 사실은 update로 확인해 볼 수도 있다.
$ cvs update
cvs update: Updating .
A Test.java
앞의 A는 추가될(added) 파일임을 의미한다. 그러므로 Test.java는 나중에 작업 내용 전체를 commit할 때 저장소에 추가될 것이다. 아니면 미리 Test.java를 인자로 commit을 하여 바로 추가되도록 할 수도 있다.
$ cvs commit -m "새 파일" Test.java
RCS file: /home/CVS/myprj/Test.java,v
done
Checking in Test.java;
/home/CVS/myprj/Test.java,v  <--  Test.javainitial revision: 1.1done
추가된 파일은 다른 개발자가 update를 할 때 그 개발자의 작업 공간에도 생겨나며, 파일 내용이 갱신되는 것과 마찬가지로 U를 써서 표시한다.
$ cvs update
cvs update: Updating .
U Test.java

파일을 지우는 것도 마찬가지이다. Test.java를 지우려면 다음과 같이 한다.

$ rm Test.java
$ cvs delete Test.java
cvs remove: scheduling `Test.java' for removal
cvs remove: use 'cvs commit' to remove this file permanently
delete 명령을 사용하기 전에 반드시 먼저 파일을 작업 공간에서 삭제하여야 한다. 이제 commit 명령을 사용하면 Test.java는 저장소에서 삭제될 것이다. 이것도 update로 확인해 본다면 다음과 같이 나올 것이다.
$ cvs update
cvs update: Updating .
R Test.java
R은 삭제될(removed) 파일임을 나타낸다. 이제 commit으로 완전히 삭제해 보자.
$ cvs commmit -m "삭제" Test.java
Removing Test.java;
/home/CVS/myprj/Test.java,v  
<-- Test.javanew revision: delete; previous revision: 1.1done
이렇게 삭제된 파일은 이후로 다른 개발자가 update를 하게 되면 다음과 같은 메시지를 출력하면서 그 개발자의 작업 디렉토리에서도 삭제된다.
$ cvs update
cvs update: Updating .
cvs update: warning: Test.java is not (any longer) pertinent

   작업 기록 열람 (log)

그 동안의 예에서 본 바와 같이 CVS는 작업 내용을 저장소에 저장할 때마다 메시지를 적도록 하고 있다. 이 내용은 저장소에 함께 저장되어 log 명령을 이용하면 언제라도 꺼내 볼 수 있다. Hello.java를 예로 들어 보자.

$ cvs log Hello.java

RCS file: /home/CVS/myprj/Hello.java,v
Working file: Hello.java
head: 1.5
branch:
locks: strict
access list:
symbolic names:
        start: 1.1.1.1
        webdox: 1.1.1
keyword substitution: kv
total revisions: 6;    selected revisions: 6
description:
----------------------------
revision 1.5
date: 1999/12/12 04:04:23;  author: minskim;  state: Exp;  lines: +1 -0
인사말 변경
----------------------------
revision 1.4
date: 1999/12/12 04:03:50;  author: sehkone;  state: Exp;  lines: +0 -1
환영합니다 추가
----------------------------
revision 1.3
date: 1999/12/12 04:01:40;  author: sehkone;  state: Exp;  lines: +2 -1
주석 추가
----------------------------
revision 1.2
date: 1999/12/12 03:51:36;  author: minskim;  state: Exp;  lines: +1 -0
인사말 추가
----------------------------
revision 1.1
date: 1999/12/12 03:50:48;  author: minskim;  state: Exp;
branches:  1.1.1;
Initial revision
----------------------------
revision 1.1.1.1
date: 1999/12/12 03:50:48;  author: minskim;  state: Exp;  lines: +0 -0
프로젝트 시작
=============================================================================
매번 commit을 할 때마다 날짜와 시각, 저장한 사람, 메시지 등이 기록되는 것을 알 수 있다. 이렇게 log 명령을 쓰면 언제 누가 어떤 작업을 했는지를 한눈에 알 수 있기 때문에 그간의 작업 내용을 쉽게 파악할 수 있다. 이 기능을 잘 활용하기 위해서는 무엇보다도 매번 commit을 할 때 알기 쉽고 작업 내용을 잘 반영할 수 있는 메시지를 적는 것이 중요하다.
Posted by theYoungman
engineering/System Eng.2007. 2. 22. 14:47
1. CVS(Current Version System)

1.1. CVS란 무엇인가?

프로젝트를 여러 명이 진행하게 되면, 소스 파일의 변경 사항을 적절히 관리해 충돌을 방지하고 변경 이력을 추적하는 것이 중요하다. CVS는 소스 파일을 관리하는 데 널리 사용되는 시스템이다. 로컬 공유 디렉터리 뿐 만 아니라 인터넷을 통한 소스 코드 공유 및 관리가 가능하다. 또한, 한 파일에 대한 동시 작업이 허용되어 병렬 개발이 가능하다.


1.2. 저장소 설정하기

일단 CVS를 사용하기위해 저장소를 설정해야 한다. 저장소의 위치는 계정 홈 디렉터리 하위의 CVS로 설정하였다. 즉, 절대경로 /home/unga10/CVS 이다.

사용자 삽입 이미지
<저장소 위치 설정>

cvs를 호출해 작업을 할 때마다 항상 -d 옵션으로 저장소를 지정해야 한다. 만약 -d 옵션이 없다면 cvs는 환경 변수 CVSROOT를 검색한다. 따라서 다음이 환경 변수를 설정하면 -d 옵션을 생략할 수 있다.

사용자 삽입 이미지

<환경 변수 설정>

이제 프로젝트를 저장소 안으로 복사해야 한다. CVS에서 프로젝트는 관련 파일과 디렉터리의 모음이다. 저장소 하위에 test라는 경로에 모든 내용을 복사했다.

사용자 삽입 이미지

<저장소로 현대 폴더의 모든 내용을 복사함>

제대로 저장소에 들어갔는지 확인하기 위해, 다른 경로로 가서 CVS로부터 코드를 얻어온다.

사용자 삽입 이미지

<CVS로부터 소스 코드를 얻어옴>

hello.c에 printf(“Have a nice day\n"); 한 줄을 추가한 뒤 cvs를 통해 바뀐 내용을 확인해 보도록 하겠다.

사용자 삽입 이미지

<바뀐 내용을 보여줌>

이제 commit을 통해 바뀐 소스를 저장소에 업데이트 할 수 있다. commit을 수행하면 로그 메시지를 입력하도록 편집기가 나온다. 로그 메시지를 입력하지 않고 그냥 편집기를 종료했더니 경고 메시지가 출력되어 무시하고 계속 진행했다.

사용자 삽입 이미지

<경고를 무시하고 그냥 커밋 함>

rdiff를 이용해 버전 이력을 추적할 수 있다. 앞선 커밋을 통해 버전 1.1에서 1.2로 변했으므로 1.1 이후의 변경 사항을 알아보도록 하겠다.

사용자 삽입 이미지
<새로 추가한 줄에 + 표시가 있음>

<네트워크로 CVS 액세스하기>

환경 변수 CVSROOT의 값을 :<서버명>:<아이디@서버주소>:/<CVS 상대경로>로 설정한 후 cvs login을 수행하고 패스워드를 입력하면 원격 저장소에 접속된다. 모든 명령은 로컬에서와 동일하나 -z3 옵션으로 코드 송수신시 압축을 수행할 수 있다.

inetd을 통해 CVS 서버를 열 수도 있다.

Posted by theYoungman
engineering/System Eng.2007. 2. 22. 12:00
첨부파일 다운로드

웹 표준코딩을 준수 했을때 어떤 가시적인 효과가 있는지 직접 검증하기 위하여 한가지 실험을 하였습니다. 첨부된 파일은 동일한 화면을 Table로 구성하였을 때와 Div로 구성하였을 때 어떤 차이가 있는지 보여드리기 위하여 작성된 html 및 css 파일입니다.

사용자 삽입 이미지
사용자 삽입 이미지

Table Layout Div Layout
HTML 코드라인 수 = 59 Line HTML 코드라인 수 = 31 Line
HTML 파일 용량 = 1.58 KB HTML 파일 용량 = 746 Byte
<table width="800" border="0" cellspacing="0" cellpadding="20">
<tr>
<td colspan="3" valign="top" bgcolor="#000000" id="logo">Table Layout</td>
</tr>
<tr>
<td width="200" height="400" valign="top" bgcolor="#666666">
<table width="100%" border="0" cellspacing="0" cellpadding="5" id="menu">
<tr>
<th bgcolor="#999999" style="height:30px">Menu List </th>
</tr>
<tr>
<td>Menu1</td>
</tr>
<tr>
<td>Menu2</td>
</tr>
<tr>
<td>Menu3</td>
</tr>
<tr>
<td>Menu4</td>
</tr>
<tr>
<td>Menu5</td>
</tr>
</table>
</td>
<td height="400" valign="top" bgcolor="#999999">
<table width="380" border="0" cellspacing="0" cellpadding="5" id="contents">
<tr>
<th bgcolor="#999999" style="height:30px">Page Title </th>
</tr>
<tr>
<td valign="top">Layout Test Page. </td>
</tr>
</table>
</td>
<td width="100" height="400" valign="top" bgcolor="#CCCCCC">
<table width="100%" border="0" cellspacing="0" cellpadding="5" id="contents">
<tr>
<th bgcolor="#999999" style="height:30px">Links </th>
</tr>
</table>
</td>
</tr>
<tr>
<td colspan="3" valign="top" bgcolor="#333333" id="copy">Copyright</td>
</tr>
</table>

<div id="logo">Table Layout</div>
<div id="page">
<div id="menu">
<h1>Menu List </h1>
<ul>
<li>Menu1</li>
<li>Menu2</li>
<li>Menu3</li>
<li>Menu4</li>
<li>Menu5</li>
</ul>
</div>
<div id="contents">
<h2>Page Title </h2>
<div id="text">Layout Test Page. </div>
</div>
<div id="links">
<h2>Links</h2>
</div>
</div>
<div id="copy">Copyright</div>

CSS 코드라인 수 = 8 Line CSS 코드라인 수 = 11 Line
CSS 파일 용량 = 919 Byte CSS 파일 용량 = 1.27 KB
#logo { font-family:Arial; font-size:100px; color:#FFFFFF; font-weight:bold}
#menu th { border-bottom:3px solid #FFFFFF; font-family:Arial; font-size:24px; font-weight:bold; color: #FFFF00; text-align:left}
#menu td { border-bottom:1px dotted #FFFFFF; font-family:Arial; font-size:14px; font-weight:bold; color:#FFFFFF}
#contents th {border-bottom:3px solid #FFFFFF; font-family:Arial; font-size:24px; font-weight:bold; color: #FFFF00; text-align:left; background:#666666}
#contents td { font-family: Verdana; font-size:12px; line-height:150%; color:#FFFFFF; text-align:justify}
#links th {border-bottom:3px solid #FFFFFF; font-family:Arial; font-size:24px; font-weight:bold; color: #FFFF00; text-align:left; background:#666666}
#links td { font-family: Verdana; font-size:12px; line-height:150%; color:#FFFFFF; text-align:justify}
#copy { font-family: Verdana; font-size:50px; font-weight:bold; color:#FFFFFF; }
h1 { width:190px; padding:5px; font-size:24px; font-weight:bold; font-family:Arial; color:#FFFF00; background:#999999; border-bottom:3px solid #FFFFFF; margin:0}
h2 { padding:5px; font-size:24px; font-weight:bold; font-family:Arial; color:#FFFF00; background:#666666; border-bottom:3px solid #FFFFFF; margin:0}
#logo { font-family:Arial; font-size:100px; color:#FFFFFF; font-weight:bold; background:#000000; width:760px; padding:20px}
#page { position:relative; width:800px; overflow:visible}
#menu { position:relative; padding:20px; width:200px; background:#666666; float:left; height:400px }
#menu ul { margin:0; padding:0}
#menu li { width:190px; padding:5px; border-bottom:1px dotted #FFFFFF; font-family:Arial; font-size:14px; font-weight:bold; color:#FFFFFF; list-style:none}
#contents { position:relative; padding:20px; background: #999999; font-size:12px; font-family:Verdana; color:#FFFFFF; width:380px; float:left; height:400px}
#text { padding:5px; line-height:150%; font-family:Verdana; text-align:justify}
#links { position:relative; float:left; background:#CCCCCC; width:100px; padding:20px; clear:right; height:400px}
#copy { position:relative; clear:both; background: #333333; font-family: Verdana; font-size:50px; font-weight:bold; color:#FFFFFF; width:760px; padding:20px }

DIV+CSS 레이아웃과 웹 표준 코딩의 장점

1. 작고 가벼운 구조와 용량

Div 레이아웃의 경우 순수한 HTML 파일의 코드 라인 수와 파일의 용량은 Table 레이아웃에 비하여대략 50%정도 절감됩니다. 이렇게표준으로 코딩된 HTML 파일은 한개 사이트에서 수십 수천 페이지에 이르기 때문에 사용자의 트래픽으로 인한 서버의 부하를 경감할 수 있습니다. 유지보수 비용과 직결되는 문제죠.

Div 를 이용한 표준 코딩이 가벼워 질 수 밖에 없는 이유는 다음과 같습니다.내용 하나를 원하는 위치에 표기하기 위하여 Table 로 코딩하려면 <table><tr><td>내용</td></tr></table> 이와 같이 3쌍의 태그로 둘러싸야 합니다.하지만 Div 로 코딩하면 <div>내용</div> 1쌍의 태그로 족합니다. HTML 태그의 코드 라인수와 용량이 반으로 줄만 하죠

CSS 코드가 조금 더 늘어나긴 하지만 CSS 파일은 해당 웹사이트에 접속시 최초 1회 다운로드 되어 사용자 PC의 캐시메모리에 저장되며, 해당 웹사이트를 빠져나갈 때까지 다시 다운로드 되는 일이 거의 없는 파일입니다. 따라서 CSS 파일의 용량이 늘어나면 HTML 코드가 줄어들어도 전체적인 트래픽량은 대동소이 하지 않을까 라고 생각하신다면 잘못된 계산 입니다. HTML코드는 페이지를 열때마다 새롭게 다운로드 하지만 CSS 파일은 한번만 다운로드 되는 파일이기 때문입니다.

2. XML포멧으로 확장가능,CSS의존도를 높여디자인개편 비용을 절감

웹 표준 코딩은 문서의 구조와 표현을 분리합니다. 구조는 HTML으로 정의하고, 표현은 CSS로 정의합니다. 예를 들면 문서의 제목을 표현함에 있어서 <h1>태그는 구조적으로 의미있는 태그 입니다. 하지만 <h1> 태그는 글자를 단지 구조적으로만 의미있게 표현할 뿐보여주기 위한 장식적인 디자인 표현은 CSS 에서 담당합니다.이렇게 되면 HTML 태그는 문서의 구조만, CSS는 표현만 따로 분리하여 정의 한 것으로이는XML 데이터 문서로의 확장을 용이하게 만들고디자인 변경시 HTML 파일의 편집비율은 줄어들고 CSS파일만 수정하면 됩니다.

비표준 웹문서의 디자인 개편시HTML 1,000 페이지를 모두 일일이 열어서 Find & Replace 방식으로 수정하여야했습니다. 또는테이블 구조가 변경되는 경우기존의 페이지를 수정하는 것보다 다시 생산하는 방식이 훨씬 빠르기 때문에 디자인 개편은 곧 사이트의 재구축을 의미하였습니다.W3C에서 표준으로 권고하는 웹 표준방식으로 제작된 웹사이트는딸랑 CSS 1개 파일을 수정함으로서 HTML 1,000 페이지를 한꺼번에 디자인 개편하는 놀라운 경험을 할 수 있습니다.CSS의 본래 목적이 바로 그것입니다. 이것을 이해하려면 먼저 CSS의 디자인 표현 가능성과 한계에 대하여 알아두셔야 합니다. CSS의 디자인 표현 능력은 상상 그이상입니다.

3. 사람이나 로봇(컴퓨터)이이해하기 쉬운구조

둘러싸는 코드가간결하고 코드라인의 양이 줄기때문에 구조를 한눈에 파악하기 쉽습니다. 또한 그뿐만이 아닙니다.Div 레이아웃에서 h1, h2 태그를 사용한 것은 제목을 표기하기 위함이며 ul, li 태그를 사용한 것은 목록을구조적으로 의미있게 표현하기 위함 입니다. h1 태그 대신 제목을 <tr><td>...</td></tr>안에표현하는 것은어떤 의미도 없으므로 사람이든 검색로봇이든 이해하기 어려운 정보 입니다. 하지만h1태그로 둘러싼 제목은 사람 뿐만 아니라 검색로봇도 의미있게 받아들이고 XML 데이터로 확장하기에도 용이합니다. 그동안 하찮게 여겼던 태그들이 얼마나 중요한지 모르실 껍니다.

4. Table 은 고지식 하고 Div 는 유연합니다

솔직히 저는 처음 Div 코딩을 제안 받았을때 이해하기 어려웠습니다. 왜 그래야 하는지. Table 태그도 분명 표준 태그이며 편하고 유용하게 사용해 왔는데 왜이제는 사용하면 안돼는지 의아해 했습니다. 하지만 Div 의 장점을 알게 되면서 부터는 더이상 그것이 궁금하지 않게 되었습니다. 물론 Div 레이아웃을 사용하는 것이 웹 표준 방식이며소고기 맛을 처음 본 사람이 돼지고기는 쳐다보지도 않는 상황과 같다고 말씀드릴 수 있겠네요. 벽에 액자를 붙인다고 합시다. Table이 콘크리트 벽에 못을 깊숙히 때려박는 행위라면 Div 는 콘크리트 벽에 실리콘 접착행어를 달아주는 것과 같습니다. 언제든지 깔끔하게 떼어서 다른 위치에 붙일 수 있죠. 이게 가장 큰 장점 입니다. 게다가 레이아웃과 디자인 표현에 관한 코드가 CSS 파일안에서 모두 정의 되므로 문서의 구조와 내용은 변경하지 않고 디자인과 레이아웃만 바꾼다면 HTML 파일은 열어볼필요도 없어집니다. 레이아웃과 디자인을언제든지 쉽게 바꿀 수 있는 유지보수의 용이성이죠.

Table 은 현재 셀과 이웃셀이 연결되어 있습니다. 따라서 서로 높낮이나 크기를 완전히 다르게 하려면 따로 Table 태그를 한번 더 사용해야 합니다. 그리고 셀과 셀이 서로 연결되어 있기 때문에 특정부분의 레이아웃을 잘못 변경하면 페이지의 나머지 레이아웃도 함께 수정해야 합니다. 하지만 Div 태그는 각자 따로 노는 스타일 입니다. 물론 서로 밀어내고 붙어있는등 유기적인 관계를 가질 수도 있으며 이것을 변경하는 것은 매우 쉽습니다. css 코드의 position 속성만 변경하면 됩니다. 그럼 서로 함께 움직이거나 따로 놀도록 설정 할 수 있습니다. 다시 한번 말하지만 Div 태그는 둥둥 떠다니는 레이어의 성격을 함께지니고있으므로 언제든지 원하는 위치로 옮길 수 있어 유지보수가 매우 용이 합니다.

5. 접근성 문제는 웹 표준만 지키면 90% 이상 해결 됩니다

행정자치부에서 발표한 "홈페이지 구축운영 표준지침 2005" 문서에는 "한국형 웹 콘텐츠 접근성 지침 1.0" 이라는 별첨문서가 있는데 2005년부터 구축되기 시작한 행정기관 홈페이지는 해당 지침을 따르도록 되어 있는 말 그대로의 지침 입니다. 아직 강제성은 없지만 이 지침을 기준으로 평가하는 행위도 이루어 지고 있고 또 앞으로 강제될 활률이 높습니다.

이러한 접근성 지침의 근간이 되는 문서는 W3C에서 발표한 "웹 접근성 가이드" 입니다. 웹 표준은 접근성을 고려하여 제정된 것이기 때문에 웹 표준만 지키면 접근성 문제는 90%이상 자동으로 해결됩니다. 따로 떼어놓고 생각할 수 있는 문제가 아닙니다.

자료출처 : http://blog.naver.com/naradesign?Redirect=Log&logNo=110001611307

Posted by theYoungman
engineering/System Eng.2007. 2. 22. 09:34

apache 리눅스 설치 중 컴파일 에러 메시지에 대한 해결 방법//

http://www.gossamer-threads.com/lists/apache/users/324906


error message :

configure: error: Cannot use an external APR-util with the bundled APR

./configure --prefix=/usr/local/apache2 --with-included-apr



evan.cooch at cornell

Jan 10, 2007, 9:21 AM

Post #3 of 5 (1335 views)
Permalink
Re: 2.2.3 -> 2.2.4 upgrade problem | APR? [In reply to]

>
>
>> checking for APR-util... yes
>> configure: error: Cannot use an external APR-util with the bundled APR
>> To check to see if this new problem is caused by some change I've
>> made to the server (unlikely - all this machine does is serve web
>> pages - the only changes I ever make are security patches), I tried
>> re-compiling 2.2.3. No problem - everything worked as before.
>>
>> So, *something* has changed in 2.2.3 -> 2.2.4 that is *broken* on my
>> machine (meaning, I can't even configure 2.2.4). In the config.log, I
>> see the following:
>
> Could you post your ./configure line?

Sure - about as basic as you can get:

./configure --prefix=/usr/local/apache2

Thats it - I get the same error message whether or not I enable any
modules or not.

>
> Could you run the commands
>
> locate apr-1-config
/usr/local/apache2/bin/apr-1-config

> locate apu-1-config
>
> and tell us what the output is?
/usr/local/apache2/bin/apu-1-config

>
> Finally, try adding --with-included-apr to ./configure . This forces
> the build process to use the apr and apr-util that come with the
> release, which I think is what you want to do. I think this is
> relatively new logic, and may be what you're running into.

OK - thanks for the suggestion. Tried

./configure --prefix=/usr/local/apache2 --with-included-apr

which seemed to work (in the sense that I wasn't getting any error
messages). I followed this with make, which seemed to work fine (given
that make seems to run fine - no errors). I'm not quite ready for the
next test (make install), since I don't want to screw up my current,
working configuration. But, the suggestion of adding --with-included-apr
seems to have solved the basic .configure problem. Thanks!

Now, for the big question - what is this new argument doing, why do I
need it, and what really has changed between 2.23 and 2.24?

Thanks very much!

---------------------------------------------------------------------
The official User-To-User support forum of the Apache HTTP Server Project.
See <URL:http://httpd.apache.org/userslist.html> for more info.
To unsubscribe, e-mail: users-unsubscribe[at]httpd.apache.org
" from the digest: users-digest-unsubscribe[at]httpd.apache.org
For additional commands, e-mail: users-help[at]httpd.apache.org



Posted by theYoungman
engineering/System Eng.2007. 2. 22. 09:16
#########################################################
# 작성자 : 김래형
# date : 2007.02.15
# modified :
# Title : 리눅스에서 윈도우 파티션 사용하기
# 출처만 밝히시라, 그럴일은 없겠지만 책써서 팔아먹든 맘대로 하시라.
#########################################################




리눅스와 윈도우는 파일시스템이 다르므로 대부분의 사용자들은 윈도우 파일을 리눅스에서 볼 수 없다고 생각한다.

하지만 ntfs 가 아닌 fat32 포맷은 리눅스에서도 파일을 확인할 수 있다.
(참고로 ntfs 에서는 테스트 해보지 못하였으나 아마도 안될꺼 같다.)

이렇게 파티션을 공유할 경우에는 여러 장점이 생기며 활용하기 나름이다. 활용은 알아서 하시라~.


내가 테스트해 본 환경은 redhat fedora core 5 엿다.
설치 방법은 다음과 같다.


// fedora core 5 에서 test
// Windows 설치완료 후 Fedora 를 설치할 것

윈도우 초기 설치할때 파티션을 분할, 임의로 분할해도 된다.

윈도우 파티션은 ntfs 로 포맷해도 상관은 없으나 리눅스 파티션과, 공용으로 사용할 파티션은 일단 포맷하지 않는다.
어차피 윈도우 설치할때 하나의 파티션을 선택해 설치하므로 윈도우 설치 완료후 자체적으로 포맷해야 한다.

즉, 파티션을
1 : 윈도우 파티션
2 : 리눅스 파티션
3 : 공용 파티션

으로 나눈다. 드라이브 순서는 상관없다.

(1번은 반드시 윈도우 파티션이 와야 한다. 만약 윈도우를 통으로 파티션으로 잡아주고 파티션매직 같은 util 을 사용하여 논리적으로 파티션을 나눠주게 되면 리눅스에서 마운트 시킬대 슈퍼블록이 깨지는 경우가 발생한다. 절대 통으로 잡고 귀찮으니 파티션매직으로 나눠야지~ 라는 생각을 갖지 말라!!!)

1번 파티션에 윈도우 설치 완료 후 부팅, 2번 리눅스 파티션은 내비두고 3번 공용으로 사용할 파티션을 fat32 로 포맷한다.

그 후 2번 파티션에 리눅스 설치, 설치시 파티션을 잡고 자동으로 /swap 등을 잡아준다.

리눅스 설치 완료 후 3번 공용 파티션을 리눅스 시스템에 마운트 시켜주면 완료.

일단 터미널에서 fdisk -l 로 현재 설정된 파티션을 확인한다.

# fdisk -l


일반 ide 는 /dev/hda 에, scsi 는 /dev/sda 에 위치한다.

즉 /dev/hda1, /dev/hda2, /dev/hda3...
/dev/sda1, /dev/sda2, /dev/sda3...

이런식으로 표기 된다.

확인 후 마운트 명령어를 사용하여 파티션을 마운트 시켜준다.


# mount -t vfat /dev/sda6 /mnt/windows


즉 내가 설정한 공용파티션의 이름이 /dev/sda6 이고 리눅스 시스템에 마운트할 위치가 /mnt/windows 가 되는 것이다.
(리눅스 파일 시스템은 etx3 이며 vfat 은 fat32 를 의미한다.)

마운트할 디렉토리(/mnt/windows)는 미리 만들어져 있어야 한다.

# mkdir /mnt/windows


새로 파티션을 마운트 하면 기존의 디렉토리 내용은 보이지 않고 새로운 파티션의 내용이 보이게 된다.
(미리 잡힌 파티션뿐 아니라 새로운 하드를 연결할때도 이와 유사하다.)

확인은 알아서들 하시면 된다. xwindows 를 쓰셔서 바로가기를 만들어 놓으시든..
직접 찾아가서 확인하시든 편한대로 하시라.


멀티부팅 등과 관련한 기타사항들은 알아서 하시고 , 기타 관련된 사항들은 열심히 구글링 해보시길... 바란다.


ps. 부팅시 항상 마운트되어야 별도의 수정과정이 필요없다.
Posted by theYoungman
engineering/System Eng.2007. 2. 12. 17:14
XML DTD
2004/10/24 오후 10:54 | XML & WebServices

회사 내부 스터디에 사용한 것.
Professional XML 2nd ed.를 정리한 것임.

DTD ?

  • Document Type Definition - 문서 형 정의
  • DTD 선언은 데이터 안에 포함하거나 별도의 다른 문서로 존재할 수 있다. DTD는 XML 데이터의 구조와
    내용을 나타내는 규칙을 정의한다. 단 하나의 DTD만이 XML 문서와 데이터 객체에 연관될 수 있다.

  • 내부/외부 서브셋
    1. 내부 서브셋 : DTD 선언을 XML 데이터 내부에 포함.
    2. 외부 서브셋 : DTD 선언이 다른 문에서 위치. .dtd 파일
    3. 내부 서브셋에 있는 DTD 선언은 외부 서브셋에 있는 선언보다 우선순위가 높다. 유사한 선언이
      양쪽에 존재할 때, 내부 서브셋의 선언이 사용된다.

XML과 DTD 연결

  • DOCTYPE 선언을 이용한다.
  • XML문서내에 DOCTYPE선언은 단 한번만 나올 수 있다.
  • XML 선언 바로 다음에 나와야 한다.

SYSTEM

<!DOCTYPE doc_element SYSTEM location [내부 서브셋] >

  • doc_element란 문서의 root 요소를 의미한다.
  • 내부 서브셋은 선택적이다.
  • SYSTEM 키워드는 DTD의 위치를 명시적으로 가리키는데 사용한다. URL형식을 사용한다.


  • <!DOCTYPE Toysco SYSTEM "http://www.wrox.com/DTDs/Toysco.dtd">
    <!DOCTYPE Toysco SYSTEM "file:///DTDs/Toysco.dtd">
    
  • 반드시 파일이나 지정된 URL에 접근 가능한 권한이 있어야만 한다.

PUBLIC

<!DOCTYPE doc_element PUBLIC identifier location [내부 서브셋] >
  • PUBLIC 키워드는 내부 시스템과 SGML 애플리케이션에서 사용하도록 제한되어 있다.


  • <!DOCTYPE Toysco PUBLIC "BigBusinessConsortium/DTDs/Toysco">
    
  • PUBLIC 위치에서 DTD를 검색하는 것은 DTD를 애플리케이션(파서)이 알아서 찾아서 처리하라는 뜻이다.
  • 보통 PUBLIC 식별자와 함께 SYSTEM 스타일의 위치 지정. PUBLIC 위치를 사용할 수 없을 경우 대체 위치 제공.

    <!DOCTYPE Toysco PUBLIC "BigBusinessConsortium/Toysco"
     "http://www.wrox.com/DTDs/Toysco.dtd">
    
PUBLIC 위치에서 DTD를 찾지 못하면 SYSTEM 위치에서 찾는다. SYSTEM 키워드는 내부적으로 암시되어 있다.

내부 서브셋

<?xml version="1.0" encoding="euc-kr"?>
<!DOCTYPE Toysco PUBLIC "BigBusinessConsortium/Toysco"
 "http://www.wrox.com/DTDs/Toysco.dtd"

[
  DTD 선언 #1
  DTD 선언 #2
 ...
]>
<!-- 외부 서브셋은 PUBLIC 키워드로 참조된다. -->
<!-- 내부 서브셋은 위의 "[]" 사이에 포함된다. -->

<Toysco>
...
</Toysco>

DTD 선언

  • DTD 선언은 <!keyword param1 param2 ... paramN> 형태로 한다.
  • 공백문자는 여러개를 사용해도 된다.
  • 단, <!keyword 에서 !와 keyword 사이에는 공백이 없다.

DTD 키워드

키워드 설명
ELEMENT XML 요소 형이름과 허가된 하위 요소들(자식)을 기술한다.
ATTLIST XML 요소 속성 이름과 허가된 또는 기본 속성 값을 기술한다.
ENTITY 특별한 문자 참조. 텍스트 매크로, 외부 소스의 반복되는 내용을 기술한다.
NOTATION 외부의 비 XML 내용(예를 들어, 바이너리 이미지 데이터)과 그 내용을 처리하는 외부 애플리케이션 기술.

* ELEMENT, ATTLIST, ENTITY가 핵심이다.

요소 형(ELEMENT) 선언

아래 두 가지 형태의 선언이 있다.
<!ELEMENT name content_category>
<!ELEMENT name (content_model)>
  • name : 요소의 이름.
  • content_category와 content_model은 이 요소가 포함할 수 있는 내용의 종류를 의미한다.

Content Category

어떤 데이타가 올 수 있는가?

Any

<!ELEMENT AnythingGoesHere ANY>

  • Well-Formed 인 XML 아무거나 올 수 있다. 유효성 검사를 무력화 시키므로 사용하지 말 것.

Empty

<!ELEMENT img EMPTY>

  • 속성 이외에는 아무것도 올 수 없다. HTML의 img 태그 등이 그 예이다.


  • <img src=http://img.yahoo.co.kr/blank.gif>
    <br></br> <!-- 태그 사이에 아무 값도 없다 -->
    

Content Model

<!ELEMENT name (content_model)cardinality>

  • 요소 선언에서 내용 모델(Content Model)은 요소 형의 구조와 내용을 기술하기 위해 사용한다.
  • 쉽게 말하면, 요소안에 어떤 다른 요소가 어떤 순서로 몇 개 올수 있는가. 아니면 텍스트만 올 수 있는가 등을 지정한다.
  • 내용 모델은 세가지로 나눌 수 있다.
    1. Text Only : 문자 데이타만 올 수 있다. 자식 요소는 올 수 없다.
    2. Element Only : 자식 요소만 올 수 있다. 자식 외부에 텍스트를 둘 수 없다.
    3. Mixed : 자식 요소와 텍스트 혼합.
  • ANY나 EMPTY 카테고리에서 내용 모델이 필요없다.
  • cardinality 란 내용 모델이 몇개 반복 될 수 있는지 결정하는 것이다.

  • 순차와 선택 리스트
    1. 순차 리스트 : 자식 요소들이 지정된 순서대로 나와야 한다. 자식 요소들의 이름을 구분하기 위해 쉼표(,)를 사용.

      <!ELEMENT name (child1, child2) >
      
    2. 선택 리스트 : 지정된 자식 요소들 중에서 단 한개만 나타날 수 있다.

      <!ELEMENT name (#PCDATA | child1 | child2) >
      

Text-Only(PCDATA)

  • #PCDATA로 지정한다.
    <!ELEMENT foo (#PCDATA) >
    

문자열만 올 수 있다.

PCDATA 카테고리를 가진 요소는 실제 데이터와 엔티티 참조만을 허용한다.

<foo>
어쩌구 저쩌구 메롱~~
</foo>

Eelement-Only

자식 요소만을 포함한다. 지정된 자식 요소만을 포함할 수 있다. 자식 요소의 외부에 텍스트를 포함할 수 없다.
<!ELEMENT foo (a_child_element, another_child, a_third_child) >

다음과 같다. 결코 자식요소 외부에 문자열이 올 수 없다.
<foo>
  <a_child_element>haha</a_child_element>
  <!-- 이런 부분에 일반 텍스트는 올 수 없다!! 주석은 어디에나 올 수 있다.-->
  <another_child>some message</another_child>
  <a_third_child>hi~</a_third_cihld>
  <!-- 여기에도 일반 텍스트는 올 수 없다!! -->
</foo>

혼합 내용

<!ELEMENT foo (#PCDATA | a_child_element | another_child)* >
  • 일반 텍스트와 a_child_element, another_child 요소가 올 수 있다. 카디널리티 연산자로 *를 지정했기 때문에 순서 없이 올 수 있다.

  • #PCDATA 키워드는 사용할 때마다 항상 내용 모델의 첫째 항목으로 나타나야 한다. 그리고나서 자식 요소 형 이름이 나와야 한다.
  • HTML의 body 태그가 대표적인 혼합 내용 요소이다.

순차/선택 리스트 복합 사용

<!ELEMENT PersonName
  (
    (Mr | Ms | Dr | Rev), FirstName, MiddleName, LastName, (Jr | Sr | III)
  )
>
  • Mr, Ms, Dr, Rev 요소 중의 하나만 먼저 나오고 그 다음 FirstName, MiddleName, LastName 이 순서대로 나오고, 다시 Jr, Sr, III 요소 중의 하나만 마지막으로 나올 수 있다.
    <PersonName>
      <Dr/>
      <FirstName>길동</FirstName>
      <MiddleName>중간이름도 있나?</MiddleName>
      <LastName>홍</LastName>
      <Sr/>
    </PersonName>
    

카디널리티 연산자

  • 카디널리티 연산자는 내용 모델에서 자식 요소들이 얼마나 많이 나타날 수 있는지를 정의한다.
카디널리티 연산자 설명
없슴 카디널리티 연산자가 없으면 자식 요소의 인스턴스가 단 한번, 필히! 나타남.
? 0번 또는 1번. 없거나 하나만 있거나.
* 0번 또는 여러번.
+ 1번 또는 여러번.

속성(ATTRIBUTE) 선언

<!ATTLIST elementName
  attrName1 attrType1 attrDefault1 defaultValue1
  attrName2 attrType2 attrDefault2 defaultValue2
  ...
  attrName3 attrType3 attrDefault3 defaultValue3
>
  • elementName은 속성들을 포함하고 있는 요소의 이름이다.
  • attrName은 속성의 이름이다.
  • attrType은 속성의 형이다.
  • attrDefaultN 은 속성이 반드시 필요한지 아닌지 여부.

속성의 디폴트 값

속성 디폴트 설명
#REQUIRED 속성은 요소의 모든 인스턴스에 반드시 나타나야 한다.
#IMPLIED 속성은 선택적이다. 있어도 좋고 없어도 좋고..
#FIXED(디폴트값과 함께 사용) 속성은 선택적이다. 만약 속성이 사용되면 반드시 디폴트값과 일치해야한다. 속성이 없어도 파서가 디폴트 값을 제공해 줄 수 있다.
디폴트값(키워드 없이 사용) 속성은 선택적이다. 속성이 사용되면 그 값을 따르고, 속성이 사용되지 않으면 파서가 디폴트 값을 제공해 줄 수 있다.

REQUIRED

<!ATTLIST AnElement its_attr CDATA #REQUIRED >
<AnElement its_atr="haha"/>

IMPLIED

<!ATTLIST AnElement ist_attr CDATA #IMPLIDE >
  • its_attr 속성은 선택적이다.
  • #IMPLIED에서는 디폴트 값 지정을 불허한다.

FIXED

  • 속성의 값을 특정값으로 고정시킨다.
    <!ELEMENT Doc (#PCDATA) >
    <!ATTLIST Doc version CDATA #FIXED "1.00">
    
  • Doc 요소에 version 속성이 있건 없건간에 무조건 "1.00"의 version 속성이 있다고 간주한다.
  • 만약 version 속성에 "1.00"이외의 값이 들어가면 유효하지 않은 XML 문서이다.
    <Doc version="1.00">haha</Doc>
    혹은..
    <Doc>haha</Doc>
    

  • version 속성의 값을 하나의 값으로 제한하면서 모든 요소의 모든 인스턴스에서 사용되지 않아도 되게 하려면 #IMPLIED 키워드를 가진 열거 속성형을 사용하면 된다.
    <!ELEMENT Doc (#PCDATA) >
    <!ATTLIST Doc version (1.00) #IMPLIED >
    

속성 형(Type)

10가지 형이 있다.

CDATA(문자 데이터)

  • 일반 텍스트
    <!ATTLIST AnElement its_attr CDATA #REQUIRED >
    
  • AnElement의 속성 its_attr은 문자열 데이터를 가지며 필수적으로 있어야한다.
    <AnElement its_attr="문자열 아무거나">...</AnElement>
    
  • CDATA 속성값에는 외부엔티티 참조를 사용할 수 없다.
  • 내부에 정의된 엔티티 참조는 사용할 수 있다.(<, >, &, ', ")
  • 문자 참조는 사용할 수 있다.(A; 등..)
  • "<" 문자는 올 수 없다. <로 대체하라.

열거형

  • 지정된 텍스트 문자열 중의 하나만을 속성 값으로 사용한다.
    <!ELEMENT PersonName (FirstName, MiddleName, LastName) >
    <!ATTLIST PersonName
      title (Mr | Ms | Dr | Rev) #IMPLIED
      suffix (Jr | Sr | III) #IMPLIED
    >
    
  • 실 사용예
    <PersonName title="Mr" suffix="Jr">
      <FirstName>John</FirstName>
      <MiddleName>Q</MiddleName>
      <LastName>Public</LastName>
    </PersonName>
    

ID 속성 형(요소 식별자)

<!ELEMENT Person (PersonName, CorpName?, Email*, Address?, Biography?) >
<!ATTLIST Person perID ID #REQUIRED >

  • ID 형을 사용하는 속성은 요소의 인스턴스에서 유일하게 식별할 수 있는 이름을 제공한다.
    ID 속성값은 XML 이름 규칙에 따라야 하고, 그 값은 사용하는 문서에서 유일해야 한다.
  • 값은 숫자로 시작할 수 없다. 문자 + 숫자 형은 가능하다.
  • 각 요소 형에서 ID는 단 한 개만 사용할 수 있다.
  • 이 형의 모든 속성은 #IMPLIED 혹은 #REQUIRED로 선언해야 한다.

IDREF / IDREFS 속성 형 (요소 사이의 관계)

<!ELEMENT Book (#PCDATA) >
<!ATTLIST Book author IDREF #REQUIRED >

혹은, 한 책에 대한 저자가 여러명 일 수 있으므로

<!ATTLIST Book author IDREFS #REQUIRED >

  • IDREF 속성 값은 적합한 XML 이름이어야 하고, 같은 문서 내의 ID 속성 값과 일치해야 한다.
  • 동일한 ID에 대해 여러 IDREF 링크들이 허용된다.
  • IDREFS는 한 속성에 여러 ID 값들을 공백으로 구분하여 가질 수 있다.

NMTOKEN, NMTOKENS 속성 형

<!ATTLIST PersonName
  title NMTOKENS #IMPLIED
  suffix NMTOKEN #IMPLIED
>
  • NMTOKEN은 CDATA와 동일하지만, 공백 문자가 올 수 없다.
  • NMTOKENS는 NMTOKEN이 한 속성에서 공백으로 구분되어 여러개 있는 것이다.
  • 열거형과 CDATA형의 중간 형태.
    <PersonName title="Rev. Dr." suffix="Jr.">...</PersonName>
    
    <!-- NMTOKENS 형인 title에는 공백으로 구분된 여러 NMTOKEN이 있을 수 있지만,
         suffix는 항상 공백없이 한개의 단어만 올 수 있다. -->
    

ENTITY/ENTITIES 속성 형

  • ENTITY의 이름을 속성의 값으로 지정한다. 엔티티의 이름을 지정할 때 "&"와 ";"는 뺀다.
  • ENTITIES는 공백으로 구분하여 여러개의 엔티티를 지정할 수 있다.

NOTATION 형

<!ATTLIST Product
  ProductID ID #REQUIRED
  hire (true | false) "false"
  imgtype NOTATION (png | jpg | gif) #IMPLIED
  imgsrc ENTITY #IMPLIED
>
  • NOTATION 속성 값은 반드시 적절한 XML이름이어야 하고, 동일한 DTD내의 <!NOTATION> 선언과 일치해야 한다.
  • 각 요소 형에 대해 NOTAION 속성 하나가 존재할 수 있다.
  • 이 속성 형은 EMPTY 요소와 함께 사용 될 수 없다.

Notation : 비 XML 데이터

  • 텍스트가 아닌 바이너리 데이터 등을 다루기 위한 선언
  • Notation은 XML 어플리케이션에서 파싱되지 않은 엔티티나 다른 비-XML 데이타를 다루기 위해 사용된다.
  • 기본 선언 형태

    <!NOTATION name SYSTEM "location" >
    <!NOTATION name PUBLIC "identifier" "location" >
    <!-- location은 비 XML 리소스의 URL이다. -->
    

일반 엔티티(ENTITY)

  • 엔티티는 엔티티가 나오면 그 부분을 지정되 다른 문자열로 대체한다.
  • 엔티티의 대체 문자열은 Well-Formed XML이어야 한다.
    <!ENTITY name "대체 문자열" >
    
  • 엔티티는 직접적으로 또는 간접적으로 자신의 이름을 참조할 수 없다. (즉, 재귀적인 참조가 허용되지 않는다.)
  • 엔티티에 속성 값의 끝 구분문자(" 혹은 ')를 포함할 수 없다.
  • 엔티티를 참조하려면 &엔티티이름;와 같이 해야한다.
    <?xml version="1.0"?>
    <!DOCTYPE Invoice
    [
    	<!ENTITY copy "©">
    	<!ENTITY Vendor "Toysco Inc. Ltd.">
    	<!ENTITY Disclaimer SYSTEM 'ProXML2e.Disclaimer.txt' >
    
    	<!ELEMENT Invoice (Notice?) >
    	<!ATTLIST Invoice name CDATA #REQUIRED >
    	
    	<!ELEMENT Notice (#PCDATA) >
    ]>
    
    <Invoice name="&Vendor;">
    	<Notice>&Disclaimer;</Notice>
    </Invoice>
    

매개 변수 엔티티

<!ENTITY % name "대체문자열" >
  • 매개 변수 엔티티는 DTD에서만 사용된다.
    <!ENTITY % CDATA_Req "CDATA #REQUIRED" >
    <!ENTITY % CDATA_Opt "CDATA #IMPLIED" >
    
    <!ELEMENT InvoiceOrder ANY >
    <!ATTLIST InvoiceOrder
      units %CDATA_Req
      notes %CDATA_Opt
    >
    
    <!-- 여기서 ATTLIST 부분이 다음과 같이 확장된다. -->
    <!ATTLIST InvoiceOrder
      units CDATA #REQUIRED
      notes CDATA #IMPLIED
    >
    

독립 문서 선언

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
  • standalone 속성을 통해 XML 프로세서가 DOCTYPE 선언을 해석하는 방법 제공.
  • yes : 문서는 자체에 포함되어 있으며 다른 데이터를 요구하지 않는다.
  • no : 문서는 외부 DTD에 포함된 마크업을 사용한다.

DTD의 제약

  1. 비 XML 구문이다.
  2. DTD는 확장할 수 없다.
  3. 오직 하나의 DTD만이 문서와 결합할 수 있다.
  4. DTD는 XML 네임스페이스를 잘 지원하지 않는다.
  5. 매우 약한 데이터 형을 사용한다.
  6. 객체 지향 형의 개체 상속을 하지 못한다.
  7. 내부 서브셋을 사용하여 외부 서브셋을 무시할 수 있다.
  8. DOM을 지원하지 않는다.
  9. 상대적으로 오래되었다.

  추천수 (0)  답글 (0)  참조글 (0) http://kr.blog.yahoo.com/kwon37xi/1236311 주소복사 
인쇄 | 추천 | 스크랩
Posted by theYoungman
engineering/System Eng.2007. 2. 9. 09:33
LDAP 서버 설치는 선행 필수 패키지 설치(설치되어 있지 않을 경우), 서버 다운로드, 소프트웨어 압축해체, Makefile 설정과 서버 구축의 다섯 단계를 통해 이루어진다.

1. 선행 필수, Pre-Requirements

LDAP 버전 3을 완전히 따르기 위해 OpenLDAP 클라이언트와 서버는 약간의 부가적인 패키지의 설치를 필요로한다:

OpenSSL TLS libraries

어떤 운영체제가 이 라이브러리를 기본 시스템의 부분 또는 선택적인 소프트웨어 컴포넌트로 제공할 수 있지만, OpenSSL은 종종 별도의 설치를 필요로 한다. OpenSSL은 다음 사이트에서 구할 수 있다.

http://www.openssl.org/

Kerberos Authentication Services

OpenLDAP 클라이언트와 서버는 Kerberos에 기초한 인증 서비스를 지원하는데 특히 Heimdal 또는 MITKerberos V 패키지를 이용한 SASL/GSAPI 인증 기구를 지원한다. Kerberos에 기초한 SASL/GSSAPI인증을 사용하고 싶다면 Heimdal 또는 MIT Kerberos V를 설치해야 한다. Heimdal Kerbero는 http://www.pdc.kth.se/heimdal로부터 구할 수 있다.

MIT Kerberos는 http://web.mit.edu/kerberos/www로부터 구할 수 있다. Kerberos가 제공하는 것과 같은 강력한 인증 서비스의 사용을 강력히 추천한다.

Cyrus's Simple Authentication and Security Layer Libraries

어떤 운영체제가 이 라이브러리를 기본 시스템의 부분 또는 선택적인 소프트웨어 컴포넌트로 제공할 수 있지만, Cyrus SASL은 종종 별도의 설치를 필요로 한다. Cyrus SASL은 http://asg.web.cmu.edu/sasl/sasl-library.html로부터 구할 수 있다. Cyrus SASL은 OpenSSL과 Kerberos/GSSAPI 라이브러리가 미리 설치되어 있다면 이들을 사용할 것이다.

Database Software

OpenLDAP의 slapd 기본 데이터베이스 백엔드인 LDBM은 엔트리 저장을 위해 호환성 데이타베이스 패키지를 필요로 한다.LDBM은 Sleepycat 소프트웨어의 BerkeleyDB(추천) 또는 자유 소프트웨어 재단(FSF, Free SoftwareFoundation)의 GNU 데이타베이스 매니저(GDBM)와 호환된다. 이러한 패키지들을 설정시 이용할 수 없다면 기본데이타베이스 백엔드 지원을 하는 slapd 데몬을 구축할 수 없을 것이다.

운영체제가 기본 시스템내에서 또는 선택적인 소프트웨어 컴포넌트로써 두 패키지 중 하나를 제공할 수 있는데 이러한 소프트웨어를 구해서 설치할 필요가 있다.

BerkekeyDB 는 Sleepycat 소프트웨어의 다운로드 페이지 ttp://www.sleepycat.com/download.html로부터 구할 수 있다. 여러 버전을 이용할 수 있는데 이 문서를 작성하는 시점에는 최신 배포본 버전 3.1 이 추천된다.

GDBM은 FSF의 다운로드 사이트 ftp://ftp.gnu.org/pub/gnu/gdbm로부터 구할 수 있는데 이 문서를 작성하는 시점에는 버전 1.8 이 최신 배포본이다.

Threads

OpenLDAP는 쓰레드를 이용할 수 있도록 설계되어 있는데 POSIX pthreads, Mach CThreads와 많은다른 변형들을 지원한다. configure 스크립트가 적합한 쓰레드 하위 시스템을 찾을 수 없다면 에러 메세지를 출력하는데 이런경우 OpenLDAP FAQ의 소프트웨어 - 설치 - 플랫폼 힌트 절을 참조하길 바란다.

http://www.openldap.org/faq

TCP Wrappers

slapd는 TCP wrappers(IP 레벨 엑세스 제어 필터)가 이미 설치되어 있다면 이를 지원하는데 개인적인 비공개정보를 포함하는 서버 보안을 위해 TCP wrappers 또는 다른 IP 레벨 엑세스 필터(IP 레벨 방화벽이 제공하는 것과같은) 사용을 추천한다.

2. 패키지 다운로드 받기

LDAP 서버로는 Michigan 대학 LDAP 서버와 OpenLDAP 서버 두 종류의 자유로이 배포되는 LDAP 서버가있으며 또한 넷스케이프 디렉토리 서버도 어떤 조건하에서는 자유로이 사용할 수 있다(예를 들면 교육기관은 자유로이 얻을 수있다). OpenLDAP 서버는 Michigan 대학 LDAP 서버의 최신 버전에 기초하는데 그 버전에 대해 이용할 수 있는메일링 리스트와 부가적 문서가 있다. 이 문서는 OpenLDAP 서버의 사용을 가정한다.

최신 tar gzipped 버전은 다음 주소에서 구할 수 있다:

http://www.openldap.org/

Michigan 대학 LDAP 서버의 최신 버전은 다음 주소에서 구할 수 있다:

ftp://terminator.rs.itd.umich.edu/ldap

이 문서를 작성하기 위해 저자는 최신 안정 버전 1.2.11과 새로이 배포된 2.0.4 버전 두가지 OpenLDAP 패키지를 사용했다. 운영체제는 커널 2.2.13인 슬랙웨어 리눅스이다.

OpenLDAP 사이트에서 늘 OpenLDAP 서버의 최신 개발 및 안정 버전을 찾을 수 있다. 이 문서가 갱신되었던시점까지 최신 안정 버전과 최신 개발 버전은 각각 openldap-stable-20000704.tgz과openldap-2.0.4.tgz 였다.

3. 패키지 압축해제 하기

tar gzipped 패키지를 구한 후 압축해제한다.

우선 패키지를 원하는 디렉토리, 예를 들면 /usr/local, 로 복사하길 바란다.

그리고 다음의 명령을 사용하길 바란다:

tar xvzf openldap-stable.tgz


마찬가지로 다음 명령을 사용할 수도 있다:

gunzip openldap-stable.tgz | tar xvf -

4. 소프트웨어 설정하기

웹사이트에 최적의 소프트웨어를 설치할 수 있도록 개별화(customization)할 수 있는 여러 옵션들이 있다.

소프트웨어는 단지 두 단계에 의해 설정된다:

* 소프트웨어를 압축해제한 디렉토리 밑의 하위 디렉토리 include에 위치한 ldapconfig.h.edit 파일을 편집한다
* configure 스크립트를 실행시킨다 (호기심이 많다면 configure 스크립트를 실행시키는 대신 Make-common 파일을 편집할 수 있다)

include/ldapconfig.h.edit 파일안에서 slapd와 slurpd 데몬의 위치와 같은 옵션을 설정할 수있다. 파일자체가 잘 주석처리 되어 있고 default 설정은 대부분의 일반적인 관리자 선택을 반영하고 있는데 급하다면 이단계를 지나칠 수 있다:

vi include/ldapconfig.h.edit

OpenLDAP 서버 소스는 설치 디렉토리, 컴파일러와 링커 플래그와 같은 옵션 설정을 위한 설정 스크립트와 함께 배포되는데 소프트웨어를 압축해제한 디렉토리에서 다음 명령을 실행시킨다:

./configure --help

이 명령은 소프트웨어를 설치하기 전에 configure 스크립트를 갖고 개별화할 수 있는 모든 선택사항을 출력할 것이다.설치 디렉토리 설정에 관한 유용한 옵션은 --prefix=pref, --exe-prefix=eprefix 와--bindir=dir 가 있다. 일반적으로 옵션없이 configure를 실행시킨다면 스크립트가 적절한 설정을 자동적으로인지해서 default로 공통 위치에 설치하기 위해 준비할 것이다. 단지 다음과 같이 실행시킨다:

./configure

모든 것이 잘 진행되는지 보기 위해 화면에 출력되는 내용을 보길 바란다.

5. 서버 구축하기

소프트웨어를 설정한 후 구축을 시작하는데 우선 다음 명령을 이용하여 의존성을 구축한다:

make depend

다음 명령을 이용하여 서버를 구축한다:

make

모든 것이 잘 진행된다면 서버는 설정된대로 구축될 것이다. 그렇지 않다면 설정 사항을 검토하기 위해 이전 단계로 돌아가길바란다. 플랫폼에 관계되는 특수한 지시를 검사해야 하는데 소프트웨어를 압축해제한 디렉토리밑의 doc/install/hints 에있다.

바이너리와 man 페이지를 설치한다. 어디에 설치하느냐에 따라 슈퍼유저일 필요가 있다.

su
make install

설치가 완료되었으며 서버 바이너리와 여러 다른 유틸리티들이 생성되었을 것이다. LDAP 서버 작동 설정 방법을 보기 위해서는 다음으로 가길 바란다.

OpenLDAP 2.0 서버의 바이너리는 slapd이다. OpenLDAP 2.0은 공식적으로 8월 30일 발표되었는데 RFC 2251에 정의된 바와 같이 Ldap 프로토콜 v2을 포함하고 있다.

OpenLDAP 2.0 의 주된 특징은 다음과 같다:

* LDAPv2 and LDAPv3 Support (RFC2251-2256,2829-2831)
* Maintenance of interoperability with existing clients
* IPv4 and IPv6 support
* Strong Authentication (SASL) (RFC2829)
* Start TLS (RFC2830)
* Language Tags (RFC2596)
* DNS-based service location (RFC2247+"locate" I-D)
* Enhanced Standalone Server
* Named References/ManageDsaIT ("nameref" I-D)
* Enhanced Access Control subsystem
* Thread pooling
* Preemptive threading support
* Multiple listener support
* LDIFv1 (RFC2849)
* Improved platform/subsystem detection

Note: LDP(Linux Document Projext)에 LDAP Implementation HOWTO 문서가 있을것이다. 이 문서는 OpenLDAP 2.0 의 새로운 특징을 이용하길 원하는 사람들에게 많은 자료를 제공할 것이다. 배포 날짜는2000년 12월 즈음이다.

OpenLDAP 패키지 최신 버전에서는 구축된 바이너리를 시험하는 것 또한 가능 한데 다음 명령을 이용하여 시험 스크립트를 실행시킬 수 있다:

make test

어떤 것이 잘 되지 않는다면 Ctrl-C를 눌러 중간에 정지시킬 수 있다. 저자의 경우 스크립트가 완전히 끝나기 전에 중간에 멈추었는데 어쨌든 OpenLDAP 설정에 대한 성공적인 메시지를 볼 수 있었다.

<출처 사이트 - http://www.beonghan.pe.kr>
Posted by theYoungman
engineering/System Eng.2007. 2. 9. 09:32
etting Up the OpenLDAP Server

All the software packages you need to set up an OpenLDAP server areincluded on the CDs or DVD that come with Fedora distributions. Withthose packages installed, you can start configuring your OpenLDAPserver.
Installing OpenLDAP packages

To configure your OpenLDAP server, you should start by installingall the openldap packages from your Fedora distribution. First, checkwhich openldap packages are installed:

# rpm -qa "openldap*"
openldap-2.2.13-2
openldap-servers-2.2.13-2
openldap-devel-2.2.13-2
openldap-clients-2.2.13-2

You only need the openldap-devel package if you are developing LDAPapplications. Otherwise, you can install the openldap package,openldap-clients and openldap-servers packages from the DVD that comeswith this book.
Configuring the OpenLDAP server (slapd.conf)

You configure the access and use of your OpenLDAP databases in the configuration file, /etc/openldap/slapd.conf.
Note

For a more complete description on features you can use in your slapd.conf file, refer to the slapd.conf man page.

1.

Edit slapd.conf. Open the /etc/openldap/slapd.conf file as rootuser, using any text editor. The following steps tell you some of theinformation you might want to change.
2.

Review the schemas. In the slapd.conf file, schemas are includedfrom the /etc/openldap/schema directory that are generally useful forcreating LDAP directories. Other schemas you might use will often relyon these schemas being included. So, unless you know you don’t needthem, don’t delete any of these schemas:

include /etc/openldap/schema/core.schema
include /etc/openldap/schema/cosine.schema
include /etc/openldap/schema/inetorgperson.schema
include /etc/openldap/schema/nis.schema
include /etc/openldap/schema/redhat/autofs.schema

The core.schema file is required for all LDAP directories. Thecosine.schema and inetorgperson.schema files are particularly useful(and needed for this procedure). The nis.schema file is used to provideNetwork Information System data in an LDAP directory.
Tip

The LDAP Schema Viewer (http://ldap.akbkhome.com) enables you toview object classes, attributes, syntaxes, and matching rules forcommon schemas for LDAP. Definitions also point to RFCs that more fullydefine each object class.
3.

Add backend database definitions. In the slapd.conf file, you needto define some backend database definitions. Each set of backenddefinitions applies to a group of databases of the same type.

Here’s an example of how the backend database definitions wouldappear for a computer in the domain named linuxtoys.net (of course, youwould replace linuxtoys and net with those of your own domain):

###################################################
# ldbm and/or bdb database definitions
###################################################

database ldbm
suffix "dc=linuxtoys,dc=net"
directory /var/lib/ldap
rootdn "cn=manager,dc=linuxtoys,dc=net"
access to * by users read

This database is of the type ldbm (Lightweight Directory AccessProtocol Proxy backend), which defines how that data for this databaseare stored. The bdb (Berkeley DB transactional backend) is anothercommon backend database type you could use. The suffix specifies thatqueries to this slapd server for linuxtoys.net are directed to thisdatabase. The directory line identifies the /var/lib/ldap directory asthe location for this LDAP directory.

The rootdn line indicates that root access can be granted tochange data in databases associated with the linuxtoys.netdistinguished name (provided the password is supplied with rootpw, asdescribed in the next step). Access control and other restrictions youmay put on the database do not apply to this user. However, accesscontrol is applied to all other users, who are given read-onlypermission.
4.

Add a password. In the slapd.conf file, you need to enter thepassword that is required to modify your OpenLDAP backend database. Bydefault, the rootpw line defines a clear-text string that is yourpassword. The password will give you full control of the backenddatabase. It will look something like the following:

rootpw mysecret

Note

If you are going to use a clear-text password, you should makesure that your slapd.conf file has read permissions closed to the world(chmod 640 /etc/openldap/slapd.conf). See the "Creating an encryptedpassword" sidebar for information on creating an encrypted password toaccess your OpenLDAP backend database.
Image from book
Creating an encrypted password

To create an encrypted password for the administrator of theOpenLDAP database you can use the slappasswd command. You can createthe password using Crypt, SSHA, SMD5, MD5, or SSH encryption. Here’s anexample of creating a password for OpenLDAP using MD5 encryption:

# slappasswd -h {md5} > /tmp/myslap
New password: ********
Re-enter new password: ********
# cat /tmp/myslap
{MD5}uBoM+LOQg5GHHJ2Z4NLu9A==

Enter a password (twice) to create an encrypted MD5 password. Thisexample directs the encrypted password into the /tmp/myslap file, youcan read into the slapd.conf file later. In this example, I had you"cat" the file so you could see what the encrypted password looks like.Your password will be different. Here’s what the rootpw line will looklike with an encrypted, rather than a clear-text password:

rootpw {MD5}uBoM+LOQg5GHHJ2Z4NLu9A==

Image from book
5.

Save slapd.conf. Save your changes to the slapd.conf file and close it.
6.

Check slapd.conf. You can check for syntax errors in your slapd.conf file by running the slaptest command, as follows:

# slaptest
config file testing succeeded

If there were something wrong with the syntax of the file (forexample, if you left off a quote or misplaced a comma), the messagewould say slaptest: bad configuration file! instead. Try to correct theproblem and check the file again.

At this point, you can try starting the OpenLDAP
Starting the OpenLDAP service

You start the OpenLDAP as you do most services in Fedora Core and otherRed Hat Linux systems, using the service and chkconfig commands. Theservice name for OpenLDAP is ldap. To start the service immediately,type the following:

# service ldap start
Starting slapd: [ OK ]

To set the ldap service to start each time the system is rebooted, type the following:

# chkconfig ldap on

By default, the ldap service will have read permissions open to everyone.
Posted by theYoungman
«이전  1 2 3  다음»