Rain Lag

세 가지 트레이스 규칙: 어떤 레거시 코드 경로도 이해하는 미니멀리스트 전략

전체 코드베이스를 다 읽지 않고도 지저분한 레거시 코드를 이해하는 방법 — 세 가지 트레이스 규칙, 최신 도구, 그리고 똑똑한 로깅을 활용해 알 수 없는 시스템을 ‘탐색 가능한 영역’으로 바꾸는 방법을 다룹니다.

소개

레거시 시스템은 대개 문서가 없고, 자주 깨지며, 대체로 두렵습니다. 그런데도 많은 비즈니스의 가장 핵심 기능을 담당합니다. 거대하고 제대로 이해되지 않은 코드베이스에 떨어졌을 때, 대부분의 사람은 “맨 위부터 시작해서 다 읽어야지”라는 생각을 합니다.

그 본능은 잘못된 겁니다.

특정 동작 하나를 안전하게 바꾸기 위해 시스템 전체를 이해할 필요는 없습니다. 필요한 것은 특정 코드 경로 하나를 안정적으로 따라갈 수 있는 방법입니다. 여기서 등장하는 것이 바로 **세 가지 트레이스 규칙(The Three-Trace Rule)**입니다. 전체 코드 투어 대신, 소수의 실제 실행 트레이스를 활용하는 미니멀리스트 전략입니다.

이 글에서 다룰 내용은 다음과 같습니다.

  • 세 가지 트레이스 규칙이 무엇이며, 어떻게 사용하는지
  • 런타임 트레이스와 정적 분석, 메트릭, 시각화를 조합하는 방법
  • 에러 처리를 계측해, 실패가 바로 “관심 있는 경로”를 가리키게 만드는 법
  • 로깅과 트레이싱을 점진적으로 개선해, 이후의 모든 조사 속도를 높이는 방법

세 가지 트레이스 규칙 한눈에 보기

세 가지 트레이스 규칙은 레거시 코드 경로를 이해하기 위한 실용적인 접근법입니다.

레거시 동작 하나를 이해하려면, 코드베이스 전체를 읽으려 하지 말고 그 동작에 대한 대표적인 런타임 트레이스 세 개를 수집해 분석하라.

이 트레이스는 예를 들어 다음과 같을 수 있습니다.

  • 실제 프로덕션 또는 스테이징 실행에서 남은 로그 라인들
  • 에러 스택 / 스택 트레이스
  • 분산 트레이싱 시스템(OpenTelemetry, Jaeger, Zipkin 등)에서 나온 트레이스
  • 디버거로 엔드 투 엔드 한 번 쭉 따라가 본 세션

핵심은 코드가 이론적으로 갈 수 있는 모든 경로가 아니라, 실제로 관찰된 실행 경로에 집중하는 것입니다.

왜 세 개일까요?

  • 트레이스 하나어떤 경로 하나를 보여 줍니다.
  • 트레이스 두 개는 “동작이 달라질 수 있다”는 걸 보여 줍니다.
  • 트레이스 세 개면, 보통 안전하게 변경하기 위해 꼭 알아야 하는 주요 분기점과 변동성을 드러냅니다.

많은 경우, 잘 고른 세 개의 트레이스만 있으면 다음을 할 수 있습니다.

  • 관련된 모듈, 함수, 데이터 구조를 파악한다.
  • 동작을 수정할 위치를 정확히 찾는다.
  • 에러 처리가 약하거나 빠져 있는 지점을 이해한다.

당신의 목표는 시스템 전체를 정복하는 것이 아닙니다. **“이 특정한 동작”**을 정복하는 것입니다.


1단계: 소스 코드가 아니라 런타임에서 시작하기

전통적인 레거시 탐색 방식은 소스 코드에서 시작합니다. 레포를 열고, 디렉터리 트리를 훑고, 파일을 읽으면서 패턴이 보이기를 바랍니다. 느리고, 지치기 쉽고, 자주 포기하게 됩니다.

