Skip to content

Split View: 디버깅은 왜 대체가 어려운가 — 배제의 기술과 가설의 문장

✨ Learn with Quiz
|

디버깅은 왜 대체가 어려운가 — 배제의 기술과 가설의 문장

고쳐졌다에서 멈추는 사람과 왜를 묻는 사람

버그가 하나 있습니다. 두 사람이 붙습니다.

한 사람은 의심 가는 곳을 고치고 새로고침합니다. 안 되면 다른 곳을 고치고 새로고침합니다. 여러 번 반복하다 어느 순간 증상이 사라집니다. 그러면 커밋하고 넘어갑니다.

다른 사람은 증상이 사라진 지점에서 멈추지 않습니다. 방금 무엇을 바꿨는지 되짚고, 그 변경이 왜 증상을 없앴는지 설명이 되는지 확인합니다. 설명이 안 되면 아직 안 끝난 것으로 칩니다. 되돌려서 증상이 다시 나오는지도 봅니다.

두 사람의 실력 차이는 이번 버그에서는 거의 드러나지 않습니다. 드러나는 것은 세 달 뒤, 같은 증상이 다른 얼굴로 다시 올라올 때입니다.

디버깅은 생성이 아니라 배제입니다

1편에서 값이 남는 기술은 검증 비용이 비싼 기술이라고 했습니다. 디버깅은 그 정의에 거의 완벽하게 들어맞습니다. 이유는 작업의 방향이 코드 작성과 반대이기 때문입니다.

코드를 쓰는 일은 가능한 것 중 하나를 만들어 내는 일입니다. 만들어 놓으면 존재 자체가 결과물입니다. 디버깅은 반대로, 가능한 원인들을 하나만 남을 때까지 지우는 일입니다. 지우는 데는 결과물이 남지 않습니다. 남는 것은 배제된 가설의 목록뿐이고, 그 목록은 대개 아무 데도 기록되지 않습니다.

그리고 디버깅에 필요한 입력은 코드 안에 없습니다. 언제부터 그랬는지, 최근 무엇이 배포됐는지, 이 증상이 특정 고객에게만 나타나는지, 3주 전에 누가 어떤 설정을 만졌는지. 이건 검색 가능한 텍스트가 아니라 조직에 흩어져 있는 상태입니다. 이 정보를 모으는 것부터가 이미 작업의 절반입니다.

가설은 반증 가능한 문장이어야 합니다

디버깅을 배웠다는 사람과 그냥 오래 해 온 사람을 가르는 지점이 여기 있습니다. 가설의 형태입니다.

"네트워크 문제인 것 같다"는 가설이 아닙니다. 틀렸을 때 무엇이 보일지가 정해져 있지 않기 때문에, 무슨 결과가 나와도 이 문장은 살아남습니다. 살아남는 추측은 좁혀 주지 않습니다.

쓸 수 있는 가설은 이런 모양입니다. "요청이 게이트웨이에서 애플리케이션으로 넘어가는 구간에서 끊긴다. 그렇다면 게이트웨이 접근 로그에는 5초 타임아웃 기록이 있고, 애플리케이션 접근 로그에는 이 요청 아이디가 아예 없어야 한다."

차이는 뒤쪽 문장에 있습니다. 확인할 것이 지정되어 있고, 예상과 다르게 나오면 가설이 죽습니다. 죽는 가설만이 후보를 줄여 줍니다.

예시 — 변경하기 전에 적는 세 줄

가설:   요청이 게이트웨이와 애플리케이션 사이에서 끊긴다
맞다면: 게이트웨이 로그에 5초 타임아웃, 앱 로그에 해당 요청 아이디 없음
틀리면: 앱 로그에 요청이 들어와 있고 처리 중 실패한 흔적이 있음

확인 결과:
다음 가설:

이 세 줄을 적는 데 1분이 걸립니다. 그 1분이 아까워 보이지만, 적지 않으면 30분 뒤에 자기가 이미 배제한 것을 다시 확인하고 있게 됩니다. 여러 사람이 붙은 장애에서는 효과가 더 큽니다. 배제 목록이 공유되지 않으면 세 사람이 같은 것을 세 번 확인합니다.

시스템을 계층으로 자르기

가설을 좁히는 가장 신뢰할 만한 절차는 이분 좁히기입니다. 코드 이력에서 쓰는 그것과 원리가 같지만, 대상이 커밋이 아니라 계층입니다.

먼저 요청이나 데이터가 통과하는 층을 순서대로 적습니다. 클라이언트, DNS, 로드밸런서, 게이트웨이, 애플리케이션, 캐시, 데이터베이스, 외부 연동. 그리고 정확히 가운데를 골라 묻습니다. 여기까지는 정상인가.

핵심은 정상 여부를 판정할 수 있는 지점을 고르는 것입니다. 판정이 불가능한 지점에서 자르면 절반을 못 지웁니다. 그래서 좋은 시스템은 자를 수 있는 지점이 많은 시스템이고, 관측 가능성이라는 말이 실제로 뜻하는 것도 이것입니다.

