에러 경로부터 리팩토링하기: 복잡한 레포지토리를 위한 장애 주입 테이블
요약
복잡한 레포지토리의 리팩토링 시 '행복한 경로' 테스트만으로는 부족하며, 에러 타입, 메시지 형태, 로그 기록 여부, 롤백 필요성 등 예외 경로를 포착해야 합니다. 이 가이드는 장애 주입 테이블을 구축하여 코드 변경 전 모든 실패 계약을 명시적으로 문서화하고 테스트하는 워크플로우를 제시합니다.
핵심 포인트
- 리팩토링 시 에러 타입과 예외 경로(예: PriceMissingError)를 반드시 포착해야 합니다.
- 장애 주입 테이블은 아티팩트이며, 하네스 작성보다 먼저 구축되어야 합니다.
- 테스트는 단일 함수와 하나의 실패 표면으로 범위를 좁혀 관리하는 것이 효과적입니다.
- 메시지는 정확한 문자열이 아닌 '형태(shapes)'로 유지하여 유연성을 확보해야 합니다.
에러 경로부터 리팩토링하기: 복잡한 레포지토리를 위한 장애 주입 테이블
결론부터 말하자면: '행복한 경로(happy-path)' 특성 테스트만으로는 리팩토링을 살릴 수 없습니다. 실제로 깨지는 계약은 예외 경로입니다. 즉, 에러 타입, 메시지, 로그 라인, 그리고 롤백 상태를 포착해야 합니다. 이 네 가지 필드를 단 한 줄의 코드를 옮기기 전에 커밋된 테이블로 만드세요.
이것은 대부분의 팀이 건너뛰는 단계입니다. 그들은 calculate_total()이 42를 반환하는 것을 스냅샷으로 찍고, 헬퍼 함수를 추출한 다음, 운영 환경에서 PriceMissingError 대신 KeyError에 대해 경고가 울리기 시작하게 만듭니다. diff는 깔끔해 보였지만, 로그는 그렇지 않았습니다.
아래는 오후 시간 안에 한 모듈에 적용할 수 있는 워크플로우입니다. 이 가이드는 로컬에서 코드를 실행하고 테스트 러너를 가지고 있다는 것을 전제로 합니다.
1. 하나의 함수와 하나의 실패 표면(failure surface)으로 범위를 좁히기
당신을 이미 겁먹게 하는 단일 진입점(entry point)을 고르세요. 패키지 전체가 아닙니다. 레이어 전체도 아닙니다. 다른 코드가 호출하고 예외를 발생시킬 수 있는 하나의 함수여야 합니다.
범위를 명확히 기록하세요. 범위 확장은 이 작업을 망칩니다:
Module: billing/pricing.py
Entry: quote(invoice, customer)
Raises: 두 호출 호프 내에서 도달 가능한 모든 것
...
만약 해당 함수가 대략 여덟 가지 이상의 뚜렷한 실패 모드(failure mode)를 가지고 있다면, 세션을 분할하세요. 한 번에 검토할 수 있는 테이블을 원합니다.
2. 기억이 아닌 기계적으로 예외 발생 지점 열거하기
인간은 두 가지 예외는 기억하지만 다섯 가지는 잊어버립니다. AST(Abstract Syntax Tree)가 실제로 무엇이 있는지 알려주게 하세요.
# scripts/list_raises.py — 제안; 레포지토리 루트에서 실행
import ast, pathlib, sys
...
모듈과 직접적인 종속성(direct dependencies)에 대해 실행하세요:
python scripts/list_raises.py billing/pricing.py
python scripts/list_raises.py billing/rates.py
순수한 raise 구문은 재발생(re-raises)을 의미합니다. 이를 스택을 따라 수동으로 추적하고 해당 타입을 목록에 추가하세요. 행의 개수를 세세요. 추정하지 마세요.
3. 어떤 하네스(harness)를 작성하기 전에 장애 테이블 구축하기
테이블이 아티팩트입니다. 하네스는 단지 그 주변을 둘러싼 배관일 뿐입니다.
| # | 트리거 | 예상 타입 | 메시지 형태 | 로깅 여부 | 롤백 필요성 |
|---|---|---|---|---|---|
| 1 | 고객 ID 누락 | PriceMissingError | 고객 ID 포함 | 경고 (warn) | 불필요 |
| ... | |||||
코드(code)를 읽어 채운 다음, 코드를 실행하여 수정하세요. 위의 3번째 행이 흥미롭습니다. 로그 라인 없이 부분적인 변경만 있는 ValueError가 바로 추출 과정에서 조용히 바뀌는 것입니다. |
메시지는 정확한 문자열이 아닌 _형태(shapes)_로 유지하세요. 전체 텍스트를 검증하면 테이블이 취약해지고 의도에 대한 아무것도 배우지 못합니다.
4. 각 장애를 강제하고 네 가지 필드를 기록하기
이제 행마다 매개변수화된 테스트(parametrized test)를 작성하세요. 이 하네스(harness)는 튜플을 기록하고 커밋된 JSON 기준선과 비교합니다.
# tests/test_error_contract.py — 제안; 리포지토리에 맞게 임포트를 조정하세요
import json, logging, pathlib
import pytest
...
장애 주입기(fault injector)는 단순한 monkeypatching입니다. rate 클라이언트를 TimeoutError를 발생시키는 스텁(stub)으로 교체하세요. id가 누락된 고객 객체를 전달하세요. 테스트를 쉽게 만들기 위해 프로덕션 코드를 건드리지 마세요.
--update-baseline 시맨틱스로 한 번 실행하고, 결과 JSON의 모든 줄을 검토한 다음 커밋하세요. 기준선(baseline)을 검토하는 것이 실제 특성화 작업입니다.
5. 노이즈의 근원지 고정하기
무작위로 실패하는 테이블은 일주일 안에 삭제됩니다. 녹색 실행(green run)을 신뢰하기 전에 비결정성(nondeterminism)을 제거하세요.
- 시간 (Clock): 고정된
now를 주입하고, 모듈 내부에서 절대datetime.now()를 호출하지 마세요. - ID: UUID 및 요청 ID 포맷팅을 제거하거나 스텁 처리하세요.
- 로케일 (Locale): 숫자 및 날짜 포맷팅에 대해 하나의 로케일을 강제하세요.
- 네트워크 (Network): 장애 스텁이 예상치 못한 모든 아웃바운드 호출에서 오류를 발생시키도록 만드세요.
- 순서 (Ordering): 로그에 대한 검증 전에 세트(set)를 정렬된 리스트(sorted list)로 변환하세요.
이 테스트 스위트를 연속으로 세 번 실행하세요. 결과가 동일해야 테이블이 준비된 것입니다.
6. 가장 작은 안전한 변경을 수행하고, 다시 실행하기
이제 리팩토링하세요. 한 부분(seam). 하나의 커밋. 각 이동 후에 테이블을 다시 실행하세요.
실용적인 규칙: 단일 커밋이 테이블의 두 행 이상을 변경한다면, 그 커밋은 너무 큰 것입니다. 분리하세요. 이 테이블은 단순히 라인 수뿐만 아니라 동작(behavior)에 대한 여러분의 diff 예산이 됩니다.
위험도 순으로 시도해 볼 만한 좋은 첫 단계들:
- 로컬 변수 이름 변경. 테이블은 이동해서는 안 됩니다.
- 순수 헬퍼 함수 추출. 테이블은 이동해서는 안 됩니다.
- 예외 발생 코드를 헬퍼 함수 내부로 이동. 메시지 형태는 이동해서는 안 됩니다.
- 정리(cleanup) 순서 변경. 테이블이 이동할 수 있습니다 — 각 행을 조사하세요.
가장 많은 추출 작업이 실제로 깨지는 곳은 3단계입니다. 왜냐하면 헬퍼 함수가 메시지 생성에 사용된 데이터에 접근하지 못하기 때문입니다.
7. 코딩 모델이 적합한 경우와 그렇지 않은 경우
모델은 여기서 두 가지 작업을 위해 진정으로 유용합니다. 소스 파일로부터 후보 장애 트리거를 생성하는 것, 그리고 테이블 diff를 읽어서 어떤 리팩토링이 이를 야기했는지 추측하는 것입니다. 둘 다 제안 작업이며, 무언가를 커밋하기 전에 모두 여러분의 검토가 필요합니다.
저는 모델을 사용해 트리거 목록 초안을 작성한 다음, 제안된 예외가 코드에 존재하지 않기 때문에 그중 약 절반을 삭제합니다. 실제로 실패하는 것을 본 적이 없는 생성된 트리거는 절대 받아들이지 마세요.
공개 정보: 이 글은 MonkeyCode의 제품 홍보의 일환으로 작성되었습니다.
이러한 종류의 위험도가 낮고 양이 많은 상호작용에 대해, 운영자는 MonkeyCode를 오픈 소스 프로젝트로 설명하며 무료 모델 접근과 이러한 작은 하네스를 실행할 수 있는 무료 서버 옵션을 제공한다고 언급합니다. 이용 가능 조건 및 모든 무료 할당량은 시간이 지남에 따라 변경되므로, 블로그 게시물에 있는 숫자를 신뢰하기보다는 프로젝트 페이지에서 현재 제한 사항을 확인하세요. 이는 트리거 아이디어를 반복적으로 테스트하는 합리적인 장소이지만, 검토된 기준(baseline)을 대체할 수는 없습니다.
한계점 및 건너뛰어도 되는 사람
이 워크플로우에는 실제 비용이 따릅니다. 그 점에 대해 솔직해지세요.
- 예외를 삼키는 것은 문제를 일으킨다. 모듈이 모든 것을 포착하고 문자열을 기록만 한다면, 아직 고정할 계약(contract)이 없습니다. 먼저 로깅을 수정하세요.
- 타임아웃 및 비동기 경로에는 가짜 시계가 필요하다. 실제
sleep은 테스트 스위트를 사용 불가능하고 불안하게 만듭니다. - 테이블이 썩는다. 의도적인 동작 변경이 있을 때마다 기준선 업데이트와 검토가 필요합니다. 이를 예산에 책정하지 않으면 파일이 노이즈가 됩니다.
- 실행 가능한 환경이 필요하다. 하나의 스테이징 파드에서만 실행되는 레거시 코드는 해당 문제를 해결할 때까지 접근하기 어렵습니다.
녹색 필드(greenfield) 코드, 즉 오류 계약을 기록하는 비용보다 설계하는 비용이 저렴한 경우 이 부분은 건너뛰세요. 기준선 차이점(baseline diffs)을 신중하게 검토할 수 없는 경우에도 건너뛰세요. 검토되지 않은 기준선은 버그를 고정시키기 때문입니다. 모듈의 실패가 스텁(stub) 처리할 수 없는 라이브 서드파티 서비스에 의존하는 경우에도 건너뛰세요.
핵심 요약 (The takeaway)
반환 값은 리팩토링의 쉬운 절반일 뿐입니다. 오류 유형, 메시지 형태, 로깅 레벨, 롤백 상태가 새벽 3시에 당신을 지치게 만드는 나머지 절반입니다.
도달 가능한 모든 실패에 대해 이 네 가지 필드를 기록하세요. 테이블을 커밋하고, 그런 다음 작은 변경 한 번을 수행하여 테이블이 이동하지 않았음을 증명하세요.
만약 이 글에서 단 하나의 것만 한다면, 오늘 가장 두려운 모듈에 AST 스캔(Abstract Syntax Tree scan)을 실행하고 발생 횟수를 세어보세요. 그 숫자는 보통 팀이 예상하는 것보다 높습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기