Claude가 추천한 "가벼운" 방안엔 숨은 대가가 있었다. 한 마디 되묻기에 다 드러났다.
smarts를 손보고 있었다 — 이더리움 스마트 컨트랙트를 "개발자 문서"처럼 렌더링하는 Rails 앱이다. 며칠 전 오래된 문제를 다시 만났다. 컨트랙트는 업그레이드된다. 시스템이 업그레이드된 프록시 컨트랙트를 오래된 ABI로 렌더링하면, 사용자에게 거짓말을 하는 셈이다.
Claude에게 프록시 업그레이드 감지 메커니즘을 같이 설계해달라고 했다. EIP-1967 / Beacon proxy / ZeppelinOS 호환까지 포함된 꽤 완성된 기술 플랜을 줬다. "언제 검사를 트리거할 것인가" 단계에서 두 가지 옵션을 늘어놓았다:
# 옵션 A: DB에 last_proxy_check_at 추가, 5분 스로틀
if @contract.last_proxy_check_at.nil? || @contract.last_proxy_check_at < 5.minutes.ago
CheckProxyUpgradeJob.perform_later(@contract)
end
# 옵션 B: Solid Cache로 스로틀, 더 가벼움
unless Rails.cache.exist?("proxy_check:#{@contract.id}")
Rails.cache.write("proxy_check:#{@contract.id}", true, expires_in: 5.minutes)
CheckProxyUpgradeJob.perform_later(@contract)
end
마지막 한 줄:
B가 더 단순함, 컬럼 추가 불필요.
이 두 코드를 잠시 응시했다. B는 확실히 단순했고, 마이그레이션도 없고, 전부 cache 호출이고, Rails 스타일에도 맞았다. 그런데 어딘가 찜찜했다.
다섯 글자를 치고 엔터를 눌렀다:
a 추천하나?
응답 (코드 블록 몇 개는 생략):
마음을 바꿈 — A 추천.
이유: 프록시 감지의 목적 자체가 ABI 신선도를 믿지 않는다는 것이다. Solid Cache로 스로틀하면, "검사했는가"를 다시 휘발성 저장소에 두는 셈이다:
- 배포 / cache wipe 후 모든 컨트랙트가 한꺼번에 "미검사" 상태로 진입 → 다음 트래픽 파도에 검사 폭풍 발생
- "이 컨트랙트가 마지막으로 재검사된 지 얼마나 됐나?" 같은 질문에 답할 수 없음
- 정기 백업 트리거가 불가능: 트래픽이 적은 컨트랙트는 몇 주 동안 lazy 트리거되지 않을 수 있음
last_proxy_check_at컬럼을 추가하면 해금되는 것들:
- Lazy 트리거 (B와 동일 로직)
- recurring.yml과 함께, 24h 이상 미검사된 컨트랙트를 배치로 큐에 넣는 정기 백업 — 사용자 트래픽과 독립적
- UI에 신선도 노출: "Implementation verified 12 min ago"
비용: 마이그레이션 1회 + 잡 종료 시
update_column1회 (콜백 안 거치므로 거의 무료).요약: B는 "더 Rails다운, 더 가볍다"처럼 보이지만 기능 한 층이 빠져 있다. A야말로 "ABI 신선도를 믿지 않는다"는 일을 실제로 실현하는 선택지다.
두 번의 추천 사이에 일어난 일을 보자:
그 사이에 새로운 사실은 들어오지 않았다. 코드량, Rails 관용, cache 동작 모두 그대로다. 바뀐 건 평가할 때 어느 층에 서 있었는가뿐이다.
1차에서는 코드량을 비교했다. 2차에서는 "이 변경이 원래 풀려던 문제를 정말 푸는가"를 비교했다.
이 다섯 글자를 칠 때 뭘 강요할 생각은 없었다. B의 코드 위에서 멈춰버려서, 본능적으로 다시 의견을 묻고 싶었을 뿐이다.
그런데 효과는 "두 옵션 중 하나 고르기"에서 "왜 이걸 하는가"로 끌어올린 것이다. 이 격차가 핵심이다.
LLM은 추천할 때 거의 항상 국소 최적에서 출발한다: 어느 diff가 작은가, 어느 코드가 더 idiomatic한가, 어느 구현이 더 깔끔하게 읽히는가. 다 진짜 축이다. 다만 이 축들은 "이 PR이 무엇을 위한 것인가"와 자주 단절된다.
A와 B는 "5분 스로틀" 축에서는 동등하다. 그러나 A는 B가 못 하는 세 가지를 덤으로 풀어준다 — 정기 백업, 관측성, UI 신선도. 스로틀 층만 보면 B가 이긴다. 왜 이 잡이 존재하는가에 신경 쓰면 A가 이긴다.
Claude가 1차에서 이 층을 못 본 건 못 봐서가 아니다. "옵션 비교"의 기본 시점이 "문법 층 / Rails 층"이라서다. 한마디 되묻기로 "문제 층"까지 끌어올리니까, B의 비용을 스스로 계산해냈다.
내 워크플로에 추가한 작은 조정 몇 가지:
Claude가 "더 단순 / 더 가벼움 / 더 자연스러움"으로 마무리하면 한 번 더 본다. 이건 미감의 단어지, 판단의 단어가 아니다. "읽기 편하다"를 말하는 것이지 "잘 작동한다"를 말하는 게 아니다.
다시 묻는 비용은 거의 0이다. "X 추천하나?" 다섯 글자. Claude는 가지 않은 각도에서 다시 평가할 수밖에 없게 된다. 최종 추천이 같아도 근거는 더 단단해지며, 그걸 따를지 판단하기에 충분하다.
추천만이 아니라 비용을 펼치게 만들어라. 이번에 잘된 건 내가 A를 밀어붙여서가 아니다. Claude가 B의 숨은 비용 (cache wipe 폭풍, 관측성 결여, 정기 작업 막다른 길)을 적어줘서다. 적기 전엔 나도 말로 표현하지 못하던 것들이다.
결국 A를 투입했다: last_proxy_check_at 컬럼 추가, lazy 트리거 + 24h 정기 백업, UI에 Implementation verified 12 min ago 한 줄. 3일 후 다른 작업을 하다가, 이 컬럼 덕분에 Contract.where("last_proxy_check_at < ?", 24.hours.ago).find_each 같은 쿼리를 쓸 수 있었다 — B를 골랐다면 절대 못 썼을 쿼리다.
다섯 글자 되묻기의 가치는, 대략 일주일 뒤의 리팩터링 한 번에 해당한다.