Debug It! 실용주의 디버깅 (원제: Debug It!: Find, Repair, and Prevent Bugs in Your Code)을 읽고 주요한 부분을 발췌 요약한다.


1부는 ‘문제의 핵심’이다.

1장. 구조적인 접근법

20p. 디버깅은 단순히 ‘버그를 없애는 것’ 그 이상이다. 효과적인 디버깅의 단계는 다음과 같다.

  • 소프트웨어가 왜 이상하게 작동하는지 알아낸다.
  • 문제를 수정한다.
  • 다른 곳이 깨지지 않게 한다.
  • 코드의 전반적 품질(가독성, 구조, 테스트 커버리지, 성능 등)을 유지하거나 향상시킨다.
  • 같은 문제가 다른 부분에는 없는지 살펴보고, 재발 방지책을 마련한다.

이중 첫번째 항목인 ‘문제의 근본 원인을 찾아내기’가 무엇보다 중요하고 다른 항목의 주춧돌이 된다.

23p. 이걸 더 간단하게 만들면 재현reproduce -> 진단dignose -> 수정fix -> 반영reflect 이다.

25p. 핵심 가이드라인.

  • 무엇을 찾으려는지 정확히 알고 나서 진행하라. 버그 리포트가 있더라도 그게 100% 옳다는 보장은 없다.
  • 한 번에 한 문제만 해결하라. 하나를 잡으려다 다른 것에 영향을 미칠 수 있다.
  • 혼자 하지 마라. 다른 팀원에게 ‘혹시 전에 이런거 본 사람?’ 이라고 질문하는 건 거의 비용이 안 들지만 엄청난 삽질을 막아줄 수 있다.

2장. 재현

30p. 버그 리포트에 적혀 있거나 암시된 방법을 그대로 따라해봐라. 버그 리포트에 충분한 정보가 담겨있지 않다고 ‘재현 불가능’ 딱지를 붙여 되돌리지 말고 그냥 해봐라. 노력도 별로 안들고, 실제로 문제가 바로 생길 수도 있다.

31p. 성공적 재현은 제어에 달려있다. 제어해야 할 것은 다음 세 부분이다.

  • 소프트웨어 자체: 버전을 확인하라.
  • 실행 환경: 특정 하드웨어나 원격 서버 등 외부 시스템과의 상호작용.
  • 입력 값: 버그가 발생한 사용자의 설정과 똑같이 만들어라.

35p. 입력 제어를 위해 필요한 정보가 부족하다면 두 가지 방법을 선택할 수 있다. 입력 추론과 입력 기록.

  • 입력 추론: 문제가 실제로 있다고 가정하고, 그 문제가 발생하기 위해 필요한 조건을 역으로 생각해본다. 입력 범위의 경계값이나, 분기를 다르게 만드는 입력값을 위주로 실험해본다.
  • 입력 기록: 로그로 사용자의 입력을 미리 기록해둘 수 있다. 로그는 유용한 도구지만 과용될 수 있기 때문에, 항상 로그가 최신 코드를 반영하도록 하고 로그를 위한 로그는 남기지 말자. 소프트웨어에 직접 로그를 추가하지 않아도 클라이언트와 서버 사이의 로깅 프록시를 둠으로써 트래픽을 기록하면 많은 걸 얻을 수 있다. 실제 사용 방식을 로깅해두면 현실적인 스트레트 테스트를 하는 것에도 도움이 된다.

45p. 재현 방법 다듬기.

  • 피드백 루프 최소화: 실험할 때는 실험 방법을 최대한 효과적으로 만들자. 가장 짧으면서도 에러가 없는 수정-컴파일-실행-재현 주기를 만들어야 한다.
  • 최대한 단순하게: 처음 찾은 재현 방법은 최적이 아닐 것이다. 재현에 불필요한 부분을 찾아 제거해야 한다. 이 때 분할 정복법이 도움이 된다.
  • 필요 시간 최소화: 재현하는데 시간이 걸리는 버그 중 많은 것이 자원 누수 때문이다. 자원 누수가 의심스럽다면 미리 자원을 잡아놔서 빠르게 자원 고갈 상태를 만들거나, 자원 할당이 실패했을 때 작동하는 다른 함수를 가짜로 호출하여 재현 상태를 만들 수 있다.
  • 비결정성 제거: 밑에서 설명.
  • 자동화: 자동 테스트 프레임워크를 사용하라. 유저의 로그를 기록해뒀다가 재생하는 것도 좋은 방법이다.

