이 글은 Max Kanat-Alexander의 Fundamental Philosophy of Debugging을 번역한 글입니다.

종종 사람들은 디버깅에 큰 어려움을 겪는다. 그리고 이런 사람들은 시스템을 디버깅을 하려면 시스템을 관찰하기보다 문제의 원인에 대해 생각해봐야 된다고 믿는다.

이게 무슨 뜻인지 예를 들어 보자. 당신이 웹 서버를 운영하는데 이 서버가 5%의 유저에게 조용히 실패한다고(silently fails) 가정하자. 당신이라면 "왜 그럴까?"라는 질문에 어떻게 대답하겠는가?

곧바로 정답을 내려고 하는가? 아니면 원인에 대해 추측하는가? 만약 그렇다면 당신은 잘못된 방식을 택한 것이다.

이 문제에 대한 답은 "잘 모르겠습니다"이다.

이것이 성공적인 디버깅을 위한 첫 걸음이다:

디버깅을 시작할 때는, 아직 "정답"을 모른다는 사실을 기억하자.

물론 이미 정답을 아는 것처럼 행동하고 싶을 것이다. 또 가끔은 당신의 추측이 정답일 수도 있다. 이런 일이 자주 일어나는 것은 아니다. 하지만 사람들이 문제에 대해 추측하는 것이 좋은 방법이라고 믿어버릴 정도의 가능성은 있다. 물론 대부분의 경우 몇 시간, 며칠, 몇 주를 추측해봐도 코드만 복잡해질뿐 문제는 고쳐지지 않는다. 사실 어떤 코드는 "버그"에 대한 "추측"과 이에 대한 "솔루션"으로 가득하다. 그리고 이러한 "솔루션"은 코드의 복잡도를 크게 증가시키는 주범이다.

이쯤에서 곁다리로 재밌는 원칙을 하나 소개할까한다. 일반적으로 버그가 "제대로" 고쳐졌다면 그 시스템은 더 단순해지고 더 나은 디자인을 가지게 된다는 원칙이다. 이에 대해서는 뒤에서 좀 더 다루겠지만 지금은 이러한 이점이 있다는 사실만 짚어두자. 대부분의 경우 버그에 대한 최고의 해결책은 코드의 양을 줄이거나 시스템의 복잡도를 낮춘다.

다시 디버깅 과정에 대한 얘기로 돌아가보자. 당신은 이제 무엇을 해야될까? 단순한 추측 혹은 문제의 원인에 대해 생각하는 것은 순전한 시간 낭비이다. 이 말은 당신이 문제를 접할 때 하는 행동이 대부분 시간 낭비라는 말이다. 그 대신 당신이 해야될 일은:

  1. 제대로 동작하는 시스템이 어떠한지 생각한다.
  2. 좀 더 많은 데이터를 얻기 위해 노력한다.

우리는 이 두 가지 원칙으로부터 디버깅의 핵심 원칙을 도출해낼 수 있다.

디버깅을 하려면 문제의 원인을 정확히 이해할 때까지 정보를 수집해야 한다.

그리고 데이터를 수집하려면 항상 무언인가를 관찰해야된다. 그 대상이 웹서버라면 페이지가 아닌 로그를 봐야할 것이고 혹은 문제가 발생할 때 어떤 현상이 일어나는지를 보기 위해 문제를 재현(reproduce)해볼 수도 있다. 그렇기 때문에 사람들은 종종 "재현 방법"에 대해 물어본다. 그렇게 함으로써 버그가 나타날 때 무슨 일이 벌어지는지 관찰할 수 있기 때문이다.

간혹 가장 우선적으로 얻어야할 데이터는 이 버그가 정확히 어떤 것인가 일것이다. 유저들은 종종 설명이 부족한 버그 리포트를 보내곤 한다. 예를 들어 유저가 "페이지를 불러올 때마다(when I load the page), 서버에서 아무 것도 돌려주지 않아요(doesn't return anything)"라는 식의 버그 리포트를 보냈다고 하자. 이 정보는 불충분하다. 어떤 페이지를 불러오려고 했는지 또 아무 것도 돌려주지 않는다는 것이 흰 페이지만 나타난다는 것인지 어떤 것인지 전혀 알 수가 없기 때문이다.

