소프트웨어 출시를 지연시키는 가장 큰 원인 중의 하나는 버그다. 그 중에도 몇몇 복잡한 버그들은 개발자들을 지독하게 괴롭힌다. 심지어는 그런 버그 때문에 개발된 제품이 폐기되기도 한다. 또한 회사는 출시 지연이 심각한 단계에 놓이면 유능한 버그 사냥꾼을 따로 고용하기도 한다. 그런 복잡한 버그에 대항하는 초보 버그 파이터들이 저지르는 실수에 대해서 살펴보고 그런 버그들을 효과적으로 디버깅하는 방법에 대해서 알아보도록 하자.
대부분의 초보 버그 파이터들이 자주 범하는 실수는 버그가 발생한 지점에 집착하는 것이다. 그들은 끊임없이 ‘도대체 버그가 어디서 발생했을까?’란 질문을 던진다. 그와 동시에 그곳을 찾기 위해서 다양한 방법을 동원한다. 가장 애용하는 방법 중에 하나는 분할 정복법이다. 마치 바이너리 서치를 하는 것처럼 그들은 프로그램을 두 조각으로 나누고 어느 부분에서 버그가 발생하는지를 찾아나간다. 물론 좀 똑똑한 버그 파이터들은 크래시 덤프나 맵 파일등을 활용하기도 한다.
어쨌든 재현할 수 있는 버그라면 그들은 버그가 발생하는 바로 그 지점을 찾아내는데 성공한다. 하지만 문제는 지금부터 시작이다. 초보 버그 파이터가 찾아낸 바로 그곳은 십중팔구 다음과 갈은 어처구니 없는 라인일 것이기 때문이다.
*dst = *src;
물론 위와 같은 라인은 똑똑한 개발자에게는 몇 가지 암시를 던져주기는 한다. src가 읽을 수 없는 메모리 영역을 가리키고 있다거나, dst가 기록할 수 없는 메모리를 가리키고 있다면 이 코드는 분명히 실패한다는 것이다. 물론 그들은 저런 가정을 디버거의 추적 기능을 활용해서 확인할 수 있다. 그런 가정이 들어맞았다고 하더라도 그들은 속수무책이다. 왜냐하면 왜 그렇게 됐는지는 모르기 때문이다. SEH와 같은 방법들을 동원하더라도 그들은 예외 부분에 무엇을 넣어야 할지를 모른다. 만약 빈 예외처리 문장을 넣는다면 그건 단지 폭탄이 터지는 시점을 연장시킬 뿐이다. 그 폭탄은 언젠가 다른 곳에서 다른 형태로 다시 터질 것이기 때문이다.
때로는 정말 어처구니 없는 OutputDebugString(“some debug msg.”) 같은 부분이 문제가 되기도 한다. 그들은 이렇게 말한다. 이 디버그 메시지를 넣으면 잘 동작하는데 이걸 빼면 잘 동작하지를 않아요. 그러면서 원인도 모른 체 이 디버그 메시지를 릴리즈될 제품에 포함시킨다.
초보 버그 파이터들이 저지르는 이러한 실수는 버그에 대한 잘못된 이해에서 비롯된다. 복잡한 프로그램에 숨어있는 잡기 힘든 버그는 대부분 두 가지 특성을 가지고 있다. 첫째는 문제의 원인이 한 곳에 있지 않다는 것이고, 둘째는 버그를 관찰하는 행동 자체가 피드백이 되어서 버그에 영향을 미친다는 것이다. 이런 기준에서 보면 그들이 그토록 집착했던 버그가 발생한 지점은 중요하지 않은 요소에 불과하다는 것을 알 수 있다. 그 곳은 단지 폭탄이 터진 장소일 뿐, 실제로 폭탄은 다른 곳에서 다른 원인에 의해서 만들어졌기 때문이다. 중요한 것은 바로 그 폭탄이 생겨난 원인이고, 완전한 디버깅은 그런 원인 요소를 제거하는 것이기 때문이다.
프로그램은 데이터에 가공을 하는 절차들의 집합이다. 이렇게 가공을 하는 개별 절차들은 일정한 흐름을 만들어낸다. 순차적이기도 하고 중첩되기도 하고, 다른 절차를 기다리기도 하는 식으로 말이다. 대부분의 복잡한 버그는 이러한 흐름이 미묘하게 변경될 때 발생한다. 따라서 그런 복잡한 버그를 효과적으로 디버깅 하기 위해서는 프로그램의 흐름에 집중하는 것이 바람직하다. 흐름에 집중한다는 것은 머릿속으로 그런 과정을 재현해 보는 것을 의미한다. 정상적인 흐름을 다르게 만들어서 재생시켜 보면 아마도 버그의 증상과 일치하는 결과를 가져오는 흐름이 있을 것이다. 그 다음은 그 틀어진 흐름이 틀어지지 않도록 바로 잡아주면 된다. 이 과정이 숙달되면 마치 일류 정비공이 엔진 소리를 듣고 문제를 판단하는 것처럼 증상만 보고도 그 원인을 알 수 있게 될 것이다.