49p. 재현에서 문제되는 것이 비결정적 버그다. 하지만 소프트웨어에 비결정성을 일으키는 원인은 몇 개 없다.

  • 초기화되지 않은 내부 상태: 디버거로 메모리를 특정 값으로 초기화해보자.
  • 외부 시스템과 상호작용: 외부 시스템을 직접 제어하는 건 어렵다. 외부 시스템을 디버깅용 다른 시스템이나 테스트 대역(test double)으로 바꿔보자.
  • 임의성: 대부분의 난수는 실제로는 유사난수다. 테스트할 때는 시드값을 동일하게 주자.
  • 다중 스레드: 어렵다. 가능하다면 싱글스레드에서 실행하자. 강제로 스레드를 멈추는 sleep() 따위를 이용해야 할 수도 있다.

55p. 진단 과정 최적화 예시.

  • 대용량의 입력 파일을 통해 문제를 재현했다.
  • 어떤 모듈이 연관되어있는지를 알아낸다.
  • 입력 파일 요소 중 어디서 버그가 생기는지 잡아내 파일 용량을 줄인다.
  • 서드파티 서버와 통신하는 서브시스템을, 항상 정해놓은 응답만 리턴하는 스텁stub으로 바꾸면 문제를 100% 재현할 수 있음을 알게 됐다.
  • 정확히 어떤 함수가 문제인지 찾았다. 특정 파라미터로 이 함수를 호출해 버그를 재현하는 단위 테스트를 만든다.

56p. 버그를 정말로 재현할 수 없을 때 할만한 일들.

  • 정말 버그가 있는가? 실제로는 버그가 없을 수도 있다. 하지만 대부분은 모든 가능성을 따져보지 않은 것이다. 만약 이 버그에 대해서는 ‘재현 안 됨’ 같은 딱지를 붙이게 되더라도, 확실한 건 사용자가 어떤 다른 문제를 겪었었다는 것이다. 시간을 내서 사용자와 직접 의사소통해보자.
  • 같은 영역에서 재현 가능한 다른 버그가 있다면 그것부터 고쳐보자. 이렇게 하면 1) 해당 영역 코드를 리팩토링할 기회가 생긴다. 2) 이 문제를 제거하고 나면 원래 찾던 문제를 더 명확히 볼 수 있다. 3) 재현 가능한 문제를 작업하다보면 관련 코드를 더 잘 이해할 수 있다.
  • 다른 사람에게 조언을 구하자. 다른 개발자도 좋고, 테스트 팀도 좋고, 버그를 리포트한 사용자도 좋다.
  • 사용자 커뮤니티를 활용하자. 특히 오픈소스 커뮤니티는 디버깅에 일반 사용자보다 훨씬 적극적으로 참여한다.

3장. 진단

61p. 디버깅할 때 가져야 하는 마음가짐은 범죄 수사나 과학 실험에 필요한 마음가짐과 비슷하다.

**64p. ** 실험할 때의 자세.

  • 실험은 무엇인가를 밝혀내야 한다. 이 실험으로 알 수 있는 결과가 진단에 도움이 되는가? 버그에 대한 핵심 가설을 세우고 그것을 증명/반증하기 위한 실험을 하라.
  • 한 번에 하나만 고쳐라. 조작변인 때문에 통제변인이 변했다는 인과관계가 확실해야 한다. 한 번에 여러 개를 고치면 시간을 절약할 수 있을 것 같지만 실제로는 실험 결과를 망칠 위험만 키운다. 고친 것으로 작동이 변한 것을 확인했다면, 다시 고친 것을 원래대로 되돌려보고 작동도 원래대로 돌아가는지 검증하라.
  • 시도한 것을 기록하라. 뭘 했는지 까먹지 않을 정도로만. 가끔씩 지금까지 해봤던 것들돠 거기에서 배운 점들을 복습하자.
  • 아무것도 무시하지 말라. 작동이 이상할 때는 무시하지 말고 멈춰라. 소프트웨어가 뭔가 이야기해주려고 하고 있다. 예상치 못한 일이 생겼다는 건 가설에 틀린 곳이 있다는 뜻이다. 틀린 곳을 찾아라.
  • 뭐든지 우리가 이해하지 못한 부분은 잠재적 버그다. 현재 버그와 관련이 없음을 확인하고 나면, 그런 부분은 한쪽에 치워놓되 기록해놓고 까먹지 말자. 우연히 발견한 것들이 꼭 수정해야 할 문제일 때가 많다.

