본문 바로가기
  • 삶의 일상에서 쉼의 여유와 흔적을 찾아서
일상 정보/*외국어

· 조엘 스폴스키-조엘 온 소프트웨어/1부/2장 기본으로 돌아가기

by 탄천사랑 2022. 8. 23.

조엘 스폴스키 - 「조엘 온 소프트웨어

[220816-193936]

 

 


저는 웹사이트에 NET과 자바 비교, XML 전략, 제품이나 웹 가이트 고착 Lock-ln 소프트웨어 설계, 
아키텍처와 같은 흥미로운 '큰 그림'에 대해 이야기하는 걸 좋아합니다.
이 모든 요소는 층층이 쌓인 케이크와도 같습니다.

가장 높은 층에 소프트웨어 전략이 있고, 
바로 아래에 NET과 같은 아키텍처가 있으며,
그 아래에 자바와 소프트웨어 개발 제품이나 원도우와 같은 플랫폼을 생각할 수 있습니다.

케이크의 좀더 아랫 부분으로 가볼까요?
DLL? 객체? 함수? 
아닙니다! 
더 아래로! 어느 시점에서 당신은 프로그래밍 언어로 쓰여진 코드를 떠올릴것입니다.

아직 충분하지 못하군요.
오늘은 CPU에 대해 생각해봅시다.
바이트가 돌아다니는 작은 실리콘 조각이지요.
스스로를 초보 프로그래머라고 가정하고, 프로그래밍, 소프트웨어, 관리 등에 대해 쌓아놓은 
모든 지식을 날려버린 후에 가장 아래층에 위치한 폰 노이만 기본 요소로 돌아갑시다. 
즉 잠시동안 마음속에서 J2EE를 지우고 바이트만 생각합시다.

도대체 이런 일을 왜 하느냐구요?
저는 사람들이 저지르는 가장 큰 실수 중 몇 가지는 
최저층에서 벌어지는 몇 가지 단순한 동작원리를 자세히 알지 못하거나 
아예 잘못 알고 있기 때문에 생긴다고 생각합니다.

심지어 최상층인 아키텍처 층에서 일어나는 실수일지라도 말입니다.
궁전을 멋지게 지었는데, 기초공사가 형편없다고 합시다.
아래에 튼튼한 시맨트 판을 대신해 잡석을 깔았습니다.
겉보기에는 멋진 궁전인데, 
평평한 바닥에 놓은 욕조가 가끔가다 왜 삐걱대는지 도대체 알 수가 없습니다.

그러니 오늘은 숨을 한번 길게 들이쉬고, 
C 언어로 된 간단한 연습문제를 풀어보기로 합시다.
C에서 문자열이 동작하는 방법을 기억해 봅시다.
C 문자열은 값이 0인*-1 널 null 문자로 끝나는 몇 바이트를 포함합니다.
이 방식에는 다음과 같은 두 가지 명백한 문제점이 눈에 들어옵니다.
*-1  C 문자열에 대한 정보가 더 필요하다면 ,
http://www-ee.eng.hawaii.edu/Courses/EE150/Book/chap7/subsection2.1.1.2.htm1  을 참조하시기 바랍니다.

1. 널 문자를 찾아서 문자열 끝까지 가보기 전에는 끝을 알아내는 방법이 없습니다.
2. 문자열 내부에는 어떤 0값도 포함할 수 없으므로, JPEG 그림과 같은 비정형 이진 자료
    Binary Large OBject(BLOB)를 C 문자열 내부에 저장할 수 없습니다.

어떻게 C 문자열을 이런 방식으로 사용하게 됐을까요" 
이유는 유닉스와 C 프로그래밍 언어를 고안했을 무렵 사용한 PDP-7 마이크로프로세서 때문입니다.
PDP-7은 ASCIZ 문자열 타입을 지원하는데, ASCIZ에는 '끝이 영(Z)인 ASCll라는 의미가 있습니다.

ASCIZ가 단순히 문자열을 저장하는 유일한 길일까요? 물론 아닙니다.
오히려 ASCIZ는 문자열을 저장하는 최악의 방법 중 하나입니다.
실전에 쓰는 프로그램, API, 운영체제, 클래스 라이브러리를 위해서는 
다른 모든 코드를 오염시키는 ASCIZ 문자열을 반드시 피해야만 합니다. 왜 그럴까요?

문자열 하나를 다른 문자열에 덧붙이는 함수인 strcat을 구현하는 작업부터 시작해 봅니다.

void strcat ( char* dest, char* src )
{
   while (*dest) dest++;
   while (*dest++ = *src++);
{

코드를 조금만 살펴보면 여기서 무엇을 하는지 볼 수 있습니다.
우선 첫째 문자열에서 널 문자를 찾아갑니다.
종결자를 찾아내면, 
둘째 문자열을 돌아다니며, 한번에 하나씩을 복사해서 첫째 문자열 뒤에 붙입니다.

이런 문자열 처리와 결합 방법은 커닝헌과 리치 *-2 시절에는 충분했을지 모르지만,
한 가지 문제점이 있습니다.
*-2 브라이언 커닝헌과 데니스 리치는<The C Programming Language, 2nd Edition>
(Prentice Hall, 1988)을 집필한 C 아버지입니다.
여러 사람 이름을 문자열 하나에 이어 붙인다고 생각해봅시다.

char bigstring [1000]; /* 얼마나 활달해야 할지 알 수 없음... */ dest, char* src )
bigstring [0] = '/0';
strcat (bigstring, "John, ") ;
strcat (bigstring, "Paul, ") ;
strcat (bigstring, "George, ") ;
strcat (bigstring, "Joel ") ;

