2.15 디버깅과 문제 해결
개요와 동기
디버깅은 시스템이 해서는 안 될 일을 왜 하는지 알아내는 규율 있는 일이고, 문제 해결(트러블슈팅)은 시간 압박 아래 실행 중인 프로덕션 시스템에 돌린 같은 기술입니다. 둘 다 결함에 적용한 과학적 방법입니다. 놀라운 동작을 관찰하고, 원인에 대한 가설을 세우고, 그것을 확인하거나 반박할 실험을 설계하고, 무엇을 바꿀지는 직감이 아니라 증거가 알려 주게 합니다. 이렇게 하면 디버깅은 배울 수 있고 가르칠 수 있는 엔지니어링 기술입니다. 민간 전승으로 하면 미신이 됩니다. 아무 줄이나 바꾸고, 서버를 재시작하고, 바라는 것입니다.
큰 팀에서 이 차이는 비쌉니다. 어려운 결함 하나가 여러 서비스에 걸친 엔지니어를 끌어들이고, 온콜 시간을 소모하고, 릴리스를 멈출 수 있습니다. 각자가 직감으로 디버깅하면 그 노력은 누적되지 않습니다. 아무도 다른 사람이 시도한 것을 재현하거나 설명할 수 없기 때문입니다. 팀이 방법을 공유하면(먼저 재현하고, 탐색으로 격리하고, 버그를 실패하는 테스트에 담고, 그다음 수정), 같은 노력이 반복 가능한 프로세스와 자라는 회귀 스위트로 바뀝니다. 디버깅은 테스트 전략(2.4장), 소프트웨어 품질(2.11장), 코드를 처음부터 진단 가능하게 만드는 구축 습관(2.9장)과 긴밀히 연결됩니다.
기업과 정부 환경에서는 이해관계가 오릅니다. 기업의 결함은 서비스와 팀의 경계를 가로지르므로, 증상을 보는 사람이 원인을 소유한 사람인 경우가 드뭅니다. 정부 시스템은 대부분의 엔지니어가 만난 적 없는 제약을 더합니다. 프로덕션에 디버거를 붙일 수 없는 망 분리되거나 제한된 환경, 산출물로 진단해야 하는 재현 가능한 빌드, 무엇을 왜 바꿨는지 기록해야 하는 감사 추적입니다. 세 경우 모두 목표는 같습니다. 추측을 증거로 대체하는 것입니다.
핵심 원칙
- 이론을 세우기 전에 재현하십시오. 요청 시 촉발할 수 없는 버그는 결함이 아니라 소문입니다.
- 디버깅은 가설 검증입니다. 믿는 바를 진술한 뒤, 여러분이 틀렸음을 증명할 수 있는 가장 싼 실험을 설계하십시오.
- 코드를 만지기 전에 오류와 스택 트레이스를 읽으십시오. 시스템은 대개 한 줄을 바꾸기 전에 어디서 깨졌는지 알려 줍니다.
- 문제 공간을 훑지 말고 탐색하십시오. 위에서 아래로 읽는 대신 각 단계에서 의심 영역을 반으로 줄이십시오.
- 최소로 줄이십시오. 본질적인 트리거만 남을 때까지 사례를 깎아내십시오.
- 한 번에 하나만 바꾸십시오. 산탄총식 수정은 어떤 변경이 중요했는지 알려 줬을 증거를 파괴합니다.
- 고치기 전에 버그를 실패하는 테스트에 담으십시오. 수정은 그 테스트가 초록이 되고 초록으로 남을 때만 증명됩니다.
- 가장 가까운 증상이 아닌 근본 원인을 찾으십시오. 증상을 숨기는 패치는 결함이 돌아오게 둡니다.
권장 사항
아무것도 바꾸기 전에 결함을 안정적으로 재현한다
첫 번째 일은 안정적인 재현, 즉 버그를 요청 시 촉발하는 일련의 단계나 자동화된 사례입니다. 그것이 없으면 증상이 통제하지 않은 이유로 오갈 수 있으므로 진짜 수정과 우연을 구분할 수 없습니다. 입력, 환경, 버전, 타이밍을 못 박으십시오. 버그가 간헐적이라면 재현이 믿을 만해질 때까지 그것이 나타나게 하는 숨은 변수(특정 데이터 레코드, 시계 경계, 동시 요청)를 찾으십시오. 안정적인 재현은 디버깅에서 가장 값진 단일 산출물입니다. 그 이후의 모든 것이 측정 가능해지기 때문입니다.
코드를 만지기 전에 오류, 로그, 스택 트레이스를 읽는다
이론 하나를 세우기 전에, 시스템이 이미 알려 준 것을 읽으십시오. 스택 트레이스(실패 순간의 호출 사슬 기록)는 보통 실패한 파일, 줄, 순서를 이름 붙입니다. 예외 메시지, 그 주변의 로그 줄, 범위 안의 값이 아무것도 바꾸기 전에 탐색 범위를 좁힙니다. 엔지니어는 트레이스백이 첫 줄에서 배제한 원인을 이론화하느라 몇 시간을 낭비합니다. 오류 출력을 첫 증인으로 다루고, 주의 깊게 완전히 읽은 뒤에야 무엇을 조사할지 결정하십시오.
문제 공간의 이진 탐색으로 격리한다
코드를 위에서 아래로 훑지 마십시오. 탐색하십시오. 이진 탐색을 쓰십시오. 상태가 아직 좋은 지점과 이미 나쁜 지점을 찾고, 중간점을 확인하고, 반복해 매번 의심 영역을 반으로 줄이십시오. 이것이 천 줄 탐색을 열 개의 질문으로 바꿉니다. 회귀가 커밋 범위에 걸쳐 나타났다면 같은 생각을 이력에 바이섹션으로 적용하십시오. git bisect가 커밋 범위를 걸어 가며, 각 리비전에 좋음 또는 나쁨을 표시하면 결함을 도입한 정확한 변경을 이름 붙입니다. 좋음-나쁨 테스트를 자동화하면 바이섹션이 저절로 돌아갑니다.
최소 재현 예제로 줄인다
버그를 촉발할 수 있게 되면, 줄이십시오. 최소 재현 예제는 여전히 실패하는 가장 작은 입력과 코드 경로입니다. 더 제거하면 버그가 사라질 때까지 데이터, 기능, 단계를 제거하십시오. 줄이기는 잡일이 아닙니다. 제거하는 각 요소는 배제한 원인이므로, 최소 사례는 결함을 곧장 가리키는 경우가 많습니다. 입력이 크거나 구조화되어 있다면 델타 디버깅, 즉 실패하는 입력의 덩어리를 체계적으로 제거해 최소 실패 부분 집합을 찾는 알고리즘으로 줄이기를 자동화하십시오. 작고 자기완결적인 재현은 다른 팀에 건네는 가능한 최고의 버그 리포트이기도 합니다.
로그로 계측한 뒤 대화형 디버거를 쓴다
도구를 버그에 맞추십시오. 로깅과 표적 계측은 시간에 걸쳐, 프로세스를 가로질러, 멈출 수 없는 환경에서 동작을 봐야 할 때 가장 좋습니다. 중단점을 두고, 한 줄씩 진행하고, 살아 있는 상태를 검사하게 해 주는 대화형 디버거는 코드를 로컬에서 실행할 수 있고 단일 실행을 가까이서 지켜봐야 할 때 가장 좋습니다. 계측을 흩어진 print 문이 아니라 가설에 묶인 의도적 실험으로 추가하고, 버그가 해결되면 제거하거나 영구적인 구조화 로깅으로 승격하십시오. 프로덕션에서는 관측 가능성 주도 디버깅에 의지하십시오. 고카디널리티 이벤트와 분산 추적(9.2장)은 하나의 요청을 여러 서비스를 가로질러 따라가게 해 주며, 이는 디버거를 붙일 수 없는 분산 시스템을 디버깅하는 유일한 방법인 경우가 많습니다.
고치기 전에 버그를 담은 실패하는 테스트를 쓴다
수정을 쓰기 전에, 버그 때문에 실패하는 테스트를 쓰십시오. 이는 세 가지를 동시에 합니다. 원인을 실제로 이해하고 있음을 증명하고, “고쳐짐”이 정확히 무엇을 뜻하는지 정의하고, 영구적인 보호가 됩니다. 그다음 수정을 하고 테스트가 초록이 되는 것을 지켜보십시오. 그 테스트는 이제 회귀 테스트 보호로 스위트에 합류해, 같은 결함이 눈에 띄지 않게 돌아올 수 없습니다. 이 실천은 디버깅을 테스트 전략(2.4장)과 직접 연결합니다. 해결하는 모든 어려운 버그가 스위트를 찾았을 때보다 더 강하게 남기며, 불안정한 테스트도 재시도 주석이 아니라 같은 처리(비결정성을 재현한 뒤 그에 대비)를 받습니다.
근본 원인을 찾고 분석을 비난 없이 유지한다
증상을 고치는 것은 버그를 고치는 것이 아닙니다. 가리는 대신 제거할 수 있는 원인에 이를 때까지 각 층에서 왜를 물으며 실패를 진정한 기원까지 거슬러 올라가십시오. 프로덕션에 닿은 결함에는 인시던트 관리(9.3장)의 일부로 비난 없는 근본 원인 분석을 실행하십시오. 그 줄을 쓴 개인이 아니라 버그가 출시되고 살아남게 한 시스템과 프로세스 조건에 초점을 맞춥니다. 비난은 정보를 지하로 내몰고, 디버깅은 정보로 굴러갑니다. 산출물은 수정과 함께, 다음에는 그 부류의 결함이 더 일찍 잡히는 방식의 변화입니다.
장단점
| 접근 방식 | 장점 | 단점 |
|---|---|---|
| 로깅과 계측 | 프로덕션과 분산 시스템에서 동작. 시간에 걸친 동작 포착 | 잡음, 비용, 로그 난립. 타이밍 버그를 교란할 수 있음 |
| 대화형 디버거 | 정밀한 살아 있는 상태 검사. 로컬 버그에 빠름 | 제한되거나 망 분리된 프로덕션에서는 쓸모없음. 동시성 버그를 가릴 수 있음 |
| 재현 우선 규율 | 추측을 측정으로 바꿈. 실패하는 테스트를 가능하게 함 | 선행으로 느림. 일부 버그는 촉발하기 정말 어려움 |
| 이진 탐색과 바이섹션 | 낯선 코드에서도 빠른 격리 | 믿을 만한 좋음-나쁨 테스트가 필요. 버그가 상호작용하면 어려움 |
| 델타 디버깅 축소 | 거대한 입력을 트리거까지 자동으로 줄임 | 설정 비용. 실패가 결정적이라고 가정 |
| 지금 증상 고치기 | 압박 아래 서비스를 빠르게 복구 | 근본 원인이 돌아오게 둠. 부채 누적 |
핵심 긴장은 속도 대 확실성입니다. 프로덕션 인시던트 중에는 서비스를 복구하려고 먼저 출혈을 멈춰야 할 수 있으며(롤백이나 증상 패치), 그것은 정당합니다. 실수는 거기서 멈추는 것입니다. 두 일을 분리해 긴장을 해결하십시오. 사용자를 보호하려고 빠르게 완화하고, 그다음 결함이 닫혔다고 간주하기 전에 재현하고, 근본 원인을 찾고, 회귀 보호를 추가하십시오. 후속 조치 없는 증상 수정은 다시 만나기로 동의한 버그입니다.
팀과 논의할 질문
누군가 어려운 버그를 만나면 가장 먼저 하는 일은 무엇이며, 재현입니까 추측입니까? 정직한 답은 팀이 공유된 방법을 가졌는지, 방 가득한 사적 민간 전승을 가졌는지 드러냅니다. 사람들에게 마지막 어려운 결함을 소리 내어 서술해 달라고 하십시오. 먼저 안정적인 재현을 얻었습니까, 아니면 코드를 바꾸고 이것저것 재시작하기 시작했습니까? 재현을 먼저 하는 팀은 재현이 함께 전달되므로 버그를 사람 사이에 넘길 수 있고, 추측하는 팀은 모든 시도가 반복 불가능하므로 그럴 수 없습니다. 이는 팀이 커질수록 더 중요해집니다. 증상을 보는 사람이 점점 고칠 수 있는 사람이 아니기 때문입니다. 기본이 추측이라면, 재현 우선을 규범으로 합의하고 깨끗한 재현을 버그 티켓의 입장료로 만드십시오.
우리가 고친 버그가 돌아오며, 돌아오면 알겠습니까? 돌아오는 결함은 근본 원인이 제거된 적 없고 수정이 테스트로 보호된 적 없는 결함입니다. 지난 분기의 인시던트와 다시 열린 티켓을 끌어와 몇 건이 이전 버그의 반복이거나 사촌이었는지 세어 보십시오. 각 반복은 팀이 증상을 패치했거나, 실패하는 테스트를 건너뛰었거나, 근본 원인 분석을 너무 일찍 멈췄다는 증거입니다. 해법은 규칙입니다. 옛 동작에서 실패하는 테스트가 새 동작에서 통과하고 스위트에 합류하기 전에는 어떤 버그도 닫히지 않습니다. 최근 반복된 버그 하나를 가져와 어떤 보호가 그것을 잡았을지 물으십시오. 그 보호가 여러분이 놓친 것이기 때문입니다.
우리가 프로덕션 시스템을 만질 수 있는 방식을 감안할 때, 프로덕션을 디버깅이나 할 수 있습니까? 기업, 특히 정부 환경에서는 디버거를 붙일 수 없고, 실제 데이터로 재현할 수 없고, 감사 추적 없이 실행 중인 시스템을 바꿀 수 없는 경우가 많습니다. 유일한 디버깅 기법이 로컬 대화형 디버거라면, 가장 어려운 버그가 사는 바로 그곳에서 눈이 먼 것입니다. 프로덕션 실패가 실제로 어떤 증거를 남기는지 물으십시오. 구조화된 로그, 분산 추적(9.2장), 코어 덤프, 재현 가능한 빌드 산출물입니다. 미래의 인시던트를 진단할 수 있도록 기본으로 무엇을 포착해야 하는지 지금 정하십시오. 이미 일어난 실패에 계측을 추가할 수는 없기 때문입니다. 규제 환경에서는 같은 흔적이 감사 의무도 충족하는지 확인하십시오.
프로덕션 인시던트가 출혈을 빨리 멈추라고 강제할 때, 그 후에도 근본 원인이 찾아지도록 어떻게 보장합니까? 인시던트 중에는 롤백이나 증상 패치가 사용자를 보호하는 올바른 첫 수이지만, 위험은 서비스가 돌아오는 순간 티켓이 닫히고 기저의 결함이 진단되지 않는 것입니다. 큰 팀에서 이는 부채가 보이지 않게 쌓이는 곳입니다. 같은 부류의 실패가 몇 달 뒤 다른 서비스와 다른 온콜 엔지니어에게 다시 나타나기 때문입니다. 최근 몇 건의 심각도 1 인시던트를 가져와 각각을 확인하십시오. 완화 뒤에 재현, 근본 원인 분석, 회귀 보호가 이어졌습니까, 아니면 이야기가 “서비스 복구”에서 끝났습니까? 완화된 인시던트는 근본 원인이 이해되고 보호될 때까지 열려 있다는 명시적 규칙에 합의하고, 그 후속 조치를 누가 소유하는지 이름 붙이십시오. 기업과 정부 환경에서는 이를 인시던트 관리 프로세스(9.3장)에 연결해 사후 검토가 다음 불이 시작될 때 미끄러지는 예의가 아니라 필수적이고 감사 가능한 단계가 되게 하십시오.
실패가 일어난 뒤 얼마나 많이 재구성할 수 있으며, 기본으로 무엇을 포착할지는 누가 결정했습니까? 이미 일어난 실패에는 계측을 붙일 수 없으므로, 어떤 인시던트의 진단 가능성이든 내보내기로 한 로그, 추적, 지표, 덤프에 의해 미리 정해집니다. 상충하는 고려는 비용과 잡음입니다. 고카디널리티 이벤트와 완전한 추적은 공짜가 아니고, 과도한 로깅은 신호를 묻으면서 저장 비용과 규제 맥락에서는 데이터 보존 노출을 부풀립니다. 실제 최근 인시던트를 가져와 그것이 어떤 증거를 남겼는지 묻고, 포착했으면 좋았을 것과 유지하는 데 드는 비용을 거꾸로 따져 보십시오. 어떤 신호를 기본으로 켜고 어떤 것을 샘플링하거나 옵트인으로 할지 의도적으로 정하고, 그 결정을 우연이 아닌 정책으로 기록하십시오. 기업이나 정부 시스템에서는 그 관측 가능성 예산에 누가 책임지는지, 포착된 흔적이 감사, 프라이버시, 데이터 거주 의무도 충족하는지를 더하십시오.
디버깅을 가르치고 측정할 수 있는 기술로 다룹니까, 아니면 신입 엔지니어가 자연스럽게 흡수합니까? 디버깅은 배울 수 있지만, 대부분의 팀은 이를 명시적으로 가르치지 않아서 주니어는 가장 가까이 있는 민간 전승을 물려받고, 재현 우선 방법은 고르지 않게 또는 전혀 퍼지지 않습니다. 긴장은 의도적인 가르침(어려운 버그의 페어링, 사후 발견의 정리, 지표 추적)이 언제나 다른 곳에서 필요한 시니어의 시간을 쓴다는 점입니다. 논의에 두 숫자를 가져오십시오. 반복 결함 비율과 진단까지의 시간입니다. 측정할 수 없다면 방법이 개선되는지 퇴화하는지 알 수 없기 때문입니다. 온보딩에 실제 디버깅 연습이 포함되는지, 근본 원인 발견이 실제로 더 이른 탐지에 공급되는지 고려하십시오. 크거나 공공 조직에서는 문서화되고 측정되는 디버깅 실천이 감사자, 규제 기관, 감독 기관이 점점 더 보고자 하는 엔지니어링 엄밀함의 증거가 되기도 합니다.
분야별 관점
스타트업. 엔지니어 몇 명과 여유 없는 상황에서 목표는 무거운 프로세스를 만드는 것이 아니라 버그를 재현하기 싸고 잊기 불가능하게 만드는 것입니다. git bisect, 빠른 로컬 재현, 고친 버그마다 하나의 실패하는 테스트에 의지하십시오. 그 습관은 몇 분이 들고, 출시하려는 동안 같은 결함의 값을 다시 치르는 것을 막습니다. 공식 사후 검토는 건너뛰어도, 회귀 테스트는 절대 건너뛰지 마십시오. 언제나 감당할 만큼 작고 언제나 간직할 만큼 값진 단 하나의 산출물입니다.
소기업. 전담 신뢰성이나 관측 가능성 전문가도 빠듯한 도구 예산도 없을 가능성이 크니, 스택이 이미 주는 것을 선호하십시오. 읽을 수 있는 스택 트레이스, 구조화된 로그, 구매한 프레임워크와 호스팅 서비스에 내장된 추적입니다. 새 플랫폼을 평가할 때 그것이 실패를 얼마나 진단 가능하게 만드는지 저울질하십시오. 무엇이 잘못되었는지 감추는 싼 도구는 라이선스가 아낀 것보다 추측 시간에서 훨씬 더 비용이 들기 때문입니다. 재현 우선과 한 번에 하나씩만 바꾸기는 아무도 여유 시간이 없을 때 가장 빠르게 본전을 뽑는 공짜 규율입니다.
대기업. 어려운 버그는 서비스와 팀 경계를 가로지르므로 증상을 보는 사람이 원인을 소유하는 경우가 드물며, 어떤 개인의 기술보다 공유된 방법이 더 중요합니다. 재현 우선, 이진 탐색 격리, 수정 전의 실패하는 테스트, 비난 없는 사후 검토를 팀 전반에 표준화하고, 하나의 요청을 서비스를 가로질러 따라갈 수 있도록 분산 추적(9.2장)에 투자하십시오. 디버깅을 측정되는 역량으로 관리하십시오. 반복 결함 비율과 진단까지의 시간을 추적하고, 같은 부류의 실패가 서비스 지도를 순회하지 않도록 근본 원인 발견을 더 이른 탐지에 되먹임하십시오.
정부. 조달 규칙, 제한된 환경, 공적 책임성이 디버깅을 어떻게 할 수 있는지를 형성합니다. 프로덕션에 디버거를 붙이거나 시민 데이터를 노트북에 복사할 수 없는 경우가 많으므로, 허용된 것으로 진단하도록 설계하십시오. 재현 가능한 빌드, 격리된 구역의 합성 레코드, 기본으로 포착되는 구조화 로그와 추적입니다. 모든 진단 단계와 모든 변경을 감사 추적에 기록하고, 공급자의 말에 의존하는 대신 실패를 독립적으로 조사할 수 있을 만큼 벤더가 텔레메트리와 빌드 재현 가능성을 노출하도록 요구하십시오.
사례
스타트업. 네 명의 엔지니어 팀이 사용자 일부에게 결제가 실패하는 것을 계속 보지만 테스트에서는 한 번도 일어나지 않습니다. 추측하는 대신, 한 엔지니어가 실패한 정확한 요청 페이로드를 재생해 안정적인 재현을 포착하고, 그동안 무시하던 스택 트레이스를 읽으니 날짜 파싱 호출을 가리킵니다. 한 주의 커밋에 대한 빠른 git bisect가 날짜 라이브러리를 바꾼 변경을 이름 붙입니다. 문제의 타임스탬프로 실패하는 테스트를 쓰고, 파서를 고치고, 테스트가 초록이 되는 것을 지켜본 뒤 스위트에 둡니다. 이론화하기 전에 재현했기 때문에 조사 전체에 한나절이 걸리고, 버그는 돌아오지 않습니다.
대기업. 한 결제 플랫폼이 어느 팀도 설명할 수 없는 간헐적 타임아웃을 보입니다. 증상은 결제에 나타나지만 원인은 세 서비스 떨어진 곳에 있기 때문입니다. 온콜 엔지니어들은 분산 추적(9.2장)으로 실패하는 하나의 요청을 서비스 경계를 가로질러 따라가며, 동시 부하 아래서 가끔 교착되는 하류 호출을 찾습니다. 스레드 사이의 운 나쁜 타이밍에 결과가 달라지는 전형적인 경쟁 상태입니다. 부하 테스트로 재현하고, 실패하는 통합 테스트에 담고, 잠금을 고치고, 다음 발생이 며칠이 아닌 몇 분 안에 잡히도록 추적 스팬과 알림을 더하는 비난 없는 사후 검토(9.3장)를 실행합니다.
정부. 한 급여 기관이 엔지니어가 프로덕션에 디버거를 붙일 수 없고 시민 데이터를 노트북에 복사할 수 없는 망 분리 환경에서 사건 시스템을 운영합니다. 대사에서 계산 결함이 나타납니다. 팀은 환경이 허용하는 것으로 디버깅합니다. 구조화된 로그, 격리된 테스트 구역에 띄울 수 있는 재현 가능한 빌드, 실패 사례를 재현하는 합성 레코드입니다. 모든 진단 단계가 감사 추적에 기록되고, 수정은 증거로서 실패 후 통과하는 테스트와 함께 출시되며, 근본 원인 분석은 새 릴리스 전 검사에 공급됩니다. 재현에 합성 데이터를 썼으므로 어떤 시민 레코드도 경계를 떠나지 않았습니다.
비즈니스 사례: 동기, ROI, TCO
규율 있는 디버깅의 수익은 추측에 쓰지 않은 엔지니어 시간과 재발하지 않는 결함으로 측정됩니다. 진단되지 않은 간헐적 버그는 시니어의 며칠과 반복되는 온콜 에스컬레이션을 소모할 수 있습니다. 재현 우선 방법은 그것을 한정되고 위임 가능한 과제로 바꾸고, 실패하는 테스트 습관은 같은 결함이 다음 분기에 다시 청구하는 것을 막습니다. 큰 조직 전반에서 같은 버그의 값을 다시 치르지 않는 누적 효과는 상당하며, 리더십이 이미 추적하는 변경 실패율과 평균 복구 시간을 직접 개선합니다.
총소유비용은 대부분 교육과 도구이며 소소합니다. 공유 관례(재현 먼저, 한 번에 하나씩 변경, 수정 전의 실패하는 테스트), 도구 체인에 이미 흔한 디버거와 추적, 9.2장에서 설명한 관측 가능성 투자가 필요합니다. 더 크고 숨은 비용은 대안입니다. 엔지니어가 산탄총식 변경을 적용하고, 증상이 패치되어 돌아오고, 온콜 부하가 끝없이 커지는 미신의 문화입니다. 온콜 잡무를 줄이는 것만으로도 투자가 정당화되는 경우가 많으며, 리더십에 대한 논거는 습관과 계측에 대한 일회성 비용으로 더 적은 반복 인시던트와 더 빠른 복구라고 말하는 것이 가장 간단합니다.
안티패턴과 함정
- 산탄총식 디버깅: 한꺼번에 많은 것을 바꿔서 수정조차 원인에 대해 아무것도 가르쳐 주지 않는 것.
- 재현 없이 고치기: 요청 시 촉발할 수 없었던 버그에 대해 승리를 선언하는 것.
- 오류 출력 무시: 스택 트레이스가 이미 배제한 원인을 이론화하는 것.
- 증상 패치: 근본 원인이 살아남아 돌아오는 동안 증상을 침묵시키는 것.
- print 문 난립: 가설에 묶인 실험 대신 잡음을 더하며 코드에 남은 흩어진 디버그 출력.
- 회귀 테스트 건너뛰기: 버그는 고치되 보호를 남기지 않아 조용히 돌아올 수 있는 것.
- 불안정한 테스트 재시도: 기저의 경쟁 상태나 관찰하려는 순간 바뀌거나 사라지는 버그인 하이젠버그를 디버깅하는 대신 재시도로 비결정성을 가리는 것.
- 비난 중심 사후 검토: 작성자를 처벌하여 디버깅이 의존하는 정보를 지하로 내모는 것.
성숙도 모델
- 1단계, 시작: 디버깅이 개인의 민간 전승이고 반응입니다. 엔지니어가 추측하고, 산탄총식 변경을 적용하고, 이것저것 재시작합니다. 버그는 증상에서 고쳐지고, 재현은 드물며, 같은 결함이 되풀이됩니다. 프로덕션은 거의 진단할 수 없고, 어떤 시도도 반복 가능하지 않아 아무도 버그를 다른 사람에게 건넬 수 없습니다.
- 2단계, 발전: 일부 엔지니어가 안정적으로 재현하고, 스택 트레이스를 읽고, 디버거를 쓰지만, 관행이 일관되지 않고 사람마다 팀마다 다릅니다. 로깅은 있지만 시끄럽고 구조화되지 않았습니다. 수정은 가끔 실패하는 테스트와 함께 출시되고 대개 그렇지 않으며, 근본 원인 분석은 누군가 고집할 때만 일어납니다.
- 3단계, 표준화: 재현 우선, 이진 탐색 격리, 한 번에 하나씩 변경, 수정 전의 실패하는 테스트가 문서화된 팀 규범으로 조직 전체에서 시행됩니다. 바이섹션과 델타 디버깅 축소가 일반적인 실천입니다. 프로덕션에는 구조화 로깅과 추적(9.2장)이 있고, 비난 없는 사후 검토(9.3장)가 빠져나간 모든 결함에 대한 표준 대응입니다.
- 4단계, 관리: 디버깅 실천이 기준선에 대해 측정되고 통제됩니다. 반복 결함 비율, 진단까지의 시간, 다시 열린 티켓 수, 회귀 테스트와 함께 출시된 수정의 비율이 팀별로 추적되고 정해진 주기로 검토됩니다. 재현과 근본 원인 완료는 선의가 아닌 관문으로 다뤄지고, 기준선 대비 추세가 도구, 교육, 관측 가능성에 투자할 곳을 이끕니다.
- 5단계, 오케스트레이션: 디버깅이 품질(2.11장) 및 인시던트 관리(9.3장)와 통합된 가르치는 기술이고, 전체 고리가 지속적으로 적응합니다. 관측 가능성이 설계에 내장되어 대부분의 프로덕션 버그가 디버거 없이 진단 가능하고, 해결된 모든 버그가 회귀 스위트를 강화하며, 근본 원인 발견이 더 이른 탐지에 공급되어 결함의 부류가 다시 진단되는 대신 예방됩니다. 조직은 시스템과 실패 양상이 진화함에 따라 노력을 재균형하고, 반복 결함 비율은 계속 떨어집니다.
논의를 위한 아이디어
- 누군가 코드를 바꾸기 전에 안정적으로 재현된 최근 버그의 비율은 얼마이며, 그 비율은 방법에 대해 무엇을 말해 줍니까?
- 회귀가 나타나면 팀은 바이섹션에 손을 뻗습니까, 아니면 누군가 발견할 때까지 코드를 손으로 읽습니까?
- 오늘 프로덕션 시스템은 얼마나 진단 가능하며, 이미 일어난 실패에 대해 포착했더라면 좋았을 것은 무엇입니까?
- 수정은 일관되게 실패 후 통과하는 테스트와 함께 출시되며, 아니라면 그 규율은 어디서 무너집니까?
- 불안정한 테스트를 어떻게 다룹니까? 비결정성을 디버깅합니까, 재시도로 덮습니까?
- 디버깅은 신입 엔지니어에게 의도적으로 가르쳐지나요, 아니면 민간 전승을 자연스럽게 흡수하도록 맡겨집니까?
핵심 요점
- 디버깅은 가설 검증입니다. 안정적으로 재현하고, 오류와 스택 트레이스를 읽은 뒤, 훑는 대신 이진 탐색과 바이섹션으로 격리하십시오.
- 실패를 최소 재현 예제로 줄이고, 큰 입력에는 델타 디버깅을 쓰십시오. 제거한 각 요소는 배제한 원인이기 때문입니다.
- 도구를 버그에 맞추십시오. 프로덕션과 분산 시스템에는 계측과 추적(9.2장), 로컬 조사에는 대화형 디버거.
- 고치기 전에 버그를 담은 실패하는 테스트를 써서, 수정이 증명되고 결함이 영구히 보호되게 하십시오(2.4장).
- 근본 원인을 찾아 제거하고, 비난 없는 사후 검토(9.3장)를 실행하고, 디버깅을 민간 전승이 아닌 배울 수 있는 기술로 다루십시오.
- 한 번에 하나만 바꾸십시오. 산탄총식 변경과 증상 패치는 증거를 파괴하고 버그를 불러들입니다.
참고 문헌과 더 읽을거리
- David J. Agans, Debugging: The 9 Indispensable Rules for Finding Even the Most Elusive Software and Hardware Problems
- Andreas Zeller, Why Programs Fail: A Guide to Systematic Debugging
- Andreas Zeller and Ralf Hildebrandt, “Simplifying and Isolating Failure-Inducing Input” (the delta debugging algorithm)
- Brian W. Kernighan and Rob Pike, The Practice of Programming (chapter on debugging)
- Andrew Hunt and David Thomas, The Pragmatic Programmer (the chapters on debugging and assertions)
- Steve McConnell, Code Complete: A Practical Handbook of Software Construction (the debugging chapter)
- John Regehr, “Reducers Are Fuzzers” and related writing on test-case reduction
- Charity Majors, Liz Fong-Jones, and George Miranda, Observability Engineering (debugging production with high-cardinality telemetry and tracing)
- Betsy Beyer, Chris Jones, Jennifer Petoff, and Niall Richard Murphy, eds., Site Reliability Engineering (blameless postmortems and production debugging)