Diff, Patch, CVS 사용법

리눅스 개발자들에게 중요한 것 중의 하나가 오픈소스 프로젝트 진행이다. 사실 국내에서도 오픈소스 프로젝트는 많은 사람들이 관심을 갖고 있지만 막상 프로젝트에 참여하는 사람들의 숫자는 여전히 부족하다. 여기서는 여러 개발자들이 동시에 오픈소스 프로젝트에 참가할 때 거의 필수적으로 쓰이는 diff, patch, CVS(Concurrent Versions System)와 같은 소스코드 버전 관리툴에 대해 살펴 보고 오픈소스 프로젝트를 진행할 때 알아두면 좋은 특성이나 작업 방식에 대해서도 알아 본다.

오픈소스 프로젝트란 도대체 어떤 것일까? 리눅스와 오픈소스의 부상과 더불어 오픈소스 개발 방식은 기존의 상용 소프트웨어 개발 방식에 비해 적은 비용으로 양질의 소프트웨어를 개발할 수 있으며 소프트웨어 시장 독점의 문제가 없다는 의견이 설득력을 계속 높여가고 있다.

그러나 필자가 보기에는 오픈소스 개발 방식은 그 효율성을 논의하기 전에 왜 이런 식의 개발 방식이 만들어 졌으며 왜 오픈소스 스타일의 개발 방식이 자연스럽게 정착되었는지를 이해하는 것이 더욱 중요하다고 생각한다. 필자의 의견으로는 오픈소스는 소프트웨어를 소프트웨어 그 자체가 지닌 특성에 맞도록 자연스럽게 개발하는 한 방법이라고 생각한다.

당연한 얘기지만 어떠한 소프트웨어의 소스코드를 공개하면 그 소프트웨어가 계속 바뀌어 나갈 수 있는 길이 열리게 된다. 소스코드를 공개해서 개발 작업을 진행하는 오픈소스 개발 방법이 두드러지게 나타난 것은 1970년대 초 AT&T에서 자사의 운영체제인 유닉스의 소스코드를 공개한 이후부터라고 보는 것이 정설이다. AT&T에서는 대학과 같은 교육, 연구 기관에 자사의 제품인 유닉스를 공급하면서 돈을 받고 소스코드를 그대로 제공하는 라이선스 방식을 취했다.

소프트웨어를 배포할 때 소스가 아닌 바이너리를 주로 배포하는 지금으로서는 언뜻 상상하기 어려운 관행일 수도 있지만 컴퓨터의 종류가 통일되어 있지 않고, 소수의 전문가 집단에서 유닉스를 주로 사용했다는 점을 생각해 본다면 AT&T 입장에서 유지보수 비용을 줄여주는 이러한 소스코드 형태의 배포는 나름대로 합리적인 선택이라고도 할 수 있겠다.

그러나 여기서 예상하지 못한 일이 벌어진 것이 이들 사용자, 혹은 사용자이면서 개발자이기도 한 사람들이 마음대로 뜯어고치고 덧붙이기 시작한 코드들이 오리지널 AT&T 유닉스보다 오히려 더 중요한 위치를 차지하게 된 것이다(BSD 유닉스의 발전도 이러한 관습에 뿌리를 두고 있다).

따라서 자유 소프트웨어나 오픈소스와 같은 용어들은 소프트웨어 개발 방식의 측면에서 볼 때 이미 존재하고 있던 개발 방식을 새롭게 재조명하고 있다고 생각할 수 있다. 자유 소프트웨어에서는 소스코드가 공개된 소프트웨어의 보호에 좀 더 중점을 두고 있으며 오픈소스에서는 소스코드가 공개된 채로 개발되는 소프트웨어의 개발 효율성에 좀 더 관심을 집중하는 편이다. 어느 경우이든 간에, 소스코드를 공개해서 개발자의 참여를 이끌어 내는 소프트웨어 개발 방식은 변함이 없으며, 오픈소스라는 단어가 아예 없던 시절에도 이것은 마찬가지인 것이다.

그렇다면, 이제 간단한 상황 하나를 가정해 보기로 하자. 여러분들이 초기 유닉스 시절 대학 전산실에 근무하던 도중 유닉스가 도입되었다고 생각해보자. 고된 포팅과 설정 작업 끝에 시스템이 제대로 돌아가기 시작했는데 이 와중에서 버그를 하나 발견하고 그 부분의 소스코드를 수정했다. 이럴 때 다음 버전의 유닉스에 여러분들이 고친 부분이 반영되도록 하려면 어떻게 해야 할까?

