리팩터링의 경제적 이점
2026년 7월 30일 Giles Edwards-Alexander
에이전트 엔지니어링이라는 새로운 세계를 익히는 과정에서 업무를 지원할 애플리케이션을 하나 만들었다. 동적 새로 고침과 조회, 모달, 자동 저장 기능을 갖춘 고품질 웹 UI부터 외부 시스템 연동, 머신러닝과 텍스트 분석, 백그라운드 작업, 완전 자동 배포가 가능한 제대로 된 환경 구성까지 포함한 정교한 앱이다. 코드 규모는 약 150,000줄이며, 대부분은 Rust(약 120 kLoC)로 작성했고 나머지는 TypeScript와 Terraform이다.
이 코드는 전부 에이전트가 작성했다. 대부분 Claude Code를 사용했고 Cursor도 일부 활용했다. 가끔 궁금해서 들여다본 경우를 제외하면 코드를 읽거나 리뷰하지 않았다.
애플리케이션을 만드는 동안 몇 가지 문제가 생기고 있다는 사실을 알아챘다. 터미널에서 파일의 4,000번째 줄을 수정하는 장면이 스크롤되는 모습을 보고 자세히 살펴봤다. 데이터 액세스 계층은 이미 6,000줄을 넘긴 상태였다. 기능이 추가될수록 계속 커졌다. 모든 쿼리와 읽기·쓰기 작업에서 똑같은 HTTP 요청 설정과 JSON 인코딩·디코딩이 반복됐다. 결국 하나의 Rust 파일이 17,155줄에 이르렀다.
리팩터링 실험
17,155줄짜리 파일 하나가 데이터 액세스 계층 전체였다. 자체적으로 완결된 단일 모듈이었다. 코드를 검토해 보니 중복 제거도, 내부 언어도 없었고 함수를 추출한 사례도 제한적이었으며 클래스를 추출한 부분은 거의 없었다. 다만 보존해야 할 인터페이스와 명확한 경계는 있었다. 리팩터링 대상으로 제격이었다.
에이전트가 작성한 코드베이스를 리팩터링하는 목적은 지금 리팩터링에 토큰을 사용해 향후 작업의 토큰 소비량을 줄이는 데 있다. 따라서 이 파일을 리팩터링할수록 같은 코드베이스에서 서로 다른 기능을 구현할 때 드는 토큰 비용이 감소한다는 사실을 실험으로 보여줄 수 있어야 했다.
에이전트는 학습하지 않기 때문에 오히려 이를 실험으로 진행할 수 있었다. 리팩터링의 각 단계가 끝날 때마다 새로운 에이전트에게 정확히 같은 변경을 요청할 수 있었다. 인간 엔지니어와 달리 이전 단계에서 배운 내용이 실험 결과에 영향을 미치지 않는다.
엄격한 리팩터링 원칙에 따라 전체 리팩터링 계획을 세운다.
하나의 프롬프트로 설명할 수 있는 대표 변경 사항을 만든다.
변경 비용의 기준값을 구한다. 하위 에이전트에서 해당 프롬프트를 실행하고 토큰 소비량도 보고하도록 요청한다.
변경 사항을 폐기한다.
다음 작업을 반복한다.
전체 리팩터링 계획에서 한 단계만 적용한다.
하위 에이전트에서 정확히 같은 변경을 실행하고 그 변경에 든 토큰 비용을 받는다.
변경 사항을 폐기한다.
기준값을 포함해 리팩터링의 각 단계가 끝날 때마다 모든 토큰 비용, 변경 실행 시간, 코드 줄 수를 기록한다.
대표 변경에 사용한 프롬프트와 적용한 리팩터링 단계는 아래 부록에 실었다.
한 가지 주의할 점이 있다. Claude는 토큰 수를 표시하고 세션별 소비량을 보고하며 토큰을 기준으로 요금을 청구하면서도, 실행 중인 토큰 수를 신뢰성 있게 계산할 방법은 제공하지 않는다. 시간이 지나면 개선될 일시적인 문제라고 가정했다. 대신 하위 에이전트가 수신하고 전송한 문자 수를 보고하게 한 뒤, tiktoken을 사용해 문자 수를 4로 나누는 방식으로 토큰 수를 근사했다.
결과
단계 | 데이터 액세스 계층 LoC | 가장 큰 파일 LoC | 전체 Rust LoC | 변경당 입력 토큰 | 변경당 출력 토큰 | 변경당 시간(초) |
|---|---|---|---|---|---|---|
기준값 | 17,155 | 17,155 | 50,359 | 159,564 | 1,705 | 342 |
1단계(FirestoreClient) | 16,706 | 16,706 | 49,910 | 155,205 | 1,723 | 530 |
2단계(extract_doc_id, new_link) | 16,562 | 16,562 | 49,766 | 159,227 | 2,105 | 574 |
3단계(link-query 헬퍼) | 16,567 | 16,567 | 49,771 | 154,054 | 2,105 | 524 |
4단계(FakeStore 조건자) | 16,577 | 16,577 | 49,781 | 154,146 | 2,060 | 654 |
5단계(value ctors) | 16,469 | 16,469 | 49,673 | 171,251 | 2,036 | 1,353 |
6단계(FieldsBuilder) | 16,469 | 16,469 | 49,673 | 171,251 | 2,036 | 1,353 |
7단계(queries.rs) | 16,474 | 15,670 | 49,678 | 151,850 | 1,800 | 587 |
8단계(traits.rs) | 16,508 | 13,845 | 49,712 | 132,558 | 1,723 | 446 |
9단계(traits/ 분할) | 16,508 | 13,845 | 49,712 | 132,558 | 1,723 | 446 |
10단계(codec.rs) | 16,521 | 12,846 | 49,725 | 131,871 | 1,750 | 540 |
11단계(fake_store.rs) | 16,535 | 11,122 | 49,739 | 133,016 | 2,460 | 600 |
12단계(store/ 분할) | 16,550 | 9,269 | 49,754 | 104,080 | 2,050 | 490 |
13단계(테스트를 관련 코드와 함께 배치) | 16,550 | 9,269 | 49,754 | 104,080 | 2,050 | 490 |
14단계(fake_store.rs 완성) | 16,553 | 7,225 | 49,757 | 107,205 | 2,453 | 523 |
15단계(store/ 분할) | 16,608 | 3,695 | 49,812 | 27,360 | 2,113 | 454 |
여기서 눈여겨볼 지표는 데이터 액세스 계층의 전체 코드 줄 수, 데이터 액세스 계층에서 가장 큰 단일 파일의 전체 코드 줄 수, 변경 사항을 만드는 동안 소비한 입력 토큰 수다.

