프로그램은 왜 실패하는가(Why Programs Fail?) 라는 책의 핵심만을 요약했다. 유일한 디버깅 관련한 서적이기 때문에 Record & Replay 를 구현하는 데, 아이디어를 얻을 수 있을 것 같다.

실패는 왜 일어나는가

일반적으로 실패는 다음과 같은 세 단계를 거쳐서 드러난다.

  1. 프로그래머가 프로그램 코드에 결함(버그 또는 잘못이라고도 한다)을 만들어낸다.
  2. 그 결함은 프로그램 상태의 감염을 야기한다.
  3. 감염은 실패, 즉 외부에서 관측할 수 있는 오류를 일으킨다.

프로그램을 디버깅할 때에는 일곱 단계들(TRAFFIC)을 거친다.

Track 데이터베이스에서 문제점을 추적한다
Reproduce 실패를 재현한다
Automate 검례를 자동화, 단순화 한다
Find 가능성 있는 감염원들을 찾는다
Focus 가장 그럴법한 감염원들에 집중한다(알려진 감염들, 상태, 코드, 입력의 원인들, 비정상들, 코드 악취)
Isolate 감염 사슬을 격리시킨다
Correct 결함을 정정한다

이 모든 디버깅 활동들 중 결함을 찾는 것(TRAFFIC 의 찾기-집중-격리 루프)에 가장 많은 시간이 걸린다.
대규모의 재설계와 관련된(그런 경우 결함을 결점이라고 부른다)것이 아닌 한, 결함을 정정하는 것은 일반적으로 쉬운 일이 아니다.
모든 결함이 감염으로 이어지는 것은 아니며, 또한 모든 감염이 실패로 이어지는 것은 아니다. 그러나 모든 실패는 어떠한 감염에서 비롯되며, 모든 결함은 결함으로부터 비롯된 것이다.

문제점 추적

현장에서 발견된 문제점에 대한 보고를 문제점 데이터베이스에 저장하고, 그 상태와 심각도에 따라 분류한다.
문제점 보고에는 해당 문제점을 재현하는 데 유관한 모든 정복 들어 있어야 한다.
문제점에 유관한 정보를 얻기 위해서는, 사용자가 반드시 입력해야 하는 항목들의 표준적인 집합을 설정해야 한다. 그런 항목들로는 다음과 같은 것들이 있다.

  1. 제품 릴리즈 : 제품의 버전 번호 또는 기타 고유한 식별자
  2. 운영환경 : 운영체제의 버전 정보
  3. 문제점 내력(history) : 문제점을 재현하는 데 필요한 최소한의 단계들(입력 내용이나 구성 파일등)
  4. 기대 행동 : 사용자 입장에서 보았을 때 일어났어야 하는 것들을 서술
  5. 실제 행동 : 문제의 증상으로서 기대된 내용과는 달리 실제로 일어난 행동을 서술

효과적인 문제점 보고가 되기 위해서는, 보고가 다음 사항을 만족해야 한다.

  1. 제대로 된 구조를 갖추어야 한다.
  2. 문제점을 재현할 수 있어야 한다.
  3. 문제점을 잘 설명하는 한 줄짜리 요약문이 포함되어야 한다.
  4. 최대한 단순해야 한다.
  5. 최대한 일반적이어야 한다.
  6. 중립적이어야 하며 사실만을 이야기해야 한다.

문제점의 재현에 도움이 될만한 정보를 수집하고 전달하는 기능을 제품자체에 집어넣을 수도 있다. 단, 이 경우 개인정보 문제를 조심할 필요가 있다.
문제점의 전형적인 수명 주기는 UNCONFIRMED 상태로부터 시작해서 CLOSED 상태로 끝난다. 그리고 CLOSED 상태가 되었을 때에는 FIXED 나 WORKSFORME 같은 특정한 처리 결과를 가진다.

디버깅 공정을 조직화하기 위해, 소프트웨어 변경 제어 위원회(SCCB)를 둔다. SCCB 는 다음과 같은 목적으로 문제점 데이터베이스를 활용한다.

  1. 해결된/해결되지 않은 문제점들을 추적한다.
  2. 개별 문제점에 우선순위를 부여한다.
  3. 개별 개발자에게 문제점을 배정한다.