이럴 때 가장 상식적인 해법은 고친 부분의 소스코드를 원저자에게 보내주면 될 것이다. 그리고 그 방법으로 가장 편리한 것은 아마도 이메일이 좋을 것이다. 인터넷이 없던 시절이라면 아마도 일반 메일을 이용했을 것이다. 약간은 논외의 이야기지만, 인터넷 초창기에는 국내에서 유즈넷 뉴스그룹에 올라온 글을 보기 위해 정기적으로 뉴스서버 데이터 백업을 외국에서 자기 테이프에 받아 소포로 전송받기도 했다고 한다. 어쨌든 이러한 이메일의 간편함 덕분에 이메일은 오픈소스 개발 작업에서 가장 중요한 통신 수단이며 패치 전송 수단으로 자리 잡게 된다.

여기서 하나 생각해 봐야 할 것이 원저자의 입장이다. 이렇게 패치를 담고 있는 메일의 숫자가 적을 때는 원저자는 그저 전송된 패치를 고맙게 받아 적용하기만 하면 되겠지만 패치의 숫자가 늘어나고, 같은 버그에 대해서도 두 종류 이상의 중복 패치가 생기게 되면 어떤 패치를 선택할 것인지, 그리고 모은 패치를 어떻게 통합해서 하나의 소스코드 트리로 만들고 그것을 배포(public release)할 것인지 선택해야 하는 문제가 생긴다. 이런 경우, 보통 오픈소스계의 관습은 원저자, 혹은 프로젝트 리더에게 어떤 패치를 받아들일 것인지의 결정권을 맡겨버리는 경향이 있다. 이럴 때 원저자나 프로젝트 리더는 ‘자비로운 독재자(benevolent dictator)’라는 역할을 맡게 되는 것이다.

또한, 패치가 전송될 때 사람들마다 통일되지 않은 방식으로 패치를 전송하게 되면 프로젝트 리더의 입장에서는 여러 종류의 패치를 하나의 소스코드 트리에 적용시키는 데 많은 혼란을 겪게 될 것이다. 이를 해결하기 위해 등장한 심플한 도구가 바로 diff와 patch이다.

diff와 patch
diff는 유닉스 사용자 튜토리얼에도 가끔씩 등장하는 간단한 유틸리티이다. diff의 역할은 두 파일간의 차이점을 보여주는 데 소스코드의 바뀐 부분을 보여 줄 때 많이 쓰인다. patch는 이러한 diff의 출력 결과를 이용해서 이 바뀐 부분을 원래의 소스코드에 업데이트할 때 쓰는 유틸리티이다.

diff의 일반용법
우선, diff의 형식은 다음과 같다.

diff [options] from-file to-file

diff는 두 개의 파일을 필요로 한다는데 주의하자. from-file은 원래의 파일, 즉 구 버전의 파일이며, to-file은 새로이 바뀐 새 버전의 파일이다. diff는 이렇게 하면 from-file에서 to-file로 어떠한 변화가 있었는지를 출력해 준다. from-file과 to-file은 모두 디렉토리가 올 수도 있는데 디렉토리가 오는 경우는 조금 뒤에 살펴보기로 하자. 참고로 간단한 예제 hello1.c와 hello2.c의 예를 들어보자. 다음에서 볼 수 있듯이 hello2.c는 hello1.c에서 hello, world 부분이 hello, the world of linux로 대치되었고 그 아랫줄에 공백 라인 하나와 printf(“Testing one two three.n”);가 추가되었음을 볼 수 있다.

*** hello1.c:
#include
#include
main()
{
   printf(“hello, world.n”);
}

*** hello2.c:
#include
main()
{
   printf(“hello, the world of Linux.n”);
   printf(“Testing one two three.n”);
}

diff 결과는 다음과 같다.

$ diff hello1.c hello2.c
2d1
< #include
6c5,7
<     printf(“hello, world.n”);

>     printf(“hello, the world of Linux.n”);
>
>     printf(“Testing one two three.n”);

첫 줄의 2d1은 hello1의 두 번째 줄에서 한 줄을 삭제(delete)하는 변화가 일어났다는 의미이다. 그리고 조금 아래의 6c5,7은 hello1의 6번째 줄을 아랫부분으로 바꾸는데(change) 그 결과가 5번째부터 7번째 라인까지 들어가게 된다는 의미이다. 그러나 실제로 프로그램 소스코드에서는 오리지널 diff의 결과물보다는 unified format의 diff 출력을 쓰는 경우가 많다. unified format을 쓰려면 diff에 -u 옵션을 추가한다.

$ diff -u hello1.c hello2.c
— hello1.c    Tue Aug  3 14:34:46 2004
+++ hello2.c    Tue Aug  3 13:25:49 2004
@@ -1,7 +1,8 @@
#include
-#include

main()
{
–    printf(“hello, world.n”);
+    printf(“hello, the world of Linux.n”);
+
+    printf(“Testing one two three.n”);
}

