Skip to content
Published on

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

공유하기
Authors

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

확인 결과:
다음 가설:

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

시스템을 계층으로 자르기

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

직접 해보기

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

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

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

이어서 읽기

비싸게 남는 기술 시리즈