Rain Lag

디버깅 마인드셋: 말 안 듣는 코드와 싸울 때 탐정처럼 생각하는 법

버그를 잡을 때 탐정처럼 접근하는 방법을 다룹니다. 가설을 세우고, 증거를 모으고, 머릿속 모델을 다듬어 매번의 버그를 장기적인 프로세스 개선으로 바꾸는 디버깅 마인드셋을 배워보세요.

소개: 코드가 도저히 이해가 안 될 때

개발자라면 누구나 이런 순간을 안다. 코드가 당연히 돌아가야 한다. 논리는 완벽해 보이고, 문법도 맞고, 열 번은 다시 살펴봤다. 그런데도 프로그램은 마치 자기 의지가 있는 것처럼 엉뚱하게 동작한다.

이때 많은 사람은 허우적거린다. 여기저기 print를 박아 넣고, 다급하게 Stack Overflow를 뒤지고, “이 부분 그냥 갈아엎어 볼까…” 하면서 손 가는 대로 고치기 시작한다. 과정은 점점 더 혼란스럽고 지치기만 하다.

더 나은 방법은 디버깅을 탐정 수사처럼 대하는 것이다.

뛰어난 디버거는 “더 빨리 찍어 맞추는 사람”이 아니다. 그들은 체계적으로 추론하고, 검증 가능한 가설을 세우며, 증거를 모으고, 시스템에 대한 이해를 계속 다듬어 가다가 어느 순간 버그가 너무나 분명해지는 지점까지 간다. 그리고 매번 고통스러운 문제를, 앞으로 비슷한 일이 덜 생기게 만드는 기회로 바꾼다.

이게 바로 디버깅 마인드셋이며, 누구나 연습해서 익힐 수 있다.


1. 도박꾼이 아니라 탐정처럼 디버깅하기

디버깅은 운의 문제가 아니다. 방법론의 문제다.

탐정은 아무나 잡아서 범인일지도 모른다고 체포하지 않는다. 대신 이렇게 한다.

  1. 증거를 수집한다
  2. 무슨 일이 일어났는지에 대한 가설을 세운다
  3. 새로운 증거로 가설을 검증한다
  4. 가설을 수정하거나 폐기한다

디버깅도 똑같이 할 수 있다.

  1. 버그를 안정적으로 재현한다
    간헐적인 버그라면, 그나마 더 자주 터지게 만드는 입력이나 실행 순서를 찾아라. 재현 가능한 버그는, 정체 불명의 버그보다 10배는 쉽게 고칠 수 있다.

  2. 증상을 면밀히 관찰한다
    정확히 뭐가 문제인가? 출력이 틀린가? 크래시가 나는가? 성능 이슈인가? UI가 깨지는가? “안 돼요”라고만 하지 말고, 기대한 동작과 실제 동작이 어떻게 다른지를 구체적으로 적어라.

  3. 가설을 세운다
    지금 시스템에 대한 당신의 지식을 바탕으로, 어떤 것들이 이런 증상을 만들 수 있을까? 20개씩 늘어놓지 말고, 1~3개의 그럴듯한 가설로 시작하라.

  4. 집중된 실험을 설계한다
    현재 가설을 확인하거나 반박할 수 있도록, 딱 한 가지를 바꾸거나 딱 한 가지 관찰 지점(로그 한 줄, 브레이크포인트, 작은 테스트)을 추가하라.

  5. 믿음을 업데이트한다
    실험 결과가 예상과 다르다면, 무작정 다른 코드를 또 고치지 말고 머릿속 시스템 모델을 업데이트하라. 당신이 “당연하다”고 생각했던 어떤 전제가 틀렸을 수 있다.

이 루프—증거 → 가설 → 실험 → 정교화—가 디버깅 마인드셋의 핵심이다.


2. 지독한 버그 하나를 예방 학습으로 바꾸기

버그를 고치는 건 절반만 끝낸 것이다. 나머지 절반은 이런 질문을 던지는 일이다.