참고로 unified format에서는 변경되는 부분만이 아닌 변경되는 부분 근처의 내용(context)도 같이 출력됨을 볼 수 있다. 사람이 좀 더 읽기 편리한 context format 출력 옵션인 -c를 사용한 결과는 다음과 같다. context format 역시 바뀌는 부분 근처의 내용도 참고하기 좋게 출력을 해 준다. 어쨌거나 오픈소스 프로젝트에서는 diff를 쓸 때 주로 -u 옵션을 붙인다는 점을 꼭 외워 두도록 하자.

$ diff -c hello1.c hello2.c
*** hello1.c    Tue Aug  3 14:34:46 2004
— hello2.c    Tue Aug  3 13:25:49 2004
***************
*** 1,7 ****
#include
– #include
 
main()
{
!     printf(“hello, world.n”);
}
— 1,8 —-
#include
 
main()
{
!     printf(“hello, the world of Linux.n”);
!
!     printf(“Testing one two three.n”);
}

이렇게 diff로 소스코드의 변경된 부분을 저장한 다음 이것을 원저자에게 메일로 보내면 된다.

$ diff -u hello1.c hello2.c > hello.diff

여러 개의 소스 파일을 diff로 비교하기
앞의 경우는 소스코드 파일 하나만이 변경되었지만 상황에 따라서는 패치 과정에 여러 파일이 수정되고 새로운 파일이 추가되는 경우가 발생할 수도 있다. diff는 디렉토리 단위의 파일 비교도 가능하다. 우선, 다음 예제를 보자.

$ pwd
/home/foobar
$ ls -F
src1/  src2/            # src1은 원본, src2는 새로운 기능 추가본
$ ls src1
hello1.c  hello2.c
$ ls src2
hello1.c  hello2.c  hello3.c
$ more src1/*c
::::::::::::::
src1/hello1.c
::::::::::::::
#include
#include

main()
{
   printf(“hello, world.n”);
}
::::::::::::::
src1/hello2.c
::::::::::::::
#include

main()
{
   printf(“hello, the world of Linux.n”);

   printf(“Testing one two three.n”);
}
$ more src2/*c
::::::::::::::
src2/hello1.c
::::::::::::::
#include

main()
{
   printf(“hello, world.n”);
}
::::::::::::::
src2/hello2.c
::::::::::::::
#include

main()
{
   printf(“hello, the world of Linux.n”);

   printf(“Testing one two three four.n”);
}
::::::::::::::
src2/hello3.c
::::::::::::::
#include
#include

main()
{
   /* needs to be filled in */
}

src2에서는 hello3.c 파일이 새로 추가되었으며, hello2.c에서 수정 부분이 있고, hello1.c에서 빠진 부분이 있다. 이 두 디렉토리 사이에서 diff를 실행하려면 다음과 같은 명령을 쓴다.

$ pwd
/home/foobar
$ ls -F
src1/ src2/             # 경로를 제대로 확인한 뒤 diff를 실행한다
$ diff -urN src1 src2
diff -urN src1/hello1.c src2/hello1.c
— src1/hello1.c       Tue Aug  3 14:34:46 2004
+++ src2/hello1.c       Tue Aug  3 13:35:44 2004
@@ -1,5 +1,4 @@
#include
-#include

