1. 가상 컴파일
-
코드에 숨어 있는 버그를 발견하는 최상의 방법은 가능한 빠르고 쉽게 그것을 발견하는 것이다. 자동으로, 최소한의 노력으로 버그를 발견하는 방법을 찾아보자.


-
버그를 발견하기 위해 필요한 프로그래머의 기술을 줄이도록 노력하자. 컴파일러 경고 옵션이나 lint 경고는 버그를 발견하는데 프로그래머의 기술을 요구하지 않는다.




2. 주관을 갖자
-
프로그램을 판매용과 디버깅용의 두 가지 버전으로 관리한다. 판매용 버전은 군더더기 코드를 빼고 날씬하게 유지한다. 그러나 버그를 빨리 발견하기 위해 가능한 한 디버깅 버전을 사용한다.


- assertion
은 디버깅 검사를 작성하는 속성의 방법이다. 결코 발생 하지 않을 규정에 어긋나는 조건을 발견하기 위해 이를 사용한다. 이들 조건과 에러 조건을 혼돈하지 않도록 주의한다. 이것은 최종적인 프로그램에서 처리해야 한다.


-
함수의 인자(argument)의 유효성을 검사하고, 프로그래머들이 정의되지 않은 어떤 작업을 하면 경고하기 위해 assertion
을 사용한다. 여러분의 함수들을 보다 엄격하게 정의할수록 인자의 유효성을 검사 하는 것이 보다 쉬워진다.


- 일단 함수를 작성하면 그것을 재검토하고 스스로에게 나는 무엇을 가정하는가?”라고 묻는다. 가정한 것이 있다는 것을 발견하면 여러분의 가정이 항상 유효한지 assert 하거나, 또는 가정을 제거하기 위해 코드를 재 작성한다. 또한 이 코드에서 가장 잘못될 만한 것은 무엇인가? 그리고 그 문제를 어떻게 하면 자동으로 발견할 수 있는가?” 묻는다. 가능한 한 가장 빠른 시간에 버그를 발견하는 검사를 구현하도록 노력한다.


- 교과서는 프로그래머에게 방어적으로 프로그램을 작성하도록 권고한다. 그러나 이러한 코딩 방식은 버그를 감춘다는 점을 기억해야 한다. 방어적인 코드를 작성할 때는 발생할 수 없는 경우가 발생할 경우 경고해 줄 수 있는 assertion을 사용한다.



3
. 서브시스템(Subsystem)을 굳건하게!
-
여러분이 작성한 서브시스템을 살펴보고 혹시 다른 프로그래머들이 잘못 사용할 여지가 없는가 확인하라. 그리고 찾기 어려우면서도 자주 발생되는 버그를 잡기 위하여 프로그램에 assert와 점검 루틴을 추가하라.


-
버그를 자주 볼 수 없다면 잡을 수도 없다. 불규칙하게 일어나는 동작이 있는가 살펴보고 만일 있다면 프로그램의 디버그 버전에서는 그것을 제거하라. 그 일례로 초기화되지 않은 메모리를 쓸모 없지만 알고 있는 값으로 채우는 것을 들 수 있다. 이렇게 하면 초기화되지 않은 메모리를 사용하는 버그 있는 코드를 실행할 때마다 항상 똑 같은 결과를 얻을 수 있다.


- 만일 여러분의 서브시스템이 메모리를 해제(release)하고 그 내용을 쓸모 없게 만든다면 그 내용이 쓸모 없다는 것을 쉽게 알 수 있도록 뒤섞어 버려라. 그렇지 않으면 여러분도 모르는 사이에 다른 함수가 그 데이터를 사용할 수도 있기 때문이다.


-
여러분의 서브시스템에 일어날 수도 있는 동작이 있다면 디버그 코드를 추가하여 반드시 일어나도록 만들어라. 평소에는 잘 일어나지 않는 동작을 자주 일어나도록 하면 여러분이 버그를 잡을 수 있는 기회가 늘어나는 셈이다.


-
테스트 루틴을 작성할 때는 그 내용을 잘 모르는 프로그래머에게도 제대로 실행될 수 있도록 작성하라. 가장 훌륭한 테스트 루틴은 그것이 있는지 조차 모르게 해 주는 것이다.