대신, 런타임 현실에서 출발하십시오.

  1. 해당 동작을 재현합니다. 가능한 한 통제된 환경(개발, 스테이징, 테스트)에서. 버그라면 의도적으로 재현합니다. 기능이라면 엔드 투 엔드로 한 번 쭉 사용해 봅니다.
  2. 그 실행에 대한 트레이스를 캡처합니다. 이미 가지고 있는 도구를 활용하세요.
    • 애플리케이션 로그(요청 ID, 상관관계 ID 등)
    • 에러에서 나오는 스택 트레이스
    • APM / 관측(Observability) 도구에서 나오는 분산 트레이스
    • 이번 조사를 위해 임시로 추가하는 간단한 디버그 로그
  3. 조건을 조금씩 바꿔가며 세 번 반복합니다.
    • 다른 입력 파라미터
    • 성공 케이스 vs 실패 케이스
    • 다른 사용자 유형이나 테넌트

각 실행은 입구에서 출구까지 뭐가 어떻게 일어났는지에 대한 상세한 흔적을 남겨야 합니다.

이제 코드를 실제로 어떻게 실행하는지 보여 주는 구체적인 예시 세 개를 손에 넣은 셈입니다.


2단계: 정적 분석과 메트릭은 ‘소설’이 아니라 ‘지도’로 쓰기

트레이스를 손에 쥐고 나면, 이미 파일명, 함수명, 모듈 이름 몇 개는 알고 있는 상태입니다. 이제 정적 도구를 사용해 막연히 헤매지 말고, 곧장 관련 지점으로 이동할 수 있습니다.

유용한 도구와 메트릭은 다음과 같습니다.

  • 호출 그래프 / 의존성 분석 도구: go callgraph, IntelliJ Call Hierarchy, VSCode 확장 등. 트레이스에 등장하는 함수 주변에서 누가 누구를 호출하는지 확인할 수 있습니다.
  • 코드 검색 도구: ripgrep, Sourcegraph, GitHub Code Search 등으로 트레이스에 나온 심볼, 로그 메시지, 에러 텍스트를 검색합니다.
  • 복잡도 메트릭: 순환 복잡도(cyclomatic complexity), 함수 길이 등으로 위험도가 높거나 리팩터링 가치가 큰 지점을 찾습니다.
  • 소유권 / 변경 이력 메트릭: git blame, 코드 변경 빈도 리포트 등을 통해
    • 마지막으로 이 코드를 수정한 사람이 누구인지
    • 자주 바뀌어서 깨지기 쉬운 파일이 어디인지 를 파악합니다.

여기서 중요한 점은 코드베이스 전체를 훑고 있는 게 아니라는 것입니다. 정적 분석 도구는 어디까지나 지도일 뿐이고, 그 지도 위에서 트레이스가 주는 좌표를 따라가는 겁니다.


3단계: ‘시스템 전체’가 아니라 ‘경로 하나’를 시각화하기

대상 모듈 범위를 몇 개로 좁혔다면, 이제 시각화 도구를 이용해 이해 속도를 높일 수 있습니다.

  • 아키텍처 / 의존성 다이어그램: 많은 IDE와 플러그인이 패키지 의존성이나 모듈 그래프를 그려 줍니다.
  • 시퀀스 다이어그램: 트레이스 하나를 시퀀스 다이어그램으로 옮겨 보면(수동이든, 로그 기반 자동화 도구든), 컴포넌트 간 상호작용이 훨씬 이해하기 쉬워집니다.
  • DB / 쿼리 시각화 도구: 트레이스에 SQL 쿼리가 보인다면, 관련 테이블과 관계를 매핑해 봅니다.

다이어그램은 범위를 꼭 좁히세요. 목표는 이 경로 하나를 시각화하는 것이지, 시스템 전체 아키텍처를 그리는 것이 아닙니다. 예를 들어 다음처럼 질문을 정교하게 정의합니다.

“할인 코드를 포함한 사용자 체크아웃에서 결제가 실패할 때, 무엇이 무엇을 어떤 순서로 호출하는가?”