문제점 추적 시스템을 요구사항들을 추적하는 용도로 사용할 수도 있다. 그런 경우 아직 만족되지 않은 요구사항들을 문제점들로 취급한다.
문제점 추적을 단순하게 유지할 것. 거추장스러워지면 사람들은 더 이상 사용하지 않을 것이다.
릴리즈된 버전들을 보존하기 위해, 구성을 사용자에게 출시할 때마다 버전 관리 시스템으로 그 구성에 적절한 꼬리표를 달 것
교정과 기능을 구분하기 위해, 교정들은 버전 관리 시스템 하의 가지들 안에, 기능들은 주 줄기 안에 둘 것
문제점과 교정을 연계시키기 위해, 문제점 보고를 변경에, 또는 변경을 문제점 보고에 연관시키는 관례를 확립하라. 문제점 추적 시스템과 통합되어서 이러한 연관 관계를 자동적으로 관리하는 진보된 버전 관리 시스템들이 존재한다.
문제점과 테스트를 연계시키기 위해, 테스트 케이스를 만드는 즉시 해당 문제점 보고를 퇴물로 만들 것. 문제점이 발생했을 때, 그것을 문제점 보고로 입력하기보다는 적절한 테스트 케이스를 작성하는 것이 더 바람직 하다.

프로그램을 실패하게 만들기

디버깅을 위한 테스트에서는 다음과 같은 과정을 거치게 된다.

  1. 문제점을 재현하는 테스트를 작성한다.
  2. 디버깅을 진행하면서 그 테스트를 여러 번 실행한다.
  3. 새 릴리스를 만들기 전에, 문제점이 확실히 사라졌는지를 그 테스트를 다시 실행해서 확인한다.

디버깅 도중에는 많은 수의 테스트들이 필요할 것이므로, 테스트들을 최대한 자동화하는 것이 바람직하다.
프로그램 수행의 자동화는 다음 세 계층에서 가능하다.

  1. 표현층
  2. 기능층
  3. 단위층

각 계층은 수행의 편의, 상호작용의 편의, 결과 평가의 편의, 그리고 변경에 대한 안정성에서 각각 서로 다른 장단점을 가지고 있다.
'표현층에서의 테스트'를 위해서는 사용자 상호작용을 시뮬레이션하는 환경을 갖추어야 한다. 사용자 상호작용은 입력 장치 수준(저수준)에서 흉내낼 수도 있고 사용자 컨트롤 수준(고수준)에서 흉내낼 수도 있다.
'기능층에서의 테스트'에서는 자동화를 위해 설계된 인터페이스를 사용한다. 보통은 특정한 스크립팅 언어를 사용하게 된다.
'단위층에서의 테스트'에서는 프로그램 단위의 API 를 이용해서 단위를 제어하고 그 결과를 평가한다.

단위를 격리하기 위해서는 의존성 반전 원리를 이용해서, 다시 말하면 단위가 세부사항보다는 추상에 의존하게 함으로써 의존성을 깨뜨린다.
디버깅을 위한 설계를 위해서는 높은 응집성과 낮은 결합도 원리들을 이용해서 의존성의 양을 줄인다. 의존성을 줄이는 데에는 모델-뷰-컨트롤러 같은 설계 패턴들이 유용하다.
알려지지 않은 문제점을 방지하기 위해 사용할 수 있는 기법들은 여러 가지이다. 몇 가지를 들자면 다음과 같다.

  1. 테스트를 일찍, 자주, 충분히 수행한다.
  2. 다른 사람들이 코드를 검토하게 하고, 더 나아가서 짝 프로그래밍을 수행한다.
  3. 컴퓨터가 코드의 비정상들과 일반적인 실수들을 점검하게 한다.
  4. 프로그램의 정확성을 공식적으로 증명한다(컴퓨터의 도움을 받을 수도 있다).

문제점 재현

일단 문제점을 발견했다면, 다음으로 할 일은 그 문제점을 개발자의 환경에서 재현하는 것이다.
문제점을 재현하기 위해서는 '그 환경을 재현하고 그 수행을 재현해야' 한다.
문제점 환경을 재현하려면 개발자의 환경에서 출발해서 문제점 환경의 상황들을 한 번에 개발자 환경에 하나씩 도입한다.
문제점 수행을 재현하기 위해서는, 프로그램에 주어지는 입력과 프로그램이 받아들이는 입력 사이에 하나의 제어층을 둔다. 그런 제어층은 프로그램의 입력을 감시, 제어, 갈무리, 재생하는 용도로 사용할 수 있다.
기술적인 측면에서, 제어층은 입력 함수들로의 호출을 가로채서 구현한다.
제어할 수 있는(그리고 많은 경우 반드시 제어해야 하는) 입력들로는 다음이 있다.

  1. 자료
  2. 사용자 입력
  3. 통신
  4. 시간
  5. 무작위성
  6. 운영 환경
  7. 프로세스 일정