잘 동작할 것 같지요? 게다가 코드도 멋지고 깔끔해보입니다.
성능 측면은 어떻습니까?  생각처럼 빠를까요? 자료가 늘어나도 제대로 동작할까요?
문자열 백만 개를 덧붙인다면, 이런 방식이 과연 올바를까요?

아닙니다.
이 코드는 러시아 페인트공 알고리즘을 사용하고 있습니다.
도대체 러시아 페인트공 알고리즘이 무엇이냐구요?  여기에 재미있는 이야기 하나를 소개합니다.



도로 차선 페인트 작업을 하는 러시아 페인트공이 있습니다.
작업 첫날 페인트 공은 페인트 통을 들고 나가서 300야드를 칠했습니다. 
깜짝 놀란 책임자는 
"정말 놀라운데! 정말 손놀림이 좋군." 이라며, 페인트공에게 1 코펙을 주었습니다.

다음날 페인트공은 겨우 150야드만 칠했습니다.
그래도 책임자는 
"음, 어제 만큼은 못하지만, 여전히 손놀림이 좋아. 150야드도 대단하지."라며, 1 코펙을 주었습니다.

그다음 날 페인트공은 30야드를 칠했습니다.
책임자는 
"고작 30야드라니! 용납할 수 없네! 
  첫날에는 어떻게 오늘보다 10배를 넘게 칠한 건가? 도대체 뭐가 문제야?"라고 윽박질렀습니다.

풀이 죽은 페인트공은 이렇게 말했습니다.
"저도 어쩔 수 없었습니다. 
  매일 페인트 통에서 점점 멀어지니까요."

 