“처음부터 이 버그가 생기지 않게 만들 수 있었던 건 뭐였을까?”

의미 있는 버그를 하나 잡을 때마다, 2분만 투자해서 다음 질문에 답을 적어 보라.

  • 어떤 기법, 패턴, 개발 습관이 있었다면 애초에 이 버그가 불가능했을까?
  • API 설계를 다르게 했다면, 잘못 쓰기 어렵지 않았을까?
  • 이름을 더 명확히 지었거나 역할을 분리했다면, 이런 혼란이 줄지 않았을까?
  • 코드 리뷰 체크리스트에 뭘 추가했으면 잡혔을까?

예를 들어:

  • 공유 상태 때문에 레이스 컨디션이 났다면? → 불변(immutable) 데이터 구조를 도입하거나, 더 명확한 동시성 모델을 설계한다.
  • 인덱스를 하나 잘못 계산한 off-by-one 버그라면? → 직접 인덱스를 돌리기보다 **범위 기반 루프(range-based loop)**나 라이브러리 함수를 우선 사용한다.
  • 함수 이름이 헷갈려서 잘못 사용됐다면? → 이름을 바꾸고, 문서에 불변조건(invariant)을 명시해 둔다.

목표는 누구를 탓하는 게 아니다. 버그 하나하나를 프로세스를 개선하는 자원으로 수확해서, 같은 종류의 문제가 다시 생길 가능성을 줄이는 것이다.

이걸 꾸준히 하면, 큰 노력 없이도 코드베이스의 품질이 눈에 띄게 올라간다.


3. 버그를 작업 흐름에 대한 피드백으로 보기

모든 버그는 이런 신호다. “당신의 프로세스 어딘가에서 이걸 미리 잡지 못했다.”

당장 버그를 고친 뒤엔 이렇게 물어보라.

  • 어떤 종류의 테스트가 이 문제를 미리 잡아줬을까?
  • 코드 안 어디에 assertion(단언)을 넣었다면, 더 일찍 실패했을까?
  • 어떤 정적 분석 규칙이나 린터(linter) 체크가 이걸 잡았을까?
  • 통합 전에, 작은 예제 프로그램이나 playground로 실험했다면 드러났을까?

그리고 나서 구체적으로 행동하라.

  • 이 버그를 재현하는 **단위 테스트(unit test)**를 추가하고, 영구히 유지하라. 과거의 실패를 미래의 안전 장치로 바꾸는 것이다.
  • 실제로 틀렸던 가정 근처에 assertion을 추가하라. (예: 배열이 비어 있지 않다, 값이 범위 안에 있다, null이 아니다 등)
  • CI 파이프라인에 새로운 체크를 늘려라. (타입 체커 옵션 강화, 린터 규칙 추가, 보안 스캐너 도입 등)

워크플로우는 버그와 함께 진화해야 한다. 실패할 때마다, 다음 개발 환경이 조금씩 더 보호막을 갖추도록 만드는 셈이다.


4. 시스템에 대한 머릿속 모델을 계속 다듬기

버그가 특히 혼란스럽게 느껴지는 진짜 이유는, 당신의 멘탈 모델(mental model)—머릿속에 그려진 코드의 모습—이 실제 시스템이 하는 일과 안 맞기 때문이다.

멘탈 모델에는 이런 것들이 들어 있다.

  • 데이터가 시스템을 통해 어떻게 흘러가는지
  • 상태가 언제, 어디서 바뀌는지
  • 항상 유지돼야 하는 불변조건(invariant)은 무엇인지
  • 컴포넌트들이 어떤 순서로, 어떻게 상호작용하는지

현실이 당신의 기대와 어긋나는 순간, 놀라게 되고—그 놀람 자체가 버그의 정체다.