69p. 문제 원인을 찾는 기법들.

  • 진단 코드instrumentation: 소프트웨어 작동에는 영향을 끼치지 않으면서 왜 그렇게 작동하는지에 대한 통찰을 제공하는 코드. 가장 중요한 진단 코드는 로그다. 언어에서 제공하는 기능을 최대한 활용하여, 데이터를 분석하고, 조건을 테스트해보자. 진단 코드는 디버그 후에 제거하는 경우가 많은데, 때로는 진단 코드를 사용하여 ‘스스로를 디버깅하는 소프트웨어’로 만들 수도 있다. 이건 추후에 설명.
  • 분할 정복: 소스코드 일부를 주석처리하거나, 한 부분에 검사 코드를 넣어놓거나 해서 탐색 범위를 줄인다. 소프트웨어가 모듈화되어있고 모듈을 껐다 켰다 할 수 있다면 그걸로도 탐색 가능하다. git 커밋을 옮겨다닌 것으로도 가능.
  • 사람에게 배우기: 같은 팀원에게 물어보고, 검색하라.
  • 오캄의 면도날: 여러 가능성 중 뭘 먼저 확인할지 선택해야 한다면 가장 간단한걸 먼저 확인하라.

77p. 실제 디버깅하면서 자주 하는 실수들.

  • 맞는 걸 고치고 있는가? 혹시 고친 파일이 아닌 다른 파일을 컴파일한 것 아닌가? 컴파일 제대로 해놓고 다른 바이너리를 실행한 것 아닌가? … 이런 함정은 의외로 흔하다. 무조건 프로그램이 죽게 되거나 그에 준하는 명백한 실패 코드를 추가하면 내가 맞는 걸 고치고 있는지 알 수 있다.
  • 잘못된 가정이 있지 않은가? 뭔가에 가로막혀 있다면 내가 어떤 가정을 세우고 있는지 다시 비판적으로 점검하자.
  • 원인이 여러 개는 아닌가? ‘이건 도저히 말이 안 되는데’ 라는 생각이 들면 이 가능성을 의심해봐야 한다. 원인이 여러 개이면 문제를 격리하여 하나의 원인에만 의존하는 버그를 재현하는 방법을 찾는다. 또는, 같은 영역에 있는 것으로 추측되는 다른 버그를 먼저 확인한다.

82p. 디버깅에 도움이 되는 기법들.

  • 다른 사람에게 도움 요청하기: 문제를 설명하다 보면 생각 정리에 도움이 된다.
  • 역할극: 여러 독립 시스템끼리 상호작용하는 문제를 설명하고 검사하기 좋다.
  • 문제 묵혀두기: 때론 잠재의식에게 맡긴 채 휴식하는 것도 필요하다. 쉬다가 뭔가가 떠올랐을 때는 잘 기록해둔다. 다만 너무 오래 쉬지는 말자.
  • 아무거나 바꿔보기: 정말 감이 안 올 때는 아무거나 바꿔보고 예상 결과와 다르게 나오는 걸 찾아본다.
  • 셜록 홈즈 법칙: 불가능한 것을 전부 제거하고 나면 아무리 불가능해 보이는 것이라고 남아있는 것이 진실이다.

4장. 수정

92p. 목표는 세 가지다. 문제 수정, 회귀 방지, 코드의 전반적 품질을 유지하거나 향상시키기.

94p. 기본적인 프로세스.

  • 기존 테스트를 실행해서 모두 통과하는지 확인한다.
  • 새 테스트를 추가하거나 기존 테스트를 수정해 버그를 보여준다(즉 실패시킨다).
  • 버그를 수정한다.
  • 제대로 수정했는지 확인한다(실패하던 테스트가 이제 실패하지 않는다).
  • 회귀가 생기지 않음을 확인한다(이전에 성공하던 테스트가 모두 성공한다).

