효율적인 디버깅 팁 — 빠르고 정확하게 버그를 잡는 사고방식
이 글의 핵심
버그를 빠르게 잡는 사람과 오래 헤매는 사람의 차이는 도구가 아니라 사고 순서에 있습니다. 재현성 확보 → 가설 수립 → 이분 탐색 → 검증이라는 일관된 프로세스와, 언제 로그를 쓰고 언제 디버거를 써야 하는지, 스택 트레이스를 제대로 읽는 법까지 실무에서 바로 쓸 수 있는 디버깅 방법론을 정리합니다.
들어가며
디버깅을 잘하는 사람과 오래 헤매는 사람을 갈라놓는 것은 도구도, 언어 숙련도도 아닙니다. 문제에 접근하는 순서입니다. 숙련된 엔지니어는 에러 메시지를 보자마자 코드를 고치지 않습니다. 먼저 버그를 재현할 방법을 확보하고, 원인에 대한 가설을 세우고, 그 가설이 맞는지 최소한의 실험으로 검증한 다음에야 코드를 바꿉니다. 반면 이 순서를 건너뛰면 “이것도 아닌가 보다, 저것도 고쳐보자” 식으로 코드를 여기저기 찔러보게 되고, 결국 버그가 왜 고쳐졌는지도 모른 채 넘어가는 경우가 생깁니다.
이 글은 특정 언어의 에러 메시지 목록이 아니라, 버그를 빠르고 정확하게 찾아내는 사고 과정 자체를 다룹니다. 특정 언어의 흔한 에러 자체를 찾고 있다면 프로그래밍 언어별 흔한 에러 해결 가이드를 참고하시고, 이 글은 그보다 한 단계 위에서 “애초에 어떻게 접근해야 시간을 낭비하지 않는가”에 초점을 맞춥니다.
재현 가능성이 전부다
디버깅에서 가장 많은 시간이 낭비되는 지점은 흔히 생각하는 “원인을 찾는 단계”가 아니라 재현이 안 되는 상태에서 헤매는 단계입니다. 재현되지 않는 버그를 고치려는 시도는 본질적으로 눈을 감고 과녁을 맞히려는 것과 같습니다. 고쳤다고 생각한 수정이 실제로 원인을 제거했는지, 아니면 우연히 증상이 한동안 나타나지 않은 것뿐인지 구별할 방법이 없기 때문입니다.
그래서 버그 리포트를 받으면 가장 먼저 해야 할 일은 원인 분석이 아니라 “이 조건에서 이 버그가 100%, 혹은 최소한 예측 가능한 확률로 재현되는가”를 확인하는 것입니다. 재현 절차가 확보되기 전까지는 코드를 한 줄도 고치지 않는 편이 낫습니다. 재현 없이 고친 코드는 검증할 수 없는 코드이기 때문입니다.
간헐적 버그를 다루는 법
항상 재현되는 버그는 사실 절반은 이긴 싸움입니다. 진짜 시간을 잡아먹는 것은 “가끔” 일어나는 버그입니다. 이런 경우에는 다음 순서로 접근하는 것이 효율적입니다.
- 조건을 좁힌다. “가끔”을 “어떤 조건에서 더 자주”로 바꿉니다. 특정 사용자, 특정 데이터 크기, 특정 시간대, 특정 네트워크 지연 조건에서 빈도가 높아지는지 로그나 통계로 확인합니다.
- 부하와 동시성을 의심한다. 간헐적 버그의 상당수는 레이스 컨디션, 타이밍 의존적 코드, 캐시 무효화 순서 문제입니다. 반복 실행 횟수를 늘리거나(예: 같은 테스트를 1000번 돌리기), 동시 요청 수를 인위적으로 늘려서 재현 확률을 끌어올립니다.
- 재현 확률이 낮아도 자동화한다. 100번에 1번 재현되는 버그라도, 그 시나리오를 스크립트로 만들어 반복 실행할 수 있으면 사람이 수동으로 100번 시도하는 것보다 압도적으로 빠릅니다. CI에서 같은 테스트를 N회 반복하는 “stress test” 잡을 임시로 추가하는 것도 실무에서 자주 쓰는 방법입니다.
- 재현이 끝내 안 되면 관측을 강화한다. 로컬에서 재현이 안 되고 프로덕션에서만 발생한다면, 재현을 포기하고 대신 다음에 같은 상황이 벌어졌을 때 필요한 정보가 전부 기록되도록 로깅과 트레이싱을 미리 강화해 두는 쪽으로 전략을 바꿉니다.
최소 재현 케이스를 만드는 이유
재현이 된다면 다음 단계는 그 재현 케이스를 최소화하는 것입니다. 수백 줄짜리 요청 핸들러 전체를 실행해야 버그가 나온다면, 그 상태로 가설을 검증하는 사이클 한 번에 몇 초에서 몇 분이 걸립니다. 반면 버그를 유발하는 핵심 로직만 5~10줄로 뽑아낸 스크립트가 있다면 사이클이 밀리초 단위로 줄어듭니다.
최소 재현 케이스를 만드는 과정 자체가 이미 디버깅의 절반입니다. 불필요한 코드를 하나씩 제거하면서 “이걸 지워도 버그가 재현되나?”를 반복하다 보면, 그 과정에서 자연스럽게 원인이 어디에 있는지 좁혀지는 경우가 많습니다. 이 작업은 아래에서 설명할 이분 탐색과 사실상 같은 원리입니다.
가설 기반 디버깅: “일단 고쳐보기”의 함정
경험이 적은 개발자일수록 에러를 보면 곧바로 코드를 바꾸고 다시 실행해보는 패턴에 빠지기 쉽습니다. null 체크를 추가해보고 안 되면 지우고, 타입 캐스팅을 넣어보고 안 되면 다른 곳을 건드리는 식입니다. 이 방식이 위험한 이유는 세 가지입니다.
- 왜 동작하는지 모른 채 넘어간다. 우연히 버그가 사라졌을 때, 그것이 진짜 원인을 제거했기 때문인지 아니면 타이밍이나 순서가 바뀌어 증상만 가려진 것인지 알 수 없습니다.
- 코드가 지저분해진다. 여러 시도를 순서 없이 덧붙이다 보면 실제로는 필요 없는 방어 코드, 중복된 체크가 쌓입니다.
- 같은 버그가 재발한다. 근본 원인을 이해하지 못했기 때문에, 비슷한 패턴을 쓰는 다른 코드에서 동일한 문제가 다시 터집니다.
이를 방지하는 방법은 코드를 고치기 전에 반증 가능한 가설을 먼저 문장으로 적는 것입니다. “이 함수가 null을 반환하는 이유는 캐시가 만료되기 전에 조회를 시도하기 때문일 것이다”처럼, 참인지 거짓인지 실험으로 확인할 수 있는 형태로 만듭니다. “뭔가 이상한 것 같다”는 가설이 아닙니다.
가설을 세운 다음에는 코드를 고치기 전에 먼저 그 가설이 맞는지 확인할 수 있는 최소한의 실험을 설계합니다. 로그 한 줄을 추가해서 캐시 만료 시각과 조회 시각을 함께 찍어보는 정도면 충분한 경우가 많습니다. 가설이 틀렸다면 그 실험 결과를 보고 다음 가설을 세우고, 맞았다면 그제서야 수정에 들어갑니다.
# 나쁜 예: 가설 없이 일단 방어 코드부터 추가
def get_user_profile(user_id):
cached = cache.get(user_id)
if cached is None: # 일단 넣어봄
cached = {}
return cached["name"] # 여전히 KeyError 가능성 있음
# 좋은 예: 가설을 세우고 검증할 로그부터 추가
def get_user_profile(user_id):
cached = cache.get(user_id)
# 가설: 캐시 워밍업이 끝나기 전에 조회가 들어와 만료 전 빈 값이 반환된다
logging.debug(
"cache lookup user_id=%s hit=%s ttl_remaining=%s",
user_id, cached is not None, cache.ttl(user_id),
)
return cached["name"]
이 예제에서 중요한 것은 로그를 추가한 줄 자체가 아니라, 그 로그가 특정 가설을 검증하기 위해 의도적으로 배치되었다는 점입니다. 목적 없이 여기저기 print를 흩뿌리는 것과, 가설을 확인하기 위해 정확한 지점에 로그를 넣는 것은 결과물은 비슷해 보여도 걸리는 시간이 크게 다릅니다. 전자는 몇 번이고 다시 실행하며 “이번엔 뭐가 나오나” 확인하는 데 그치지만, 후자는 한두 번의 실행으로 가설을 기각하거나 확정할 수 있습니다.
이분 탐색: git bisect를 넘어서는 사고방식
git bisect는 널리 알려진 도구지만, 그 밑바탕에 있는 원리—원인이 있을 수 있는 공간을 절반씩 줄여나간다—는 커밋 이력 말고도 훨씬 넓게 적용됩니다.
코드 범위에 대한 이분 탐색
수백 줄짜리 함수 어딘가에서 상태가 이상해진다면, 절반을 주석 처리하거나 조기 반환(early return)을 넣어 실행을 중단시키고 그 시점까지 상태가 정상인지 확인합니다. 정상이면 문제는 나머지 절반에 있고, 비정상이면 앞쪽 절반에 있습니다. 이 과정을 반복하면 로그를 촘촘히 깔지 않아도 O(log n) 번의 시도로 원인 위치를 좁힐 수 있습니다.
커밋 이력에 대한 이분 탐색
버그가 언제부터 시작되었는지 알고 있다면 git bisect가 가장 효율적입니다. 수동으로 커밋을 하나씩 체크아웃하며 테스트하는 대신, 자동화된 테스트 스크립트를 붙이면 수십 개 커밋 중 원인 커밋을 로그 단위 시간 안에 찾아낼 수 있습니다.
git bisect start
git bisect bad HEAD # 현재 커밋에서 버그 재현됨
git bisect good v1.4.0 # 이 태그에서는 정상이었음
# 재현 스크립트가 있다면 완전 자동화
git bisect run ./scripts/reproduce_bug.sh
git bisect run에 넘기는 스크립트는 버그가 있으면 0이 아닌 값을, 없으면 0을 반환하기만 하면 됩니다. 여기서 앞서 만든 “최소 재현 케이스”가 그대로 재현 스크립트의 재료가 됩니다. 재현 케이스를 먼저 만들어야 하는 이유가 여기서도 드러납니다.
데이터에 대한 이분 탐색
특정 입력 데이터에서만 파이프라인이 실패한다면, 데이터를 절반으로 잘라 각각 실행해봅니다. 실패하는 절반을 다시 절반으로 자르는 과정을 반복하면 수만 건의 레코드 중 문제를 일으키는 몇 건을 빠르게 좁힐 수 있습니다. 대용량 배치 작업이나 ETL 파이프라인에서 특히 유용한 접근입니다.
설정과 구성 요소에 대한 이분 탐색
마이크로서비스 환경에서 특정 요청이 실패한다면, 미들웨어나 프록시 계층을 하나씩 비활성화하거나 우회하며 어느 계층에서 문제가 생기는지 좁힙니다. 피처 플래그를 절반씩 꺼보는 것도 같은 원리입니다. 설정값이 많은 시스템에서 “어떤 조합에서만” 발생하는 버그는 이 방식이 로그를 읽는 것보다 훨씬 빠릅니다.
이분 탐색이 강력한 이유는 원인의 위치에 대한 사전 지식이 전혀 없어도 적용할 수 있기 때문입니다. “감”에 의존하지 않고 기계적으로 탐색 공간을 줄여나가므로, 익숙하지 않은 코드베이스에서도 똑같이 효과적입니다.
스택 트레이스와 에러 메시지를 제대로 읽는 법
스택 트레이스를 받아도 대부분의 사람은 이를 제대로 읽지 않습니다. 흔한 실수는 두 가지입니다.
첫째, 가장 위에 있는 줄만 보고 판단하는 것입니다. 예외가 발생한 정확한 위치는 스택의 맨 위에 있지만, 그 예외를 일으킨 조건은 종종 몇 프레임 아래, 즉 호출한 쪽의 잘못된 인자나 상태에서 시작됩니다. NullPointerException이 라이브러리 내부 코드에서 발생했다고 해서 라이브러리 버그로 단정하기 전에, 그 라이브러리를 호출한 애플리케이션 코드 프레임까지 반드시 확인해야 합니다.
둘째, 에러 메시지의 타입과 위치만 보고 원인을 넘겨짚는 것입니다. TypeError: Cannot read property 'name' of undefined라는 메시지는 “어디서” 문제가 터졌는지는 정확히 알려주지만 “왜” undefined가 되었는지는 알려주지 않습니다. 왜를 알아내려면 그 값이 undefined가 된 경로를 거슬러 올라가야 합니다. 이때는 스택 트레이스만으로 부족하고, 해당 값이 어디서 초기화되고 어디서 재할당되는지 코드를 역추적하거나, 그 변수에 조건부 로그를 걸어 실제로 어느 호출 경로에서 undefined가 유입되는지 확인해야 합니다.
스택 트레이스를 읽을 때 실무적으로 도움이 되는 순서는 다음과 같습니다.
- 예외 타입과 메시지로 “무엇이” 문제인지 파악합니다.
- 스택 최상단 프레임으로 “어디서” 터졌는지 확인합니다.
- 그 아래 프레임들을 애플리케이션 코드가 나올 때까지 따라 내려가며 “누가 이 상황을 만들었는지” 찾습니다. 라이브러리 내부 프레임은 보통 증상이고, 원인은 더 아래에 있습니다.
- 타임스탬프가 함께 로깅되어 있다면 직전 로그 몇 줄을 함께 봅니다. 예외 발생 직전의 상태가 원인 파악에 결정적인 단서가 되는 경우가 많습니다.
로그 디버깅 vs 디버거: 언제 무엇을 쓸 것인가
“프린트 디버깅은 초보자용이고 디버거를 쓰는 게 정석”이라는 말은 절반만 맞습니다. 실제로는 상황에 따라 둘 중 하나가 명백히 더 빠릅니다.
로그(print) 디버깅이 유리한 경우
- 시간에 따른 상태 변화의 흐름을 봐야 할 때. 브레이크포인트로 매번 멈춰서 변수를 확인하는 것보다, 여러 지점에 로그를 찍고 한 번에 실행해서 전체 흐름을 시계열로 보는 것이 훨씬 빠릅니다.
- 반복문이나 재귀가 많아서 같은 지점에서 수백, 수천 번 멈추게 되는 코드. 조건부 브레이크포인트를 쓸 수도 있지만, 조건을 정확히 모르는 초기 단계에서는 로그로 전체 패턴을 먼저 훑는 편이 낫습니다.
- 동시성 버그. 여러 스레드나 프로세스가 얽힌 상황에서 브레이크포인트로 한쪽을 멈추면 타이밍 자체가 바뀌어 버그가 재현되지 않는 경우가 흔합니다(이른바 “하이젠버그”). 이때는 로그가 실행 흐름을 바꾸지 않고 관찰할 수 있는 거의 유일한 방법입니다.
- 프로덕션 환경. 실행 중인 서비스에 디버거를 붙여 멈추는 것 자체가 불가능하거나 위험합니다. 구조화된 로그와 트레이싱이 사실상 유일한 선택지입니다.
디버거가 유리한 경우
- 특정 시점의 복잡한 자료구조를 깊이 들여다봐야 할 때. 중첩된 객체나 대형 컬렉션의 내부 상태를 로그로 전부 출력하려면 코드도 지저분해지고 출력량도 감당하기 어렵습니다. 디버거의 변수 뷰어로 즉시 탐색하는 것이 훨씬 빠릅니다.
- 어디에 로그를 찍어야 할지조차 감이 잡히지 않을 때. 일단 의심되는 함수 초입에 브레이크포인트를 걸고 스텝 실행(step into/over)하면서 실제로 어떤 코드 경로를 타는지 관찰하는 편이 가설을 세우는 데 도움이 됩니다.
- 콜스택을 위로 거슬러 올라가며 “이 값이 어디서 넘어왔는지”를 대화형으로 탐색해야 할 때. 디버거의 call stack 패널은 이런 작업에 로그보다 훨씬 직관적입니다.
결론적으로 둘은 경쟁 관계가 아니라 상호 보완적인 도구입니다. 로그로 문제가 발생하는 대략적인 범위를 좁힌 다음, 그 범위 안에서 디버거로 상세한 상태를 들여다보는 조합이 실무에서 가장 자주 쓰이는 패턴입니다.
말로 설명하기: 러버덕 디버깅이 실제로 작동하는 이유
러버덕 디버깅(rubber duck debugging)—문제를 아무 대상에게나(고무 오리든, 동료든, 백지든) 처음부터 끝까지 소리 내어 또는 글로 설명하는 것—이 우스갯소리처럼 들리지만 실제로 효과가 있는 이유는 구체적입니다.
머릿속에서 코드를 읽을 때는 뇌가 “당연히 이렇게 동작하겠지”라고 넘겨짚는 부분을 자동으로 채워 넣습니다. 하지만 그 과정을 다른 사람이 이해할 수 있도록 순서대로 설명하려면, 넘겨짚었던 가정들을 하나하나 명시적으로 말해야 합니다. “이 함수가 호출되면 우선 캐시를 확인하고… 어? 캐시가 비어있으면 어떻게 되지?”처럼, 설명을 시도하는 도중에 스스로 놓쳤던 가정을 발견하는 경우가 매우 흔합니다.
이 효과는 실제로 상대가 코드를 전혀 몰라도, 심지어 사람이 아니라 무생물이어도 동일하게 나타납니다. 핵심은 상대의 피드백이 아니라 설명을 위해 스스로 사고를 순서대로 재구성하는 과정 자체에 있기 때문입니다. 팀에서 일한다면 옆자리 동료를 붙잡고 3분만 설명해보는 것을, 혼자 일한다면 이슈 트래커나 메모장에 문제 상황을 처음부터 글로 적어보는 것을 권합니다. 글로 적는 것은 말보다 느리지만, 그만큼 더 꼼꼼하게 가정을 검토하게 만드는 효과가 있습니다.
추측을 멈추고 근거로 돌아갈 때
몇 번의 가설과 실험을 반복했는데도 진전이 없다면, 그 시점에서는 추가로 추측하는 대신 원본 소스, 공식 문서, 스펙을 직접 읽는 쪽으로 전환해야 합니다. 특히 다음과 같은 신호가 보이면 추측 기반 디버깅을 멈출 때입니다.
- 같은 종류의 가설을 이름만 바꿔가며 세 번 이상 세우고 있다(“타이밍 문제인가?” → “아니 캐시 문제인가?” → “아니 인코딩 문제인가?” 식으로 근거 없이 옮겨 다니는 경우).
- 사용 중인 라이브러리나 프레임워크의 동작을 “이렇게 동작할 것”이라고 가정만 하고 실제로 확인한 적이 없다.
- 문서화되지 않은 서드파티 API나 프로토콜의 경계 조건에서 문제가 발생하는 것 같다.
이런 상황에서는 라이브러리라면 해당 버전의 소스 코드를 직접 열어 실제 구현을 확인하고, 프로토콜이나 포맷 문제라면 스펙 문서(RFC, 공식 사양)를 원문으로 대조합니다. 스택오버플로나 블로그 글은 특정 버전, 특정 상황에서만 맞는 답을 일반화해서 설명하는 경우가 많아, 미묘하게 다른 상황에 적용하면 오히려 시간을 더 낭비하게 만듭니다. 원본 근거로 돌아가는 것이 번거로워 보여도, 막연한 추측을 반복하는 것보다 결과적으로 항상 더 빠릅니다.
버그를 고친 뒤에도 끝난 게 아니다
증상이 사라졌다고 디버깅이 끝난 것은 아닙니다. 수정을 커밋하기 전에 스스로에게 다음 질문을 던져야 합니다. “나는 이 버그가 왜 발생했는지 메커니즘을 설명할 수 있는가, 아니면 그저 증상이 없어진 것을 확인했을 뿐인가?”
이 차이는 실무에서 큰 결과 차이를 만듭니다. 메커니즘을 이해하고 고친 경우에는 다음과 같은 후속 작업이 자연스럽게 따라옵니다.
- 같은 패턴을 쓰는 다른 코드가 있는지 코드베이스를 검색해서 동일한 버그가 잠재해 있지 않은지 확인합니다.
- 이 버그가 재발하지 않도록 회귀 테스트를 추가합니다. 증상만 없앤 수정은 테스트를 작성하기도 어렵습니다. 어떤 조건에서 실패해야 하는지 자체를 모르기 때문입니다.
- 커밋 메시지나 PR 설명에 원인과 수정 이유를 남겨, 나중에 비슷한 버그를 마주친 사람(자신을 포함해서)이 맥락을 빠르게 파악할 수 있게 합니다.
반대로 원인을 모른 채 증상만 없앤 수정은 시한폭탄에 가깝습니다. 조건이 조금만 바뀌어도 같은 버그가 다른 모습으로 재등장하고, 그때는 “지난번에 고쳤는데 왜 또”라는 상태에서 다시 처음부터 조사를 시작해야 합니다.
버전 관리 이력을 디버깅 도구로 쓰기
git log와 git blame은 단순한 백업 수단이 아니라 그 자체로 훌륭한 디버깅 도구입니다. 특정 함수가 의심스러울 때 git blame으로 그 줄이 언제, 어떤 커밋에서, 어떤 맥락으로 작성되었는지 확인하면 “왜 이렇게 짜여 있는지” 파악하는 데 필요한 시간을 크게 줄일 수 있습니다. 얼핏 이상해 보이는 코드가 사실은 특정 엣지 케이스를 방지하기 위한 의도적인 우회였다는 사실을, 커밋 메시지나 연결된 PR을 확인하고서야 깨닫는 경우가 드물지 않습니다.
# 특정 파일에서 의심스러운 줄이 언제 바뀌었는지 확인
git blame -L 40,60 src/handlers/payment.py
# 그 커밋의 전체 맥락(메시지, diff, 연관 이슈 번호)을 확인
git show <commit-hash>
# 특정 함수 시그니처가 바뀐 이력만 추적
git log -p --follow -- src/handlers/payment.py
버그가 특정 시점부터 시작되었다는 확신이 있다면, 앞서 다룬 git bisect와 결합해서 “언제부터”와 “왜”를 함께 좁혀나가는 것이 가장 빠른 경로입니다.
로컬 디버깅과 프로덕션 디버깅은 다른 게임이다
로컬 개발 환경에서의 디버깅과 프로덕션 환경에서의 디버깅은 전제 자체가 다릅니다. 로컬에서는 코드를 자유롭게 멈추고, 상태를 조작하고, 몇 번이고 재실행할 수 있습니다. 프로덕션에서는 이 모든 것이 제한되거나 아예 불가능합니다. 실행을 멈추면 실제 사용자 요청이 지연되고, 재실행은 실제 부작용(결제, 알림 발송 등)을 동반할 수 있습니다.
그래서 프로덕션 디버깅은 사후 조사 중심으로 전환해야 합니다.
- 구조화된 로그: 자유 형식 텍스트가 아니라 검색·집계가 가능한 키-값 구조로 로그를 남겨야 나중에 특정 요청 ID나 사용자 ID로 이벤트를 재구성할 수 있습니다.
- 분산 트레이싱: 여러 서비스에 걸친 요청이라면 하나의 요청이 어느 서비스에서 얼마나 시간을 썼는지, 어디서 실패했는지 트레이스 ID로 엮어서 봐야 전체 그림이 보입니다.
- 에러 발생 시점의 컨텍스트 스냅샷: 예외가 발생한 순간의 요청 파라미터, 사용자 상태, 관련 설정값을 함께 기록해 두면, 나중에 로컬에서 그 조건을 그대로 재현할 확률이 크게 올라갑니다.
- 점진적 배포와 카나리아: 원인이 최근 배포로 의심된다면, 전체 롤백보다 일부 트래픽에만 새 버전을 노출하는 카나리아 배포로 영향 범위를 좁히면서 동시에 증거를 수집할 수 있습니다.
핵심은 프로덕션에서는 “지금 당장 재현해서 확인”하는 대신 “다음에 같은 일이 생겼을 때 충분한 증거가 남도록 미리 준비”하는 방향으로 사고를 전환해야 한다는 점입니다. 이 관점은 사고가 터지기 전에 관측성 인프라를 갖추는 일이 왜 중요한지와도 직결됩니다.
정리: 디버깅은 절차다
효율적인 디버깅은 재능이나 직관의 문제가 아니라 반복 가능한 절차입니다.
- 재현 가능한 최소 케이스를 먼저 확보한다. 재현 없이는 아무것도 검증할 수 없다.
- 반증 가능한 가설을 세우고, 코드를 고치기 전에 가설을 검증할 최소한의 실험(로그, 조건부 중단)을 설계한다.
- 원인의 위치를 모를 때는 코드, 커밋, 데이터, 설정 중 어디에든 이분 탐색을 적용해 탐색 공간을 기계적으로 줄인다.
- 스택 트레이스는 최상단 프레임만 보지 말고 애플리케이션 코드가 나올 때까지 따라 내려간다.
- 로그와 디버거는 경쟁 관계가 아니라 상황별 도구다. 흐름을 봐야 하면 로그, 순간의 상태를 깊이 봐야 하면 디버거를 쓴다.
- 막힐 때는 더 추측하지 말고 소리 내어 설명하거나, 원본 소스와 스펙으로 돌아간다.
- 증상이 사라진 것과 원인을 이해한 것은 다르다. 메커니즘을 설명할 수 있어야 회귀 테스트와 재발 방지가 가능하다.
git blame,git bisect는 이력 조회 도구가 아니라 적극적인 디버깅 도구로 쓴다.- 로컬과 프로덕션은 다른 게임이다. 프로덕션에서는 재현보다 증거 수집(로그, 트레이싱, 스냅샷)을 우선한다.
이 순서를 몸에 익히면 새로운 언어, 새로운 코드베이스, 처음 보는 프레임워크에서도 똑같이 적용할 수 있습니다. 언어별 문법이나 특정 에러 메시지는 검색으로 금방 찾을 수 있지만, 문제에 접근하는 순서 자체는 경험으로만 쌓입니다.