한 층씩 순서대로 훑는 방식과 비교하면 차이가 큽니다. 여덟 개 층을 순서대로 보면 평균 네 번을 봐야 하고, 절반씩 자르면 세 번 안에 끝납니다. 층이 스무 개로 늘면 격차는 열 번 대 다섯 번이 됩니다. 그리고 실제 장애에서는 한 번 확인하는 데 몇 분이 걸리므로, 이 차이가 곧 복구 시간입니다.

재현되지 않는 문제를 다루는 법

가장 어려운 종류는 재현이 안 되는 문제입니다. 하루에 두어 번 일어나고, 보려고 하면 안 일어납니다.

여기서 흔한 실패는 재현을 목표로 삼는 것입니다. 며칠을 재현에 쓰고 못 만들고 끝납니다. 방향을 바꾸는 편이 낫습니다. 목표를 재현이 아니라 다음번 관측으로 옮기는 것입니다.

절차는 이렇습니다. 먼저 이 증상이 일어났을 때 어떤 증거가 있어야 원인을 특정할 수 있는지 목록으로 적습니다. 그다음 그 증거가 지금 남고 있는지 확인합니다. 대부분은 안 남고 있습니다. 그러면 남도록 계측을 심고 기다립니다. 다음번에 일어날 때 증거가 손에 들어옵니다.

이 접근의 장점은 실패해도 남는 것이 있다는 점입니다. 심어 둔 계측은 이 버그가 아니어도 다음 버그에서 쓰입니다. 반면 재현 시도는 실패하면 아무것도 남기지 않습니다.

주의할 것도 있습니다. 타이밍과 경합이 원인인 문제는 계측을 넣는 행위 자체가 타이밍을 바꿔 증상을 감춥니다. 이럴 때는 실행 경로에 끼어들지 않는 관측을 골라야 합니다. 이미 남고 있는 로그의 시각을 맞춰 보거나, 표본 추출로 부담을 줄이거나, 사후에 상태를 덤프하는 쪽입니다.

왜 연차와 격차가 비례하지 않는가

디버깅은 경력이 쌓이면 저절로 늘 것 같지만 실제로는 그렇지 않습니다. 이유가 세 가지 있습니다.

첫째, 명시적으로 가르치는 곳이 거의 없습니다. 언어와 프레임워크는 배우지만 좁히는 절차는 어깨너머로 배웁니다. 옆에 절차를 가진 사람이 없으면 10년을 해도 추측과 변경의 반복에 머뭅니다.

둘째, 피드백이 잘못 걸려 있습니다. 증상이 사라지면 보상이 옵니다. 왜 사라졌는지 모르는 상태도 똑같이 보상받습니다. 그래서 운으로 고친 경험이 실력으로 오인됩니다.

셋째, 좋은 디버깅은 흔적이 안 남습니다. 30분 만에 정확히 좁혀 고친 사람과 세 시간 헤매다 우연히 고친 사람의 커밋은 같아 보입니다. 그래서 조직이 이 능력을 알아보기 어렵고, 알아보지 못하면 육성되지도 않습니다.

여기에 대한 대응은 흔적을 남기는 것입니다. 배제한 가설과 확인 방법을 짧게라도 이슈에 적으면, 그 기록이 자기 훈련이 되는 동시에 팀의 자산이 됩니다.

직접 해보기

이번 주에 만나는 버그 하나를 골라, 코드를 고치기 전에 위의 세 줄을 적어 보세요. 가설, 맞다면 보일 것, 틀리면 보일 것. 세 번째 줄이 안 써지면 그건 아직 가설이 아니라 느낌입니다.

  • 리눅스 터미널 시뮬레이터 — 로그와 프로세스와 네트워크 상태를 손으로 확인하는 연습을 안전하게 반복할 수 있습니다.
  • FDE 엔지니어 키우기 RPG — 제한된 시간과 접근 권한 안에서 원인을 좁혀 진단을 확정하는 미션이 31개 있습니다. 도메인 레벨이 오르면 같은 로그가 다르게 읽히도록 설계돼 있습니다.

이 조언이 안 통하는 경우도 있습니다. 서비스가 지금 죽어 있는 동안에는 진단보다 완화가 먼저입니다. 롤백하거나 트래픽을 돌려 출혈을 멈춘 뒤에 좁히세요. 가설을 예쁘게 적는 것은 사용자가 기다리지 않을 때 할 일입니다.

이어서 읽기

비싸게 남는 기술 시리즈

Why Debugging Resists Replacement — The Craft of Elimination

Stopping at Fixed Versus Asking Why

There is a bug. Two people take it.

One changes a suspicious spot and reloads. Nothing. Changes another spot and reloads. After several rounds, the symptom disappears. They commit and move on.

The other does not stop where the symptom disappeared. They retrace what they just changed and check whether that change explains the disappearance. If it does not explain it, the work is not finished. They also revert to see whether the symptom comes back.