96p. 증상이 아닌 원인을 고친다.

  • 버그가 아키텍처 깊숙히 있거나, 제대로 고치려면 광범위하게 수정해야 하는데 위험할 때, 또는 하위 호환성이 깨질것 같을 때 등 ‘땜질 코딩’의 유혹에 빠지고 싶을 때가 많다. 하지만 근본 원인을 고치지 않을 때마다 코드의 전반적 품질이 확 떨어지며, 이는 깨진 유리창을 더 만들어내는 효과도 있다. 근본 원인을 고치지 않는 것이 적당할 때도 있지만 항상 최후의 수단으로만 생각해야 한다.
  • 눈앞에 있는 문제를 고치는 것은 3가지 목표중 하나일 뿐이다. 회귀도 막아야 하고, 품질도 유지해야 한다. 근본 원인을 모른 채 수정하면 굉장히 무모한 일이다.

100p. 버그가 있는 코드를 작업하고 있다는 것 자체가 이 코드에는 좀 더 이해하기 쉽게 개선할 가능성이 높음을 의미한다. 리팩토링은 버그 수정만큼이나 중요하다. 하지만 버그 수정을 포함한 기능 변경은 리팩토링과 같이 하면 절대 안 된다. 그렇게 하면 리팩토링을 잘못한 것인지 기능이 변경되어 테스트 결과가 바뀌는 것인지 알 수 없기 때문이다.

5장. 반영

109p. 근본 원인 분석: 어떤 버그가 코드 안에 들어갈 수 있었다는 사실을 개발 프로세스의 어딘가에 무엇인가가 잘못됐다는 것을 보여준다. 정확히 언제? 왜?

  • 요구사항: 요구사항이 애매하거나, 이상하게 해석되거나, 잘못 이해되지는 않았는가?
  • 설계: 아키텍처나 설계에서 놓친 점은 없는가? 고려하지 못한 점, 허용해야 했던 부분은 없는가? 설계는 괜찮은데 구현을 제대로 못한 것은 아닌가?
  • 테스트: 해당 영역에 대한 테스트가 충분했는가? 테스트 자체에 문제는 없었나?
  • 구현: 코드를 작성하는 데 실수가 있었는가? 기초 기술의 어떤 부분을 잘못 이해하고 있었나?

110p. 재발 방지하기

  • 문제가 많이 생기는 영역, 자주 실수하는 부분, 같은 문제의 다른 예 등을 주의깊게 살핀다. 실수가 여기 하나에만 있을까? 다음에 다시 실수하지 않게 강제하는 방법은 없을까?
  • 문제가 반복해서 발생한다면 코드 구조나 인터페이스 때문일 수 있다.
  • 프로세스를 개선해보자. 요구사항 문서의 품질을 살피는 단계나, 설계 검토 과정을 도입해야 할 수도 있다. 코드 리뷰를 할 때 ‘자주 하는 실수 목록’을 보고 하게 하는 것도 좋다.

2부는 ‘큰 그림’에 대해 다룬다.

6장. 문제 발견

121p. 좋은 버그 리포트에는 무엇이 들어있을까?

  • 상세하고, 분명하며, 구체적이어야 한다. 진단해보기 전에는 어떤 정보가 관련있는지 알 수 없기 때문에 좋은 버그 리포트에는 필요한 것 이상으로 많은 정보가 담겨 있다.
  • 동시에 최대한 작고 유일해야 한다. 버그 재현에 꼭 필요한 정보만 담겨 있는 것이 좋다. 그리고 기존에 리포트된 버그라면 (새로 리포트를 만드는 게 아니라) 기존 리포트에 정보가 추가되어야 한다.
  • 환경과 설정 정보를 자동으로 수집해야 한다.

126p. 대부분의 고객은 뭐가 잘못되더라도 아무 얘기도 안해줄 것이다. 자기가 잘못했다고 생각할 수도 있고, 그냥 속으로 욕하며 소프트웨어를 재시작할 수도 있다. 간단한 버그를 피해가는 꼼수를 찾는다고 몇 시간을 쓸 수도 있다. 한 명이 버그를 리포팅했다면 같은 문제를 겪고도 아무 말 안하는 사람이 10~100명은 있다고 봐야 한다.

127p. 사용자의 멘탈 모델은 개발자의 멘탈 모델과 엄청나게 다르다. 사용자 입장에서 어떻게 보일지 생각해보자. 우리가 신뢰할 수 있는 ‘고객의 관찰’과, 고객의 멘탈 모델로부터 영향을 받은 ‘고객의 해석’을 분리할 수 있어야 한다.