물리적 영향과 디버깅 도구가 프로그램의 행동에 의도하지 않은 방식으로 영향을 미치기도 한다.
프로그램을 가상 기계 상에서 수행하는 것이 상호작용의 기록과 재생 가능성이 가장 큰 방식이다.
단위 행동의 재현을 위해서는, 단위에 주어지는 입력과 단위가 받아들이는 입력 사이에 하나의 제어층을 둔다.
모크 객체는 임의의 단위의 상호작용을 기록하고 재생하는 일반적인 수단이 된다.

문제점 단순화

단순화의 목표는 상세한 문제점 보고로부터 하나의 간단한 테스트 케이스를 만들어내는 것이다.
단순화된 케이스 테스트는 의사소통하기 쉽고, 디버깅을 편하게 하며, 중복된 문제점 보고를 식별할 수 있게 한다.
테스트 케이스를 단순화하기 위해서는, 모든 무관한 상황들을 제거한다. 어떠한 상황이 존재하든 존재하지 않든 문제점이 발생한다면 그 상황은 문제점에 대해 무관한 것이다.
단순화를 자동화하기 위해서는 문제점이 발생하는지의 여부를 점검하는 자동적인 테스트와 유관한 상황들을 결정하는 전략이 필요하다. 델타 디버깅 알고리즘 ddmin 이 그러한 전략의 하나이다.
단순화할 상황들에는 자료로서의 프로그램 입력뿐만 아니라 프로그램의 결과에 영향을 줄 수 있는 모든 상황들(예를 들면 사용자 상호자용 등)이 포함된다.
입력에서 실패 유발 부분을 드러내는 무작위 테스팅을 단순화에 결합하는 것도 가능하다.
자동 단순화의 속도를 높이기 위해 사용할 수 있는 방법들로는 다음과 같은 것들이 있다.

  1. 캐싱을 활용한다.
  2. 일찍 중단한다.
  3. 구문적 수준 또는 의미론적 수준에서 단순화한다.
  4. 상황들을 격리하는 대신 실패를 야기하는 차이들을 격리한다.

과학적 디버깅

실패 원인을 격리하기 위해, 다음과 같은 과학적 방법을 사용한다.

  1. 실패를 관찰한다.
  2. 그 관찰과 모순되지 않는, 문제점의 원인에 대한 하나의 가설을 고안한다.
  3. 그 가설을 이용해서 예측을 한다.
  4. 실험과 추가적인 관찰을 통해서 그 가설을 테스트한다.(실험이 예측을 만족한다면 가설을 정련한다, 실험이 예측을 만족하지 않는다면 또 다른 가설을 만든다)
  5. 가설을 더 이상 정련할 수 없을 때까지 단계 3과 4를 반복한다.

주어진 문제점을 이해하기 위해서는 문제점을 명시적으로 만들어야 한다. 종이에 적어 내려가거나 친구에게 설명하라.
끝없는 디버깅 세션들을 피하려면 개별 단계들을 명시적으로 만들어야 한다. 일지를 작성할 것
함수적 프로그램이나 논리적 프로그램에서 오류를 찾아내려면, 알고리즘 디버깅 기법을 사용해 볼 것
알고리즘 디버깅은 오류의 근원에 대한 가설을 제시하고 사용자(또는 어떠한 신탁)에게 판단을 요구함으로써 디버깅 공정을 주도한다.
간이 디버깅은 그냥 열심히 고민해서 문제점을 해결하는 것이다. 그러나 일정 시간이 지나도 답을 얻지 못했다면 공식적인 방법들로 넘어가야 한다.
가설을 이끌어내기 위해서는 다음의 근원들을 고려한다.

  1. 문제점 설명
  2. 프로그램 코드
  3. 실패한 실행
  4. 교체 실행들
  5. 이전의 가설들

프로그램을 추론하기 위해 사용할 수 있는 기법들로는 다음이 있다.

  1. 연역(실행 0 회)
  2. 관찰(실행 1 회)
  3. 귀납(여러 번 실행)
  4. 실험(다수의 제어된 실행)

