
Claude Code용 python-guide 스킬을 처음 만들었을 때는 꽤 자신 있었다. uv로 패키지 관리하고 ruff로 린트 걸고 pytest로 테스트 짜는 패턴을 정리해두면 대부분의 Python 프로젝트에서 잘 먹힐 거라고 생각했다.
실제로 그랬다. 처음 몇 개 프로젝트에서는. 그런데 GPU 모델 3개를 subprocess로 돌리는 transcription 파이프라인을 만들기 시작하면서 스킬의 빈 곳이 보이기 시작했다.
python-guide는 Claude Code가 Python 프로젝트를 작업할 때 자동으로 적용하는 가이드라인이다. 핵심 스택은 이렇다.
uv로 의존성 관리 (uv add)ruff + ty로 린트/포매팅/타입체크pytest로 테스트, fixture 패턴과 AAA 구조테스트 관련해서는 pytest 사용, fixture 패턴, AAA(Arrange-Act-Assert) 구조 정도만 적어뒀다. 웹 앱이나 데이터 처리 파이프라인에서는 이 정도면 충분했다.
문제는 GPU가 끼어들면서부터였다.
transcription 파이프라인은 화자 분리(pyannote) → 음성 인식(VibevoiceXL) → 강제 정렬(Qwen) 순으로 GPU 모델 3개를 돌린다. 각 모델이 서로 다른 torch 버전을 요구해서 독립 venv + subprocess로 격리했는데, 이 구조를 테스트하려니 기존 스킬에 답이 없었다.
구체적으로 세 가지가 빠져 있었다.
초기에는 모든 테스트가 GPU를 필요로 했다. CI에서 돌릴 수가 없으니 개발 중 피드백 루프가 느려졌다. GPU가 없어도 검증할 수 있는 로직과 GPU가 반드시 필요한 부분을 나눠야 했는데, 스킬에는 이런 분리 기준이 없었다.
결국 WORKER_URL 환경변수 유무로 통합 테스트를 자동 skip하는 패턴을 만들었다.
WORKER_URL = os.environ.get("WORKER_URL")
@pytest.mark.skipif(not WORKER_URL, reason="WORKER_URL not set")
def test_diarize_via_http():
resp = httpx.post(f"{WORKER_URL}/run/diarize", json={"wav_path": "..."})
# NDJSON 스트리밍 응답 파싱 ...파이프라인의 각 단계가 독립 프로세스라서 일반적인 함수 mock으로는 안 됐다. subprocess.run을 mock해서 각 worker의 JSON 응답을 시뮬레이션하는 패턴이 필요했다.
def test_correct_parses_structured_output(mock_subprocess, segments):
mock_subprocess.return_value = CompletedProcess(
args=[], returncode=0,
stdout=json.dumps({
"structured_output": {
"segments": [{"index": 0, "text": "교정된 텍스트"}]
},
"usage": {"input_tokens": 100, "output_tokens": 50},
"total_cost_usd": 0.001,
}),
)
result = correct(segments)
assert result[0].text == "교정된 텍스트"PersistentWorker(상시 떠 있는 worker 서버) 테스트는 더 까다로웠다. stdin/stdout/stderr을 모두 mock하고 SimpleQueue와 threading.Event의 상호작용까지 시뮬레이션해야 했다.
GPU 없이도 파이프라인의 데이터 변환 로직을 검증하고 싶었다. 실제 파이프라인 출력을 JSON으로 캡처해서 tests/fixtures/에 넣어두고, 각 단계의 입출력이 데이터 모델 스펙에 맞는지 확인하는 방식을 썼다.
def test_diarize_to_asr_data_flow(diarize_data, asr_data):
for seg in diarize_data:
assert hasattr(seg, "speaker")
assert hasattr(seg, "start")
assert hasattr(seg, "end")
for seg in asr_data:
assert hasattr(seg, "text")
assert hasattr(seg, "language")Golden File Testing이라고도 불리는 이 패턴은, 파이프라인 출력 형식이 바뀌면 fixture도 같이 갱신해야 한다는 함정이 있다. 한번 fixture 갱신을 까먹었더니 테스트가 거짓 통과해서 한참 뒤에야 버그를 발견한 적이 있다.
이런 시행착오 끝에 정착한 구조는 3단계 테스트 피라미드다.
맨 아래는 GPU가 전혀 필요 없는 단위 테스트다. SRT 생성 로직, 오디오 추출 mock, Claude Code CLI subprocess mock, Worker 서버 mock 등이 여기에 들어간다. CI에서 매 커밋마다 돌릴 수 있다.
가운데는 fixture 기반 데이터 흐름 테스트다. 실제 파이프라인 출력에서 추출한 JSON(diarize, asr, corrected, aligned)을 써서 각 단계의 데이터 모델 변환을 검증한다. GPU 없이 돌아가지만 실제 데이터 형태를 그대로 쓴다는 점에서 단위 테스트보다 현실적이다.
꼭대기는 HTTP 통합 테스트다. 실제 worker 서버에 요청을 보내서 전체 파이프라인을 검증한다. WORKER_URL 환경변수가 없으면 자동으로 건너뛴다.
이 경험에서 얻은 가장 큰 교훈은 스킬 자체의 설계 방식에 관한 거다.
처음에 python-guide를 만들 때 나는 "완벽한 가이드"를 목표로 했다. 모든 상황을 미리 커버하려고 했다. 근데 그건 불가능하다. 어떤 가이드라인도 실전에서 만나는 모든 상황을 예측할 수 없다.
더 나은 접근은 이거다.
이 루프가 빠르게 돌수록 스킬이 빨리 성숙해진다. transcription 프로젝트에서 GPU 테스트 격리 패턴을 발견하고 스킬에 추가한 뒤, 이미지 생성 파이프라인에서도 같은 패턴을 바로 적용할 수 있었다.
python-guide 스킬의 초기 버전에는 GPU 의존 테스트 격리, subprocess mock 패턴, fixture 기반 데이터 흐름 테스트가 빠져 있었다. 웹 앱 중심의 경험만으로는 ML 파이프라인의 테스트 요구사항을 예측하기 어려웠기 때문이다.
스킬은 한번 만들어두고 끝나는 게 아니다. 실전 프로젝트에서 부딪히고 깨지면서 점점 두꺼워진다. 완벽한 가이드를 처음부터 만들려 하기보다 빠르게 적용하고 빠르게 피드백받는 루프를 돌리는 편이 훨씬 낫다.