The gap between these two barely shows on this bug. It shows three months later, when the same cause returns wearing a different face.

Debugging Is Elimination, Not Generation

Part 1 argued that the skills holding value are the ones expensive to verify. Debugging fits that definition almost perfectly, because the direction of the work is the reverse of writing code.

Writing code produces one of the possible things. Once produced, its existence is the deliverable. Debugging erases possible causes until one remains. Erasing leaves no artifact. What remains is a list of eliminated hypotheses, and that list usually gets recorded nowhere.

The inputs debugging needs are also not inside the code. Since when has it done this, what shipped recently, does the symptom hit only one customer, who touched which setting three weeks ago. That is not searchable text; it is state scattered across an organization. Gathering it is already half the job.

A Hypothesis Has to Be Falsifiable

Here is what separates someone who learned debugging from someone who has merely done it a long time: the shape of the hypothesis.

"Seems like a network problem" is not a hypothesis. Nothing is specified about what you would see if it were wrong, so the sentence survives every observation. A guess that survives everything narrows nothing.

A usable hypothesis looks like this. "The request is dropped between the gateway and the application. If so, the gateway access log holds a five-second timeout and the application access log has no record of this request id at all."

The difference is the second sentence. Something specific is nominated for checking, and if the check comes back differently the hypothesis dies. Only hypotheses that can die reduce the candidate set.

Example — three lines to write before you change anything

Hypothesis: the request is dropped between gateway and application
If true:    gateway log shows a 5s timeout, app log has no such request id
If false:   app log shows the request arriving and failing during processing

Observed:
Next hypothesis:

Writing those three lines takes a minute. The minute looks wasteful until you skip it and find yourself, thirty minutes later, re-checking something you already ruled out. On an incident with several responders the effect is larger: if the eliminated list is not shared, three people check the same thing three times.

Cutting the System Into Layers

The most reliable way to narrow hypotheses is bisection. Same principle as bisecting a commit history, except the objects are layers rather than commits.

First, write down in order the layers a request or a record passes through: client, DNS, load balancer, gateway, application, cache, database, external integration. Then pick the exact middle and ask whether things are still correct at that point.

The crucial part is picking a point where correctness can actually be judged. Cut where judgment is impossible and you erase nothing. That is why a good system is one with many cuttable points, and it is what observability substantively means.

Compare with walking the layers in order. Eight layers checked in sequence averages four checks; halving finishes in three. At twenty layers the gap becomes ten versus five. And in a real incident each check costs minutes, so this difference is recovery time.

Working a Bug That Will Not Reproduce

The hardest category is the one you cannot reproduce. It happens twice a day and never while you are watching.

The common failure here is making reproduction the goal. Days go into it and it ends with nothing. Better to change direction and move the goal from reproduction to the next observation.

The procedure: first list what evidence would let you identify the cause the next time the symptom occurs. Then check whether that evidence is currently being retained. Usually it is not. So add the instrumentation that retains it, and wait. Next time it happens, the evidence is in your hands.

The advantage of this approach is that failure still leaves something behind. Instrumentation you planted gets used on the next bug even if it misses this one. A failed reproduction attempt leaves nothing.

One caution. When the cause is timing or a race, the act of adding instrumentation changes the timing and hides the symptom. Then you need observation that stays out of the execution path: line up timestamps in logs already being written, sample to reduce overhead, or dump state after the fact.

Why the Gap Does Not Track Years of Experience

You would expect debugging to improve on its own with experience. It does not, for three reasons.

First, almost nobody teaches it explicitly. Languages and frameworks get taught; the narrowing procedure gets absorbed over someone's shoulder. Without a person nearby who has a procedure, ten years can pass inside the guess-and-change loop.

Second, the feedback is wired wrong. The symptom disappearing is rewarded. Not knowing why it disappeared is rewarded identically. So a lucky fix gets mistaken for competence.

Third, good debugging leaves no trace. The commit from someone who narrowed it precisely in thirty minutes looks the same as the commit from someone who flailed for three hours and got lucky. Organizations therefore have trouble seeing this capability, and what cannot be seen does not get developed.

The response is to leave a trace. Writing the eliminated hypotheses and how you checked them into the issue, even briefly, trains you and becomes a team asset at the same time.

Try It This Week

Pick one bug you hit this week and write the three lines above before you touch any code: hypothesis, what you would see if true, what you would see if false. If the third line will not come, what you have is not a hypothesis yet but a feeling.

  • Linux Terminal Simulator — safe repetition of checking logs, processes, and network state by hand.
  • FDE Career RPG — 31 missions that make you narrow a cause and commit a diagnosis under limited time and access, designed so that leveling a domain changes what you can read in the same log.

Where this advice does not apply: while the service is down, mitigation comes before diagnosis. Roll back or shift traffic to stop the bleeding, then narrow. Writing tidy hypotheses is what you do when users are not waiting.

Further Reading

What Stays Expensive series