물론 유저가 어떤 의미로 이런 버그 리포트를 보냈는지 추측할 수는 있다. 하지만 많은 경우에는 정확하지 않은 가정을 내리게 될 것이다. 또 유저가 프로그래밍 지식이나 컴퓨터에 대한 이해가 부족할수록 상황을 설명하는데 어려움을 겪을 것이다. 이런 경우에는 긴박한 상황이 아니라면 당신이 제일 먼저 해야될 일은 버그 리포트를 조금 더 명확하게 보내달라고 요청하고, 더 나은 리포트가 올 때까지 기다리는 것이다. 나는 리포트가 더 명확해지기 전까지는 리포트를 쳐다보지 않는다. 만약 내가 문제를 완전히 이해하기 전에 해결부터 하려고 들면 시스템에서 문제와 아무 관련 없는 부분을 쳐다보느라 시간을 낭비하게 될 것이다. 나는 대신 유저가 더 나은 버그 리포트를 보내길 기다리면서 생산적인 일을 하는 편이 낫다고 생각한다. 그리고 충분히 명확한 버그 리포트가 도착하면 그 때 문제 해결을 시작하는 것이 더 바람직하다.

그러나 이 말이 유저가 불충분한 버그 리포트를 보냈을 때 무례하거나 불친절하게 대하라는 말은 아니다. 당신이 시스템에 대해 더 잘 알고 그들이 시스템에 대해 잘 모른다는 것이 당신을 유저를 깔볼 수 있는 우월한 존재로 만들어주는 것은 아니다. 이보다는 유저에게 친절하고 알기 쉬운 말로 더 정확한 정보를 얻어내려고 노력해라. 버그를 제보하는 사람들은 대부분의 경우 의도적으로 불충분한 정보를 보내지 않는다. 그들은 그저 시스템에 대해 잘 알지 못하는 것 뿐이다. 그리고 그들이 더 나은 정보를 제공하게 만드는 것도 당신이 해야하는 일의 일부이다. 만약 많은 수의 버그 리포트가 불충분한 정보를 담고 있다면 유저들이 정확한 정보를 전달할 수 있게끔 설문지 양식을 제공할 수도 있다. 핵심은 유저들이 정확한 정보를 전달하게끔 도움으로써 문제를 더 쉽게 해결할 수 있는 구조를 만드는 것이다.

버그의 원인이 명확해지고 나면 이제 시스템의 다양한 부분을 관찰할 차례이다. 어떤 부분을 관찰할지는 당신의 시스템에 대한 지식에 달려있다. 보통은 로그나 모니터링툴, 에러 메시지, 코어 덤프 혹은 시스템의 다른 출력 메시지가 될 것이다. 만약 이런 것들이 제대로 갖춰지지 않았다면 먼저 이러한 것들을 갖춘 새 버전을 배포하는 것이 나을 수 있다. 이러한 노력이 겨우 버그 하나 고치는 것에 비해 큰 것처럼 느껴질 수 있지만 실제로는 제대로된 정보 하나 없이 추측만으로 시스템의 이곳 저곳을 수정하면서 헤매는 것보다 더 빠른 해결책인 경우가 많다.

혹시 위에서 내가 제대로 동작하는 시스템은 어떤 가에 대해 생각해봐야 한다고 말했던 것을 기억하는가? 여기에서 또 다른 디버깅의 원칙을 도출해낼 수 있다.

디버깅을 하려면 제대로 동작하는 시스템에 대한 정보와 내가 현재 가진 정보를 비교해야 한다.

그러므로 로그를 관찰할 때 그것이 일반적인 메시지를 나타내는지 혹은 에러인지 정확히 알 필요가 있다. "경고: 유저 데이터가 존재하지 않습니다.(Warning: all the user data is missing.)"라는 로그는 언뜻 보기에는 에러같다. 그러나 실제로는 그저 웹 서버가 부팅할 때마다 생성되는 평범한 로그일 수도 있다. 이 경우에는 당신은 웹 서버가 부팅할 때마다 이런 로그를 생성한다는 사실을 먼저 이해해야 한다. 그리고 당신은 제대로 동작하는 시스템이라면 하지 않을 동작이나 출력을 찾아야한다. 또 그러한 메시지가 의미하는 바가 무엇인지 이해해야 한다. 이 경우에 메시지는 웹 서버가 당신이 사용하지 않는 유저 데이터를 가지고 있기 때문에 발생하는 것일 수 있다.

