한줄평
프로그램 대가들이 알려주는 실전팁
책 소개
프로그래밍 수련법은 브라이언 W. 커니핸과 롭 파이크가 쓴 책이다. 이 두분은 소프트웨어 분야에 있으면 한 번은 들었을 이름들이다. 간단히 소개하면 브라이언 커니핸은 C언어를 개발한 데니스 리치와 함께 최초의 C언어 해설서인 "The C Programming Language"를 썼으며, Unix의 많은 프로그램을 작성하였고 유명한 awk
스크립트 언어를 공동개발하였다. awk
의 k
가 커닝핸의 약자라고 한다. (출처:위키백과 링크) 롭 파이크는 벨 연구소의 Unix 팀의 멤버로 Unix 시스템 개발자 중 한 명이다. 또한 Plan9
, Inferno
라는 운영체제 개발에 참여했었다. 최근에는 구글에서 Go language
를 개발에 참여한 것으로 알려져 있다. (출처:위키백과 링크)
원서는 1999년도에 출판되었으며 그 때가 Java언어가 득세할 시기였다. 번역판은 2008년도에 김정민, 장혜식, 신성국이 번역하여 발행되었다.
감상평
서문에 있듯이 프로그래밍할 때 필요한 요소들에 대해 코드와 함께 실질적으로 설명한다. 사례로 들어있는 코드는 c, cpp, Java로 되어 있어 어찌보면 구식이라 할 수 있다. 하지만 이 언어들은 현재에도 언어 사용 순위 Java 1위(15.876%), C 2위(12.424%), CPP 4위(7.444%) (출처: TIOBE 2019 2월링크) 이고 약 36%를 차지하는 것을 보면 책 내용이 현재에도 충분히 효용이 있다고 할 수 있다. 사례 코드의 점유율은 제처두더라도 책에서 알려주는 방법들은 사례와 경험에 대한 이야기들은 내가 전에 고민 했었던 것들을 시원하게 해결해주는 부분들이어서 나에게 피가 되고 살이 되는 경험을 갖게 하였다.
책 구성을 보면 스타일, 알고리즘과 데이터 구조, 설계와 구현, 인터페이스, 디버깅, 테스트, 성능, 호환성, 표기법으로 개발을 할 때 매번 마주치게 되는 주제들이다. 이 책에 특징으로 각 장마다 연습문제와 더 읽어보기는 책을 이해하는데 더 많은 도움을 주었다. 연습문제 답이 없어 혼자 고민하는 수 밖에 없지만 답을 맞추고 틀리는 것이 중요한게 아니라 고민해서 나만의 답을 내어 보는 과정이 중요하다고 생각된다. 이번 독서 모임(개발자 한 달에 책 한 권 읽기)에서 연습문제에 대해 토론할 게 많을 것 같다.
2창 알고리즘과 데이터 구조부터 예제로 드는 코드가 내용 이해하는 데 중요한 요소라 코드 분석하는 데 시간을 많이 들였다. 몇몇 코드들은 눈으로는 이해하기가 어려워 연습장에 값들을 검토하고서 이해할 수 있었다.
다른 여타 책보다 책에 메모를 많이 하게 된 걸 보면 읽으면서 생각을 많이 하게 된거 같다. 이 책은 동료들에게 꼭 추천하고 싶은 책이다.
내용 갈무리
중요하다고 밑줄 친 부분이 너무 많다. 여기에 다 실을 순 없고 각 장에 대한 감상만 적어야 겠다. 각 장 시작에 주제에 따른 명언이 실려있는데 맘에 드는 명언들도 같이 적어서 내용을 상기시키려 한다.
1장 스타일
훌륭한 작가들이 때때로 작문법 규칙을 무시하는 일은 예부터 볼 수 있었다. 그러나 그럴 경우 독자들은 대신 틀린 작문법을 상쇄할만한 훌륭한 문장을 발견하기 마련이다. 만약, 그렇게 할 자신이 없다면 규칙을 따르는 게 최상의 방책일 것이다.
- 윌리엄 스트렁크(William Struck)와 E. B. 화이트(White), "The Elements of Style"
이 말 그대로 내가 훌륭한 코딩 스타일을 만들 자신이 없다면 코딩 스타일을 따르는 게 가장 좋은 방법이다.
내 경험상 개인 프로젝트이든 회사 프로젝트이든 코딩 스타일이 정해져 있지 않으면 개인이 코딩할 때마다 그 날의 변덕에 따라 바뀌었었다. 책에서 말하는 스타일을 일단 따르는 것도 스타일이 없을 땐 좋을 것 같다.
- 전역변수에는 서술적인 이름을, 지역변수에는 짧은 이름을 붙이라
- 프로그래머들은 때로 문맥과 상관없이 긴 변수 이름을 쓰려고 하지만, 이는 잘못된 것이다. 코드의 명료성은 간결함을 통해 얻을 수 있는 경우가 많다.
for (theElementIndex = 0; theElementIndex < numberOfElements; theElementIndex++)
elementArray[theElementIndex] = theElementIndex;
for (i = 0; i < nelems; i++)
elem[i] = i;
위처럼 긴 변수보단 아래 처럼 짧게 쓰라고 조언한다. 개인적으로는 첫번째 스타일로 주로 코딩하였는데 확실이 주저리주저리 코드가 길어지는 문제가 있었다. 두번째처럼 짧게 하려면 영어문화권이 아닌 우리로서는 좋은 코드들에서 어떻게 변수명을 줄이는 지 확인해보는게 좋을 것 같다. 최근에 본 "오픈소스의 네이밍 특징"(출처: 최규민 kakao brunch 오픈소스의 네이밍 특징) 블로그를 참고하면 괜찮은 이름을 만들때 도움이 될 것 같다.
- 일관성을 지켜라
- 서로 관련이 있는 것들에는 연관된 이름을 붙여 상관관계를 보여줌과 동시에 그 차이점이 눈에 띄도록 한다.
queue.queueCapacity
보단queue.capacity
로
- 서로 관련이 있는 것들에는 연관된 이름을 붙여 상관관계를 보여줌과 동시에 그 차이점이 눈에 띄도록 한다.
- 함수 이름에는 능동형을 쓰라
- 능동형으로 쓰면 함수가 어떤한 동작을 한다라는 의미를 담는 것으로 보인다.
now = data.getTime();
putchar('\n');
if (checkoctal(c)) // X 참,거짓 반환인지 모호함
if (isoctal(c)) // O 참, 거짓인지 명확하게
- 정확한 이름을 쓰라
- 표현식을 자연스럽게 쓰라
// 부정을 포함하는 조건식은 언제나 이해하기 어렵다
if (!(block_id < actblks) || !(block_id >= unblocks))
// not 을 없애보자
if ((block_id >= actblks) || (block_id < unblocks))
추가로 내가 특정 숫자 범위를 검사할 때 쓰는 코드다.
// 0 <= x < 10 표현
if ((0 <= x) && (x < 10))
- 부수효과를 조심하라
-
++
같은 연산자 사용을 조심하라라는 의미이다.
-
str[i++] = str[i++] = ' '; // ???
// 나눠서
str[i++] = ' ';
str[i++] = ' ';
- 들여쓰기와 중괄호
{}
를 쓰는 스타일에서는 일관성을 지키라
코드를 수정할 때, 자신만의 스타일을 절대 쓰지 않도록 한다. 코드의 일관성은 개발자 자신의 스타일보다 중요하다. 이 규칙을 따르면 삶이 더 편해지기 때문이다.
이 말에 100% 공감한다.
- 일관성을 위해 관용 표현을 사용하라
- c++에서 매크로 함수를 멀리하라
- c++에서는 인라인함수가 있음
- 매직넘버에 이름을 달아주라
- 개별 숫자들의 의미가 코드에 전혀 나타나있지 않았기 때문에 이 숫자들이 모두 신비롭게(magical) 보일 뿐이다.
- 숫자는 매크로로 쓰지 말고 상수로 정의하라
- 아스키 문자는 숫자 코드 말고 문자 상수로 쓰라
- 나쁜 코드에 대해 설명하지 말고 코드를 새로 짜라
- 주석과 코드가 모순되게 하지 말라
- 주석은 코드에 일치하는 것이 돼서는 안 되며, 코드를 보충하는 것이 돼야 한다.
왜 그렇게 귀찮게 구는가? 스타일에 왜 그리 걱정하는 것일까?
...중략...
답은 잘 짠 코드는 대충 던져놓고 한 번도 정리하지 않은 코드보다 읽기도 쉽고 이해하기도 쉬울뿐더러 대부분의 경우에 에러도 적고 분량도 적은 편이기 때문이다.
2장. 알고리즘과 데이터 구조
결국 어떤 문제가 생겼을 때 올바른 해결책을 제시하는 것은 현장에서의 기법과 도구에 대한 숙련도뿐이며, 전문가다운 결과를 지속적으로 뽑아내는 것은 일정 수준 이상의 경험뿐이다.
- 레이먼드 필딩(Raymond Fielding), "특수효과 촬영 기법"
만약 생소한 분야의 프로그램을 개발하려 한다면, '반드시' 어떤 것들이 이미 되어 있는지를 알아내어서, 다른 사람들이 이미 잘해 놓은 것을 어설프게 다시 하느라 시간을 낭비하지 않도록 해야 한다.
아이작 뉴튼이 인용했던 내가 더 멀리 보았다면 이는 거인들의 어깨 위에 올라서 있었기 때문이다.
문장과도 같은 의미이다.
2장에서는 검색, 정렬, 배열, 리스트, 트리, 해시 테이블을 코드와 쓰이는 목적을 잘 설명하고 있다. 눈으로만 보지 않고 연습장에 코드를 분석해서 이해하고 보니 저자가 말한 바를 더 쉽게 이해할 수 있었다.
2장은 요약 발췌로 마무리한다.
알고리즘을 선택할 때에는 여러 단게를 거쳐야 한다. 첫째, 여러 알고리즘과 데이터 구조들을 평가한다. 또, 프로그램이 처리할 데이터 양이 얼마나 많을지 예측해 봐야 한다.
...중략...
그 다음, 가능하다면 라이브러리나 언어에서 기본적으로 제공하는 기능을 사용하라.
...중략...
데이터 구조마다 각 연산에서 예상되는 소요 시간이 서로 다르며, 이런 특징을 기준으로 삼아, 쓰기에 적당한 데이터 구조를 선택하는 것이 좋다.
...중략...
전문적인 문제를 해결하기 위해서 존재하는 여러 데이터 구조들도 많지만, 대부부느 소프트웨어를 작성하는 데에는 앞에서 다룬 기본적인 데이터 구조만으로도 충분할 것이다.
3장. 설계와 구현
여러분이 테이블을 감추고 흐름도만 보여준다면 나는 뭐가 뭔지 계속 모를 것이다.
테이블을 보여 달라. 그러면 대개 흐름도는 안 봐도 된다. 너무 뻔할 테니까.
- 프레드릭 P.브룩스 주니어(Frederick P.Brooks,Jr.), "The Mythical Man Month"
브룩스의 고전에 따르면 프로그램을 만들 때 가장 중요한 것은 데이터 구조의 설계다. 일단 데이터 구조를 만들었다면, 알고리즘은 보통 바로 결정할 수 있고, 그에 따른 코딩도 비교적 쉽다.
나도 프로그램을 짜기 전 계획할 때 데이터 구조를 먼저 정해야 동작을 정의하는데 수월했었다.
3장은 마르코프 체인 알고리즘을 설계, 구현하는 과정을 보여준다. 특히 이 장은 앞에서부터 이해를 하지 않고는 다음 페이지로 넘어가기가 힘들었다. 그래서인지 3장 읽는데 4시간 반정도 소요됐다. 시간이 오래 걸렸지만 이해할 수 있어서 만족한다.
이 장의 핵심은 교훈 소챕터에 있다.
c++ STL처럼 규모가 큰 컨포넌트나 스크립트 언어가 제공하는 결합 배열과 문자열 처리는 코드 길이를 줄여주고, 개발 기간도 줄여준다. 물론 이런 기능을 쓰면 성능이 떨어질 수 있다는 대가를 치러야 한다. 하지만 앞에서 만든 마르코프 프로그램처럼 몇 초내에 끝나는 프로그램을 만들 때에는 큰 문제가 되지 않을 것이다.
더 잘 드러나지 않는 문제는, 시스템이 제공해 주는 코드 부분이 너무 커져서 프로그램 내부에서 무슨 일이 일어나는지 아무도 모르게 되므로 개발자가 프로그램을 제어하고 이해하는 능력을 잃어버릴 수 있다는 것이다.
설계를 완벽히 끝내고 프로그램을 작성하는 것은 쉽지 않다. 실제로 프로그램을 만들 때에는 여러 가지 반복과 실험이 필요할 것이다.
...중략...
가능하다면 최대한 간단한 것부터 시작해서, 경험을 토대로 발전시켜 나아가길 바란다.
4장. 인터페이스
이 장 역시 예제 코드 이해가 되어야 했었다.
설계의 핵심은 서로 대립하는 목표와 제약 사이에 균형을 잡는데 있다.
설계 단계에서 다뤄야 할 문제 들은 다음과 같다.
* 인터페이스: 어떤 서비스와 접근 권한을 제공할 것인가?
* 정보 은닉: 어떤 정보를 드러내고, 어떤 정보를 숨김 것인가?
* 자원 관리: 메모리나 기타 한정된 자원들을 누가 관리할 것인가?
* 에러 처리: 누가 에러를 감지하고, 누가 이 에러를 보고할 것인가?
이 질문들을 기억할 필요가 있을 것 같다. 좋은 질문이 좋은 대답을 가져다 줄 확률이 높다.
좋은 라이브러리 설계 또는 좋은 인터페이스 설계는 단번에 얻어지지 않는다. 프레드 브룩스가 "한 번은 버릴 마음을 먹어라. 어차피 그렇게 될 것이므로."라고 썼을 정도다.
...중략...
보통은 임시 버전으로라도 직접 프로그램을 만들고 사용해 본 다음에야, 제대로 설계를 할 수 있을 만큼 관련 쟁점들을 충분히 이해할 수 있다.
설계와 구현, 인터페이스 장에서는 브룩스 교수님의 맨먼스미신 책의 내용이 인용된다. 맨먼스미신을 읽기 잘했다. 꼭 읽어야 할 고전이다.
이렇게 망가지기 쉬운 프로토타입은 개인적인 목적이나 접근 방식의 현실성을 보기 위한 용도로는 사용 할 수 있겠지만 그 이상은 무리다. 다른 구현 형태를 시도하기 전에 이 설계에 대해 다시 생각해 보자.
다른 사람이 쓸 수 있는 인터페이스를 만들려면 이 장 제일 처음에 나열했던 인터페이스, 정보 은닉, 자원 관리, 에러 처리를 고려해야 한다.
여기가 이 장의 핵심이다. CSV 처리 라이브러리를 만들때 고려하는 내용들과 c언어로 구현, c++로 구현하는 과정을 보면 대가들이 개발하는 과정을 보는 느낌이였다.
5장. 디버깅
bug(버그)
b. (1889년 "Pall Mall Gaz" 3월11일자 1-1호) 내가 듣기로 에디슨씨는 이틀밤을 꼬박 새우고서야 축음기에서 '벌레(bug)'를 발견했다. - 곤란한 문제를 해결할 때 쓰는 표현.
어떤 상상의 벌레가 안에 숨어 모든 문제를 일으킨다는 뜻에서 나온 말이다.
- 옥스포드 영어 사전 2판
이번 장은 사례를 들면서 조언을 해주는 것이 많아 실용적이다.
좋은 프로그래머는 코드 작성에 쓰는 시간과 비슷한 시간을 디버깅에 쏟으며 실수에서 배움을 얻으려 한다. 버그를 찾을 때마다, 이후에 비슷한 버그가 생기는 것을 방지하거나 재발한 버그를 찾는 법을 배우게 된다.
디버깅은 힘든 작업이고 예측 불가능한 많은 시간이 소요될 수도 있기 때문에 가능하면 디버깅할 상황을 만들지 않는 것을 목표로 삼아야 한다. 디버깅 시간을 줄요주는 테크닉에는 좋은 설계, 좋은 스타일, 경계 조건 테스트, 단정문(assertion), 코드의 정상성(sanity) 체크, 방어적 프로그래밍, 잘 설계된 인터페이스, 한정된 전역 데이터, 검사 도구가 있다.
치료보다는 예방이 우선이다.
치료보다는 예방이 우선이다
이라는 마지막 문장처럼 디버깅을 덜 할 수 있게 해야 한다는 데에 동의한다. 무턱대고 코딩하기 보다 준비를 차근차근해서 코딩하는게 낫다.
언어는 어떤 역할을 할까? 프로그래밍 언어를 진화시킨 주요 동력의 하나는 언어 자체의 기능요소를 통해 버그를 예방하려는 열망이었다. 몇몇 기능요소는 에러가 덜 발생하도록 해준다.
...중략...
어떤 문제를 예방하는 언어의 기능요소는 또 그 대가를 치른다. 고수준 언어를 쓰면 단순한 버그가 자동으로 없어지는 반면, 고수준 버그가 생기기 더 쉽다. 세상의 어떤 언어도 사용자의 실수를 예방해 줄 수는 없다.
우리가 아무리 그러지 않길 바라더라도, 현실적으로 프로그래밍 시간의 대부분은 테스트와 디버깅에 소요된다.
테스트와 디버깅 효율을 높여야 하는 이유다.
프로그램 실행 흐름을 한 단계씩 깊이 들어가 보는 것보다, 열심히 생각한 다음 중요한 위치에 자기검증 코드나 출력문을 추가하는 편이 더 생산적이다.
디버거를 쓰는 것보다 더 중요한 디버깅 방법이다. 생각없이 중단점(breakpoint)을 걸고 변수값을 보면서 그 의미를 파악하는데 다시 시간을 쏟았던 적이 많았었다.
게다가, 디버깅을 위해 추가한 코드는 프로그램에 남지만, 디버거 세션은 한번 실행하면 사라진다.
디버거 세션은 한번 실행하면 사라진다
는 이 상황은 수도 없이 겪었다. 임베디드 시스템에 넣을 프로그램을 개발할 때는 디버깅하는 거 자체가 불가능하거나 가능해도 디버깅 동작을 수행하는데 시간을 많이 잡아 먹는다. 뭘 봐야할 지 모른채 열심히 디버거를 연결해서 동작을 시켜 문제에 도달해도 문제 상황이 어떻게 된건지 몰라 시간낭비한 경우가 많았다. 보드에 Jtag jig를 연결하려고 커넥터를 구걸하고 납땝해서 힘들게 jig 연결 후 디버깅하던 추억이 떠오른다.
실마리가 뚜렷한 버그
- 자주 나오는 패턴을 찾으라
- 가장 최근에 변경한 부분을 검사하라
- 같은 실수를 두 번 반복하지 말라
- 오늘 할 디버깅을 내일로 미루지 말라
- 스택 추적값을 확인하라
- 작성하기 전에 읽으라
- 코드를 다른 사람에게 설명하라
- 내가 자주 시도하는 디버깅이다. 효과가 좋다.
실마리가 없는 어려운 버그
- 버그를 재현할 수 있게 하라
- 각개격파하라
- 문제를 발생시키는 입력의 범위를 더 좁힐 수는 없을까?
- 어떻게 고치면 에러가 없어질까?
- 수가 의미하는 바를 연구하라
- 때로 문제를 발생시키는 숫자의 패턴이 탐색 범위를 좁힐 실마리를 주는 경우가 있다.
- 결과를 출력해서 탐색 범위를 좁혀라
- 프로그램이 무슨 일을 하는지 이해할 수 없다면, 더 많은 정보를 볼 수 있도록 출력문을 추가하는게 가장 쉽고도 효율적인 방법이 될 수 있다.
- 자가검증 코드를 작성하라
- 더 많은 정보가 필요하다면 조건을 테스트하고 관련된 변수값을 출력한 다음에 프로그램을 중단시키는 check함수를 직접 작성한다.
- 로그 파일을 작성하라
- 디버깅용 고정 형식 스트림 출력을 담는 로그 파일을 작성하는 것이다.
- 그림을 그려라
- 도구를 사용하라
- diff 같은 프로그램은 문제없는 경우와 잘못된 경우의 출력을 비교하여 바뀐 부분에 집중할 수 있게 해준다.
- 기록하라
- 버그를 찾으면서 어느 정도 시간이 흐르면, 무엇을 시도해 봤고, 무엇을 깨달았는지 잊어 버리기 시작한다. 테스트와 그 결과를 기록해 둔다면, 뭔가를 간과하거나, 실제로는 확인하지 않은 것을 확인했다고 착각할 가능성이 줄어들 것이다.
최후의 수단
이런 조언들을 다 적용해 봐도 해결이 되지 않을 때는 어떻게 할까? 바야흐로 좋은 디버거를 써서 프로그램을 한 단계씩 살펴볼 때다.
...중략...
디버거가 생각하는 방법을 바꿔 줄 수 있다.
그 동안 디버깅 순서가 최후의 수단
, 실마리가 없는 어려운 버그
, 실마리가 있는 버그
였다. 이제부터는 조급해 하지 말고 디버깅 순서를 거꾸로 해봐야 겠다.
다른 사람의 버그
다른 사람의 코드를 디버깅할 때도 자기 코드를 디버깅할 때 쓰라고 말한 모든 방법이 다 그대로 적용된다. 하지만 시작하기 전에 먼저, 프로그램이 어떻게 구성되어 있고 제작자가 어떻게 생각하고 작성했는지 어느 정도 파악해야만 한다.
타인의 프로그램에서 버그를 찾았다고 생각한다면, 첫 번째로 해야 할 일은 그게 진짜 버그라는 사실을 확실히 검정하는 것이다.
기존에 알려지지 않은 버그인지 확인하라.
마지막으로, 여러분의 버그 리포트를 읽는 사람 입장이 되어 보라. 제작자에게 가능한 좋은 테스트 케이스를 알려 줘야 한다.
내가 만들지 않는 코드를 볼 때 짜증이 날 때가 많다. 마음이 안 좋으면 볼 수 있던 것도 안 보인는 거 같다.
자기 자신을 알고, 자신이 만드는 에러를 알라. 일단 버그를 하나 찾고 고쳤다면, 비슷한 다른 버그도 다 확실히 제거해야 한다. 왜, 어떻게 버그가 발생했는지 고민해서 다음에는 비슷한 실수를 반복하지 않도록 한다.
6장. 테스트
손이나 계산기를 이용하는 간단한 겟나 작업은 보통 게산 과정을 점검하다가 에러를 발견하며 그 에러가 나온 위치부터 뒤짚어 가는 과정을 통해 문제범위를 좁혀나간다.
- 노버트 위너(Norbert Winener), 사이버네틱스(Cybermetics)
디버깅 챕터도 그렇지만 테스트 챕터로 실용적인 내용이 많다.
디버깅은 프로그램이 망가졌다는 것을 알았을 때 하는 일이다. 그리고 테스트는 제대로 돌아간다고 생각되는 프로그램을 의도적이고 체계적으로 망가뜨려 보는 시도다.
애처 다익스트라가 관찰했듯이, 테스트는 버그가 존재함을 보여줄 수는 있으나 부재함을 보여줄 수는 없다.
테스트 의미에 대해 이런 공감대를 형성해야 한다고 생각한다.
코딩하면서 테스트하기
- 경계에서 테스트하라
- 빈 입력, 한 개의 입력, 꽉 찬 배열 등 데이터와 프로그램에 있는 태생적인 경계를 점검
- 사전.사후 조건을 테스트하라
- 단정문을 사용하라
- 단정문이 실패로 끝나면 바로 프로그램이 중단되기 때문에, 보통은 정말 예상치 못한 문제가 생겨 회복시킬 방법이 없는 경우가 아니라면 자제해야 한다.
- 방어적으로 프로그래밍하라
- 리턴값을 검사하라
개발하면서 하는 테스트는 최소한의 노력으로 후한 결과를 가져다 준다. 프로그램을 작성하면서 동시에 테스트를 생각하면 더 나은 코드가 나온다. 바로 그때가 그 코드에서 어떤 일을 해야 하는지 가장 잘 알 때이기 때문이다.
개발하면서 테스트를 해야하는 이유.
체계적인 테스트
- 점층적으로 테스트하라
- 프로그램 일부를 작성하고, 테스트하고, 몇몇 코드를 더 추가하고, 테스트하고 ... 하는 식이어야 한다.
- 만약 두 패키지를 각각 따로 작성하고 테스트했다면, 마지막에 둘을 연결할 때 같이 잘 돌아가는지도 테스트한다.
- 단순한 부분을 먼저 테스트하라
- 어떤 결과가 나와야 하는지 알라
- 모든 테스트에서 정답이 무엇인지 아는 것은 필수적이다.
- 보존 속성을 검증하라
- 많은 프로그램은 입력의 특정 속성들을 보존한다.
wc
,sum
과 같은 툴은 결과가 같은 크기, 같은 단어 수, 같은 바이트를 특정 순서대로 갖고 있는지 등을 검증한다.
- 많은 프로그램은 입력의 특정 속성들을 보존한다.
- 독립적인 구현 버전을 비교하라
- 테스트 범위를 측정하라
- 테스트의 목표 중 하나는 테스트하는 동안 프로그램의 모든 명령문 하나 하나를 전부 실행하는 것이다.
테스트 자동화
- 회귀 테스트를 자동화하라
자동화의 가장 기본적인 형태는
회귀 테스트(regression testing)
로, 새 버전과 예전 버전을 비교하는 일련의 테스트를 수행하는 것이다.
...중략...
회귀 테스트의 목저은 대상의 동작 방식이 예상 외의 방식으로 변하지 않았다는 것을 확인하는 것이다.
- 자급자족형 테스트를 창조하라
- 필요한 입력과 예상되는 출력까지 포함한 자급자족형 테스트는 회귀 테스트를 보완해 준다.
절대 테스트를 없애 버리지 마라. 버그 리포트가 아직 유효한지 확인하거나, 이미 고친 문제를 기록하는 용도로 그 테스트를 쓸 수도 있다. 버그, 변경, 수정사항의 기록을 계속 유지하라.
테스트 작업발판
컴포넌트를 독립적으로 테스트하기 위해서는 보통 테스트가 돌아갈 때 시스템의 다른 부분에 대한 인터페이스를 제공하고 그 외 지원 역할을 하는 프레임워크나 작업발판(scaffold)을 필수로 만들어야 한다.
좋은 테스트 케이스는 대개 다양한 프로그램에서 함께 쓰이곤 한다.
...중략...
이런 유용한 테스트 파일들을 묶어 보관해 뒀다가, 다른 프로그램을 테스트할 때 또 테스트 프로그램을 만들 필요 없이 꺼내서 사용하는 것도 좋은 방법이다.
테스트 팁
- 점검 코드가 일반적인 입력 크기에 비해 배열 크기가 너무 큰 경우엔 아예 실행되지 않을지도 모른다. 이 부분을 테스트하려면 임시로 배열 크기를 아주 작게 만들면 된다.
- 상수 하나를 리턴하는 해시 함수를 만들어서 모든 원소를 같은 해시 버킷에 넣는다.
- 코드를 출하하기 전에 성능에 영향을 끼칠 수 있는 테스트성 제약들을 모두 비활성화한다.
- 배열과 변수들을 보통의 기본값인 0이 아니라 더 눈에 띄는 값으로 초기화한다.
-
0xDEADBEEF
같은 상수라면 디버거에서 인식하기 쉽다.
-
누가 테스트를 하는가?
테스트를 하는 이유는 버그를 찾기 위한 것이지, 프로그램이 잘 돌아간다고 주장하기 위해서가 아니다.
테스트에서 무엇보다 중요한 규칙은 테스트를 하는 것이다.
그래 맞다. 테스트를 꼭 하자.
7장. 성능
최적화의 첫 번째 법칙은
하지 않는다
이다.
시간 측정과 프로파일링
- 시간 측정을 자동화하라
- 프로파일러를 사용하라
- 프로파일러는 어떤 프로그램이 어디에서 시간을 소비하는지 측정해 준다.
- 과열지역에 집중하라
- 그림을 그려 보라
- 그림은 특히 성능이 어느 정도인지 표현하기에 좋다.
속도를 위한 전략
- 더 나은 알고리즘이나 데이터 구조를 사용하라
- 프로그램을 빠르게 만드는 데 있어 가장 중요한 요소는 알고리즘과 데이터 구조의 선택이다.
- 컴파일러의 최적화 기능을 켜라
- 한 가지 알아둬야 할 점은 컴파일러가 더 공격적으로 최적화 할수록, 컴파일된 프로그램에 버그를 초래할 가능성이 더 높아진다는 것이다.
- 코드를 미세조정하라
- 중요하지 않은 것은 최적화하지 말라
코드 미세조정
- 공통된 부분 표현식을 하나로 모으라
- 비싼 연산을 싼 연산으로 대체하라
- 루프를 펼치거나 제거하라
- 빈번히 사용되는 값을 캐싱하라
- 특수한 메모리 할당 함수를 작성하라
- 입력과 출력을 버터링하라
- 특수한 경우를 따로 처리하라
- 결과를 사전계산하라
- 근사값을 사용하라
- 저수준 언어로 재작성하라
- 이것은 가벼운 마음으로 해 볼만한 단계가 아니라, 최후의 선택에 가깝다.
공간 효율성
공간 최적화에 접근하는 제1방침은 속도 개선에 대한 것과 똑같다.
신경쓰지 않는다.
책에서 말한대로 근시안적으로 최적화를 했다가 더 큰 문제를 일으킬수가 있다.
- 최소 데이터 타입을 사용해서 공간을 절약하라
- 쉽게 재게산할 수 있는 값을 저장하지 말라
8장. 호환성
다양한 표준과 인터페이스, 그 프로그램이 돌아가야 할 환경의 여러 교집합에서 다 잘 돌아가는 소프트웨어를 작성하기 위해 노력하라. 모든 호환성 문제를 특수 코드를 추가하는 방법으로 해결하지 말고, 대신 그 소프트웨어가 새로운 제약 하에서 작동할 수 있게 전체적으로 조정해야 한다.
8장은 내가 회사에서 경험했던 안 좋은 경험들이 하지 말라는 예제로 등장한다. 읽으면서 공감한 내용이 너무 많다.
- 표준을 유지하라
- 호환 가능한 코드를 작성하는 첫 단계는 당연히 프로그램을 고차원 언어로 작성하고 만약 그 언어에 표준이 있다면 그 표준을 유지하는 것이다.
- 주류를 따라 프로그래밍하라
- 주류가 뭔지 아는 것은 어려운 일이지만, 주류에서 벗어난 구조를 인식하는 것은 수운 일이다.
- 언어의 골칫거리 부분을 인식하라
- 여러 컴파일러로 시도하라
#if
,#ifdef
로 구조화된 헤더 파일은 난해하고 유지보수하기도 어렵다는 사시을 보여준다. 각 컴파일러와 환경마다 다른 헤더 파일을 쓰는 편이 더 쉬울 것이다.
맞다 차라리 전처리기가 없었으면 좋겠다라는 생각을 한 적이 많다.
우리가 추천하는 접근방법은 교집합식이다. 모든 대상 시스템에 존재하는 기능만 사용하고 어디서나 쓸 수 있는 기능이 아니라면 사용하지 않는 방법이다.
#ifdef
와 비슷한 전처리 지시문으로 수행하는 조건 컴파일은 정보가 소스크드 전체에 흩어지는 경향이 강하기 때문에 관리하기가 어렵다.
이건 관리 자체가 안되고 이 방식을 없애기도 너무 힘들다.
#ifdef
는 나중에 이 부분이 활성화 되었을 때 컴파일을 중단시킬만한 문법 에러를 숨겨버릴수도 있다.
프로그램을 새 환경에 맞게 고쳐야 할 대 전체 프로그램의 복사본을 가지고 시작하지 말아야 한다.
...중략...
하나의 프로그램에는 가능한 하나의 소스만 존재해야 한다.
복사본으로 하자는 데에는 정치적이고, 조직의 편협한 이익에 따르는 경우가 많다.
하지만 가장 악질적은 문제는 아직 언급하지 않았다. 즉, 조건 컴파일은 테스트하기가 불가능한 거나 마찬가지다.
구분
물론 모든 시스템에서 변경 없이 컴파일되는 하나의 소스를 갖고야 싶지만 이건 비현실적일 가능성이 높다. 그래도 호환 불가능한 코드를 프로그램에 뿌려두는 것은 잘못이다. 이것은 조건 컴파일에서 생기는 문제 중 하나이기도 하다.
- 시스템 의존성을 별도 파일에 지역화해 담아라
- 시스템 의존성을 인터페이스 뒤에 숨겨라
데이터 교환
- 데이터 교환에는 텍스트를 사용하라
텍스트 데이터는 어떤 시스템에서 다른 시스템으로 손쉽게 옮겨지기 때문에, 시스템 간에 임의의 정보를 교환하는 가장 간단하고도 호환성 있는 방법이다.
- 데이터를 교환할 때 고정된 바이트 순서를 사용하라
호환성과 업그레이드
- 명세를 바꾼다면 이름도 바꾸라
- 기존 프로그램 및 데이터와 호환성을 유지하라
국제화
- ASCII라고 전제하지 말라
- 유니코드 문자집한은 전세계 모든 언어를 아우르는 하나의 인코딩 방식으로 이런 상황을 바꿔보려는 하나의 시도다.
- 영어라고 전제하지 말라
9장. 표기법
아마도 인간의 몯느 창조물 중에 으뜸으로 놀라운 것은 언어로다.
- 자일스 리턴 스트레이치(Giles Lytton Strachey), Words and Poetry
처음엔 표기법을 코드 스타일로 착각했는데 데이터를 형식화하는 방법을 찾거나 없으면 만드는 과정을 설명한다. 정규표현식을 구현하는 내용은 인상깊다.
컴퓨터에 하고 싶은 말("내 문제를 해결해.")과 일을 시키기 위해 해야 하는 말 사이에는 항상 간극이 존재한다. 이 간극은 좁힐수록 좋다. 좋은 표기법은 우리가 하고 싶은 말을 더 쉽게 하게 만들고, 실수로 잘못된 말을 하기 어렵게 만든다.
작은 언어는 좁은 영역을 위한 특수 표기법이다. 좋은 인터페이스를 제공할 뿐 아니라 그 인터페이스를 구현하는 프로그램을 잘 체계화하는 일까지 도와준다. 다음과 같은 printf함수의 제어 문자열이 좋은 예다.