129p. 사용자가 버그 트래킹 시스템을 쓸 수 있게 하자. 이 방법의 장점은:

  • 다른 사람의 리포트를 진지하게 받고, 대응해주고, 결국 해결하는 모습을 보여주어 다른 사용자들도 버그 리포트 작성이 헛되지 않으리라는 확신을 받는다.
  • 버그 리포트 전에 검색할 수 있게 하면 중복 리포트된 버그가 훨씬 줄어든다.
  • 다른 사람의 버그 리포트를 읽은 사용자가 문제를 해결할 수 있는 중요한 기억이나 통찰을 떠올릴 수 있다.
  • 버그 리포트 형식을 잘 모르는 사람이 다른 사람의 것을 보고 학습할 수 있다.

132p. QA와 고객지원 팀을 존중하고, 협력하라. 때론 그 안에서 일해보면서 고객의 소리를 직접 느껴봐라.

7장. 실질적인 무관용

135p. 빠른 버그 수정은 늦은 버그 수정보다 훨씬 좋은 전략이다. 다음 2가지 원칙에 따른다.

  • 개발하는 동안 버그를 찾을 수 있는 과정(테스트, 코드 리뷰, 배포)을 계속 반복한다.
  • 버그 수정의 우선순위가 가장 높다.

이렇게 하는 목적은 소프트웨어에 있는 버그 갯수를 최대한 작게 유지하는 것이다. 이는 소프트웨어의 불확실성을 줄여준다. 버그를 찾아보기 전에는 발견 안 된 버그가 얼마나 남아있는지 알 수 없으며, 실제로 고쳐보기 전에는 얼마나 걸리는지도 알 수 없다. 따라서 일정 조정이 어렵다. 버그를 나중에 고치다 보면 겉보기에는 진도가 잘 나가는 것처럼 보이겠지만 사실은 기술 부채만 늘어나게 된다.

137p. 버그를 고치는 데 걸리는 시간 추정하기.

  • 기본적으로는 불가능하다. 진단이 끝나고 나면 수정에 얼마나 걸릴지 추정할 수 잇겠지만, 대부분의 버그가 진단하는 데 가장 시간이 오래 걸리기 때문에 큰 의마가 없다.
  • 하지만 지금까지 고친 버그에 대한 통계 자료를 만들 수는 있다. 지난 주에 평균 20개의 버그를 잡았다면 이번 주도 그러리라 예상하는 것. 실제 버그 수정이 얼마나 걸렸는지를 측정해두고 나중에 예측할 때 자료로 삼는다.

138p. 깨진 창문 없애기.

  • 소프트웨어를 작성하고 유지보수하는 것은 엔트로피와의 끝없는 전쟁이다.
  • 낮은 품질은 전염되기 쉽다. 유일하면서 확실한 치료법은 버그를 발견하자마자 근절하는 것이다.
  • 목표는 항상 버그의 갯수를 0에 가깝게 유지하는 것이다. 이런 접근법을 깨진 창문 없애기라고 한다.
  • 스프린트로 지친 개발자에게 깨진 창문 없애기 정책을 따르기란 불가능하게 보일 수도 있다. 하지만 버그를 최대한 빨리, 개발 첫 날부터 찾는다면 절대 불가능하지 않다. 그렇게 하면 남아있는 버그(= 알고 있는 버그 + 잠재적 버그)의 갯수가 걷잡을 수 없이 늘어나는 일은 없을 것이다.

139p. 디버깅의 마음가짐.

  • 한쪽 극단에서는 버그를 피할 수 없는 것으로 받아들이고 두려워하지 말자고 한다. -> 버그를 허용해버리게 된다.
  • 반대쪽 극단에서는 버그를 완벽하게 없애기 위해 노력한다(무관용, Zero tolerance). -> 사실상 불가능하다. 그리고 피할 수 없는 버그가 나올 때마다 실패한 것처럼 느끼게 될 수도 있고, 비난하는 문화가 생길 수도 있다.
  • 가장 생산성이 좋은 마음가짐은 실질적 무관용이다. 굉장히 무관용에 가깝되 실용주의를 가미한 것이다.
  • 버그 없는 소프트웨어를 달성할 수 있는 궁극적 목표로 두고, 목표에 다가갈 수 있게 최대한 노력한다. 버그가 생겼다면 교훈을 얻어 재발하지 않게 할 수 있는 행동을 다한다. 하지만 이 동안에도 얼마나 궁극적 목표에 가깝게 도달할 수 있는 것인지에 대한 현실적 견해를 유지해야 한다.
  • 목표에 도달하지 못했다고 해서 자책하거나 책임 소재를 따지면 안된다. 어떤 버그는 피할 수 없다는 것을 알고, 버그가 있더라도 최대한 소프트웨어가 견고하게 실행될 수 있게 만들어야 한다.