main()
{
diff -urN src1/hello2.c src2/hello2.c
— src1/hello2.c       Tue Aug  3 13:25:49 2004
+++ src2/hello2.c       Tue Aug  3 13:35:57 2004
@@ -4,5 +4,5 @@
{
    printf(“hello, the world of Linux.n”);

–    printf(“Testing one two three.n”);
+    printf(“Testing one two three four.n”);
}
diff -urN src1/hello3.c src2/hello3.c
— src1/hello3.c       Thu Jan  1 09:00:00 1970
+++ src2/hello3.c       Tue Aug  3 13:37:02 2004
@@ -0,0 +1,7 @@
+#include
+#include
+
+main()
+{
+    /* needs to be filled in */
+}

diff 명령에서 -r 옵션은 recursive 옵션으로 서브 디렉토리까지 diff가 모두 탐색하라는 의미이고, -N 옵션은 hello3.c와 같이 새로 만들어진 파일까지도 포함해 diff 출력을 생성하라는 의미다. 이 옵션 역시 -urN으로 외워 두는 것이 좋다.

patch 사용하기
이렇게 만들어진 diff의 결과물은 patch 명령을 통해서 원저자의 소스코드로 업데이트된다. patch 명령은 -p 옵션만 정확히 이해하면 사용하는데 무리가 없다.
-p 옵션은 strip 옵션이라고 부르는데 diff 파일에 명시되어 있는 디렉토리에서 몇 단계를 벗겨(strip)낼 것인가를 결정한다. -p0 옵션은 디렉토리 단계를 하나도 벗겨내지 않겠다는 것이고, -p1 옵션은 한 단계를 벗겨낸다는 의미이고 -p2는 두 단계를 의미한다. 쉽게 이해하기 위해 <표 1>를 보자. foobar/include/net 디렉토리가 있다고 할 때 p 옵션을 적용하면 다음과 같이 디렉토리가 벗겨져 나간다.

p0 foobar/include/net
p1 include/net
p2 net

이것이 diff로 생성시킨 패치를 적용할 때 어떤 의미를 가지게 될까? 우선, 파일 두 개를 비교했을 때 생성된 diff 패치와 디렉토리 두 개를 비교했을 때 생성된 diff 패치의 헤더 부분을 비교해보자.

◆ 파일 두 개를 비교했을 경우:
$ diff -u hello1.c hello2.c
— hello1.c    Tue Aug  3 14:34:46 2004
+++ hello2.c    Tue Aug  3 13:25:49 2004
(이하 생략)

◆ 디렉토리 두 개를 비교했을 경우:
$ diff -urN src1 src2
diff -urN src1/hello1.c src2/hello1.c
— src1/hello1.c       Tue Aug  3 14:34:46 2004
+++ src2/hello1.c       Tue Aug  3 13:35:44 2004
(이하 생략)

즉, diff가 생성한 패치 파일에는 원본 파일과 바뀐 파일의 디렉토리가 명시되어 있음을 알 수 있다. patch 명령을 사용할 때는 이 경로명을 고려해서 patch 명령을 실행시켜 줘야 한다.

패치 적용하기
여러분이 원저자의 입장에서 제공받은 패치를 적용시키려면 다음과 같은 방법을 사용하면 된다. 패치 파일은 표준 입력(standard input)으로 들어가며, 항상 -p 옵션을 주의 깊게 사용해야 한다. 이번 예제에서는 디렉토리 두 개를 비교한 패치가 전송되었다고 가정해 보자. 원저자는 diff 패치의 헤더 부분을 읽고 패치의 경로명을 확인한 다음 적절한 디렉토리로 가서 patch 명령을 실행한다. 우선, -p0 옵션 사용 예부터 보자. diff 패치의 헤더 부분은 다음과 같다.

diff -urN src1/hello1.c src2/hello1.c
— src1/hello1.c       Tue Aug  3 14:34:46 2004
+++ src2/hello1.c       Tue Aug  3 13:35:44 2004
… 이하 생략 …

따라서 원저자는 이 패치를 hello.diff로 저장한 다음 자신의 소스코드가 있는 src1까지 가서 패치를 적용한다.

$ cd projects
$ pwd
/home/foobar/projects
$ ls -F
hello.diff   src1/

diff 파일에 기술된 경로명과 현재 경로명이 일치하고 있음을 주의깊게 보자.

$ patch -p0 < hello.diff
$ patch -p0 < hello.diff
patching file src1/hello1.c
patching file src1/hello2.c
patching file src1/hello3.c

패치 전과 패치 후의 결과를 비교해 보면 다음과 같다.

패치 전:

$ pwd
/home/foobar/projects/src1
$ ls -al
total 16
drwxr-xr-x    2 jwsohn   jwsohn       4096  8월  3 16:35 .
drwxr-xr-x    3 jwsohn   jwsohn       4096  8월  3 16:35 ..
-rw-r–r–    1 jwsohn   jwsohn         82  8월  3 14:34 hello1.c
-rw-r–r–    1 jwsohn   jwsohn        116  8월  3 13:25 hello2.c

패치 후:

$ pwd
/home/foobar/projects/src1
$ ls -al
total 20
drwxr-xr-x    2 jwsohn   jwsohn       4096  8월  3 16:41 .
drwxr-xr-x    3 jwsohn   jwsohn       4096  8월  3 16:35 ..
-rw-r–r–    1 jwsohn   jwsohn         62  8월  3 16:41 hello1.c
-rw-r–r–    1 jwsohn   jwsohn        121  8월  3 16:41 hello2.c
-rw-r–r–    1 jwsohn   jwsohn         83  8월  3 16:41 hello3.c

이제 -p1 옵션을 적용해서 patch 명령을 써 보자. diff 파일에 기술된 경로명에서 디렉토리를 한 단계 벗겨내면 src1 디렉토리가 없어지므로 다음과 같은 방식으로 패치 파일이 적용된다.

$ pwd
/home/foobar/projects
$ ls -F
hello.diff  src1/
$ cd src1
$ patch -p1 < ../hello.diff
patching file hello1.c
patching file hello2.c
patching file hello3.c

참고로 diff 파일의 경로는 어디에 위치하든 상관이 없다. 파일 하나에 대한 패치를 적용할 때는 -p0 옵션을 쓰면 될 것이다.

CVS 사용
CVS는 약자 중 Concurrent가 의미하듯이 한번에 여러 명의 개발자가 동일한 소스코드 트리에 동시에 수정을 가하면서 작업을 할 수 있도록 도와주는 도구이다. 사실 오픈소스 프로젝트에 참가하는 일반 개발자의 입장에서는 CVS에 대한 일반적인 지식은 별다른 필요가 없다고 생각해도 무방하다. 실제 소스코드 트리를 수정할 수 있는 권한을 갖고 있는 사람들은 프로젝트 리더인 자비로운 독재자 한 사람이나 프로젝트와 관련이 깊은 소수의 개발자들일 것이기 때문이다.

즉, 소스코드 수정 사항이 생기면 CVS를 굳이 쓸 필요가 없이 그냥 FTP나 http로 다운받은 소스코드 위에 diff를 돌려서 패치만 이메일과 같은 수단으로 보내 주면 소스코드 수정 권한이 있는 그쪽 사람들이(보통 ‘커미터’라고 부른다) 알아서 처리를 해 줄 것이기 때문이다.
하지만 가장 최신 버전의 nightly build된 소프트웨어는 CVS 서버에 접속해야만 구할 수 있는 경우가 대부분이다. 따라서 오픈소스 개발 프로젝트에 관심이 있는 개발자라면 다음 측면에서 CVS 사용법을 알아야 할 필요가 있을 것이다.

[1] FTP나 http 서버에서 소스코드를 다운받는 대신에
[2] CVS 서버에서 가장 최신 소스코드를 다운 받고
[3] 자신이 패치한 소스코드와 CVS 서버의 소스코드의 diff를 생성하는 방법

익명 CVS 체크아웃
이제 CVS에서 소스코드를 다운받는 방법을 알아보기로 하자. 일반적으로 오픈소스 프로젝트들은 대부분 CVS 서버를 읽기 전용으로 세팅해 놓고 누구든지 들어와서 소스코드를 다운받아 갈 수 있도록 해 놓고 있다. 익명 FTP(anonymous FTP)와 비슷한 개념으로 생각하면 되겠다. CVS 서버에 사용자가 접속을 해서 소스코드를 한 카피 다운받아 가는 것을 CVS에서는 체크아웃(check-out)이라는 용어로 표현한다.
참고로 CVS는 로컬 서버에서 사용할 수도 있고, 따로 CVS 서버를 두고 원격으로 접속할 수도 있다. 여기서는 CVS가 원격 서버라고 가정하기로 한다. CVS 서버에서 소스코드를 다운받는 명령은 다음과 같다. KLDP.net 서버에 위치한 moniwiki 프로젝트를 예로 들어 보겠다.

$ cvs -d:pserver:anonymous@cvs.kldp.net:cvsroot/moniwiki login
$ cvs -d:pserver:anonymous@cvs.kldp.net:cvsroot/moniwiki checkout moniwiki

익명 FTP에 접속할 때 보다는 조금 복잡해 보인다. 우선, 첫줄의 cvs 명령은 CVS 서버에 anonmous, 즉 익명 사용자로 로그인하는 과정이다. CVS 서버가 암호를 요구하면 그냥 엔터 키를 쳐 주면 인증이 끝나고 읽기 권한이 부여된다. 일단 로그인을 한번 한 다음부터는 사용자의 홈 디렉토리에 .cvspass 파일이 생기면서 인증 절차가 생략된다. 즉, 매번 접속할 때마다 anonymous 인증 과정을 거쳐야 하는 익명 FTP와는 달리, CVS에서는 한번만 익명 로그인을 해서 .cvspass 파일을 생성하고 나면 다시 cvs login 명령으로 인증 과정을 거칠 필요가 없다.

그 다음 -d 옵션은 CVS 서버에서 제공하고 있는 루트 디렉토리를 의미한다. moniwiki 프로젝트의 경우는 cvsroot/moniwiki로 지정되어 있다. 앞의 pserver는 CVS 명령이 소스코드를 다운받으면서 쓸 프로토콜명이며 뒤의 anonymous@cvs.kldp.net은 CVS 로그인시 사용할 계정이다.

두 번째 cvs 명령은 소스코드 한 카피를 다운받는 체크아웃 과정을 실행하게 된다. 체크아웃(checkout)한 다음의 moniwki는 CVS 서버에서 지정해 놓은 프로젝트 이름이며(모듈이라고 부른다) 이곳 CVS 서버에서는 moniwki로 지정해 두었다. 두 번째 명령을 실행하면 사용자의 현재 디렉토리에 moniwiki라는 디렉토리가 생성되고 소스코드 다운로드가 시작된다. 여기에, -z3 옵션을 주면 전송시 압축을 사용하기 때문에 전송 속도가 빨라진다.

$ cvs -z3 -d:pserver:anonymous@cvs.kldp.net:cvsroot/moniwiki checkout moniwiki

그런데 CVS 서버에 접속할 때마다 -d 옵션 뒤의 긴 디렉토리 이름을 타이핑하기는 아무래도 불편한 감이 있다. -d 옵션을 생략하려면 환경변수 CVSROOT에 -d 옵션을 등록해 둔다.

$ export CVSROOT=:pserver:anonymous@cvs.kldp.net:/cvsroot/moniwiki

그 다음부터는 다음 명령으로도 충분하다.

$ cvs login
$ cvs checkout moniwiki

소스코드를 받은 지 시간이 어느 정도 지났다면 그동안 CVS 서버의 내용이 새롭게 업데이트되어 있을 수도 있다. 바뀐 부분을 다운받으려면 CVS에서 update 명령을 사용한다.

$ cvs update -dP

여기서 -P 옵션은 Prune 옵션으로 비어있는 디렉토리를 자동으로 삭제해 주는 역할을 한다. -d 옵션은 그동안 서버 쪽에 새로 만들어진 디렉토리가 있으면 다운 받은 이쪽에도 동일한 디렉토리를 만들어 준다.

CVS 서버 원본에서 diff로 패치 파일 만들기
이제 기본적인 CVS 서버에서 소스코드 다운로드 방법을 알았으니 직접 CVS 서버의 소스코드를 이용해서 패치 파일을 만들어 보자. CVS는 diff 명령을 아예 자체적으로 내장하고 있다. 조금 전 다운받은 moniwiki의 소스코드를 예로 들어보자. 여기서, 필자는 monisetup.php 파일에 간단히 /* testing one two three */라는 주석문을 하나 삽입했다.

$ cd moniwiki
$ ls
COPYING                 doc                     secure.sh
CVS                     imgs                    theme
INSTALL                 index.html              tools
README                  lib                     wiki.php
THANKS                  locale                  wikihttpd.php
applets                 monisetup.bat           wikilib.php
config.php.default      monisetup.php           wikismiley.php
css                     monisetup.sh
data                    plugin
$ cvs diff -u -p monisetup.php
Index: monisetup.php
===================================================================
RCS file: /cvsroot/moniwiki/moniwiki/monisetup.php,v
retrieving revision 1.11
diff -u -p -r1.11 monisetup.php
— monisetup.php       3 Jan 2004 14:26:50 -0000       1.11
+++ monisetup.php       3 Aug 2004 09:14:21 -0000
@@ -486,4 +486,5 @@ if ($_SERVER[‘REQUEST_METHOD’]!=”POST”)

}

