sungyup's.

becoming_a_better_programmer / you.write(code); / 1.10 버그 사냥하기

1.10버그 사냥하기

버그 예방법 및 디버깅 방법

TL;DR

경제적 우려

케임브리지 대학교 저지 경영 대학원의 1985년 연구 결과에 따르면, 프로그래머들이 소프트웨어를 디버깅하는 데는 연간 약 3,120억 달러에 달하는 비용이 소요된다고 한다. 지금은 아마 기하급수적으로 늘었을 것이다.

대비책

버그가 생긴 뒤에 고치기보다는 버그가 처음부터 생기지 않도록 적극적으로 예방하는 편이 낫다. 섬세한 설계, 코드 검토, pair programming 그리고 심사숙고한 테스트 전략은 가장 중요한 방법들이다. assert, 방어 프로그래밍, 코드 커버리지 도구와 같은 기법으로도 오류를 놓칠 가능성을 최소화할 수 있다.

버그를 피할 수 있는 가장 좋은 충고는 믿기 힘들 정도로 영리한 코드를 만들지 말라는 것이다. 브라이언 커니핸에 따르면, "디버깅은 코드 작성보다 두 배는 힘들다. 가능한 한도 내에서 최대한 영리한 코드를 작성하면, 디버깅하기 위해선 2배로 영리해야 한다." 또, 마틴 파울러도 이렇게 말했다:"미련한 프로그래머는 컴퓨터가 이해할 수 있는 코드를 만들고, 좋은 프로그래머는 사람이 이해할 수 있는 코드를 만든다."

버그 잡기

일반적으로 프로그램은 체계적으로 접근하기 그리 어렵지 않으나, 어떤 버그들은 오랜 시간을 잡아먹는다. 버그가 고치기 얼마나 어려운지를 나타내는 두 요소는 다음과 같다:

  1. 재현 가능성이 얼마나 되는지?(항상 재현되는지, 또는 재현하기 쉬운지)
  2. 버그의 원인인 코드가 포함된 시점과, 이를 발견한 시점 사이의 간격이 얼마나 되는지?

가장 중요한 것은 버그를 체계적으로 조사하고 특징을 잡아내는 것이다.

  1. 버그 재현 과정을 가능한한 단순하게 줄여라.(도움되지 않고 산만한 항목들은 걸러내라.)
  2. 단 하나의 문제에 집중해라. 서로 관련된 두 버그를 하나로 착각하고, 이를 깨닫지 못한다면 코드가 뒤죽박죽이 된다.
  3. 해당 문제를 얼마나 반복할 수 있는지 알아보라. 재현 과정을 거쳤을때 문제가 얼마나 자주 재현되는가? 간단한 행위에 의존하는가, 소프트웨어 설정이나 실행된 기계 타입에 따라 발생하는가?

버그를 재현하는 과정을 통합했다면, 버그를 추적하는 과정 대부분을 완성한 것이다. 유용한 디버깅 전략들을 알아보자.

함정 파기하기

시스템이 제대로 작동하는 시점과 시스템의 상태가 부적절한 시점을 파악해, 그 두 시점 사이의 코드 경로에 덫을 놓아라. 즉, 진단용 출력 코드를 추가해 코드의 상태를 살펴라.

다만, 쓸모없는 로그로 코드를 어지르지 마라.

이진 탐색 배우기

가능한 한 빨리 버그에 포커스를 맞추도록 이진 탐색 전략을 목표로 해라.

코드를 한 줄씩 따라가기보단 일련의 사건의 시작과 끝을 확인하라. 그런 다음 문제 공간을 두 개로 나누고, 가운데 시점에서 코드가 괜찮은지 확인하라. 이 정보를 기초로 문제 영역을 계속해서 절반으로 줄여나갈 수 있다. O(n)보다 O(log n)이 훨씬 효율적이다!

소프트웨어 고고학을 채택하라

소프트웨어 고고학이란, 버전 관리 시스템에서 과거 이력을 찾아보는 것이다. 버그가 생성되기 이전의 가장 최근 코드베이스 하나를 선정하고, 어떤 코드 변경 모음으로 파손이 발생했는지 찾는다. 그 뒤에 앞의 이진 탐색 전략을 사용한다.

테스트하고 테스트하고 테스트하라

소프트웨어를 개발했다면 단위 테스트를 작성하는데 시간을 투자하라. 이 테스트는 향후 변경에 대비한 훌륭한 조기 경보장치가 되어준다.

간단하고 재현 가능한 단위 테스트 케이스는 완전히 가동되는 프로그램보다 디버깅하기 훨씬 쉬운 발판 역할을 한다.

예리한 도구에 투자하라

DUMA 같은 메모리 확인 도구나 Valgrind같은 메모리 릭을 찾아주는 툴 등 많은 도구들을 활용한다. 물론 디버깅 시 가장 좋은 도구는 디버거로, 현재 실행 중인 프로그램을 정지시키고 간단한 지시를 통해 한 단계씩 앞으로 나아가게 하거나, 특정 함수에 들락거리도록 할 수 있다.