141p. 품질 결함의 수렁에서 빠져나오기. 엄청나게 버그가 많은 코드에서, 어떻게 하면 빠져나올 수 있을까? 우선, 즉효약은 없다. 시간과 노력, 헌신이 필요하다. 가장 확실한 해결책은 모든 작업은 품질 문제가 해결된 다음에만 진행될 것이라고 선언하는 것이다. 하지만 이런 순간은 오지도 않을 뿐더러 어떤 조직도 좋아하지 않을 것이다. 그러면 어떻게 해야 할까.

  • 더 나빠지지 않게 하기: 기존의 모든 코드를 표준에 맞추진 못하더라도, 새로 작성하는 코드는 표준에 맞게 한다. 만약 시초가 서있지 않다면 기초부터 세운다. 기초란 소스 관리 시스템, 빌드 자동화, 테스트 자동화, 일일 빌드 및 통합/배포 시스템을 뜻한다.
  • 더러운 코드로부터 깨끗한 코드 격리하기: 잘 작성되고 테스트가 잘 되어있는 코드와 ‘더러운’ 코드 사이의 경계를 분명하게 해준다. 그리고 기회가 날 때마다 이런 경계를 오래된 코드에까지 넓힌다. 오래된 코드를 작업할 때는 수정 중인 버그나 그 외 건드린 것에 대해 테스트를 작성한다.
  • 버그 선별: 버그 목록을 검토하고, 버그가 미칠 수 있는 영향을 공유하고, 적당한 우선순위를 매긴다.
  • 버그 대청소: 상대적으로 짧은 기간 동안 팀 전원이 다른 일은 다 제쳐두고 버그만 잡는다. 우선순위와 상관없이 최대한 버그를 많이 잡는 게 목적이다.

146p. 테스트가 없는 코드를 리팩토링하는 방법: 없다. 먼저 테스트를 작성하자.


3부는 ‘디버깅 비급’이다.

8장. 특수한 경우

151p. 기존 릴리즈를 패치하는 핫픽스를 낼 때는, 근본 원인을 고치는 것보다는 리스크를 줄이는 데 집중한다. 이 때는 개발 버전에 있는 버그도 고쳐야 하는데, 개발 버전에서는 똑같이 버그를 픽스하는 게 아니라 근본적인 원인을 찾아 고쳐줘야 한다.

핫픽스가 일상처럼 벌어진다면 이런 문제가 있을 수 있다. 프로세스를 바꾸자.

  • 릴리즈 사이의 기간이 너무 긴 것 아닌가?
  • 이전 릴리즈에서 ‘안 넘어오는’ 사용자가 있는 것 아닌가? 이 사용자들이 업그레이드하게 만드려면 무엇을 해야 할까?
  • 버그가 너무 많아 대처하기 어려운 것이라면 품질 결함의 수렁에서 빠져나오기를 참고.

153p. 수정하려는 부분에서 호환성 문제가 생길 수 있을 때, 높은 품질로 문제를 고치는 것과 동시에 하위 호환성 문제로 발생하는 고통을 최소화해야 한다. 이는 동시에 충족하기는 어렵고, 결국은 타협해야 하는데 어떻게 타협하는 게 좋을까.

  • 마이그레이션 방법을 제공한다.

  • 호환성 모드를 구현한다. 이건 유저와 개발자 모두에게 비용이 많이 드는 해결책이다.

  • 곧 deprecate된다는 것을 사전에 경고한다.

  • 버그를 고치지 않는다. 좋진 않지만 가끔은 이게 실용적일 때도 있다.