오류 연역

값 근원들을 격리하려면, 문제의 문장으로부터 의존성들을 거꾸로 추적한다.
의존성들을 통해서 코드 악취를 발견할 수 있다. 특히, 초기화되지 않은 변수 사용, 사용되지 않는 값들, 도달할 수 없는 코드 같은 일반적인 오류들을 찾을 수 있다.
디버깅을 시작하기 전에 자동적인 검출 도구(컴파일러 등)가 보고한 코드 악취를 제거할 것
프로그램을 슬라이싱할 때에는, 문장 S 로부터 의존성을 따라가면서 다음과 같은 문장들을 모두 찾는다.

  1. S 에 영향을 받을 수 있는 문장들(전방 슬라이스)
  2. S 에 영향을 줄 수 있는 문장들(후방 슬라이스)

연역만을 사용한다면 코드 불일치, 관련 세부사항의 과도한 추상화, 부정확함 같은 위험이 생긴다.
어떤 종류의 연역도 멈춤 문제에 제한을 받으며, 그러면 보수적 근사에 의지해야 한다.

사실 관찰

상태를 관찰할 때, 관찰 행위가 상태에 간섭하게 해서는 안 된다. 무엇을 언제 관찰할 것인지를 명확히 하고 체계적으로 진행하라.
상태를 관찰하기 위해 사용할 수 있는 수단으로는 '로깅 함수, 애스펙트, 디버거' 가 있다. 로깅문(“printf 디버깅”)은 사용하기는 쉽지만 코드와 출력을 지저분하게 만드는 경향이 있다.
디버깅 코드를 캡슐화, 재사용하려면, 로깅 전용 프레임웍 또는 애스펙트를 사용하라.
로깅 전용 함수들은 성능에 영향을 주지 않고 끌 수 있도록 설정할 수 있다. 적절한 규칙을 지킨다면, 심지어는 제품용 코드에 그대로 남겨두어도 될 정도로 깔끔한 로깅 코드를 만들 수 있다.
애스펙트를 이용하면 모든 로깅 코드를 한 장소에 모아두거나 같은 로깅 코드를 여러 장소에 적용하는 등의 일을 우아한 방식으로 달성할 수 있다.
디버거를 이용하면 임의의 사건을 유연하고 빠르게 관찰할 수 있다. 그러나 로깅 코드를 재사용하는 것은 힘들다.
폭주한 프로그램의 최종 상태를 관찰하려면, 디버거로 사후 메모리 덤프를 관찰한다. 메모리 덤프가 없다면 디버거 안에서 실행을 반복한다. 진보된 디버거들로는 사건들을 유연하게 질의할 수 있으며 프로그램 자료를 시각화할 수 있다.

근원 추적

수행 내력을 탐색하려면, 프로그램 수행 전체를 기록하고 수행의 모든 측면에 임의로 접근할 수 있게 하는 전지적 디버거를 사용한다.
특정 수행에 대해서 값 근원들을 격리할 때에는 정적 슬라이싱 대신 동적 슬라이싱을 사용한다.
동적 슬라이스들은 프로그램의 특정한 하나의 실행에만 적용되나, 정적 슬라이스들보다 훨씬 정밀하다.
현재 나와 있는 최고의 상호작용적 디버거들은 전지적 디버깅, 정적 슬라이싱, 동적 슬라이싱을 이용해서 어떠한 일이 왜 일어나는지 또는 왜 일어나지 않는지를 진단해 준다.
감염을 추적하려면, 의존성들을 되짚어 나가면서 근원들을 관찰하는 과정을 감염 근원들에 대해 반복한다.

기대의 단언

관찰을 자동화하기 위해 단언을 사용하라.
단언은 감염이 프로그램 상태를 통해 전파되고 그 근원이 감춰지기 전에 감염을 잡아낸다. 관찰 명령문들과 마찬가지로, 단언들이 실제 계산에 간섭해서는 안 된다.
단언의 용도는 전제조건 점검, 자료 불변식 점검, 사후조건 점검이다.

  1. 전제조건은 함수의 호출에 필요한 요구사항들을 명세한다. 전제조건이 점검되었다는 것은 해당 요구사항들이 만족되었다는 뜻이다.
  2. 자료 불변식은 자료에 대해 작동하는 각 공개 함수의 시작과 끝에서 자료에 대해 반드시 성립해야 하는 속성들을 뜻한다. 불변식이 점검 되었다는 것은 그 자료가 온전하다는 뜻이다.
  3. 사후조건은 함수의 효과를 명세한다. 사후조건이 점검되었다는 것은 함수의 결과가 정확하다는 뜻이다.

