Free

Claude에게 리팩터링 맡기기

리팩터링은 증상이 없다. Claude가 틀려도 안 보인다. 테스트·원자 커밋·수동 클릭 세 가드레일로 묶는다.


리팩터링은 Claude에게 가장 위험한 작업이다. 버그는 증상이 있다——버튼 눌러도 반응 없음, 값이 undefined, 스택 트레이스——그래서 Claude의 수정이 맞는지 판단할 수 있다. 리팩터링은 증상이 없다. "그래도 돌아가요"는 "테스트는 통과해요"와 같을 수 있고, 그 뒤에서 동작이 조용히 바뀌었고, 못 알아챘고, 일주일 후 프로덕션이 터진다.

최근 how2claude에서 Claude에게 꽤 큰 리팩터링을 시켰다: x402 암호화폐 결제를 직접 짠 PaymentHandler + FacilitatorClient(139줄)에서 x402-rails gem으로 이전, 동시에 두 컨트롤러에 중복돼 있던 Purchase.create! / Subscription.create!의 필드 매핑을 model 클래스 메서드로 추출. 커밋 하나, 4개 파일 변경, 2개 삭제, 2개 추가.

내가 준 프롬프트는 한 단어였다: "리팩터."

이렇게 짧을 수 있었던 건 주변에 가드레일이 있었기 때문이다.

가드레일 #1: 테스트 없이 리팩터링하지 않는다

이 브랜치가 여기까지 왔을 때 테스트가 221개였다. 결제 플로우의 크리티컬 패스는 전부 커버돼 있었다.

Claude가 리팩터링 전에 기본적으로 하는 동작은 "먼저 테스트를 본다"가 아니다. 그래서 먼저 bin/rails test를 돌려서 그린 확인한 뒤 손대라고 시킨다.

리팩터링 끝나고 다시 돌린다. 여전히 그린. 이건 "회귀 없음"을 의미하지 않는다——알려진 동작이 망가지지 않았다는 것만 의미한다.

코드패스에 테스트 커버리지가 없다면, Claude에게 현재 동작을 고정하는 최소 테스트를 먼저 짜게 한다. 통과시키고, 커밋. 그다음에 리팩터. 그렇지 않으면 Claude가 하는 건 리팩터링이 아니라 재작성이다——전후 동등성을 검증할 방법이 없다.

가드레일 #2: 변경을 원자적 커밋으로 쪼개게 한다

이 리팩터는 사실 두 가지였다:

  1. x402 백엔드: 직접 구현 → gem
  2. Purchase / Subscription 필드 매핑: controller → model 클래스 메서드

프론트엔드에 세 번째가 있었다: viem + x402-fetch로 JS 측 서명 플로우 재작성.

Claude에게 자연스러운 경계로 쪼개게 했다: 백엔드 + 모델 추출을 한 커밋(9f3e239), 프론트엔드를 별도 커밋(93746d8). 각 커밋에 완전한 설명, 변경 파일 목록, 왜 이렇게 바꿨는지가 들어간다.

장점:
- diff 가독성. 한 커밋, 한 가지.
- 롤백 단위 제어. 프로덕션에서 프론트엔드 버그 터지면 git revert 93746d8로 프론트만 되돌리고 백엔드는 유지.
- Claude 자신의 주의 집중. 한 커밋 한 가지면, Claude의 주의도 그 한 가지에만 쏠린다.

가드레일 #3: 완료 선언 전에 diff를 읽는다

리팩터링 끝나면 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로 대응해야 한다.
  • "하는 김에" 변경. Claude는 리팩터링 중에 "뭔가 이상한데" 싶은 부분을 하는 김에 고치는 걸 아주 좋아한다——에러 메시지 어구 바꾸기, 변수 이름 바꾸기, 자기가 보기에 뽑아야 할 것 같은 메서드 추출. 경계해야 한다. 리팩터링의 약속은 "동작 등가"다. 하는 김에 바꾸면 그게 깨진다.

Claude가 이번에 빠진 두 함정

함정 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-리팩터 전체 플로우

  1. 전체 테스트 스위트 실행, 그린 확인. 대상 코드에 커버리지가 없으면, 현재 동작을 고정하는 최소 테스트를 Claude에게 먼저 짜게 해서 커밋.
  2. 추출하고 싶은 surface를 명시. "리팩터"가 통하는 건 중복이 명백할 때; 아닐 때는 "X의 필드 매핑을 Y의 클래스 메서드로 추출."
  3. 원자적 커밋 분할. 커밋 하나에 한 가지.
  4. diff를 직접 읽는다. 뭐가 삭제됐는지, 필드 매핑이 유지되는지, 하는 김에 바꾼 게 없는지.
  5. 테스트 재실행, 그린.
  6. 사용자 가시 경로를 건드렸다면, 기능을 수동으로 클릭해본다. JS, importmap, CDN, YAML 파싱처럼 테스트가 닿지 못하는 층은 눈으로 확인할 수밖에 없다.

리팩터링은 "Claude에게 운전 맡기기" 중 리스크가 가장 높은 시나리오다. 가드레일은 Claude를 위한 게 아니다, 너를 위한 거다——Claude가 틀렸을 때, 프로덕션이 불타는 순간이 아니라 5분 안에 알아챌 수 있도록.