158p. 병렬 소프트웨어의 버그를 고치기.

  • 스레드 간의 상호 작용을 간단하게 유지한다. 스레드가 잘 정의된 몇 가지 방법으로 잘 정의된 위치에서만 상호작용한다면 디버깅할 때도 똑같이 보장하기가 쉽다.
  • 병렬 소프트웨어의 버그 대부분은 아주 ‘일반적’이지만, 진단할 때 병렬성 때문에 진단이 어렵다. 따라서 어떻게든 스레드를 하나로 제한하든지 해서 소프트웨어가 병렬이 아니게 빌드할 수 있으면 좋다.
  • sleep() 으로 병렬 문제를 해결하려고 하지 말자. 이런 건 대부분 문제를 숨겨서 오히려 근본적 원인을 해결하기 어렵게 만든다.

161p. 하이젠버그 는 진단하려고만 하면 재현이 안되는 버그를 말한다. 소프트웨어의 작동을 검사하는 모든 기법은 어느 정도는 소프트웨어에 영향을 준다. 적용한 기법이 어떻게 영향을 주는지 알고 있다면 다른 방법을 써서 영향을 최소화하는 수밖에 없다.

164p. 성능 버그. 성능 문제를 푸는 핵심은 다른 버그와 마찬가지로 근본 원인을 찾는 데 있고, 대부분의 근본 원인은 전체 성능을 제한하는 일부 병목에 있다. 따라서, 성능 버그를 찾는 데 가장 뛰어난 도구는 프로파일러다. 진단하기 전에 코드를 프로파일링하자.

프로파일러도 다른 도구와 마찬가지로 소프트웨어 동작에 영향을 미치기 때문에, 다음을 지켜야 한다.

  • 가능한 한 실제 제품과 비슷하게 빌드해 프로파일링한다. 최적화 수준도 같게 만들어야 한다.
  • 소프트웨어 실행 환경을 실제 제품의 실행 환경과 비슷하게 만든다.
  • 실제 데이터와 비슷한 데이터로 소프트웨어를 실행한다.

병목이 안 보인다면 전체적으로 성능에 영향을 미치는 요인을 살펴봐야 한다.

  • 메모리 등의 자원 고갈
  • 가비지 컬렉션
  • 캐시 미스

170p. 서드파티 소프트웨어 버그.

  • 서드파티에도 버그가 있을 가능성은 충분히 있지만 너무 빨리 비난하진 말자. 서드파티 코드는 사용자도 훨씬 많고 훨씬 테스트도 잘 되어있을 것이다. 따라서 우리 코드를 먼저 의심하자.
  • 서드파티 코드에 버그를 찾았다면 1) 문제를 보고하거나 2) 직접 패치할 수 있을텐데, 직접 패치를 하면 그 뒤부터 해당 서드파티 코드의 업데이트를 따라가기 쉽지 않을 수 있다. 당장은 문제를 우회하고, 장기적으로 서드파티 코드의 정식 릴리즈에 수정 사항이 포함되도록 보고하는 게 좋다.

9장. 이상적인 디버깅 환경

내용이 오래됐거나, 내가 잘 알고 있거나, 이미 자동으로 도와주는 소프트웨어를 쓰고 있기 때문에 패스.

10장. 소프트웨어가 스스로를 디버깅하게 만들기

197p. 좋은 설계와 디버깅하기 좋은 구조는 서로 충돌하지 않는다. 관심의 분리, 중복 금지, 정보 은닉 같은 일반적으로 좋은 소프트웨어 개발 원칙을 따랐다면 소프트웨어의 구조가 잘 잡혀있고 이해하기 쉬우면서 디버깅하기도 쉬울 것이다.

198p. 단언문 사용하기 (assert)

  • 단언문은 단위 테스트와 상호보완적이다. 단위 테스트는 테스트에서 호출하지 않는 버그는 찾을 수 없다. 단위 테스트는 정기적으로 모든 단언문을 실행시켜보는 방법이라고 생각할 수도 있다.
  • 소프트웨어가 특정 조건이 되었을 때 제대로 동작하지 않는 게 확실하다면, 단언문을 사용해 그 조건에서 소프트웨어가 종료되게 만들고 적절한 에러 메시지를 띄울 수 있다. 즉 소프트웨어가 스스로를 디버깅하게 된다.

203p. 단언문으로 체크해야 하는 것들. -> 사실 전부 테스트다.

  • 선행조건: 메소드가 기대하는 작동을 하기 위해 호출 전에 맞춰줘야 하는 것.
  • 후행조건: 메소드가 호출된 후에 보장되어야 하는 것.
  • 불변식: 어떤 상황에서도 참이어야 하는 것.