단언은 명세 역할을 할 수 있으며(Eiffel 이나 JML 에서처럼), 따라서 인터페이스를 문서화하는 역할을 할 수 있다.
“외부” 명세 언어와 달리, 단언들은 코드에 직접 삽입되기 때문에 실행시점에서 쉽게 점검할 수 있다.
JML 같은 본격적인 명세 언어를 이용하면 단언(실행시점 점검)에서 정적 점검 및 검증(컴파일 시점 점검)으로 매끄럽게 전환할 수 있다.
기준 프로그램에 대해 프로그램을 점검하기 위해서는 상대적 디버깅을 사용한다.
메모리 무결성을 점검하기 위해서는, 메모리 관리의 오류를 점검하는 특화된 도구를 사용한다. 단, 그런 도구는 다른 모든 디버깅 방법들을 적용하기 전에 적용되어야 한다.
아주 정교한 도구들은 메모리 사용을 그림자 메모리를 통해서 추적함으로써 메모리 오용을 검출한다.

저수준 언어에서 메모리 오류를 방지하려면, C 언어용 Cylone 같은 보다 안전한 파생 언어의 사용을 고려해 볼 것.
단언을 이용해서 코드를 최대한 일찍 실패하게 만들 것. 그러면 감염을 잡아낼 가능성이 커진다. 또한 감염에서 실패로의 사슬이 짧아진다.
단언은 성능을 떨어뜨릴 수 있다. 그러나 디버깅에는 도움이 되고, 잘못된 계산에 의한 위험도 피할 수 있다. 그러한 장점이 비용을 상쇄하고도 남는 경우가 많다. 따라서 가벼운 단언들은 제품용 코드에 그대로 남겨두는 것이 좋다. 대신 선언이 실패했을 때 사용자 친화적인 방식으로 프로그램을 복구할 수 있는 수단도 제공할 필요가 있다.
치명적 결과나 외부 보건에 대해서는 단언을 사용하지 말 것. 대신 직접 작성한 오류 처리부를 사용하라.

비정상 검출

결함은 비정상 행동을 일으키기 쉬우며, 그래서 비정상들이 결함을 가리키는 경우가 자주 있다.
비정상은 결함도 아니고 실패 원인도 아니나, 그 둘 중 하나나 모두에 강하게 연관된 것일 수 있다.
비정상 행동을 결정하기 위해서는 통과된 실행의 정상 행동을 파악하고 그것이 실패한 실행들과 어떻게 다른지를 본다. 이는

  1. 요약된 속성들을 직접 비교함으로써, 또는
  2. 그 속성들을 단언들로 바꾸고, 그것들로 실패한 결함의 비정상들을 검출함으로써

수행한다. 행동을 요약하기 위해서는, 여러 프로그램 실행들을 그 모든 실행들에 대해 성립하는 하나의 단언으로 요약하는 귀납적 기법을 사용한다.
비정상을 검출하기 위해서, 연구자들은 지금까지 포괄도, 함수 반환값, 자료 불변식에 초점을 두었다.
포괄도를 비교하기 위해서는, 프로그램을 계장해서 실패한 실행들과 통과된 실행들의 포괄도를 얻고 요약한다. 그리고 실패한 실행들에서만 수행된 문장들에 집중한다.
반환값을 표본화하기 위해서는, 각 호출 지점에서 각 부류의 반환값 개수를 센다. 그리고 오직 실패한 실행들에서만 발생한 부류에 집중한다.
현장에서 자료를 수집할 때에는 성능에 미치는 악영향을 최소화하기 위해 표본화 전략을 사용한다.
불변식들을 파악하기 위해서는 Daikon 이나 비슷한 도구들로 계장 지점에서 주어진 불변식들이 성립하는지 점검한다.
여러 사건들 중 후보를 선택해야 할 때에는 우선 비정상적인 것에 집중한다. 여기서 논의한 기법들은 비교적 최근에 나온 것이며 아직 완전히 평가되지는 않았다.

원인과 결과