멘탈 모델을 개선하려면:

  1. 다이어그램을 그려라
    데이터 흐름, 컴포넌트 경계, 핵심 상태들을 대충이라도 그려 보라. 대강의 박스와 화살표만으로도, 어디를 제대로 이해 못 했는지 금방 드러난다.

  2. 실행 과정을 입으로 설명해 보라
    “먼저 이 함수가 X를 인자로 호출되고, 그다음 Y를 호출해서 Z를 업데이트하고…” 같은 식으로 말로 풀어 보라. 설명이 막힌다면, 당신의 모델이 불완전한 것이다.

  3. 불변조건을 찾아라
    항상 참이어야 하는 것은 무엇인가? (예: “주문 총액은 각 라인 아이템의 합과 같아야 한다”) 이런 것들을 적어 두고, 중요한 것들은 코드 수준의 assertion으로 만드는 것이 좋다.

  4. 코드를 개념에 맞게 정렬하라
    함수 이름, 모듈 경계, 데이터 구조가, 당신이 시스템을 이해하고 생각하는 방식과 최대한 일치하도록 정리하라.

멘탈 모델이 명확해지면, 버그를 더 빨리 찾을 뿐 아니라 처음부터 버그를 덜 만들어내게 된다.


5. 순수 TDD가 아니어도, 테스트 주도적인 사고 적용하기

꼭 엄격한 Test-Driven Development(TDD)를 해야만 테스트 주도적인 사고의 이득을 얻을 수 있는 건 아니다.

  • 내부 구현보다 **인터페이스와 동작(behavior)**에 집중하라.
  • 함수, 모듈, 서비스가 구체적인 상황에서 어떻게 동작해야 하는지 정의하라.
  • 각 테스트를 하나의 **계약(contract)**처럼 대하라. 이 입력과 조건이 주어지면, 시스템은 반드시 X를 해야 한다는 약속이다.

이 접근은 디버깅에 두 가지 큰 장점을 준다.

  1. 실패가 깨진 가정을 정확히 가리킨다
    테스트가 깨졌다는 건, 특정한 행동이 더 이상 계약과 일치하지 않는다는 의미다. “앱이 뭔가 이상하다”라는 막연한 상태보다 훨씬 추적하기 쉽다.

  2. 안전망을 제공한다
    디버깅과 리팩토링을 하는 동안, 테스트는 예전 버그가 다시 살아나지 않았고, 새로운 버그도 만들지 않았다는 걸 확인해 주는 안전망이 된다.

실천 팁:

  • 버그를 발견하면, 먼저 그 버그를 재현하는 실패하는 테스트를 작성하고, 그다음에 코드를 고쳐라.
  • 새 기능을 만들 땐, 최소한 핵심 시나리오 몇 개에 대한 테스트를 미리 작성해 기대 동작을 고정해 두라.
  • 빈 입력, 최대 크기, 잘못된 데이터, 이상한 호출 순서 같은 엣지 케이스를 테스트로 탐색하라.

이런 사고방식은, 디버깅을 깜깜한 방에서 더듬거리며 걷는 일에서, 잘 표시된 표지판을 따라가는 일로 바꿔 준다.


6. 체계적인 디버깅 연습하기

체계적인 디버깅은 탐색 범위를 줄이고, 가정을 하나씩 검증하는 것이다. 무작정 휘젓는 게 아니다.

