리팩터링은 증상이 없다. Claude가 틀려도 안 보인다. 테스트·원자 커밋·수동 클릭 세 가드레일로 묶는다.
리팩터링은 Claude에게 가장 위험한 작업이다. 버그는 증상이 있다——버튼 눌러도 반응 없음, 값이 undefined, 스택 트레이스——그래서 Claude의 수정이 맞는지 판단할 수 있다. 리팩터링은 증상이 없다. "그래도 돌아가요"는 "테스트는 통과해요"와 같을 수 있고, 그 뒤에서 동작이 조용히 바뀌었고, 못 알아챘고, 일주일 후 프로덕션이 터진다.
최근 how2claude에서 Claude에게 꽤 큰 리팩터링을 시켰다: x402 암호화폐 결제를 직접 짠 PaymentHandler + FacilitatorClient(139줄)에서 x402-rails gem으로 이전, 동시에 두 컨트롤러에 중복돼 있던 Purchase.create! / Subscription.create!의 필드 매핑을 model 클래스 메서드로 추출. 커밋 하나, 4개 파일 변경, 2개 삭제, 2개 추가.
내가 준 프롬프트는 한 단어였다: "리팩터."
이렇게 짧을 수 있었던 건 주변에 가드레일이 있었기 때문이다.
이 브랜치가 여기까지 왔을 때 테스트가 221개였다. 결제 플로우의 크리티컬 패스는 전부 커버돼 있었다.
Claude가 리팩터링 전에 기본적으로 하는 동작은 "먼저 테스트를 본다"가 아니다. 그래서 먼저 bin/rails test를 돌려서 그린 확인한 뒤 손대라고 시킨다.
리팩터링 끝나고 다시 돌린다. 여전히 그린. 이건 "회귀 없음"을 의미하지 않는다——알려진 동작이 망가지지 않았다는 것만 의미한다.
코드패스에 테스트 커버리지가 없다면, Claude에게 현재 동작을 고정하는 최소 테스트를 먼저 짜게 한다. 통과시키고, 커밋. 그다음에 리팩터. 그렇지 않으면 Claude가 하는 건 리팩터링이 아니라 재작성이다——전후 동등성을 검증할 방법이 없다.
이 리팩터는 사실 두 가지였다:
Purchase / Subscription 필드 매핑: controller → model 클래스 메서드프론트엔드에 세 번째가 있었다: viem + x402-fetch로 JS 측 서명 플로우 재작성.
Claude에게 자연스러운 경계로 쪼개게 했다: 백엔드 + 모델 추출을 한 커밋(9f3e239), 프론트엔드를 별도 커밋(93746d8). 각 커밋에 완전한 설명, 변경 파일 목록, 왜 이렇게 바꿨는지가 들어간다.
장점:
- diff 가독성. 한 커밋, 한 가지.
- 롤백 단위 제어. 프로덕션에서 프론트엔드 버그 터지면 git revert 93746d8로 프론트만 되돌리고 백엔드는 유지.
- Claude 자신의 주의 집중. 한 커밋 한 가지면, Claude의 주의도 그 한 가지에만 쏠린다.
리팩터링 끝나면 Claude를 멈추고 git diff --staged를 보여달라고 한다. 테스트 돌리지 말고, 앱 띄우지 말고, 먼저 diff부터 읽는다.
내가 스캔하는 신호:
app/services/x402/payment_handler.rb 통째로 삭제——OK, gem 이전의 핵심. 근데 내가 건드리라고 안 한 걸 삭제했다면 즉시 물어본다.Purchase.create!(wallet_address: verify_result["payer"], ...) → Purchase.record_x402!(payment:, settlement:) 안에서는 payment[:payer]. 소스가 바뀌었다(gem의 request.env vs 예전 client의 반환값) 하지만 필드는 1대1로 대응해야 한다.함정 1: gem의 Stimulus 컨트롤러가 조용히 로드되지 않음
x402-rails gem은 자체 Stimulus 컨트롤러를 포함한다. Claude가 코드를 짜고 테스트는 전부 그린. 내가 직접 결제 버튼을 눌렀다——반응 없음.
이유: config/importmap.rb에서 @hotwired/stimulus의 pin이 존재하지 않는 vendor 파일을 가리키고 있어서 importmap이 그 pin을 조용히 버렸다. 결과적으로 gem의 컨트롤러가 로드되지 않음. 테스트로 못 잡는다, bin/rails test는 JS를 실행하지 않기 때문.
함정 2: credentials에서 0x...가 YAML에 의해 정수로 파싱됨
wallet_address: 0x833589...——따옴표 없음. YAML이 0x 접두사 보고 16진수 정수로 해석. 읽어 들이면 integer. Facilitator가 non-string을 받아서 거부. Claude는 config 쓰면서 YAML 파싱 규칙을 의식하지 않았다.
두 함정 모두 내가 실제로 버튼을 클릭해서야 발견했다. 테스트 통과 ≠ 기능 동작. 리팩터링 후 수동 검증은 생략 불가.
리팩터링은 "Claude에게 운전 맡기기" 중 리스크가 가장 높은 시나리오다. 가드레일은 Claude를 위한 게 아니다, 너를 위한 거다——Claude가 틀렸을 때, 프로덕션이 불타는 순간이 아니라 5분 안에 알아챌 수 있도록.