디버깅 도중 관찰할 수 있는 모든 상황들에서, 가장 가치 있는 것은 원인이다. 사건 A 가 사건 B 보다 먼저 일어나며, 사건 A 가 일어나지 않으면 사건 B 도 일어나지 않을 때 사건 A 를 원인(cause), 사건 B 를 결과(effect) 라고 부른다.
원인이라는 것은 결과가 발생한 세계와 결과가 발생하지 않은 대안 세계 사이의 차이로 볼 수 있다.
인과관계를 증명하려면, 원인이 발생하지 않게 하는 실험을 만든다. 만일 그 실험에서 해당 결과도 발생하지 않는다면(그리고 오직 그럴 때에만) 인과관계가 증명된다.
원인을 찾으려면, 과학적 방법을 이용해서 가능한 원인에 대한 가설을 만들고, 실험을 통해서 인과관계를 검증한다.
실제 원인은 실제 세계와 결과가 발생하지 않는 가장 가까운 세계 사이의 차이이다.
가장 가까운 세계를 택한다는 원리를 오컴의 면도날이라고도 한다. 오컴의 면도날은, 어떤 결과를 설명하는 여러 이론들이 경쟁할 때 가장 간단한 것을 고르라는 뜻이다.
실제 원인을 찾으려면, 초기 차이를 과학적 방법을 통해서 좁혀나간다. 실제 세계들 사이의 공통 문맥은 검색 공간에서 원인들을 제외시킨다.

실패 원인의 격리

실패 원인을 자동으로 격리하려면, 다음과 같은 것들이 필요하다.

  1. 실패가 여전히 존재하는지의 여부를 점검하는 자동화된 테스트
  2. 차이를 좁혀 나가는 수단
  3. 그러한 과정을 진행시키는 전략

한 가지 가능한 전략이 바로 델타 디버깅 알고리즘 dd 이다. dd 는 두 구성들(입력, 일정, 코드 변경, 기타 상황들) 사이의, 주어진 테스트에 기준한 유관한 차이를 결정한다.
그 차이는 실패의 실제 원인이다.
입력에서 실패 원인을 격리하려면, 두 프로그램 입력들, 즉 테스트를 통과하는 입력과 실패하는 입력에 dd(또는 다른 전략)를 적용한다.
스레드 일정에서 실패 원인을 격리하려면, 두 일정들, 즉 테스트를 통과한 것과 실패한 것에 dd(또는 다른 전략)를 적용한다. DejaVu 같은 일정 재생, 조작 수단이 필요하다.
실패 유발 코드 변경을 격리하려면, 두 프로그램 버전들, 즉 테스트를 통과하는 버전과 실패하는 버전에 dd(또는 다른 전략)를 적용한다. 변경 집합을 소스 코드에 적용한 후 프로그램을 자동으로 재구축하는 수단이 필요하다.
델타 디버깅이 돌려준 것 같은 실제 원인들은 실패가 더 이상 발생하지 않게 만드는 데 사용할 수 있다. 그러나 그렇다고 원인이 곧 결함이라는 뜻은 아니다. 또한 실제 원인이 오직 하나만 존재한다는 뜻도 아니다.
상태에 대한 델타 디버깅은 비교적 최근에 나온 기법으로, 아직 완전히 평가되지 않았다.

인과 사슬의 격리

프로그램 실행 도중에 실패 원인이 어떻게 전파되는지를 이해하려면, 델타 디버깅을 프로그램 상태에 적용해서 실패를 일으키는 변수들과 값들을 격리한다. 프로그램 상태를 갈무리하려면, 구체적인 메모리 장소들로부터 추상화된 표현(메모리 그래프 등)을 사용한다.
프로그램 상태들을 비교하려면, 큰 공통 부분그래프를 계산한다. 그 부분 그래프에 포함되지 않은 값은 차이가 된다.
실패 유발 프로그램 상태들을 격리하려면,

  1. 메모리 그래프 차이들의 한 부분집합을 취해서
  2. 적절한 디버거 명령들을 계산하고
  3. 그것들을 통과 실행에 적용하고
  4. 실패가 여전히 발생하는지를 판정하는