이 3가지에 대해 단언문으로 작성한다면 광범위한 버그를 알아서 잡아내는 소프트웨어를 만들 수 있다.

단언문은 개발 환경에서만 사용되어야 한다. 단언문이 사용된 채 프로덕션에 나가면 간단한 오류에도 소프트웨어가 죽어버린다. 프로덕션 소프트웨어는 방어적 프로그래밍을 이용해 fault tolernt하게 작성되어야 한다.

205p. 방어적 프로그래밍: 버그가 있을 때도 어느정도 올바르게 작동하게 하는 것.

  • 방어적 프로그래밍은 양날의 검이다. 방어적 프로그래밍을 하면 디버깅하기가 어려워진다.
  • 프로덕션에서는 견고하고, 디버깅할 때는 잘 깨지는 코드가 좋은 코드다.
  • 방어적 코드를 단언문으로 보호한다면, 그리고 단언문을 개발 모드에서만 켠다면, 디버깅할 때는 잘 깨지고 프로덕션에서는 견고한 코드를 만들 수 있다.

209p. 단언문을 사용할 때 피해야 할 두 가지.

  • 사이드 이펙트가 있는 단언문
  • 버그 대신 에러를 찾고 처리하는 데 사용되는 단언문

11장. 안티 패턴

225p. 우선순위 인플레이션: 버그 우선순위를 정하고 나서, 버그가 너무 많다 보니 아무도 우선순위의 최고 레벨이 아니고서는 신경쓰지 않는다. 그래서 그보다 더 높은 우선순위를 만들어내게 된다.

  • 이럴 때는 주기적으로 버그를 제거하고, 검토하면서 버그의 우선순위가 상황을 반영하는지 확인해야 한다.
  • 사용자가 버그 우선순위를 제어하는 게 아니라, 엄격한 과정을 통해 우선순위를 선별한다.
  • 우선순위 숫자는 없애버리고, 버그가 우선순위별로 정렬되게 하자.

결국에는, 품질 문제를 해결하는 것만이 유일하면서 진정한 해결책이다.

227p. 프리마돈나: 누군가가 개발 프로세스를 무시하고 대충 코드만 짜서 던지고 있다면?

  • ‘확실하게 끝내야 끝’임을 분명히 한다. 즉, 모든 기능은 작동, 테스트, 검토, 문서화 등의 모든 과정을 통과해야 끝난 것이다.
  • 큰 작업을 작고 구체적인 작업으로 나눈다.
  • 누구든 버그를 만든 사람이 버그를 수정하는, 오염자 부담 원칙을 도입한다. 프리마돈나가 작업한 부분에 버그가 있다면 그가 어떤 중요한 작업을 하고 있었든지 그 버그를 잡도록 하고, 프리마돈나가 하던 일은 다른 사람에게 넘긴다.

232p. 화재 진압은 심각한 문제가 쌓여있어서, 불길이 다른 곳으로 번지기 전에 재빨리 끄는 상태를 얘기한다. 하지만 너무 자주, 또는 오랫동안 화재 진압이 지속된다면 문제가 된다. 화재 진압은 절대 품질 문제를 근본적으로 해결하지 못한다. 1주일간 화재 진압을 해도 품질 문제가 해결되지 않는다면 단기적 결과에 더이상 연연하지 않고 근본 문제를 고쳐야 할 수 있다.

233p. 때로는 지저분한 코드를 다 버리고 완전히 새로 작성하고 싶은 유혹에 빠질 수도 있다. 그러나 지금 코드의 구조도 안 좋고, 테스트나 문서화가 제대로 작성되지 않았더라도, 꽤 오랫동안 시장에 나가 있었다면 대부분의 경우에는 제대로 실행될 것이다. 즉, 문제 도메인에 대한 엄청난 (코드가 실행되게 하는) 지식이 코드 안에 암묵적으로 들어있을 것이다. 소프트웨어를 재작성할 때 조심하지 않으면 과거에 겪었던 문제를 하나씩 다시 해결해야 할 수도 있다.

  • 비용 대비 효과를 꼼꼼히 따져보자.
  • 효과가 있다고 판단했더라도, 빅뱅 식으로 한번에 고치려고 하지 말고 점진적으로 코드를 다시 작성할 방법을 찾아보자.
  • 기존 코드에 대해 테스트하고, 새로 작성한 코드에서도 그 테스트가 통과하는지 검증하자.