-
가능하다면 테스트 루틴을 프로그램에 추가하는 식이 아니라 처음 코딩 시에 함께 작성하도록 하라. 코드를 다 작성한 뒤에 작성된 코드에 맞게 테스트할 방법을 찾는 식은 바람직하지 않다. 프로그램을 처음 설계 할 때 다음과 같이 스스로에게 질문을 해보자. ‘이 코드를 완벽하게 테스트할 수 있는 방법이 무엇일까?’ 만일 그 코드를 테스트 하기가 매우 힘들거나 거의 불가능하다고 생각된다면 설계 자체를 바꾸도록 하라. 심지어는 코드가 커지고 속도가 떨어지더라도 테스트를 할 수 있도록 하는 것이 훨씬 중요하다.


- 테스트 루틴 때문에 프로그램이 너무 느려지고 메모리를 많이 사용하더라도 가능한 한 테스트 루틴을 제거하지 말라. 테스트 루틴은 시판용 프로그램에는 포함되지 않는다는 것을 잊지 말아야 한다. 테스트 루틴 때문에 프로그램이 너무 커지고 느려질 것 같다는 생각이 든다면 한번 이렇게 생각해 보라. ‘테스트는 계속하면서도 조금 작고 빠르게 할 수 있는 방법은 없을까?’



4
. 코드를 추적하라
-
버그는 저절로 생기지 않는다. 버그는 프로그래머가 코드를 새로 작성하거나 기존의 코드를 고친 결과인 것이다. 버그를 찾는 가장 좋은 방법은 그 코드를 컴파일한 직후에 코드를 한줄 한줄 추적하면서 확인하는 것이다.


-
여러분이 코드를 추적 확인하는 것은 너무 시간이 많이 든다라고 부정적인 생각을 갖는다면 그것은 잘못된 생각이다. 물론 이 같은 작업이 습관으로 굳어질 때까지는 시간이 좀더 걸릴 것이다. 일단 습관이 된다면 코드를 추적하지 않고는 견디지 못할 것이다.


-
코드의 모든 진행 경로를 최소한 한번 이상 추적했는가 확인하라. 특히 에러 대비용 코드를 주의하라. ‘&&’, ‘||’, ‘?: 연산자는 두 개의 경로는 만든다는 것을 절대로 잊지 말라.


-
경우에 따라서는 어셈블리어 수준에서 코드를 추적해야 할 필요도 있다. 이 같은 경우가 자주 있는 것은 아니지만 필요할 때는 절대로 꺼려 하지 말자.


5. 자동 판매기 인터페이스
-
함수의 인터페이스를 사용하기 편리하고 이해하기 쉽게 작성하라. 입력과 출력은 모두 단 한 가지 데이터 형(type)을 사용해야 한다. 입력값출력값에 특별한 목적이 있는 값과 에러값을 섞어서 사용한다면 인터페이스에 혼란만 일으킬 뿐이다.


-
함수의 인터페이스를 설계할 때는 그 함수를 사용하는 프로그래머들이 중요한 사항들(예를 들어 에러에 대한 대책)을 빠짐 없이 생각하도록 하는 형식을 사용하라. 세부적 사항이라도 빠뜨리거나 무시하기 쉬운 형식은 사용하지 말라.


-
다른 프로그래머가 여러분이 작성한 함수를 어떻게 사용해야 할까를 생각해 보라. 혹시 프로그래머들이 자기도 모르는 사이에 버그를 일으킬 가능성이 함수의 인터페이스에 숨어 있지는 않은가 확인하기 바란다. 하지만 무엇보다도 중요한 것은 다음 사항이다. ‘항상 작동에 성공하는 함수를 만들어 프로그래머들이 에러 상황을 대비하지 않아도 되게 하라.’


- 호출 하는 입장에서 보아서 이해하기 쉽도록 함수를 작성하면 버그가 일어날 가능성을 줄일 수 있다. ‘마술 숫자라고 불리는 변수는 이 같은 우리의 의도에 저해 요소로 작용한다.


- 다 기능 함수는 여러 개의 단 기능 함수로 나누어라. 이렇게 하면 함수의 이름을 통하여 그 기능을 쉽게 알 수 있을 뿐만 아니라(예를 들어 realloc 보다 ShrinkMemory가 알기 쉽다) 잘못된 변수 값을 자동적으로 발견할 수 있도록 assert 사용할 수 있게 된다.


-
함수에 자세한 주석문을 삽입하여 다른 프로그래머들에게 여러분이 작성한 함수의 올바른 사용법을 소개하라. 그와 더불어 위험한 부분도 함께 경고하라.


6
. 위험한 사업
-
데이터 형(type)을 주의하여 선택하라. 비록 안시(ANSI) 기준이 char, int, long 등 여러 가지 데이터의 형을 정해 놓았지만 명확하게 정의 되어 있는 것은 아니다. 따라서 안시 기준이 정한 기준에만 의존하다가는 버그를 일으킬 수도 있다.


