저자: 『IT 백두대간, C 언어 펀더멘탈: 견고한 프로그램을 위한 기본 원리』의 저자 전웅
프로그램은 제작자인 프로그래머 자신이 처음 생각했던 것보다 긴 시간, 다양한 환경에서, 여러 사람에 의해 사용되는 경향이 있습니다. 즉, 사적인 용도로 단기간동안 사용하기 위해 개발했던 프로그램도 후일 프로그래머 자신은 상상치도 못했던 환경에서 제작자 자신에 의해서 혹은 전혀 친분이 없는 사람들에 의해서 여러 형태로 사용될 수 있다는 뜻입니다. 이러한 일반적 경향은 프로그램을 작성하는 과정에서 왜 이식성(portability), 유지보수성(maintainability), 정확성(correctness), 가독성(readability) 같은 가치가 큰 중요성을 갖는지 설명해 줍니다.
필자의 지인(知人)은 얼마 전 전임자가 gcc 2.x에서 컴파일 되도록 만들어 놓은 프로그램을 gcc 3.x 환경으로 옮기는 작업을 하게 되었습니다. gcc는 2.x나 3.x 버전 모두 합리적인 수준으로 C 언어의 표준(정확히는, C90)을 지원해주기 때문에 골치 아픈 문제는 전혀 없으리라 예상했습니다. 하지만 당시 원 제작자가 의존한 gcc의 고약한 특징 때문에 그 분은 수만 라인의 소스를 모두 뒤져야 하는 고행을 겪어야 했습니다. gcc 2.x에서 특정 조건이 만족되면 반환형이 정수형인 함수에서 반환값이 없는 return;문은 0을 반환하는 것처럼 행동합니다. 하지만 gcc 3.x를 사용하는 환경에서 그 조건이 달라지자 갑자기 예측 불가능한 값이 반환되었던 것입니다. 물론 이러한 변화는 정당한 것입니다. gcc 2.x에서 보았던 행동은 최적화 옵션만 변경해도 결과가 달라질 정도로 표준이 의미를 전혀 보장해주지 않는 것이었습니다. 비록 기계적인 작업이지만 전혀 예상치 못한 일에 시간을 버려야 했던 지인은 이러한 사실을 전임자에게 알렸고, 전임자는 어차피 gcc를 포함한 그러한 변화를 예측하는 것은 힘들지 않느냐는 답변을 했다고 합니다. 이 상황에서 잘잘못을 따지는 의도는 절대 아니지만, 만약 전임자가 불필요한 부분에서 사소한 편의를 위해 gcc와 실행 환경의 미묘한 특성에 의존하지 않고 표준을 따르기 위해 노력했다면, 비록 큰 일은 아니지만 이와 같은 에피소드는 일어나지 않았을 것입니다. gcc와 실행 환경의 모든 변화를 예측하는 것은 분명 불가능한 일이지만, gcc를 포함해 표준을 따르는 임플리멘테이션(conforming implementation)의 공통 분모인 표준은 쉽게 변하지 않으며 설사 변한다 해도 하위 호환성(backward compatibility)을 강하게 유지해주기 때문입니다.
이 예에서처럼 주변을 살펴보면 작은 편의를 위해 혹은 무지를 이유로 심각한 고민 없이 전혀 이식성을 갖지 않는 행동이나 정확성이 없는 프로그램 구조에 의존하는 경우를 어렵지 않게 찾아볼 수 있습니다. 일단, 대표적인 몇 가지 사례를 짤막하게 소개해 보겠습니다.
첫 번째 사례: 책과 강의실에서…
특정 컴파일러를 통해 구현된 C 언어가 아닌 보통의 C 언어를 설명한다는 책에서 다음과 같은 예제를 만나는 것은 그리 어려운 일이 아닙니다.
[1]
void main()
{
printf("Hollo, world!\n");
}
또한, c 라는 대상체(변수)에 저장된 문자가 영문 알파벳 대문자인 경우 소문자로 변환하는 코드를 다음과 같이 작성하는 일도 많은 책에서, 또 강의실에서 비일비재합니다.
[2]
if (c >= "A" && c <= "Z") c = c - 17;
더구나 종종 프로그램의 가독성을 생각한다는 이유로 다음과 같이 변형된 형태가 소개되기도 합니다.
if (c >= "A" && c <= "Z") c = c - "A" + "a";
두 번째 사례: 인터넷 게시판에서…
C 언어를 다루는 많은 인터넷 게시판에서는 임시 대상체(변수)를 도입하지 않아도 다음과 같은 비트 연산자(bitwise operator)를 사용해 저장된 두 값을 바꿀 수 있다고 이야기하며,
[3]
#define SWAP(a, b) ((a)^=(b)^=(a)^=(b))
2
n으로 나누거나 곱하는 것보다 좌우측 시프트 연산자 (<<, >>) 를 사용하는 것이 더 빠르고
[4], i = i + 1; 나 i += 1; 이라는 문장보다 증감 연산자를 사용한 i++; 이 훨씬 더 빠른 코드를 생성해 낸다고 강조합니다.
[5]
많은 프로그래머들은 C 언어를 마치 준(準) 어셈블리어라고 생각하며 처음부터 이식성 같은 "고급스러운" 가치를 포기하고 위험천만한 트릭에 의존하는 경향이 강합니다("의존한다" 보다 "즐긴다"는 표현이 더 어울리는지도 모르겠습니다). 이는 이식성을 강조하는 C 언어의 철학과 매우 거리가 있는 생각이며, 동시에 앞서 소개한 (사실상 잘못된) 코드들이 프로그래머가 의도한대로 동작하기 위해서 얼마나 많은 가정이 만족되어야 하는지에 대한 고민이 거의 이루어지고 있지 않음을 단적으로 보여주는 것입니다.
현실적인 문제에서 많은 임플리멘테이션이 프로그래머가 한번도 고민해보지 않은 가정을 의도적으로 혹은 우연히 만족해주어 잘못된 혹은 이식성 없는 프로그램이 잘 동작한다고 해도, C 라는 프로그래밍 언어를 통해 자신의 의도를 표현하는 프로그래머가 항상 의존할 수 있는 최소한의 가정을 만들어 주는 것은 바로 C 언어 표준의 역할입니다. 이러한 점에서 C 언어의 표준은 C 프로그래밍의 전부는 아니지만 분명 시작점이라 말할 수 있습니다. 그럼에도 불구하고 C 언어를 다루는 많은 국내서는 (혹은 일부 일본 서적의 번역서는) 표준에 대한 기술이 절대적으로 부족한 형편입니다. 지금 이 순간에도 C 언어를 통해 제품을 개발하는 프로그래머 중 다수는 ISO/IEC 수준의 C 언어 국제 표준이 존재한다는 사실도, 또 그 국제 표준이 최근(1999년)에 새로 개정되었다는 사실도 알지 못하고 있습니다.
[6] 이는 표준에 기반을 둔 상당히 정확한 내용을 다루는 영문 뉴스그룹이나 다수의 원서와는 대조적인 모습입니다.
세 번째 사례: 결핍된 지식…
C 언어에 대해 존재하는 오해는 비단 잘못된 프로그램에 한해서만 발생하는 것은 아닙니다. 예를 들어, volatile 과 const 는 1989년 C 언어의 첫 표준이 생기면서 도입된 형한정어(type qualifier) 입니다. 프로그램의 최적화(optimization)와 관련하여 아주 편안하게 이해할 수 있는 이 두 개념을 많은 서적은 표면적인 모습과 구체적인 예만을 통해 소개할 뿐이며, 그나마 volatile의 경우에는 분명 C 언어의 한 부분임에도 불구하고 딱딱한 내용의 시스템 프로그래밍 서적이 아니면 아예 소개하지 않는 경우도 많습니다.
[7]
for (i = 0; i < 10; i++) …
for (p = &a[0]; p < &a[10]; p++) …
또한, 많은 C 프로그래머들이 사용하는 관례만을 나열하기에 급급하여 위와 같은 비대칭 경계(asynchronous bound)를 사용하는 for 문을 왜 아래의 대칭 형태로 작성하지 않는지 고민할 기회조차 제공하지 않습니다. 특히나 이 과정에서 비대칭 경계를 지원하기 위해 C 언어가 보장해주는 포인터와 관련된 아주 독특한 가정에 대한 설명이 이루어질 리 만무합니다.
[8]
for (i = 0; i <= 9; i++) …
for (p = &a[0]; p <= &a[9]; p++) …
물론, 필자 역시 처음부터 C 표준을 기준으로 C 언어와 C 프로그램을 바라본 것은 아니었습니다. 아주 우연한 기회에 C 표준과 표준을 공부하는 사람들을 만나게 되었고 긴 시간 "돌아가기만 하는 생각 없는" 프로그램에 익숙한 채 가지고 있던 많은 오해와 편견을 깨나가는 혹독한 과정을 수년동안 거쳐야 했습니다. 사실 이 과정은 아직도 진행 중입니다.
『IT 백두대간, C 언어 펀더멘탈: 견고한 프로그램을 위한 기본 원리』는 지금까지 언급한 내용을 포함해 C 언어와 표준에 대해 만연한 오해와 편견을 깨나가는 과정을 고스란히 담고 있습니다. 분명 많은 프로그래머가 필자가 비슷한 과정을 거쳐 "결핍된" C 언어를 익혔거나 익히게 될 것이라 가정하고 가능한 적은 고통으로 가능한 빠른 시간 내에 C 언어의 마지막 권위라는 "C 표준"이라는 렌즈를 통해 C 언어와 프로그램을 바라볼 수 있기를 바라는 마음으로 책을 준비했습니다.
책의 난이도에 대해서
안타깝게도 C 표준을 준수하는 범위 내에서 독자 여러분을 현혹할만한 "재미있는" 프로그램을 작성하는 것은 사실상 불가능합니다. 따라서 이 책은 다소 추상적이고 이론적인 관점에서 C 언어를 기술합니다. 그 가운데서도 독자가 흥미를 잃지 않도록 하기 위해 간단하면서도 실용성 있는 예제를 도입하려고 노력했지만 화면을 통해 즉각적이고 화려한 볼거리를 제공하는 예제와는 아무래도 거리가 있습니다. 하지만, 사소한 프로그램 "조각"에서도 실제적인 프로그래밍 과정에서 십분 활용할 수 있는 다양한 "생각할 거리"를 제공하고 있습니다. 예를 들어 배열을 설명하기 위해 도입했던 단순한 형태의 스택(stack) 프로그램을 (부족한 부분이 없지는 않지만) ADT(abstract data type)를 구현하는 단계까지 단계별로 확장해 나가며, 손쉽게 구현할 수 있으면서도 언어를 이해하는데 큰 도움이 되는 일부 표준 라이브러리 함수를 직접 구현해 보기도 합니다.
또한, C 언어의 가장 기초와 기본이 되는 내용을 소개하고 설명하는 것이 책의 목표이지만 책 자체가 C 언어의 입문자를 대상으로 한다고 말하기에는 어려움이 있습니다. 하지만, 필자의 개인적인 경험에 비추어보아 최소한 다른 C 언어 서적을 부분만이라도 보았거나 C 언어 강의를 한두 번 정도 들은 경험이 있는 독자라면 "어려운 이야기"로 분류해 둔 일부 내용을 제외하면 C 언어를 익히는 과정에서 반드시 만나게 되고 또 이해할 수 있는 수준의 내용이라고 생각합니다. 책이 두꺼운 만큼 단기간 내에 책 전체를 정독하기에는 현실적인 어려움이 있겠지만, 틈틈이 시간이 허락하는 대로 반복하여 읽어가거나 독자 여러분의 수준에 맞춰 필요한 내용을 "체계적으로" (체계적이지 않을 경우 또 다른 오해를 낳기 쉽습니다) 발췌독하는 것도 이 책을 보는 현명한 방법 중 하나라고 생각합니다.
집필시 어려웠던 점
책을 집필하는 과정에서 책이라는 매체가 갖는 지식 전달의 일방성에 주목해야 했습니다. 독자가 책과 관련된 홈페이지나 정오표를 직접 찾아보는 적극성을 보이지 않는 이상 (그릇된 내용을 전달하는 다른 책들과 마찬가지로) 이 책 역시 잘못된 지식을 그럴싸하게 전달하는 수준에 머물 수 있다는 생각에, 책을 집필하는 시간 대부분을 기술적 내용을 검토하는데 할애하였습니다. 최근 3년간 수집한 자료와 7여권의 책을 통해 모든 내용을 재확인했습니다. 또한, C 언어는 지금 이 순간에도 계속 변화, 발전하고 있기 때문에 C 언어의 표준이 크게 변하면 책의 내용은 자연히 틀려질 수 밖에 없습니다. 따라서 앞으로 변화해가는 정확한 방향을 잡아내기 위해 표준화 위원회에서 큰 영향력을 갖는 분들과 논의한 내용을 선별하여 반영하기 위해 노력하였습니다.
앞으로의 계획
현재 독자 여러분께 도움이 될 수 있는 잘 정리된 정오표, 설명이 부족한 부분에 대한 부연 설명과 참고 자료, 게시판 등의 추가적인 정보를 제공하기 위한 홈페이지를 정비 중입니다.
http://www.woong.org에서 확인하실 수 있습니다. 이 작업이 마무리되는 대로 책의 마무리 글에서 언급했던 (C 언어를 구성하는 나머지 반쪽인) 표준 라이브러리를 동일한 철학과 관점에서 전달하는 책을 장기적인 계획으로 꼼꼼히 준비할 예정이며(라이브러리를 다루는 책에서는 독자 여러분의 시간과 지면을 절약하기 위해 이번 책에서 소개한 내용을 바탕으로 가능한 중복된 설명 없이 보다 흥미로운 소재로 채워나갈 계획입니다), 아마도 그 이전에 C로 실용성 있는 프로그램을 작성하는 모습을 보여줄 수 있는 또 다른 책을 통해 독자 여러분을 만나게 되지 않을까 생각합니다.
주석
[1] 13장 02. 프로그램의 실행과 종료
[2] 03장 02. 문자세트
[3] 11장 01. 수식의 기본 개념, 02. 연산자
[4] 11장 02. 연산자
[5] 01장 02. 프로그래밍 언어랑 무엇일까?
[6] 02장 02. 표준과 확장 그리고 진단 메시지
[7] 06장 02. 선언의 형식
[8] 07장 02. 배열