+/* testing one two three */
?>

cvs diff 명령이 마치 로컬에서 monisetup.php 파일에 diff 명령을 실행한 것과 같은 결과가 나왔음을 알 수 있다. cvs diff 명령 뒤의 -u 옵션은 diff와 마찬가지로 unified format을 의미하며 -p 옵션은 cvs diff 의 출력을 표준 출력(standard output)으로 보내라는 의미이다. 디렉토리 안의 모든 파일에 대해 diff 명령으로 비교를 하려면 다음과 같은 방법을 사용한다.

$ cvs -Q diff -u -p              
Index: monisetup.php
===================================================================
RCS file: /cvsroot/moniwiki/moniwiki/monisetup.php,v
retrieving revision 1.11
diff -u -p -r1.11 monisetup.php
— monisetup.php       3 Jan 2004 14:26:50 -0000       1.11
+++ monisetup.php       3 Aug 2004 09:19:27 -0000
@@ -486,4 +486,5 @@ if ($_SERVER[‘REQUEST_METHOD’]!=”POST”)

}

+/* testing one two three */
?>
Index: css/log.css
===================================================================
RCS file: /cvsroot/moniwiki/moniwiki/css/log.css,v
retrieving revision 1.1
diff -u -p -r1.1 log.css
— css/log.css 11 Feb 2004 08:48:27 -0000      1.1
+++ css/log.css 3 Aug 2004 09:19:27 -0000
@@ -1,4 +1,5 @@
/* MoniWiki CSS 2003/11/01 by wkpark */
+/* another testing comment */
body {
  font-family:Georgia,Verdana,Lucida,sans-serif;font-size:12px;
  background-color:#FFFFFF;

여기서는 monisetup.php 파일과 css/log.css 파일에 수정된 부분이 있음을 알 수 있다. cvs diff 명령에서 -Q 옵션은 Quiet 옵션으로 diff 출력 이외의 다른 메시지를 출력하지 않도록 해 준다. 그런데 여기서 하나 의문이 들 수 있다. 분명히 CVS에서는 cvs를 이용해서 직접 소스코드에 수정을 가할 수 있을 것인데 여기에서 왜 굳이 구식 diff를 이용한 방법을 또 사용하고 있는 것을까?

그 이유는 앞에서도 잠깐 언급했듯이, CVS 서버의 내용을 수정할 수 있는 권한이 있는 개발자는 소수이기 때문이다. 그리고 자비로운 독재자라는 용어에서 알 수 있듯이 제출된 패치를 받아들일지 아닐지의 여부는 관습적으로 보통 그쪽 프로젝트 리더들의 몫이 된다.

따라서 CVS 서버에 읽기 전용의 권한만을 갖고 있는 일반 오픈소스 프로젝트 참가자들은 이메일이나 혹은 메일링 리스트에 diff를 이용해서 패치 파일을 포스팅하는 것이 오픈소스 프로젝트에 참가하는 가장 무난한 방법이다. 일반적으로 프로젝트의 참가자가 지나치게 많지 않은 경우를 제외하고는 오픈소스 프로젝트에 올라오는 패치는 아무리 사소한 것이라도 쉽게 CVS 서버의 원본 소스코드에(repository라고 부른다) 반영 된다.

따라서 일반 개발자의 입장에서는 프로젝트 리더나 메인 개발자가 되기 전에는 CVS 서버에서 쓰기 권한이 그다지 필요하지는 않다. 이제 간단하게 CVS 서버에서 자주 쓰이는 기본적인 개념과 용어에 대해 알아보기로 하자.

CVS의 개념과 여러 용어
CVS 모델, copy-moodify-merge model
CVS는 이전에 많이 쓰이던 RCS(Revision Control System)과는 달리 lock-modify-unlock이 아닌 copy-modify-merge 모델을 사용한다. 여러 명의 개발자가 하나의 소스코드 트리에서 개발 작업을 할 때 가장 큰 문제는 같은 부분의 소스코드에 두 명 이상의 개발자가 서로 다른 소스코드를 작성하고 있을 때 발생한다. 이것을 CVS에서는 conflict이 발생했다고 한다.

RCS의 lock-modify-unlock 접근방식은 이러한 conflict를 한번에 두 명 이상의 개발자가 같은 소스코드 부분에 접근할 수 없도록 해서 conflict 상황을 미연에 방지한다. 하지만 CVS에서는 이러한 conflict 상황이 발생하는 것을 허용한다. 그렇다면 CVS에서는 이런 소스코드 conflict 상황이 발생하면 어떻게 대처할까?

재미있게도 CVS에서는 이런 상황에서 아무 일도 하지 않는다. 다만, CVS는 어느 부분에서 소스코드 conflict가 발생했는지, 그리고 어떤 사람이 conflict에 관계되어 있는지만 정확하게 알려 준다. 따라서 소스코드의 conflict 문제를 해결하는 것은 기계가 아닌 사람의 몫이 된다.

lock-modify-unlock 모델은 프로젝트의 전체 개발자 숫자가 소수이고 각각의 개발자들이 다른 개발자들이 현재 어떤 작업을 하고 있는지 쉽게 알 수 있는 상황에 유리하다. 그러나 개발자의 숫자가 많아지면 이미 잠금이 걸려 있는 파일에는 다른 개발자들이 접근할 수 없기 때문에 전체적인 업데이트가 늦어지는 문제가 발생하게 되고 좀 더 유연한 환경을 제공하기 위해 CVS가 도입되기에 이르렀다. 따라서 copy-modify-merge 모델의 CVS를 사용하는 개발자는 기본적으로 다음과 같은 과정을 거치며 개발 작업을 진행해 나가게 된다.

[1] 개발자는 CVS 서버로부터 소스코드 카피를 다운로드(check-out)한 다음
[2] 소스코드를 수정하고
[3] 완료된 작업은 다시 CVS 서버로 올려준다(check-in, commit)

여기서 conflict의 발생 여부는 commit 작업 단계에서 알 수 있다. 그리고 조금 전에 잠깐 살펴보았듯이 현재 개발자의 컴퓨터의 소스코드를 cvs 서버 쪽과 가능한 한 동일하게 유지하려면 update를 자주 해 주는 것이 좋다.

CVS와 관련된 용어
CVS와 관련된 용어는 통일된 번역이 아직 존재하지 않는 것 같아 원문 그대로 싣고 여기에 설명을 덧붙이도록 하겠다.

◆ repository : CVS 서버에 들어가 있는 바로 그 원본(master copy) 소스코드를 의미한다. 나중에 최종 출시가 되는 소스코드가 바로 이 repository이며 repository는 그동안 버전업되어 온 정보를 모두 포함하고 있다. cvs respository의 개수는 하나가 된다.

◆ working copy : 각각의 개발자가 작업을 위해 cvs repository에서 복사해 갖고 나간(check-out) 소스코드가 working copy이다. 따라서 working copy는 개발자의 수만큼 존재한다고 볼 수 있다. 개발자는 이 working copy에서 소스코드 수정 작업을 한 다음 나중에 check-in 혹은 commit 단계를 거쳐 원본 cvs repository에 수정을 가하게 된다.

◆ check-out : 개발자가 자신이 작업할 working copy를 CVS 서버에서 한 카피 복사해 가는 것을 check-out이라고 한다.

◆ update : update는 check-out과 비슷하게 cvs repository에서 원본 소프트웨어를 한 카피 가져오는 역할을 하지만, update는 단어의 원래 뜻 그대로 수정된 부분만 가져와서 현재 개발자가 쓰고 있는 working copy가 항상 최신의 갱신본이 될 수 있도록 해 준다. 일반적으로, 개발자는 한번 working copy를 check-out 한 뒤에는 최신의 소스코드를 유지하기 위해 이 update 명령을 주로 쓰게 된다.

◆ check-in 혹은 commit : 여기서 잠깐 조심해서 봐야 할 것이 update와 check-in의 관계이다. 얼핏 보기에는 CVS에서 check-out과 check-in이 반대의 개념이 되어야 할 것 같은데 실상 check-in의 반대 개념은 update이다. check-in은 내가 작성한 소스코드를 repository에 병합시켜서 다른 개발자들도 내가 작성한 수정 부분을 볼 수 있게 해 주는 소위 publish 작업이기 때문이다. 따라서 그 반대 개념은 내가 다른 사람들이 작성한 소스코드 수정본을 나의 working copy에 반영시키는 것인데 이 작업이 update 과정이다. 따라서 check-in 과정은 오히려 commit이라는 단어를 써서 표현하는 것이 혼동을 미리 방지하는 효과가 있지 않나 싶다. 참고로 commit 권한은 잠시 언급했듯이 소수의 주요 프로젝트 리더들이 갖게 되는 경우가 많다. 어떤 오픈소스 프로젝트에서 커미터의 역할을 담당한다는 것은 그 사람의 기여도가 높고 그만큼 높은 위치를 배정받았다는 뜻이 된다.

update와 commit에 관해서 또 하나 알아놓을 상식은 update와 commit 작업은 가능하면 자주 해 주는 것이 좋다는 사실이다. 다른 사람들이 내가 어떤 작업을 하는지 잘 알고, 나 역시 다른 사람들이 어떤 작업을 하고 있는지 잘 아는 것이 효율적인 협업(collaboration)의 기초가 됨은 두말할 필요가 없다. 추가적으로 CVS의 commit 명령 사용 예는 다음과 같다. -m 옵션 뒤에는 이번 commit에 대한 간략한 설명이나 주석문이 따라온다.

$ cvs commit -m “added additioinal messages” hello2.c   # 파일 하나 commit 예

$ pwd
/home/foobar/projects/src1
$ cvs commit -m “removed redundant declarations”       # 전체 디렉토리 commit 예

◆ log message : 수정된 소스코드를 commit할 때 이 소스코드가 어떤 역할을 하는지, 어떤 부분이 바뀌었는지 등을 기술하는 주석문 역할의 메시지이다. 이것 역시 코딩시 주석문 작성에 신경을 많이 쓰는 것처럼 수정된 소스코드를 commit할 때 마다 간결하고 정확하게 써 주는 것이 좋다.

오픈소스 프로젝트에 많은 참여를
지금까지 여러 명의 개발자, 특히 오픈소스 프로젝트에 참가할 때 필수적으로 쓰이는 유틸리티인 diff와 patch, 그리고 CVS의 기본 사용법에 대해서 알아보았다. 최근 들어서는 문서 작성도 오픈소스와 비슷하게 여러 사람이 동시에 같은 페이지를 고쳐 나가는 방식이 정착되어 가고 있다. 위키(wiki)의 사용이 늘어나고 있는 것이 바로 그것인데 위키는 내부적으로 RCS를 사용해서 문서의 버전 컨트롤을 해 나가는 경우가 많다. 즉, 위키를 통한 문서 작성 방식도 CVS를 이용한 오픈소스 소프트웨어 개발과 근본적으로 다르지 않은 것이다.

앞에서도 강조했듯이, 오픈소스 프로젝트에 참여하기 위해서 가장 중요한 것은 diff와 patch의 사용법을 아는 것이다. 일견 간단해 보이는 이 유틸리티들이 이메일, 그리고 이메일의 확장판인 메일링 리스트와 초기 유즈넷 뉴스그룹과 결합되면서 인터넷상에서 중요 오픈소스 소프트웨어 개발의 문을 열어젖힌 주역들이다.

또한, 이렇게 오픈소스 프로젝트에 참여하는 것이 생각만큼 어렵지 않다는 점을 일반 개발자들이 인지하는 것도 중요하다고 하겠다. 필자 역시 CVS의 사용법을 알기 전에는 내가 CVS의 사용법을 모르는데 오픈소스 프로젝트 참여가 가능하겠는가라는 쓸모없는 생각을 했던 적이 있다. 필자의 경우는 실제 떨어지는 코딩 능력이 오픈소스 프로젝트 참여에 장벽이 되고 있는데 이 글을 읽는 많은 독자들이 이번 연재를 계기로 오픈소스 프로젝트에 좀 더 많이 참여해 보는 기회를 가질 수 있으면 좋겠다는 것이 필자의 바람이다. 단 한 줄의 diff로 만든 패치라도 오픈소스 개발자에게는 많은 도움이 되며 또한 격려의 응원이 된다.

답글 남기기