-
여러분의 알고리즘에는 이상이 없지만 프로그램이 실행될 기종에 적합하지 않아서 버그가 일어날 경우도 있다. 특히 여러분의 코드 안에 포함되어 있는 계산과 테스트가 오버플로우나 언더플로우를 일으키지는 않는지 반드시 확인하기 바란다.


-
설계를 정확하게 구현하라. 구현 과정에서 설계와의 차이점이 생기면 아주 찾기 힘든 버그를 일으킬 가능성이 매우 높다.


-
모든 함수는 정확하게 정의된 단 한 가지 기능만을 담당해야 한다. 반드시 한 가지 이상의 기능을 가져야 한다면 최소한 그 기능이 발휘되는 방법이라도 한 가지어야 한다. 입력에 상관없이 항상 같은 코드가 실행되도록 함수를 작성하면 버그가 숨어 있는 경우를 줄일 수 있다.


- if 문은 여러분이 불필요한 일을 하고 있다는 좋은 경고가 된다. 여러분의 코드에서 불필요한 if 문을 제거하기 위하여 자기 자신에게 이런 질문을 해 보자. ‘함수 설계를 어떻게 바꾸면, 특별한 상황을 배제할 수 있을까?’ 때로는 데이터 구조를 변경해야 할 수도 있고 데이터 구조를 바라보는 관점을 바꾸어야 할 때도 있을 수 있다. 단 이것은 잊지 말라안경은 잘 보이기만 하면 되지 블록인가 오목 렌즈인가는 중요하지 않다.’


- if
문은 while 또는 for 루프의 제어문으로 변장하기도 한다. 그리고?: 연산자는 if 문의 또 다른 표현 형태라는 것도 잊지 말라.


- 프로그램 언어의 위험한 용법을 주의하라. 하는 일은 같으면서도 보다 안전한 용법을 찾아서 사용하라. 특히 속도가 향상될 것처럼 여러분을 현혹하는 코딩의 묘수들을 주의하라. 그것들은 전체적인 효율을 크게 향상시키지도 못하면서도 여러분을 위험한 지경으로 몰고 갈 수도 있기 때문이다.


- 표현식을 작성할 때는 서로 다른 종류의 연산자를 섞어서 사용하지 말라. 만일 꼭 섞어서 사용해야 할 경우에는 괄호를 사용하여 연산 순서를 확실하게 지정해 놓아라.


-
특별한 경우 중에서도 가장 특별한 경우는 에러 대비 이다. 실패 할 가능성이 있는 함수는 가능한 한 사용하지 말라. 하지만 이 같은 함수를 사용해야만 할 경우에는 에러 대비용 코드를 서브루틴 등으로 따로 분리하여 놓아라. 이렇게 하면 에러 대비용 코드에서 혹시 있을지도 모르는 버그를 찾기가 훨씬 수월해 진다.


-
경우에 따라서는 함수가 실패할 상황을 미연에 제거하는 방법으로 일반적인 에러를 없앨 수 있다. 즉 초기화 과정에서만 한번 에러를 만날 수도 있고 아니면 함수의 설계 자체를 변경할 수도 있다는 뜻이다.


7
. 꾀부리는 프로그래머
-
자신의 것이 아닌 데이터를 사용하는 작업을 할 경우에는 그 데이터에 쓰기를 해서는 안된다. 아무리 데이터를 읽어 오는 것은 안전하다고 하더라도 메모리 입출력에서 데이터를 읽는 것은 하드웨어에 영향을 줄 위험이 있으므로 삼가라.


-
일단 소유권을 해제(release)한 메모리를 다시 참조(reference)하는 것은 절대 삼가하여야 한다. 자유 메모리(free memory)를 참조하는 것은 버그를 일으킬 확률이 매우 높기 때문이다.


-
효율을 향상시킬 목적으로 데이터를 전역 버퍼 또는 정적 버퍼로 보내는 것은 버그가 도사리고 있는 위험한 지름길과 같다. 호출자에게만 필요한 데이터를 만들어 내는 함수를 작성 할 때는 호출자에게 그 데이터를 리턴하여야 한다. 그렇지 못할 경우에는 데이터가 변경되지 않도록 조치를 취해 놓아야 한다.


-
특별한 명령어나 함수에 의존하는 함수를 작성하지 말라. 본문에서 보았던 FILL 루틴은 CMOVE 정해진 방법대로 호출하지 않으면 아무 일도 하지 못한다. 이 루틴은 잘못된 프로그래밍의 전형적인 예인 것이다.