하나의 test 함수가 필요하다. 이 test 함수를 델타 디버깅 틀 안에서 사용함으로써 1-최소 실패 유발 프로그램 상태를 얻게 된다.
델타 디버깅이 돌려주는 실패 유발 변수의 값을 변경함으로써 실패가 더 이상 일어나지 않게 만들 수 있다. 따라서 그런 변수는 실제 원인이다.
그러나 그 변수가 반드시 감염되었다는 뜻은 아니다. 또한 그런 실패 유발 변수가 단 하나만 존재한다는 보장도 없다.
실패의 원인이 되는 코드를 찾기 위해, 변수 A 가 더 이상 실패 원인일 가능성이 없어짐과 함께 변수 B 가 실패 원인일 가능성이 생기는 원인 전이를 자동으로 검색할 수 있다. 그런 원인 전이는 실패를 교정할 수 있을만한 장소이며, 따라서 결함일 가능성이 있다.
인과 사슬을 따라 결함을 좁혀 나가려면, 온전한 변수가 감염된 변수로 변하는 원인 전이를 찾는다.
상태에 대한 델타 디버깅은 상당히 최근에 나온 기법으로, 아직 완전히 평가되지 않았다.
실패 원인 찾기는 완전하게 자동화할 수 있지만, 실패를 일으키는 결함을 찾는 데에는 언제나 사람의 개입이 필요하다.

결함 고치기

감염 사슬을 격리하기 위해서는, 감염 근원들을 추이적으로 되짚어나간다.
가장 유망한 근원을 찾기 위해서는, 다음에 집중한다.

  1. 실패한 단언들
  2. 상태, 코드, 입력의 원인들
  3. 비정상들
  4. 코드 악취

함수와 패키지 경계는 감염 근원을 점검하기에 좋은 장소이다.
각 근원에 대해, 그것이 감염인지, 또는 원인인지 점검한다.
정정이 너무 비싸거나 위험하다면 우회책(결함을 프로그램에 남겨두지만, 적어도 실패는 더 이상 발생하지 않게 하는)을 적용한다.
결함의 정정은 다음을 예측할 수 있게 될 때까지 미루어야 한다.

  1. 코드의 변경이 감염 사슬을 끊을 것인지
  2. 그것이 실패를 더 이상 발생하지 않게 할 것인지

정정이 성공적인지를 확인하려면 다음을 점검한다.

  1. 정정에 의해 실패가 더 이상 발생하지 않는지
  2. 정정이 새로운 문제점을 도입하지는 않는지
  3. 결함으로 이어진 실수가 다른 비슷한 결함들을 만들어 내지는 않았는지

새 문제점의 도입을 피하는데 도움이 되는 기법들로는

  1. 정정을 동료가 검토하게 한다.
  2. 회귀 테스트를 준비한다.

실수에서 배우려면, 문제점 데이터베이스를 활용해서 자주 교정된 코드와 자주 발생한 오류 종류를 점검한다.

결함은 추측으로 찾는다. 여기에는 다음과 같은 것들이 포함된다.

  1. 디버깅 명령문들은 프로그램 여기저기에 흩뜨려 놓는다.
  2. 뭔가 제대로 작동할 때까지 코드를 변경해 본다.
  3. 코드의 이전 버전을 백업해두지 않는다.
  4. 프로그램이 어떻게 행동해야 하는지를 굳이 이해하려 들지 않는다.

문제점을 파악하는 데 시간을 낭비하지 말라. 어차피 대부분의 문제점들은 사소한 것이다.
가장 명백한 교정을 적용한다. 눈에 보이는 증상만 고치면 된다.

x = compute(y);
// compute() 는 y == 17 일 때 정확하지 않으므로 이렇게 고친다.
if(y == 17)
 x = 25.15;

굳이 compute() 의 코드를 일일이 살펴볼 필요가 있겠는가?

  1. Eclipse 와 Mozilla 프로젝트에서, 모든 변경들의 30 에서 40% 정도가 교정이다.
  2. 교정들은 크기가 다른 변경들에 비해 2, 3 분의 1 이다.
  3. 교정들은 다른 변경들에 비해 실패를 도입할 가능성이 크다.
  4. 한 줄짜리 변경의 단 4% 가 코드에 새 오류를 도입한다.
  5. 다른 모듈들보다 1년 더 오래된 모듈은 다른 모듈들보다 오류가 30% 적다.
  6. 새로 작성된 코드에 결함이 있을 확률은 오래된 코드보다 2.5 배이다.
  • computer/rtcclab/why_programs_fail.txt
  • Last modified: 3 years ago
  • by likewind