이때 주의해야 할 것은 코드를 한 단계씩 아무 생각 없이 따라간다면 미시적 단계에 사로잡혀 거시적이며 전반적인 코드의 형태에 대해 신경을 쓰지 못할 수도 있다는 것이다. 디버거 사용법을 잘 익히고, 필요한 때에 사용한다.

원인 분석 과정에서 제외하기 위해 코드를 제거하라

오류로 향하는 길에 산재한 많은 코드를 제거하거나 건너뜀(주석)으로 실제 구동에 포함되지 않게 해라.

청결은 감염을 막는다

버그가 소프트웨어에 오래 머물지 않도록 한다. 버그가 오래 남아있으면 추적 중인 다른 버그를 가릴 수도 있으므로 빨리 처리한다.

간접적 전략

때로는 몇 시간 동안 씨름하다가 결국 아무 것도 얻지 못할 때도 있다. 이럴 때는 간접적 접근 방법을 시도한다.

  • 쉬어가기: 작업을 멈추고 코드에서 떨어져 있어야 할 때를 배우는 것은 중요하다. 휴식을 통해 새로운 관점을 얻을 수 있고, 이를 통해 더욱 신중해질 수 있다. 무턱대고 코드에 달려들기보단, 문제에 대한 서술과 코드 구조를 생각할 수 있도록 휴식 시간을 가져보라.
  • 다른 사람에게 설명하기: 문제를 다른 사람들에게 설명하면 자기 자신에게도 설명하게 되고 문제를 해결하는 경우가 많다. 심지어 고무 오리에게도 말해볼 수 있다.(고무 오리 전략) 책상에 놓여 있는 무생물에게 말을 걸어보라.

도망가지 말라

버그를 찾고 해결할 때 마구잡이로 달려들면 안된다. 잠시 숨을 고르면서, 해당 코드 구역에 숨어 있는 다른 관련된 문제들이 있는지 고려하라.

재현할 수 없는 버그

때로는 논리와 추론을 거부하는 해괴한 버그들을 발견하기도 한다. 이럴땐 이렇게 한다:

  • 실패를 유발하는 요소들을 기록한다. 이를 반복하다보면, 어떤 패턴을 찾아내 공통된 원인들을 식별할 수 있다.
  • 더 많은 정보를 수집하다보면 결론을 지을 수 있다.
  • 베타 버전이나 출시 버전에 더 많은 로그를 추가하여, 실제 사용자의 사용 시 정보를 획득한다.

🥸 생각해보기

1. 자신이 얼마나 많은 시간을 디버깅에 할애하는지 평가해보라. 시스템에 새로운 코드를 작성하지 않는 모든 활동도 고려하라.

오만 다양한 케이스로 디버깅을 하는데, 정말 일정하지 않은 시간인것 같다. 가끔은 복잡한 기능에 대해 발생하는 사소한 문제를 고치려다보니 다 뜯어고쳐야 한다는 사실을 깨달아 아주 많은 시간을 쓰기도 했고, 어떤때는 고칠 수 있나 싶었던 문제를 찾아보니 아주 쉽게 고친적도 있던것으로 기억한다.

2. 자신이 작성한 새로운 코드에 디버깅 시간을 더 많이 할애하는가, 아니면 기존 코드를 조정하는 데 더 많은 시간을 할애하는가?

다른 사람의 코드보단 내 코드를 더 많이 디버깅했던것 같은데, 이는 자신이 쓴 코드를 자신이 가장 잘 아니 제일 빨리 고칠 수 있다는 팀원 내의 믿음 때문인것 같다. 물론 팀원의 코드도 디버깅한다.

3. 기존 코드를 위한 단일 테스트들은 디버깅 시간에 변화를 주는가, 아니면 디버깅 방법에 변화를 주는가?

테스트들이 있다면 디버깅할 코드의 범위가 좁아지고, 테스트를 기준으로 디버깅을 시작할 수 있는 지점이 정해진다. 따라서 테스트가 있고 없고는 디버깅 방식에 구조적 변화를 준다.

4. 버그 없는 소프트웨어를 목표로 삼는 것은 현실적인가? 이것은 실현 가능한가? 버그 없는 소프트웨어를 진짜 목표로 삼는 때는 언제가 적절한가? 소프트웨어에서 버그의 양을 결정하는 요소는 무엇인가?

인간이 하기 쉽지 않은 일이라 개발했을 정도의 복잡성을 가진 소프트웨어라면 현실적으로 버그가 아예 없기를 바라는건 쉽지 않다고 생각한다. 물론 은행 앱이라던가 병원 앱 같이 아주 중요한 기능이 포함되어 있다면 엄청나게 철저하게 버그가 없도록 목표를 설정해야할 것이고, 이런 경우는 최대한 다른 기능들은 복잡하지 않게 만드는게 하나의 방법이라고 생각한다.

소프트웨어에서 버그의 양을 결정하는 요소는 아주 다양하다고 생각하는데, 프로그래머의 실력이나 프로그램의 복잡도 등이 큰 영향을 끼칠 것이라고 생각한다.