- 프로그래밍을 할 때는 사용하는 언어의 여러 가지 기능을 원래의 목적에 맞게 사용하여야 한다. 제대로 실행되는 프로그램이라 할지라도 이상한 점이 있는 프로그램을 작성하지 말라. 그리고 기억할 것은 프로그래밍 언어의 기준은 언제라도 바뀔 수 있다는 것이다.


- C 코드가 짧을수록 기계어 코드도 짧아질 것 같지만 항상 이 법칙이 성립하는 것은 아니다. 몇 줄로 깔끔하게 정리된 C 코드를 무리하게 한 줄로 줄이려고 하지 말라. 그러기 전에 기계어 코드로 짧아지는가를 먼저 확인하기 바란다. 알아보기 어려울 만큼 어지럽게 코드를 줄여서 작성하더라도 그 부분이 효율이 크게 향상되지는 않으며 부분적으로 효율이 개선된 것도 전체적 속도에는 거의 영향을 주지 못한다.


-
마지막으로 법률가가 계약서를 쓰듯이 코드를 어렵게 쓰지 말라. 대다수의 프로그래머들이 여러분이 작성한 코드를 읽고 이해하지 못한다면 그것은 필요 없이 어렵게 작성된 것이다. 단순하게 고쳐 써라.


8
. 남은 것은 자세뿐
-
버그가 저절로 생기는 법이 없듯이 버그는 스스로 없어지지도 않는다. 버그가 발견되었다는 보고는 받았지만 다시 나타나지 않는다고 해서 시험자가 잘못 보았다고 생각해서는 안된다. 그 프로그램의 이전 버전을 뒤져서라도 반드시 버그를 찾아내야 한다.


-
버그를 나중에 수정하지 말라. 발표가 예정되었던 주요 제품들이 취소되는 경우가 가끔 있는데 가장 큰 이유는 저절로 사라졌다가 나중에 나타난 버그가 많기 때문이다. 만일 버그를 발견하자마자 수정했다면 프로젝트가 이 같은 비극으로 끝나지는 않았을 것이다. 항상 버그가 거의 없는 상태를 유지한다면 사라지는 버그도 당연히
있을 수 없을 것이다.


-
어떤 버그를 추적할 때는 혹시 이 버그가 또 다른 커다란 버그의 단편적 증상이 아닌가 생각해 보자. 물론 증상을 치료하기가 훨씬 쉽다. 하지만 반드시 버그의 진정한 원인을 찾아서 제거하도록 노력해야 한다.


-
필요 없이 코드를 추가하거나 고치지 말라. 여러분의 경쟁자가 대단히 뛰어나지만 쓸모는 전혀 없는 기능을 추가하려 한다면 결국 그는 버그를 잡다가 납기일을 넘기고 말 것이다. 왜냐 하면 공짜 기능은 없기 때문이다. 필요 없는 코드에서 발생된 버그와 씨름하면서 시간을 낭비하도록 그를 그냥 놔두어라. 그것이 여러분이 경쟁에서 이기는 길이다.


- ‘융통성이 크다는 것이 사용하기 쉽다는 뜻은 아니다. 함수나 기능을 설계할 때는 사용상의 용이성에 초점을 맞추어라. 함수나 기능이 융통성만 크다면( realloc. 함수와 마이크로소프트 엑셀의 칼라 표시 형식처럼) 아무리 애를 써도 그것들을 사용하기 쉽게 만들기는 어려울 것이다. 그 대신 버그를 찾기만 더욱 어렵게 만들 것이다.


-
원하는 결과를 얻기 위하여 이것저것 해 보는 태도는 버려야 한다. 닥치는 대로 시도하면서 낭비하는 시간을 올바른 방법을 찾는 데에 사용하라. 필요하다면 여러분이 사용하고 있는 운영 체제를 제작한 회사의 담당자 또는 개발 책임자와 상의할 수도 있다. 따분하더라도 정확한 해결책을 구하는 것이 우연히 원하는 결과를 내는 즉 나중에는 어떻게 될는지도 모르는 방법을 사용하는 것보다 훨씬 안전하다.


- 코드를 작성할 때는 완벽하게 테스트 할 수 있는 최소한의 단위로 작성하고 절대로 테스트를 뒤로 미루지 마라. 반드시 기억할 것은 자기가 코드를 테스트하지 않는다면 그 코드는 아무도 테스트하지 않을 수도 있다는 것이다. 그리고 또 한 가지 테스트 그룹이 여러분을 위하여 코드를 테스트해 줄 것이라고 기대하지 마라.

by 흥배 2009. 3. 21. 13:14