다이어그램에는 세 개 트레이스에 등장하는 소수의 서비스와 함수만 포함시키면 충분합니다.


4단계: 정확한 경로를 위해 에러 처리를 계측하기

레거시 시스템의 실패 메시지는 종종 이렇게 생겼습니다. Something went wrong, Null reference exception. 어디서, 어떻게 잘못됐는지는 아무것도 알려 주지 않습니다.

세 가지 트레이스 규칙을 진짜 효과적으로 쓰려면, 고해상도(high-fidelity) 에러 경로가 필요합니다.

에러를 래핑하고 컨텍스트를 붙이기

에러가 콜 스택을 타고 위로 올라갈수록, 점점 더 많은 맥락(context)을 쌓게 해야 합니다.

  • 파일/라인 정보(스택 트레이스나 래퍼에서 추출)
  • 함수/모듈 이름
  • 핵심 파라미터 값(필요하다면 마스킹/비식별 처리)
  • 상위 수준 작업 이름(예: applyDiscount, chargeCustomer, sendEmail)

여러 언어에서의 예는 대략 이렇습니다.

  • Go: fmt.Errorf("applyDiscount: %w", err)처럼 wrapping하고, 에러 발생 지점에서 스택 트레이스를 캡처합니다.
  • Java / C# / Node.js / Python: 원래 예외를 보존하면서, 상위로 올라가며 컨텍스트 메시지를 추가합니다.

목표는, 에러가 발생했을 때 최종 로그나 에러 리포트가 **‘빵부스러기 경로(breadcrumb trail)’**를 포함하게 만드는 것입니다.

OrderService.createOrder → DiscountService.applyDiscount → RuleEngine.evaluate → DatabaseTimeoutError

이렇게 되면, 에러 리포트 한 건이 사실상 세 가지 트레이스 중 하나 역할을 하게 됩니다.

에러를 추적하기 쉽게 만들기

  • 구조적 로깅(JSON, key-value 형태 로그)을 사용해 도구가 에러 타입, 요청 ID, 사용자 ID로 쉽게 쿼리할 수 있게 합니다.
  • 서비스 간에 공통으로 사용할 에러 ID / 상관관계 ID를 표준화해, 단일 실패를 여러 서비스에 걸쳐 추적할 수 있게 합니다.
  • 스택 트레이스는 일관된 형식으로 캡처해, 파싱과 검색이 가능한 상태로 둡니다.

에러 처리가 제대로 계측되면, 시스템이 어떤 경로를 탔는지 더 이상 추측할 필요가 없습니다. 그대로 보이기 때문입니다.


5단계: 트레이스가 ‘어디를 읽고 고칠지’를 이끌게 하기

이제 세 개의 탄탄한 트레이스와 정비된 에러 계측이 준비됐습니다. 다음 순서는 이렇습니다.

  1. 트레이스에 드러난 **진입점(entry point)**부터 시작합니다. HTTP 핸들러, CLI 커맨드, 큐 컨슈머, 크론 잡 등입니다.
  2. 트레이스를 가이드 삼아 호출 체인(call chain)을 따라갑니다. 이때 읽어야 할 것은:
    • 트레이스에 실제로 등장하는 함수들
    • 해당 동작에 현저히 영향을 줄 것 같은 함수들(조건 검사나 분기 로직 등)
  3. 관련 없는 분기는 과감히 건너뜁니다. 어떤 함수에 여러 코드 경로가 있더라도, 세 개의 트레이스가 항상 특정 부분 경로만 사용한다면, 우선 그 부분만 깊게 파고듭니다.
  4. **최소 수정 지점(minimal modification point)**을 찾습니다.
    • 값이 어디서 계산되는가?
    • 의사결정(if/else, switch, 룰 엔진 평가 등)이 어디서 내려지는가?
    • 최종 출력이나 사이드 이펙트(DB write, 외부 API call, 이메일 발송 등)는 어디서 발생하는가?

