Effective Debugging/Chapter 5. 프로그래밍 기법

created : 2020-04-07T11:44:40+00:00
modified : 2020-09-26T14:28:09+00:00

Item 38. 의심스런 코드를 검토하고 손으로 실행해보기

  • 코드를 작성할 때 흔히 저지르는 실수를 하지 않았는지 살펴본다.
  • 코드가 정확하게 동작하는지 직접 손으로 실행해서 확인한다.
  • 복잡한 자료구조는 그림으로 쉽게 표현한다.
  • 복잡한 코드를 쉽게 표현할 때는 종이나 화이트보드의 공간을 넉넉히 확보하고 색깔도 적절히 활용한다.
  • 실제 물체를 활용하면 문제에 좀 더 집중할 수 있다.

Item 39. 동료 검토하기

  • 자신이 작성한 코드를 고무 오리에게 설명한다.
  • 동료 검토를 비롯한 코드 리뷰 과정을 거친다.
  • 다양한 개체가 엮인 코드에서 발생한 오류를 디버익할 떄는 역할 놀이 방식을 적용한다.

Item 40. 디버깅 기능 추가하기

  • 작성하는 프로그램에 디버그 모드로 진입하는 옵션을 추가한다.
  • 프로그램의 상태를 조작하고, 연산의 수행 과정을 로그로 남기고, 런타임 복잡도를 줄이고, 사용자 인터페이스 ㅐ비게이션 과정을 건너뛰고, 복잡한 자료구조를 화면에 표시하는 명령을 추가한다.
  • 임베디드 장치나 서버를 디버깅할 수 있도록 명령줄 인터페이스, 웹 인터페이스, 시리얼 인터페이스 등을 제공한다.
  • 외부 오류 상황을 재현할 수 있는 디버그 모드 명령을 추가한다.

Item 41. 로그 남기기

  • 로그를 남기는 문장을 추가하여 지속적으로 관리할 수 있는 형태의 디버깅 infrastructure를 구축한다.
  • 로깅 프레임워크를 처음부터 새로 만들지 말고 가급적 기존에 나와 있는 것을 활용한다.
  • 로그로 남기고 싶은 주제와 세부 사항을 로깅 프레임워크에서 적절히 설정한다.

Item 42. 단위 테스트 사용하기

  • 단위 테스트를 통해 의심 가는 루틴을 검색하여 오류가 발생한 지점을 정확히 찾아낸다.
  • 단위 테스트 작업의 효율을 높이도록, 적절한 단위 테스팅 프레임워크를 골라서 프로그램에 단위 테스트를 수행하는 코드를 추가하고, 자동으로 단위 테스트를 수행하도록 설정한다.

Item 42. Assertion 사용하기

  • 프로그램이 시작하는 지점에서 CPU의 아키텍쳐 종류에 관련된 속성을 검사하는 경우
  • 루틴의 시작 시점에서 전달된 매개변수의 타입이 정확한지 그리고 유효한 값인지, 적절한 값으로 들어왔는지 등을 검사하는 경우
  • 루틴이 끝나는 지점에서 겨로가가 정확한지 검사하는 경우
  • 자주 호출되는 복잡한 메서드의 시작과 끝 부분에서 클래스의 상태가 일관성 있게 유지되는지 검사하는 경우
  • 정상적으로 동작해야할 API 루틴을 호출한 뒤에 진짜 오류가 없는지 확인하는 경우
  • 소프트웨어에서 사용할 자원을 불러온 뒤에 제대로 처리됐는지 확인하는 경우
  • switch문의 default 케이스에서 assertion으로 처리하지 않는 나머지 모든 경우를 다루고 있는지 확인할 때
  • 자료구조를 초기화한 후에 예상했던 값을 유지하고 있는지 확인할 때

기억할 사항

  • 단위 테스트에 assertion을 가미하면 오류가 발생한 지점을 더욱 정확히 찾아낼 수 있다.
  • 복잡한 알고리즘을 디버깅할 때 assertion을 활용하여 선행 조건과 불변 속성과 후행 조건을 검사한다.
  • 문서화를 위해 assertion을 사용하면 코드를 이해하기 쉬워지기 때문에 나중에 문제가 발생하여 디버깅하거나 테스트할 떄 도움된다.

Item 44. 코드를 바꿔보면서 검증하기

다음과 같은 질문을 해보기

  • 이 루틴의 매개변수로 null이 전달될 수 있는가?
  • 변수의 값이 999ms 이상일 경우에도 코드가 정확하게 작동하는가?
  • 이 루틴에 진입하면서 lock이 걸리면 경고 메시지가 로그에 기록되는가?
  • 현재 나타난 문제가 메서드들을 호출하는 순서와 관련이 있는가?
  • 현재 사용하는 API보다 더 잘 작동하는 API는 없는가?