이 과정을 따라가다 보면 당신은 제대로 동작하는 시스템이 하지 않는 것을 찾아낼 것이다. 그럼에도 이것이 문제의 원인이라고 섣부르게 판단해서는 안 된다. 예를 들어 "에러: 곤충들이 쿠키를 먹고 있음"이라는 로그를 발견했을 때 이 버그를 해결하는 방법 중 하나는 단순히 로그를 지워버리는 것이다. 그렇게 해서 제대로 동작하는 시스템과 똑같이 동작하도록 할 수 있을까? 그렇지 않다. 버그는 사라지지 않았다. 물론 이런 예시는 멍청하게 들릴 수 있다. 하지만 많은 사람들이 이것보다는 조금 덜 멍청한 방식으로 문제를 땜질하곤 한다. 그들은 근본적인 원인을 해결하기 보다는 단순히 문제를 우회해버리는 단순한 방식을 통해 코드의 복잡도를 영원히 증가시켜버리고 만다. "이 부분을 고치면 버그가 고쳐지기 때문에 나는 버그의 진짜 원인을 발견했다"라는 말은 정확하지 않다. 더 정확한 말은 "이 부분을 고치면 이 문제가 다시는 발생하지 않을 것이라고 자신있게 말할 수 있기 때문에 나는 버그의 진짜 원인을 발견했다"가 옳은 말이 된다.

물론 이 말이 절대적인 것은 아니다. 버그에도 "얼만큼" 고쳐졌는가가 존재하기 때문이다. 버그는 완전히 고쳐질 수도 혹은 부분적으로 고쳐질 수도 있다. 이는 당신이 얼마나 "깊게" 파고드는지 또 얼마만큼의 시간을 할애하는지에 따라 달라진다. 일반적으로 당신이 문제에 대한 그럴 듯한 원인을 찾아냈다면 그 버그가 고쳐졌다고 선언할 수 있다. 내가 말하고 싶은 것은 버그의 원인을 찾지 않고 증상만 제거하는 경우를 경계해야된다는 것이다.

그리고 당연히 원인을 찾아냈다면 고쳐야 한다. 물론 이 단계는 이전 단계를 제대로 거쳐왔다면 크게 어렵지 않다.

이 경우 디버깅의 네 가지 단계는 아래와 같다:

  1. 제대로 동작하는 시스템에 대해 이해한다.
  2. (현재 알지 못하는) 문제의 원인이 무엇인지 파악한다.
  3. 문제의 원인을 제대로 파악할 때까지 정보를 수집한다.
  4. 증상을 제거하기보다는 문제의 원인을 해결한다.

꽤 단순한 이야기처럼 들릴지 모르겠다. 하지만 많은 사람들이 이 규칙을 지키지 않는다. 내 경험 상 대부분의 프로그래머는 버그를 발견하면 앉아서 원인에 대해 추측해보거나 동료들과 원인에 대해 이야기하는 편을 선호한다. 물론 대화의 상대방이 시스템에 대한 정보를 제공해줄 수 있거나 어떤 부분에서 데이터를 수집해야되는지에 대한 조언을 해줄 수 있다면 괜찮다. 하지만 대화를 통해 버그의 원인을 추측하는 것은 혼자 앉아서 추측해보는 것에 비해 나을 것이 없다. 대부분의 경우 이는 다른 사람의 시간까지 낭비하게 만들 것이다.

그러니 다른 사람들의 시간을 빼앗거나 코드에 필요 이상의 복잡도를 증가시키는 태도는 지양하도록 하자. 위에서 언급한 디버깅 방법은 "잘" 동작할 뿐만 아니라 어떤 상황, 어떤 시스템, 어떤 코드에든 적용 가능하다. 종종 데이터를 수집하는 과정은 힘들고, 그 버그가 재현하기 어려운 경우라면 더 힘들겠지만 최악의 상황에도 코드를 관찰하거나 제대로 동작하는 시스템에 대한 다이어그램을 그려보면서 정보를 수집할 수 있다. 물론 이런 방법은 최후의 수단으로서만 사용하길 바란다. 그럼에도 이런 방법이 무엇이 잘못되었을지 추측하거나 이미 문제의 원인을 알고 있는 것처럼 행동하는 것보다는 나을 것이다.

가끔은 버그의 원인을 이해할 때까지 정보를 얻고, 이런 방식으로 버그가 고쳐지는 과정이 마법처럼 느껴지기도 한다. 이 방식을 사용해보고 그 결과를 보다 보면 디버깅이 즐겁게 느껴질 수도 있다.