핵심 습관은 다음과 같다.

  1. 문제를 고립시켜라

    • 실패하는 시나리오를 가능한 한 작은 재현 케이스로 줄인다.
    • 버그와 상관없어 보이는 코드와 설정을 하나씩 걷어내면서도, 버그가 여전히 발생하는 지점을 찾는다.
    • 이렇게 불필요한 것들을 벗겨 내다 보면, 진짜 원인이 드러나는 경우가 많다.
  2. 반씩 줄여가며 좁혀라

    • 이분 탐색하듯이 문제 영역을 좁혀라. 과정 중간 지점에서 로그를 찍거나 상태를 검사해 보라. 거기까지는 정상인가?
    • 그렇다면 문제는 두 번째 절반에 있다. 아니라면 첫 번째 절반을 다시 쪼개 살펴라. 이 과정을 반복한다.
  3. 한 번에 한 가지만 바꿔라

    • 여러 파일, 여러 로직 분기, 여러 설정을 한 번에 수정하지 말라.
    • 매번 변경 후에는, 기대한 방향으로 동작이 변했는지 꼭 다시 확인하라.
  4. 모든 가정을 의심하고 검증하라
    스스로에게 이렇게 물어보라.

    • 이 함수가 정말 호출되고 있다는 걸 확신하는가?
    • 이 값이 정말 null/빈 값/범위 밖이 아니라고 확신하는가?
    • 이 설정이 정말 이 환경에서 로딩되고 있다고 확신하는가?

    그리고 로그, assertion, 브레이크포인트를 추가해 그 가정을 실제로 검증하라.

이런 훈련된 접근은 디버깅을 짜증 나는 찍기 놀이에서, 통제 가능한 조사 과정으로 바꿔 준다.


7. 도구는 쓰되, 사고가 먼저다

디버깅 도구는 매우 강력한 아군이다.

  • 디버거로 코드를 한 줄씩 stepping하며 실행 흐름을 보는 것
  • 브레이크포인트와 watch expression으로 특정 시점의 변수를 들여다보는 것
  • **로그(logging)**로 여러 실행과 환경에서 무슨 일이 일어나는지 기록하는 것
  • **프로파일러(profiler)**로 성능 문제를 분석하는 것

하지만 도구가 생각 자체를 대신해 줄 수는 없다.

명확한 질문—“내가 지금 무엇을 확인하려고 하는가?”—없이 디버거를 띄우면, 금세 의미 없이 클릭만 하다가 끝나기 쉽다.

도구는 이렇게 사용할 때 빛난다.

  • 구체적인 가설을 검증할 때 (“여기서 이 변수가 null이 되는 일이 실제로 있나?”)
  • 실행 순서를 확인할 때 (“이 콜백이 X보다 먼저, 아니면 나중에 호출되나?”)
  • 중요한 의사결정 지점에서 상태를 보는 데 (“이 줄에서 캐시 안에는 뭐가 들어 있지?”)

도구가 당신의 추론 능력을 증폭하게 만들고, 그걸 대체하게 두지 말라.


결론: 침착하고 호기심 많은 디버깅 문화 만들기

버그는 단순한 장애물이 아니다. 버그는 피드백이다. 코드, 설계, 그리고 개발 프로세스 전체에 대한 피드백이다.

디버깅 마인드셋을 가지면, 당신은 이렇게 변한다.

  • 도박꾼이 아니라, 탐정처럼 문제에 접근한다.
  • 골치 아픈 버그 하나하나를, 다음 번 예방을 위한 학습 재료로 바꾼다.
  • 실패를 계기로 테스트와 워크플로우를 강화한다.
  • 시스템에 대한 멘탈 모델을 끊임없이 정교하게 다듬는다.
  • 테스트 주도적인 사고로, 동작과 계약에 주의를 집중한다.
  • 탐색 범위를 줄이고 가정을 검증하는 체계적인 디버깅을 실천한다.
  • 도구를, 명확한 논리적 사고를 뒷받침하는 수단으로 사용한다.

시간이 지나면, 이는 단지 버그를 더 잘 고치는 수준을 넘어서서, 처음부터 이해하기 쉽고, 테스트하기 쉽고, 유지보수하기 쉬운 코드를 쓰는 능력으로 이어진다.

다음에 코드가 전혀 말이 안 되는 행동을 보이더라도, 당황해서 아무 줄이나 고치지 말라. 속도를 늦추고, 탐정처럼 생각하라. 그러면 시스템이 스스로, 당신의 이해—그리고 당신의 코드—가 어디에서 바뀌어야 하는지를 말해 줄 것이다.

디버깅 마인드셋: 말 안 듣는 코드와 싸울 때 탐정처럼 생각하는 법 | Rain Lag