기억할 사항

  • 코드에 나온 값이 정확한지 확인하기 위해 다른 값으로 직접 변경해본다.
  • 어떤 방식으로 코드를 작성해야 할지에 대한 가이드를 찾을 수 없다면 직접 여러 방법으로 구현해서 비교한다.

Item 45. 정상적인 코드와 문제가 발생한 코드의 차이점 줄이기

  • 오류를 발생시킨 요소를 찾으려면 정상 작동하는 예제와 일치할 때까지 코드를 점진적으로 가지치기하거나, 반대로 정상 작동하는 예제가 오류가 발생한 코드와 비슷해질 때까지 수정하는 방식으로 진행한다.

Item 46. 의심스런 코드 간소화하기

  • 코드에 존재하는 오류가 더욱 잘 드러나도록, 커다란 코드 블록 덩어리를 잘 선별하여 가지치기한다.
  • 코드의 실행 상태를 살펴보거나 테스트하기 쉽도록 복잡한 문장이나 함수를 잘게 쪼갠다.
  • 복잡하고 버그가 많은 알고리즘을 좀 더 간결한 알고리즘으로 대체하는 방법을 찾아본다.

Item 47. 의심스런 코드를 다른 언어로 작성해보기

  • 더 이상 오류를 해결하기 힘들 떄는 코드에서 발생할 수 있는 오류를 최대한 줄일 수 있도록 좀 더 표현력이 뛰어난 언어로 다시 작성해본다.
  • 풍부한 디버깅 기능을 활용할 수 있도록 프로그래밍 환경을 개선한 뒤 오류가 있는 코드를 여기서 실행해본다.
  • 제대로 작동하는 대체 구현을 확보했다면 이를 직접 활용하거나 기존 코드의 오류를 찾아내기 위한 참고용으로 활용한다.

Item 48. 의심스런 코드의 가독성과 구조 향상시키기

  • 산탄총으로 수술하기(shortgun surgery) 패턴
    • 변경해야 할 필드와 메서드를 모두 동일한 클래스에 옮긴 다음 이들이 서로 일관성 있게 구성되어 있는지 확인해본다.
  • 데이터 덩어리(data clumps) 패턴
    • 매개변수나 반환값을 표현할 떄 하나의 클래스에 담아서 전달한다.
  • 기본적으로 제공하는 자료형만으로 값을 표기하지 말고 새롭게 정의한다.
  • 인터페이스를 여러가지 방식으로 표현할때 일관성있게 비슷한 스타일로 표현한다.
  • 루틴에 코드를 너무 많이 담지 않는다.
  • 쓸데없이 친한코드를 제거한다.
    • account.getWoner().getName()account.getOwnerName()이라는 위임 메서드로 표현한다.
  • 데드 코드와 추측으로 일반화한 코드를 제거한다.

기억할 사항

  • 에러 패턴이 눈에 띄기 쉽도록 코드의 포맷을 일관성 있게 맞춘다.
  • 잘못 작성되거나 쓸데없이 복잡한 코드 속에 숨어 있는 버그가 드러나도록 코드를 리팩토링한다.

Item 49. 버그의 증상이 아닌 원인 고치기

버그의 증상만을 고치는 예시

  • 널 포인터 참조 값 사용 피하기

    if (p != null) p.aMethod();

  • 0으로 나누기 피하기

    if (nVehicleWheels == 0) return weight; else return weight / nVehicleWheels;

  • 잘못된 숫자를 논리적인 버위에 억지로 집어넣기

    a = surfaceArea() if (a < 0) a = 0;

  • 잘려진 성(surname) 고치기

    if (surname.equals(“Wolfeschlegelsteinha”)) surname = “Wolfeshlegelsteinhausenbergerdorff”;

위와 같이 증상만을 없앨 경우 문제

  • 몇 가지 기능을 끊어 버리는 방식으로 버그를 해결하면 더욱 찾기 힘든 버그가 나타날 가능성이 높아진다.
  • 근본 원인을 수정하지 않으면 버그로 인해 발생하는 현상 중에서 잘 드러나지 않는 것들은 그대로 남아 있기 때문에 나중에 다시 나타날 때는 더욱 교묘하게 숨은 형태로 나타날 수 있다.
  • 프로그램의 코드가 쓸데없이 복잡해져서 이해하거나 수정하기 힘들어진다.
  • 임시방편으로 수정하면 버그로 인해 나타나는 현상이 사라져서 근본 원인을 찾기가 더 어려워진다. 이를테면, 근본 원인의 실마리가 되는 충돌 현상을 숨겨버리면 디버깅이 힘들어진다.

기억할 사항

  • 절대로 코드의 증상만 피해가는 방식으로 수정하지 말고 근본 원인을 찾아서 고친다.
  • 특수한 경우에 대한 문제만 해결하지 말고 가능하면 복잡한 경우의 수를 일반화한다.