(시간이 남는 분이 계시면, 정확한 숫자를 한번 계산해보시겠습니까?"  *-3 )
(*-3    http://discuss.fogcreek.com/techlnterview/default.asp?cmd=show&ixPost=153
을 살펴보면 힌트가 나옵니다.)

이런 어수룩한 일화는 앞서 작성한 strcat을 사용할 때 어떤 문제가 발생하는지를 정확히 설명합니다.
strcat의 첫 부분은 매번 빌어먹을 널 문자를 찾아 목적지 문자열을 헤매 다녀야 하므로,
strcat 함수는 필요 이상으로 느끼며, 작업규모가 커지면 성능이 현저히 떨어집니다.

매일 사용하는 코드 대부분이 이런 문제점을 갖고 있습니다.
파일 시스템 대다수를 이런 방식으로 구현하는 바람에, 특정 디렉토리에 너무 많은 파일을 넣게 되면 문제가 생깁니다.
이렇게 되는 이유는 디렉토리 하나에 파일 수천 개를 담을 경우 성능이 급격히 떨어지기 때문입니다.
파일이 가득 차 있는 윈도우 휴지통을 얼어 봅시다.

휴지통을 여는 과정에서 시간이 제법 오래 걸리는데, 이는 휴지통에 들어있는 개수에 직접 비례하지는 않습니다.
그보다는 코드 어딘가에 러시아 페인트공 알고리즘이 존재하기 때문입니다.
틀림없이 선형 효율을 보여야 하는 곳에서 n제곱 효율*-4 이 나타난다면 
어딘가에 숨어있을 러시아 페인트공을 의심하십시오. 
(*-4 알고리즘분야에서는 선형 효율을 0(n)으로, n 제곱 효율을 0(n2)으로 기술합니다.
  여기서 n은 자료 개수입니다.)

이따금 라이브러리에 숨어있을지도 모릅니다.
strcat 함수 몇 개를 늘어 놓은 것이나 루프 안에 들어있는 strcat 하나를 봐서는 
좀처럼 'n제곱 효율'이라는 점이 명확해 보이지 않지만 실제로는 분명히 그렇게 돼 있을 겁니다.
이와 같은 문제점을 어떻게 고칠까요.
몇몇 똑똑한 C 프로그래머는 다음과 같이 mystrcat이라는 함수를 독자적으로 구현합니다.

char* mystrcat ( char *dest, char* src )
{
while (*dest) dest++;

while (*dest++ = *src++);

return --dest;

}

어떤 트릭을 사용했을까요? 
별다른 노력을 들이지 않고서도 새로 만들어진 더 긴 문자열의 끝을 가리키는 포인터를 반환합니다.
이런 방식으로 함수를 호출하는 함수는 
문자열을 새로 읽지 않고서도 덧붙이는 장소를 결정할 수 있습니다.  

char bigstring [1000]; /* 얼마나 활달해야 할지 알 수 없음... */ char *P = bigstring;
bigstring[0] = '/0' ;
P = mystrcat (p, "John, ") ;
P = mystrcat (p, "Paul, ") ;
P = mystrcat (p, "George, ") ;
P = mystrcat (p, "Joel ") ;

물론 n제곱이 아니라 선형 효율을 발휘하므로, 대량으로 문자열을 결합할 경우에 성능이 떨어지지는 않습니다.

파.스칼 설계자들은 이미 이런 문제점을 알고 있었으며,
문자열의 첫 바이트에 바이트 개수를 저장하는 방법으로 이를 해결했습니다.
바로 이런 문자열을 파스칼 문자열이라고 합니다.

파스칼 문자열은 0을 포함할 수 있으며, 널 문자로 끝나지 않습니다.
바이트에 들어가는 가장 큰 숫자가 255이기에 파스칼 문자열은 255바이트로 길이를 제한합니다.
하지만 널 문자로 끝나지 않기 때문에 ASCIZ 문자열과 같은 크기만큼 기억공간을 차지합니다.
파스칼 문자열의 가장 멋진 특징은 문자열 길이를 알아내기 위해 단순히 루프를 둘 필요가 없다는 점입니다.
파스칼에서 문자열 길이를 알아내는 작업은 전체 루프를 도는 대신에 어셈블리어 명령 하나로 족하며,
훨씬 빠른 성능을 보여줍니다.

옛날에 사용했던 매킨토시 운영체제는*-5  모든 곳에서 파스칼 문자열을 사용했습니다.
*-5  맥 OS X이 아닌나온 맥 OS 9 이하 계열 운영체제를 의미합니다.   
매킨토시가 아닌 다른 플렛폼에서도 많은 C프로그래머가 속력 문제로 파스칼 문자열을 사용하곤 했습니다.

엑셀은 내부적으로 파스칼 문자열을 사용하므로 엑셀 곳곳에서 문자열을 255바이트로 제한합니다.
물론 이런 구현 방식은 엑셀이 번개처럼 빠른 이유 중 하나가 되겠습니다.
오랫동안 파스칼 문자열을 C 코드에 넣기 위해 아래처럼 코드를 작성해야 했습니다.

char*  str = '/006Hello!' ;

바로 그렇습니다.
바이트를 하나씩 손으로 헤아려, 문자열의 첫 바이트에 수작업으로 집어 넣어야만 합니다.
게으른 프로그래머는 이런 작업을 손쉽게 하기 위해 조금 느리게 돌아가는 프로그램을 작성합니다. 

char*  str = '*Hello!' ;
str[0] = strlen (str) - 1 ;

이렇게 프로그램을 작성할 경우 파스칼 문자열을 따르지만,
널 문자로 끝난다. (컴파일러가 이렇게 만들었습니다)는 사실에 주목하셔야 합니다.
저는 '널 문자로 끝난 파스칼 문자열' 대신에 빌어먹을 문자열이라고 부르는 편을 선호하지만,
점잖은 자리에서는 제대로 된 긴 이름, 
널 문자로 끝난 파스칼 문자열로 불러주시기 바람니다.  (p11)
※ 이 글은 <조엘 온 소프트웨어>실린 일부 단락을 필사한 것임.



조엘 스폴스키 - 조엘  소프트웨어

역자 - 박재호
에이콘출판 - 2005. 04. 15.

댓글