이 차트에는 네 가지 지표가 표시돼 있다. 첫 지점은 기준값인 0단계이며, 이후 각 리팩터링 단계를 적용한 뒤에 같은 지표를 다시 측정했다.
데이터 액세스 계층 전체의 코드 줄 수다. 처음에는 시작점이었던 단일 파일 하나뿐이다. 리팩터링을 적용하면서 여러 파일로 나뉘었고, 마지막에는 Rust 파일이 19개가 됐다.
데이터 액세스 계층에서 가장 큰 단일 파일의 코드 줄 수다. 처음에는 데이터 계층 전체가 최초의 단일 파일에 들어 있었다. 마지막에는 테스트 라이브러리가 가장 큰 단일 파일이 됐다. 이후 리팩터링에서도 여기에 같은 접근법을 적용할 수 있다.
하위 에이전트가 대표 변경을 적용하며 소비한 전체 입력 토큰 수다.
하위 에이전트가 대표 변경을 적용하며 생성한 전체 출력 토큰 수다.
리팩터링은 토큰 소비량을 줄인다
결과는 명확하다. 가장 큰 파일의 크기가 줄기 시작할 때까지 입력 토큰 수는 큰 변화 없이 유지되다가 감소하기 시작했고, Claude의 표현을 빌리면 이내 절벽에서 떨어지듯 급감했다.
기준값과 최종 리팩터링 결과를 비교하면 같은 작업의 입력 토큰 수가 159,564개에서 27,360개로 줄었다. 132,204개, 즉 **83%**를 절약한 셈이다. 게다가 이 절감 효과는 일회성이 아니다. 이제부터 데이터 액세스 계층을 건드리는 모든 변경에 드는 비용이 크게 줄어든다.
얼마나 절약했을까? 글을 쓰는 시점의 Sonnet 5 가격인 $3/MTok을 적용하면 39.7센트다. 큰 금액은 아니다. 하지만 이 효과가 누적되면 어떨까? 디버깅이나 더 복잡한 기능에서는 어떤 결과가 나올까? 이번에는 코드베이스의 일부만 리팩터링했는데, 전체 코드베이스를 적극적으로 리팩터링해 곳곳에서 비용을 줄일 수 있을까? 그리고 그런 리팩터링 자체에는 얼마나 많은 비용이 들까?
비용이 줄어든 이유는 에이전트가 읽어야 할 코드가 감소했기 때문이다. 하지만 읽을 코드 자체가 줄어든 것은 아니다. 데이터 액세스 계층 전체의 코드량은 거의 그대로 유지됐다. 따라서 이 절감 효과를 실제로 얻으려면 에이전트가 읽어야 할 최소한의 파일 집합을 정확히 찾아낼 수 있어야 한다. 결과를 보면 실제로 그렇게 작동한 것으로 보인다. 변경이 적용되는 동안 Claude Code가 출력한 사고 과정과 파일 읽기 요약을 살펴봐도 하위 에이전트가 매번 더 작은 코드 영역을 정확히 읽었다는 사실을 확인할 수 있었다.
다시 말해 파일을 무작위로 잘게 나누는 것만으로는 이만큼 효과를 얻기 어렵다. 파일 하나하나는 작아져도 에이전트가 관련 코드를 찾느라 여러 파일을 읽어야 하기 때문이다. 가장 큰 효과가 마지막 단계에서 나타났지만, 그에 앞선 리팩터링들이 이 절감을 가능하게 만들었다. 의도적으로 계획한 결과는 아니었다. 중복을 추출하는 파일 내부 변경부터 시작해 반복되는 핵심 구조가 드러나면 여러 작은 파일로 나누는 일반적인 리팩터링 과정에서 자연스럽게 나온 결과였다.
리팩터링한다고 대표 변경 자체가 작아진 것은 아니다. 코드를 작성하며 생성한 토큰 수는 거의 영향을 받지 않아 출력 토큰 수에는 큰 변화가 없었다. 출력 토큰의 가격은 입력 토큰보다 다섯 배 비싸다. 다만 출력 토큰 수 자체가 훨씬 적다. 출력 토큰 생성을 줄이는 리팩터링도 있을까? 이 질문을 살펴보려면 더 복잡한 표본 변경이 필요하다. 비결정적 코드 생성 과정의 잡음 때문에 코드 구조 변화에서 생긴 차이가 드러나지 않고 있다.
진행 과정에서 확인한 점
Claude는 리팩터링을 잘하지 못했다. 아래 프롬프트와 리팩터링 단계를 읽어 보면, 결과로 나온 리팩터링이 프롬프트에 직접 반응해 생성됐다는 사실을 알 수 있다. Claude는 코드를 보고 일반적인 리팩터링 방법을 검토한 뒤 어떤 방법을 적용해야 하는지 스스로 판단하지 못한다. 사람이 적극적으로 안내해야 한다. 이 앱을 개발하며 얻은 전반적인 경험과도 일치한다. 개발 하네스에는 명시적인 리팩터링 단계가 들어 있지만, 그 단계도 Claude가 이 파일을 개선하게 만들지는 못했다. 개인적인 경험으로는 Claude.ai가 Claude Code보다 나았다. 리팩터링 계획을 세울 때 두 인터페이스를 모두 사용했는데, Claude Code는 첫 단계로 함수 추출을 찾아냈다. Claude.ai는 더 나아가 클라이언트 클래스 전체를 추출할 수 있다는 점까지 발견했다.
Claude는 리팩터링을 실제로 적용하는 능력도 부족했다. 기계적인 리팩터링 작업은 grep과 sed를 사용하는 Python 스크립트를 작성해 수행했다. 이 스크립트는 들여쓰기 때문에 자주 혼란을 겪었다. 참으로 아이러니한 일이다. 게다가 가장 가치가 큰 단일 리팩터링을 첫 시도에서 빠뜨려 후속 단계로 다시 적용해야 했다. 그래서 그림에 표시된 단계 수와 부록의 리팩터링 단계 수가 일치하지 않는다.
전체 실험을 마치는 데 약 8시간이 걸렸다. 대부분은 지켜보지 않아도 됐다. 개입한 것은 6시간 40분 뒤 작업이 끝난 듯했지만 해당 단계를 건너뛴 사실을 발견하고 다시 지시했을 때뿐이다. 실험은 느린 호텔 WiFi에서 진행했다. 처음에는 이것이 시간이 오래 걸린 원인인지 궁금했다. 하지만 코드베이스를 자세히 분석해 보니 cargo 임시 빌드 캐시가 매우 커져 있었고, 이 때문에 테스트 실행 속도가 크게 느려졌다.
추가 연구와 더 넓은 의미
아쉽게도 리팩터링 계획을 만들고 실행하는 데 필요한 토큰 수를 계산해야 한다는 생각은 작업이 모두 끝난 뒤에야 떠올랐다. 실험을 설계하고 실행한 작업까지 포함해 이 일을 진행한 기간의 전체 소비량을 살펴봤지만, 리팩터링에 정확히 몇 개의 토큰이 필요했는지는 말할 수 없다. 다만 상한은 500만 개다. 여기에는 리팩터링 계획을 두 번 만든 작업, 대표 변경을 포함한 실험 설계 작업, 그 밖의 여러 작업이 모두 포함돼 있다. 앞으로는 리팩터링에 소비한 토큰 수를 더 정확히 계산해야 한다.
이번 결과는 아직 처음부터 새로 만드는 단계이고 한 명의 개발자가 구축하고 유지보수하는 상당한 규모의 애플리케이션에서 진행한 단 한 번의 실험에 불과하다. 하지만 잠재적으로 의미 있는 첫걸음이라고 생각한다. 이 실험은 리팩터링이 시간과 비용 측면에서 어떤 가치를 제공하는지 보여준다. 동시에 리팩터링 자체에 드는 비용도 측정할 수 있다. 더 복잡한 변경, 더 광범위한 리팩터링, 지속적인 리팩터링, 서로 다른 리팩터링 방식의 상대적 가치도 살펴보면 흥미로울 것이다.
이제 시작일 뿐이다.
부록
참고: 이 부록에는 실제로 사용한 프롬프트와 반환된 출력이 들어 있다. 구체적인 코드 변경 사항을 제거한 것 외에는 편집하지 않았다. 에이전트에 어떤 지시를 내렸는지 그대로 보여주기 위해 원문 그대로 실었다. 숨겨진 요령은 없다. 따라서 일부 표현이 혼란스러울 수 있으며, 해당 오류도 원문에 있던 것이다.
대표 변경
다음은 각 하위 에이전트에 전달한 프롬프트다. 코드베이스와 함께 제공된 아키텍처 문서 외에는 추가 컨텍스트를 주지 않았다. 모든 하위 에이전트가 정확히 같은 정보로 작업을 시작했다.
~/dev/your-project-name에 있는 Rust 프로젝트에서 작업하고 있습니다. 기존 패턴을 정확히 따라 Firestore 계층에 새로운 공개 비동기 트레이트ItemWatchStore를 추가하세요. 트레이트에는 다음 세 메서드가 있어야 합니다.
async fn watch_item(&self, item_id: &str, user_id: &str) -> Result<>
async fn unwatch_item(&self, item_id: &str, user_id: &str) -> Result<>
async fn watched_items_for_user(&self, user_id: &str) -> Result<Vec<String>>watch 데이터는 Firestore의item_watches컬렉션에 저장합니다. 각 문서에는itemId(문자열),userId(문자열),createdAt(타임스탬프) 필드가 있습니다. watch 레코드용 Rust 구조체는 없습니다. 메서드는Vec<String>(item ID)을 반환합니다.FakeStore에는FakeStoreInner에 추가한 메모리 내Vec<(String, String)>필드를 사용하고,FirestoreStore에는 이 파일의 다른 store 구현에서 사용하는 것과 같은 HTTP 패턴을 사용해 트레이트를 구현하세요. 응답의 맨 마지막에 정확히 다음 JSON 블록을 출력하세요. 실제 값을 채워야 합니다.{ "files_read": [ {"path": "src/firestore.rs", "chars": 123456}, ... ], "response_chars": 7890 }변경 사항을 커밋하지 마세요. 코드를 작성한 뒤 중단하세요.
리팩터링 단계
다음은 리팩터링 계획을 만드는 데 사용한 프롬프트다.
리팩터링을 정확성을 보존한다는 사실을 증명할 수 있는 일련의 코드 수정이라고 엄격히 정의하고, Martin Fowler의 『Refactoring』 제2판을 근거로 @src/firestore.rs를 검토하세요. 이 파일은 17K LoC 규모의 Rust 파일입니다. 어떤 파일도 이렇게 길어서는 안 됩니다. 쿼리를 작성하고 관리하기 위한 내부 언어도 거의 확실히 사용하지 않고 있습니다. 인터페이스를 전혀 변경하지 않으면서 이 파일의 코드 줄 수를 대폭 줄일 수 있는 리팩터링 순서를 만들고 설명하되, 실제로 실행하지는 마세요.
다음은 Claude가 만들고 실행한 계획에서 적용한 리팩터링 설명만 추출한 것이다. 실제 계획에는 예상 코드 변경도 들어 있다. 각 리팩터링에는 따라야 할 세부 단계가 나열돼 있었다. 모든 단계는 개별적으로 테스트할 수 있었고 실제로 하나씩 테스트했다. 대부분의 인간 엔지니어가 따르는 방식보다 더 엄격한 리팩터링이다.
Claude가 가장 가치 있는 단일 리팩터링인 store의 하위 파일 분할을 첫 시도에서 건너뛰었고, 이후 두 단계를 추가해 마쳐야 했다. 그래서 여기에 나열한 단계는 위에서 측정한 변경 단계와 정확히 일치하지 않는다.
1단계 — 클래스 추출: FirestoreClient(Fowler §7.5) + 함수 추출 × 4(Fowler §6.1)
Fowler 참고: 각 기본 작업에 Extract Class(7.5), Extract Function(6.1) 적용
현재 FirestoreStore에는 두 가지 책임이 뒤섞여 있다.
도메인 쿼리 조정 — 어떤 쿼리를 실행하고 어떤 문서를 쓰며, 결과를 어떻게 도메인 타입으로 파싱할지 결정한다.
Firestore HTTP 전송 — 인증 헤더, URL 생성, Firestore wire 타입의 JSON 인코딩·디코딩, PRECONDITION_FAILED 발생 시 재시도를 처리한다.
Fowler §7.5는 클래스 데이터와 동작 중 일관된 하위 집합을 식별할 수 있을 때 새로운 클래스로 추출하라고 설명한다. 전송 책임에 속하는 항목은
client: reqwest::Client,project_id: String,MetadataAuth,documents_url과auth_header다. 이를 새로운FirestoreClient구조체로 추출한다.예상 절감량:
FirestoreStore구현에서 약 1,200줄 감소,FirestoreClient추가로 순증가 약 120줄.
2단계 — 함수 추출: extract_doc_id와 new_link(Fowler §6.1)
Fowler 참고: Extract Function(6.1)
extract_doc_id—doc.name.rsplit('/').next?.to_string표현식이 20개parse_*_document함수의 시작 부분에 똑같이 등장한다. 이를 추출한다.new_link—metadata: HashMap::new,provenance: None, 새 UUID를 사용해Link구조체를 만드는 코드가 62번 등장한다. 팩터리 함수로 추출한다.예상 절감량: 약 500줄. 10줄 안팎의 구조체 생성 코드 62개가 약 2줄의 호출로 바뀌고, 20개 parse 함수에서 상용구가 각각 1줄씩 사라진다.
3단계 — 함수 추출: link-query 파이프라인 헬퍼(Fowler §6.1)
Fowler 참고: Extract Function(6.1)
link 쿼리를 실행한 뒤 FirestoreStore 트레이트 구현 안에서 두 가지 하위 패턴이 반복된다.
패턴 A — 쿼리 행에서 모든 link 문서를 수집한다(약 15곳).
패턴 B — link를 쿼리해 정확히 하나의 대상 ID를 반환하고, 없으면 오류를 낸다(약 8곳).
예상 절감량: 약 200줄.
4단계 — 함수 추출: FakeStoreInner의 FakeStore link 조건자(Fowler §6.1)
Fowler 참고: Extract Function(6.1)
FakeStore 구현 내부의 메서드 약 15개에서 inner.links.iter...의 변형이 반복된다.
FakeStoreInner에 메서드 두 개를 추출한다. 그러면 15개 호출 지점이 한 줄로 줄어든다. 두 번째 조건자로 추가 필터링하는 메서드(예: to_kind도 확인하는 경우)는 헬퍼 결과에 .into_iter.filter(...)를 연결한다.
예상 절감량: 약 120줄.
5단계 — 인라인 코드를 함수 호출로 교체 × 4: Firestore 값 생성자
Fowler 참고: Replace Inline Code with Function Call(8.5)
codec 블록 앞에 파일 수준의 비공개 자유 함수 네 개를 추가한다. 128개가 넘는 인라인 json!({"stringValue":...}), json!({"timestampValue":...}) 등의 표현식을 이 함수들의 호출로 바꾼다. 여러 단어로 구성된 각 json 매크로 호출이 짧은 단일 호출로 바뀐다.
예상 절감량: 약 80줄. 대부분 여러 줄짜리 json 매크로가 한 줄로 줄면서 감소한다.
6단계 — 클래스 추출: FieldsBuilder(Fowler §7.3)
Fowler 참고: Extract Class(7.3)
약 20개의 인코더 함수가 모두 다음 형태를 따른다.
let mut fields = serde_json::Map::new();
fields.insert("foo".to_string(), str_val(&x.foo));
fields.insert("bar".to_string(), ts_val(x.bar));
json!({"name": path, "fields": fields})작은 빌더를 추출하고 각 인코더 함수가 이 빌더를 사용하도록 다시 작성한다. 약 40줄짜리 인코더가 약 12줄로 줄어든다.
예상 절감량: 20개 인코더 함수 전체에서 약 500~600줄.
7단계 — 함수 이동: src/firestore/queries.rs 추출
Fowler 참고: Move Function(8.1)
src/firestore.rs를 모듈 디렉터리로 바꿔 src/firestore/mod.rs로 이름을 변경한다. 그런 다음 src/firestore/queries.rs를 만들고 32개 LinkQuery 상수와 LinkQuery, EqFilter, EqValue, Ordering, Direction 타입 정의를 모두 옮긴다. mod.rs에는 pub(super) use queries::*;를 추가한다.
동작은 바뀌지 않는다. 모든 호출 지점은 이미 단일 파일에서 스코프에 들어온 이름을 참조하고 있다.
mod.rs가 약 800줄 줄어든다.
8단계 — 함수 이동: src/firestore/traits.rs 추출
Fowler 참고: Move Function(8.1)
17개의 pub trait 정의와 관련 오류 타입을 모두 src/firestore/traits.rs로 옮긴다. mod.rs에서 pub use traits::*;로 다시 내보낸다.
mod.rs가 약 1,900줄 줄어든다. 추가 분할이 필요한 약 1,900줄짜리 traits.rs가 생긴다.
9단계 — 함수 이동: traits.rs를 traits/ 모듈 디렉터리로 분할
Fowler 참고: Move Function(8.1)
17개 트레이트를 도메인에 따라 네 파일로 묶어 src/firestore/traits.rs를 모듈 디렉터리로 바꾼다.
파일 | 트레이트 | 대략적인 줄 수 |
|---|---|---|
|
| 약 650 |
|
| 약 550 |
|
| 약 300 |
|
| 약 400 |
traits/mod.rs는 순수한 재내보내기 파일이 된다(약 20줄). FocusPassError, SuggestionDecisionError 같은 관련 오류 타입은 해당 오류를 생성하는 트레이트와 함께 옮긴다.
트레이트 정의나 호출 지점은 전혀 바뀌지 않고 위치만 달라진다. 분할된 파일은 각각 300~650줄이 된다.
10단계 — 함수 이동: src/firestore/codec.rs 추출
Fowler 참고: Move Function(8.1)
모든 문서 인코더·디코더 함수(*_document, parse_*_document, kind_str, parse_kind, parse_capture_source, parse_outcome 등)와 5·6단계의 FieldsBuilder 및 값 생성자를 src/firestore/codec.rs로 옮긴다. 접근 수준은 pub(super)로 지정한다.
6단계를 마친 뒤 이 모듈은 약 1,200줄이 아니라 약 400~500줄이 된다.
6단계 적용 후 기준으로 mod.rs가 약 500줄 줄어든다.
11단계 — 함수 이동: src/firestore/fake_store.rs 추출
Fowler 참고: Move Function(8.1)
FakeStore, FakeStoreInner, FakeStore의 트레이트 구현 블록 18개를 모두 src/firestore/fake_store.rs로 옮긴다. mod.rs에서 pub use fake_store::FakeStore;로 FakeStore를 다시 내보낸다.
FakeStoreInner와 헬퍼 메서드는 모듈 내부 비공개 상태로 유지한다.
mod.rs가 약 4,700줄 줄어든다.
12단계 — 함수 이동: FirestoreStore 구현을 src/firestore/store/ 아래의 트레이트별 파일로 분할
Fowler 참고: Move Function(8.1)
FirestoreStore 구조체 정의, impl FirestoreStore(생성자와 1단계의 FirestoreClient), MetadataAuth가 들어 있는 src/firestore/store/mod.rs를 만든다.
그런 다음 논리적 도메인 그룹별로 파일을 하나씩 만든다.
각 파일에는 use super::*; 또는 명시적 import와 트레이트 구현 블록만 둔다. 타입 정의나 헬퍼는 넣지 않는다. 여러 구현 블록에서 사용하는 헬퍼는 store/mod.rs에 남긴다.
약 10,000줄짜리였을 파일이 각각 120~650줄인 파일 10개로 나뉜다. mod.rs는 약 100줄짜리 재내보내기 목록이 된다.
13단계 — 함수 이동: 테스트를 관련 모듈과 함께 배치
Fowler 참고: Move Function(8.1)
기존 #[cfg(test)] 모듈은 특정 도메인 영역을 테스트하므로 하나의 tests.rs가 아니라 12단계에서 만든 모듈에 두는 것이 적합하다. 각 테스트 모듈을 대상 파일 맨 아래의 #[cfg(test)] mod tests {... } 블록 안으로 옮기고, 모듈 내부 항목에 접근할 수 있도록 use super::*;를 사용한다. 테스트는 변경하지 않고 위치만 옮긴다.
이미 fake_store.rs에 있는 공유 테스트 픽스처(FakeStore::new, 헬퍼 빌더)는 기존 use super::fake_store::FakeStore import 체인을 통해 접근할 수 있다.
mod.rs가 약 2,000줄 줄어든다. 각 대상 파일에는 테스트 대상 코드 바로 옆에 200~700줄의 테스트가 추가된다.
source https://martinfowler.com/articles/exploring-gen-ai/refactoring-economic-benefit.html
HN에서는 리팩터링이 AI 시대에 새로 등장한 기법이 아니라, 오래된 개발 원칙의 경제성이 토큰 비용으로 다시 확인됐다는 반응이 두드러졌다. 명확한 모듈 경계와 코드에 담긴 문서, 프로젝트 전체를 설명하는 맥락은 원래 사람에게도 중요했다. 다만 작업할 때마다 코드베이스를 새로 파악해야 하는 에이전트에게는 그 효과가 더 빠르고 직접적으로 나타난다.
에이전트는 잘 정리된 코드를 활용하는 데는 강하지만, 처음부터 적절한 구조와 추상화를 만드는 데는 서툴다는 경험이 반복됐다. 파일을 작게 나누는 것만으로는 부족하다. 어떤 기능을 묶고 무엇을 공통화할지는 시스템의 목적을 아는 사람이 정해야 한다. 반면 사람이 좋은 패턴과 범위를 구체적으로 제시하면, 이를 코드베이스 전반에 반복 적용하는 작업에는 에이전트가 유용하다는 평가도 있었다.
토큰 절감만으로 리팩터링의 경제성을 계산하기 어렵다는 견해도 설득력을 얻었다. 읽기 쉬운 코드는 장애 대응 시간을 줄이고 결함을 예방하며, 기능 개발과 시스템 인수인계에도 영향을 준다. 견고한 테스트는 회귀를 막는 안전망이자 사람과 에이전트가 함께 참고할 수 있는 명세다. 구조를 개선하고도 전체 코드량이 거의 줄지 않았다는 결과는 줄 수가 품질 지표가 될 수 없음을 보여준다.
결국 확인할 것은 파일 크기나 당장의 토큰 단가가 아니다. 반복되는 변경에서 맥락 파악 비용이 실제로 줄어드는지, 테스트가 기존 동작을 보호하는지, 사람이 구조와 책임을 설명할 수 있는지가 중요하다. 리팩터링 비용과 이후 절감 효과를 함께 재고, 설계 판단은 사람이 맡되 명확한 지시 아래 반복 작업을 자동화하는 방식이 현실적인 기준에 가깝다.
