23장: 디버깅
23장: 디버깅
- 디버깅은 오류의 근본적인 원인을 수정하는 과정임.
- 디버깅은 전체 개발 중 상당부를 차지함.
- 디버깅이 가장 어려운 부분일 필요는 없음.
23.1 디버깅 이슈 소개
소프트웨어 품질에서 디버깅의 역할
- 테스트와 마찬가지로 디버깅은 소프트웨어 품질 향상 보단 결함을 진단하는 방법임.
- 좋은 소프트웨어를 만드는 가장 좋은 방법은 요구사항을 주의 깊게 설계하고 개발하는 것임.
- 디버깅은 최후의 수단임.
디버깅 성과의 차이
- 디버깅 능력에는 상당한 차이가 존재함.
- 결함을 찾는 속도나 정확도 등에서 프로그래머 간 차이가 크게 나타남.
- 품질을 향상 시키는 것은 개발 비용을 줄이는 것이며, 개발 비용을 줄이기 위해서는 결함을 빠르게 고칠 줄 알아야 함.
기회로서의 결함
- 결함은 프로그램을 정확히 이해하고 복잡성을 해결 할 수 있는 학습의 기회임.
- 발생하는 결함의 종류를 분석하여 자신이 자주 만드는 작성 패턴이나 실수를 파악할 수 있음.
- 결함을 고치는 과정에서 어떻게 하는 것이 더 나은 방법인지 배울 수 있음.
비효과적인 접근 방식
- 추측만으로 원인을 정확히 이해하지 못한 채 증상만 고치는 방식은 상황을 악화시킴.
- 사소한 문제의 경우, 완벽한 문제 이해를 위해 시간을 쓰는건 낭비일 수 있음.
- 특정 문제를 수정하지 않고 전체 프로그램에 영향을 끼치는 원대한 작업의 수정은 시간을 낭비함.
미신을 따르는 디버깅
- 컴파일러나 OS의 버그일 것이다.라는 식의 지레짐작은 문제 해결을 지연시킴.
- 자신이 작성한 코드에 결함이 없을 것이라는 고정관념에서 벗어나야 함.
23.2 결함 발견
- 디버깅은 결함을 발견하고 수정하는 과정으로, 결함을 찾아 원인을 이해하는 것이 전체 작업의 약 90%를 차지함.
- 무작위 추측이나 감에 의존하기보다 논리적 추론과 데이터에 기반한 과학적인 방법을 적용해야 함.
과학적인 디버깅 방법
- 디버깅은 가설을 세우고 실험을 통해 검증하는 과학적 프로세스를 따름.
- 단계별 절차:
- 오류를 재현한다.
- 오류(“결점”)의 원인을 찾아낸다.
- a. 결함을 만들어내는 데이터를 수집한다
- b. 수집된 데이터를 분석하고 결함에 대한 가설을 세운다.
- c. 프로그램을 테스트하거나 코드를 살펴봄으로써 가설을 증명하거나 반증할 방법을 결정한다.
- d. 2(c)에서 규명한 절차클 사용하여 가설을 증명하거나 반종한다
- 결함을 수정한다
- 수정 내용을 테스트한다
- 유사한 오류를 찾는다.
오류를 재현하라
- 예측할 수 없거나 간헐적으로 발생하는 오류는 초기화 오류, 시간 문제, 댕글링 포인터 문제일 가능성이 높음.
- 오류 발생 원인을 명확히 분리하기 위해 오류 행동이 변경되는 가장 간단한 테스트 케이스로 범위를 좁혀야 함.
- 테스트 케이스 단순화는 관련 없는 요소를 하나씩 제거하며 최소한의 오류 조건만 남기는 방식으로 진행함.
오류의 원인을 찾아라
- 수집된 출력 결과를 관찰하여 패턴이나 특이점을 파악하고 원인 가설을 구체화해야 함.
- 가설이 반증되면 결과를 바탕으로 더 정교한 가설을 세워 재검증함.
결함을 찾는데 도움이 되는 팁
-
가설을 세우기 위해서 사용할 수 있는 모든 데이터를 사용하라: 가설을 세울 때는 사용 가능한 모든 데이터를 설명할 수 있어야 함. 가설에 맞지 않는 데이터라도 버리지 말고, 가설을 지속적으로 수정·개선해 나가야 함.
-
오류를 만드는 테스트 케이스를 개선하라: 가설 검증이 막힐 때는 테스트 케이스의 요인을 다각도로 변경하여 새로운 돌파구를 모색함.
-
단위 테스트에서 코드를 다루어라: 통합된 거대한 시스템보다는 단위 테스트를 통해 고립된 작은 환경에서 결함을 찾는 것이 훨씬 효율적임.
- 도구를 사용하라: 대화식 디버거, 메모리 검사기 등 올바른 도구를 활용함.
- 예: 특정 메모리 위치를 감시하는 메모리 중단점을 설정해 덮어쓰기 오류 발생 지점을 포착함.
-
여러 가지 방법으로 오류를 발생시켜라: 오류 케이스와 유사하지만 약간 다른 테스트를 시도하여 결함 지점을 정확히 파악함.
-
더 많은 가설을 세우기 위해서 더 많은 데이터를 만들어라: 이미 알고 있는 테스트 케이스와 다른 케이스를 실행해 더많은 데이터들을 얻고, 그 데이터들을 이용해 새로운 가설을 만듬.
-
부정적 테스트의 결과를 사용하라: 테스트가 가설 검증에 실패하였다면, 결함이 생각했던 영역에 있지 않았다는 사실을 이용함.
-
가능한 가설에 대해서 브레인스토밍하라: 가설 브레인스토밍을 진행하여 단일 추론에 갇히는 정체 현상을 극복함.
-
연습장을 준비해서 시도해 볼 목록을 만들어라: 디버깅 도중 막다른 골목에 빠지지 않도록 시도해 볼 가설 리스트를 만들어 체계적으로 점검함.
-
의심스러운 코드 영역을 좁혀라: 프린트/로그 출력으로 의심 영역을 좁히고, 필요시 코드를 절반씩 주석/제거해 나가는 이진 탐색 기법을 활용함.
-
이전에 결합이 있었던 클래스와 루틴을 의심하라: 이전에 결함이 빈번했던 클래스나 루틴에서 결함이 존재 할 확률이 높으므로 최근 변경 및 추가된 코드를 우선적으로 검사함.
-
최근에 변경한 코드를 검사하라: 원인을 찾기 어려운 오류가 있다면, 일반적으로 최근에 변경한 코드에서 결함이 존재할 확률이 높음. 또한 결함이 없던 이전 버전과 새로운 버전의 차이가 무엇인지 비교함으로써 결함을 찾아냄.
-
의심스러운 코드 영역을 확장하라: 생각했던 곳에서 결함을 발견하지 못했다면 그곳엔 결함이 없을 수 있다는 사실을 인지해야함.
-
점진적으로 통합하라: 시스템에 한부분씩 추가한다면 디버깅시 고려할 사항이 줄어듬.
-
일반적인 결함을 검사한다: 코드 품질 체크리스트를 통해 결함을 찾아냄.
-
프로그램에 대해 다른 사람과 이야기를 나누어라: 다른 사람에게 결함을 설명하는 과정에서 자연스레 해결책을 떠올릴 수 있음.
- 문제로부터 떨어져 휴식을 취하라: 과도한 집중은 근시안적으로 디버깅을 하게 함. 때로는 해결책이 일상속에서 문득 나타날 수 있음.
무차별 대입을 통한 디버깅
무차별 대입 기법 리스트
- 망가진 코드에 대해 전체적인 설계와 코드 검토를 수행한다.
- 문제가 발생한 코드 섹션을 버리고 처음부터 새로 설계하거나 새로 작성한다.
- 전체 프로그램을 버리고 처음부터 새로 설계하거나 새로 작성한다.
- 완전한 디버깅 정보를 이용해 코드를 컴파일한다.
- 코드를 가장 까다로운 경고 수준으로 컴파일하고 까다로운 컴파일러 경고를 모두 수정한다.
- 단위 테스트를 이용하여 새로운 코드를 고립된 환경에서 테스트한다.
- 자동화된 테스트 도구를 작성해 밤새 실행한다.
- 오류 상황을 만날 때까지 디버거에서 큰 반복문을 직접 하나씩 실행해 본다.
- 코드에 프린트나 화면 출력, 다른 로깅 명령문을 추가한다.
- 다른 컴파일러로 컴파일한다.
- 다른 환경에서 프로그램을 컴파일하고 실행한다.
- 코드가 부정확하게 사용되었을 때 경고를 생성하는 특별한 라이브러리나 실행 환경에 코드를 링크하거나 실행한다.
- 사용자와 같은 컴퓨터 환경을 구성한다.
- 새로운 코드를 작은 부분에 통합하고 통합할 때 각 부분을 완전하게 테스트한다.
- 빠르고 지저분한 디버깅에 대한 최대 시간을 설정하라.
- 무차별 대입 기법에 대한 목록을 작성하라.
구문 오류
-
컴파일러 메시지에 있는 줄 번호를 믿지마라: 오류가 보고된 위치의 바로 앞뒤 문맥을 신중히 점검함.
-
컴파일러의 오류 메시지를 믿지 말라: 컴파일러의 오류 메시지를 문장 그대로 받아들이기보다 행간을 읽어 실제 의미를 파악함.
-
컴파일러의 두 번째 오류 메시지를 믿지 말라: 첫 구문 오류 발생 후 연달아 출력되는 두 번째 이후의 오류 메시지는 의미 없는 경우가 있기에 첫 번째 오류 위주로 수정한 뒤 재컴파일함.
-
분할 정복하라: 잘 해결되지 않는 구문 오류는 코드 일부를 주석 처리하거나 제거하여 컴파일해 보는 분할 정복 기법을 적용함.
-
잘못된 주석과 인용 부호(따옴표)를 찾아라: 닫히지 않은 주석이나 인용부호(따옴표)가 원인일 수 있으므로 구문 강조 편집기를 활용하거나, 특수 기호 조합(
/*"/**/)으로 주석/문자열 단절 상태를 테스트함.
23.3 결함 수정
-
디버깅에서 결함을 찾기는 어려워도 수정하기는 쉬운 편이나, 수정한 내용이 또 다른 오류를 유발할 확률이 높으므로 주의가 필요함.
-
수정하기 전에 문제를 이해하라: 문제를 완벽히 이해하지 못한 채 수정하면 코드가 더 악화되므로, 오류가 재현되는 상황과 그렇지 않은 상황을 명확히 구분하여 패턴을 파악해야 함. 또한 정확하게 오류의 발생을 예측 할 수 있을만큼 반복함.
-
문제만 이해하지 말고 프로그램을 이해하라: 문제가 발생한 지점의 주변 문맥과 프로그램의 전반적인 작동 방식을 이해해야 부작용 없는 수정을 할 수 있음.
-
결함 분석을 확인하라: 가설을 증명하는 테스트와 반증하는 테스트를 모두 실행하여 단 한 가지 원인에만 치우치지 않았는지 확실하게 검증함.
-
긴장을 풀어라: 일정이 급하다고 서두르면 불완전한 결함 분석과 검증 없는 수정으로 이어지므로, 잠시 휴식을 취하며 올바른 해결책인지 판단하는 여유를 가져야 함.
-
원본 소스코드를 저장하라: 코드를 수정하기 전 상태로 언제든 복원할 수 있도록 백업하거나 버전 관리 도구를 활용하여 변경 사항을 쉽게 비교함.
-
증상이 아니라 문제를 해결하라: 근본적인 원인을 고치지 않고 특정한 값에 대해서만 임시방편으로 예외 처리를 추가하면 유지가 불가능한 코드가 됨.
- 잘못된 증상 수정 예시:
for ( claimNumber = 0; claimNumber < numClaims[ client ]; claimNumber++ ) { sum[ client ] = sum[ client ] + claimAmount[ claimNumber ]; } // 특정 클라이언트의 오류 금액을 임의로 보정하려는 잘못된 코드 if ( client == 45 ) { sum[ 45 ] = sum[ 45 ] + 3.45; } else if ( ( client == 37 ) && ( numClaims[ client ] == 0 ) ) { sum[ 37 ] = 0.0; } - 초기화 오류 등으로 인한 무작위적 결함은 임시 코드로 막을 수 없으며, 코드가 지저분해지고 새로운 오류를 유발함.
- 잘못된 증상 수정 예시:
-
타당한 이유가 있을 때만 코드를 변경하라: 원인을 모른 채 임의로 변수 값을 바꿔가며 고치는 주술적인 프로그래밍을 지양하고, 결과에 대한 확실한 예측과 확신이 있을 때만 수정함.
-
한 번에 한 가지만 변경하라: 한 번에 여러 곳을 바꾸면 어떤 변경이 오류를 고쳤는지, 혹은 다른 부작용을 일으켰는지 추적하기 어려워짐.
-
수정한 내용을 검사하라: 코드를 고친 뒤 스스로 검토하거나 동료 리뷰를 거쳐 수정한 내용이 문제를 완벽히 해결했는지 확인함. 또한 전체 프로그램을 재실행하여 변경의 부수 효과를 검사함.
-
결함을 노출하는 단위 테스트를 추가하라: 테스트 케이스묶음에서 찾아내지 못했던 결함이라면 동일한 오류가 재발하지 않도록 이를 검증하는 테스트 케이스를 새로 작성함.
-
유사한 결함을 찾아라: 한 지점에서 발견된 패턴의 오류는 다른 위치에서도 발생했을 가능성이 높으므로, 문제의 근원적인 유형을 파악해 함께 수정함.
23.4 디버깅에서 심리학적으로 고려해야 할 사항
- 디버깅은 자신이 작성한 코드의 결함을 비평적으로 바라보는 엄격한 사고가 필요하므로 자아를 배제하고 객관적으로 가설을 검증해야 함.
심리적 고착이 디버깅 실명에 미치는 영향
- 예상하거나 친숙한 패턴만 보려는 심리적 고착 현상 때문에 단순한 오타나 구조적 오류를 놓치는 디버깅 실명이 발생함.
// 실제 작성된 코드 (괄호 누락) if ( x < y ) swap = x; x = y; y = swap; // 프로그래머가 착각하여 보게 되는 코드 if ( x < y ) { swap = x; x = y; y = swap; } - 가독성 좋은 명명 규칙과 포맷팅 습관은 심리적 고착에 의한 결함을 노출시키는 데 도움을 줌.
- 탐색 영역을 성급히 좁히다 결함 위치를 잘못 배제할 경우 디버깅이 정체될 수 있음.
“심리적인 거리”가 어떻게 도움을 줄 수 있는가?
- 두 항목을 얼마나 쉽게 구별할 수 있는지를 나타내는 ‘심리적 거리’가 가까울수록 변수나 루틴의 오타를 발견하기 어려움.
-
식별자 생성 시 서로 명확히 구별되는 완전히 다른 이름을 사용해 심리적 거리를 충분히 확보해야 함.
첫 번째 변수 두 번째 변수 심리적 거리 stopptstcppt거의 알아보기 힘들다. shiftrnshiftrm거의 없다. dcountbcount좁다. claims1claims2좁다. productsum멀다.
23.5 디버깅 도구 – 분명한 도구와 그렇지 않은 도구
- 쉽게 구할 수 있는 디버깅 도구를 적극적으로 활용하여 디버깅 과정의 정교하고 반복적인 작업을 체계적으로 처리함.
소스코드 비교 도구
- Diff 등의 소스코드 비교 도구를 활용해 최근 변경 사항이나 기억나지 않는 지점을 손쉽게 확인함. (WinMerge, Code Compare)
- 정상 작동하던 이전 버전과 문제 발생 버전을 비교하여 결함 유발 지점을 효과적으로 식별함.
컴파일러 경고 메시지
- 컴파일러 경고 수준을 최고로 설정하고 모든 경고를 수정해야 함.
- 경고를 무시하거나 끄는 행위는 오류를 눈에서 가릴 뿐 근본적인 문제를 해결해주지 못함.
- 컴파일러 경고를 언어적 특성을 새로 배울 수 있는 학습의 기회로 받아들여야 함.
- 경고를 오류로 취급하는 컴파일러 옵션을 설정하여 경고의 심각성을 인지하고 빌드/링크 과정에서 확실히 검증함.
- 프로젝트 전반에 표준화된 make 파일이나 빌드 스크립트를 적용해 팀 전체가 동일한 컴파일러 설정을 유지함.
확장된 문법과 논리 검사
- C의 lint 유틸리티처럼 컴파일러보다 코드를 더욱 상세하고 엄격하게 검사하는 정적 분석 도구를 활용함.
실행 프로파일러
- 프로파일러를 통한 실행 시간 분석으로 기대와 다른 비효율적인 코드 패턴이나 의외의 결함 지점을 찾아냄.
- 예: 메모리 관리 성능 개선을 위해 해시 테이블을 도입하려 했으나, 프로파일링을 통해 원인이 검색 알고리즘이 아닌 메모리 할당 루틴의 결함임을 파악함.
테스트 프레임워크/비계
- 의심스러운 코드 모듈을 분리한 뒤 전용 테스트 코드를 작성해 독립된 환경에서 격리 테스트를 수행함.
디버거
- 오늘날 디버거는 중단점 설정, 메모리 및 데이터 구조 조사, 생성된 어셈블리어, 변수 값 실시간 수정 등 강력한 기능을 제공함.
- 도구의 남용이나 감에 의존한 무작위 수정을 경계해야 하지만, 두뇌를 활용한 논리적 추론과 디버거 도구 활용을 병행하는 것이 가장 효과적임.
체크리스트: 디버깅 관련 사항
결함을 찾아내기 위한 기법
- 가설을 세우는 데 사용할 수 있는 모든 데이터를 사용했는가?
- 오류를 생산하는 테스트 케이스를 개선했는가?
- 단위 테스트 스위트에서 테스트를 조사했는가?
- 사용할 수 있는 도구를 활용했는가?
- 오류를 여러 가지 방법으로 재현했는가?
- 더 많은 가설을 세우기 위해 더 많은 데이터를 만들었는가?
- 부정적인 테스트의 결과를 사용했는가?
- 가능한 가설에 대해 브레인스토밍했는가?
- 연습장에 시도할 목록을 작성해 두었는가?
- 코드에서 의심스러운 부분의 범위를 좁혔는가?
- 이전에 결함이 있었던 클래스와 루틴을 의심해보았는가?
- 최근에 변경된 코드를 검사했는가?
- 의심스러운 코드 부분을 확장했는가?
- 점증적으로 통합했는가?
- 일반적으로 자주 발생하는 결함을 검사했는가?
- 문제에 관해 다른 사람에게 이야기해보았는가?
- 문제로부터 떨어져 휴식을 취했는가?
- 신속하고 지저분한 디버깅을 위한 최대 시간을 설정했는가?
- 무차별 대입 기법에 대한 목록을 작성하여 사용했는가?
구문 오류를 위한 기법
- 컴파일러 메시지에 있는 줄 번호를 믿지 않는가?
- 컴파일러의 오류 메시지를 문장 그대로 믿지 않는가?
- 컴파일러의 두 번째 메시지를 무시하고 첫 오류에 집중했는가?
- 코드 일부를 제거하며 분할 정복했는가?
- 잘못된 주석과 인용 부호(따옴표)를 찾기 위해서 구문 인식 편집기를 사용했는가?
결함을 수정하기 위한 기법
- 수정하기 전에 문제를 완벽히 이해했는가?
- 문제만 이해하지 않고 주변 프로그램을 함께 이해했는가?
- 결함 분석을 확실히 확인했는가?
- 성급히 고치려 하지 않고 긴장을 풀었는가?
- 자신의 해결책이 맞는지 확인하기 위해 휴식을 취했는가?
- 수정 전에 원본 소스코드를 저장해 두었는가?
- 증상이 아닌 근본적인 문제를 해결했는가?
- 한 번에 한 가지만 변경했는가?
- 수정한 내용을 검사했는가?
- 결함을 노출하는 단위 테스트를 추가했는가?
- 유사한 결함을 함께 찾아보았는가?
디버깅에 대한 일반적인 접근 방법
- 디버깅을 프로그램과 실수, 코드 품질, 문제 해결 방법에 대해 배우는 기회로 활용하는가?
- 시행착오적이고 미신에 의한 디버깅 접근 방법을 피하는가?
- 오류가 자신의 실수라고 기본적으로 가정하는가?
- 간헐적인 오류를 안정화하고 결함을 찾기 위해 과학적인 방법을 사용하는가?
- 매번 같은 접근법 대신 다양한 기법을 적용하는가?
- 수정 사항이 정확한지 항상 검증하는가?
- 컴파일러 경고 메시지, 실행 프로파일링, 테스트 프레임워크, 디버거 등을 유용하게 활용하는가?
참고 자료
- David Agans, Debugging: The Nine Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems : 모든 언어 및 환경에 적용 가능한 일반적 디버깅 원칙 제시.
- Glenford J. Myers, 소프트웨어 테스팅의 정석 (7장) : 디버깅 주제를 전적으로 다룸.
- Eric Allen, Bug Patterns In Java : 자바 기반의 버그 패턴 규명 및 과학적 디버깅 방법 소개.
- John Robbins, Debugging Applications for Microsoft .NET and Microsoft Windows & Everett N. McKay, Mike Woodring, Debugging Windows Programs : 일반적 디버깅, 어설션 활용, 버그 예방 습관 안내.
요점 정리
- 디버깅은 소프트웨어 개발의 성패를 좌우함. 가장 좋은 방법은 결함을 처음부터 피하는 것이지만, 디버깅 능력에 따라 개발자 간 성과 차이가 10배 이상 나므로 기술 향상에 투자할 가치가 있음.
- 오류를 찾고 수정하는 데 체계적으로 접근하는 것은 성공에 매우 중요하다. 각 테스트가 한 걸음 더 나아갈수 있도록 디버깅에 초점을 맞춘다. 과학적인 디버깅 방법을 사용하라.
- 문제를 수정하기 전에 문제의 원인을 이해하라. 오류의 원인을 임의로 추측하고 수정하면 프로그램은 수정 하기 전보다 더 나쁜 상태가 될 것이다.
- 컴파일러의 경고를 가장 까다로운 수준으로 설정하고 컴파일러가 보고하는 오류를 수정하라. 분명한 오류를 무시하면 미묘한 오류를 수정하기가 어려워진다.
- 디버깅 도구는 소프트웨어 개발에 도움이 되는 강력한 도구다. 그러한 도구를 찾아서 사용하는 동시에 자신의 두뇌를 사용하는 것도 잊지 말라.