세 개의 트레이스는 사실상 읽어야 할 함수와 파일의 좁은 목록을 정의해 줍니다. 그 범위를 확실히 이해한 다음에야 인접 영역으로 확장해 나가도 늦지 않습니다.


6단계: 로깅과 트레이싱을 끊임없이 개선하기

레거시 코드 경로를 한 번 파고드는 일은, 다음 번 조사를 더 쉽게 만들 기회이기도 합니다.

조사 과정에서 다음을 수행하세요.

  • 도움이 되지 않던 로그 메시지 업그레이드
    • 핵심 식별자(주문 ID, 사용자 ID, 리전 등)를 추가합니다.
    • 단계 표시를 추가합니다. (예: "start", "after validation", "before DB write")
  • 서비스 전반에 로그 포맷을 표준화해 상관 분석을 쉽게 합니다.
  • 가능하다면 Trace ID / Span ID를 도입하거나, 이미 있다면 적극 활용합니다.
  • 샘플링 전략을 조정해, 드물게 발생하거나 실패하는 경로는 항상 전체 트레이스를 남기도록 합니다.

시간이 지나면서 시스템은 불투명한 레거시에서, 트레이스가 잘 잡히는 관측 가능한 시스템으로 변해 갑니다. 그러면 새로운 질문 대부분은 아래 세 단계로 답할 수 있습니다.

  1. 동작을 재현한다.
  2. 트레이스 몇 개를 살펴본다.
  3. 작고 정확한 코드 변경을 한다.

현대적인 실천과 함께 대규모 코드베이스 관리하기

세 가지 트레이스 규칙은, 거대한 시스템을 길들이기 위한 현대적인 도구/관행과 함께 사용할 때 가장 빛납니다.

  • 모노레포 또는 잘 구조화된 멀티 레포를 통해 의존성 분석을 쉽게 한다.
  • 자동화 테스트를 도입한다. (비록 뒤늦게라도, 중요한 레거시 경로 주변부터)
  • 코드 리뷰 정책을 정해, 작고 집중된 변경을 권장한다.
  • **CI(Continuous Integration)**를 활용해 회귀(regression)를 빠르게 잡는다.
  • 정적 분석 파이프라인(린터, 취약점 스캐너 등)을 구축해 숨어 있는 문제를 드러낸다.

이런 관행들이 레거시 코드를 마법처럼 현대화해 주지는 않습니다. 하지만 한 번 이해하고 개선해 둔 경로가 다시 망가질 확률을 줄여 줍니다.


결론

레거시 시스템을 한 번에 정복할 필요는 없습니다. 필요한 것은 한 번에 한 가지 동작을 reliably 이해하고 수정할 수 있는 방법입니다.

세 가지 트레이스 규칙은 그 방법을 제공합니다.

  • 추상적인 코드 읽기가 아니라, 실제 런타임 동작에서 출발합니다.
  • 관심 있는 경로를 정의하기 위해 대표 트레이스 세 개를 사용합니다.
  • 그 경로 주변에서만 정적 분석, 메트릭, 시각화를 활용합니다.
  • 실패가 정확한 실행 경로를 드러내도록 에러 처리를 계측합니다.
  • 모든 조사를 계기로 로깅과 트레이싱을 개선해, 다음 조사를 더 빠르게 만듭니다.

전체 코드베이스를 상대로 싸우는 대신, 적은 수의 구체적인 트레이스를 상대로 정밀하게 싸우는 것입니다. 그렇게 하면 압도감은 줄어들고, 정밀함은 커집니다. 시간이 흐르면서, 당신이 하나하나 밝혀낸 경로들이 모여 레거시 시스템을 검은 상자에서, 잘 이해되고 관리 가능한 동작들의 집합으로 바꿔 줍니다. 그리고 바로 거기서부터 진짜 현대화가 시작됩니다.

세 가지 트레이스 규칙: 어떤 레거시 코드 경로도 이해하는 미니멀리스트 전략 | Rain Lag