VibeCoding 365 로고 VibeCoding 365

AI 리팩터링이 매번 커지는 이유 | 되돌릴 수 있는 상태로 자르는 법

AI 에이전트가 리팩터링 diff를 키우는 구조적 원인과, 커밋과 워크트리, 특성화 테스트로 롤백 지점을 먼저 확보하는 실무 순서입니다. 체크포인트만으로는 부족한 이유와 REFACTOR.md 중단 조건까지 함께 다룹니다.

심층 학습 가이드

AI 리팩터링이 매번 커지는 이유 | 되돌릴 수 있는 상태로 자르는 법

심층 학습 가이드

AI 리팩터링이 매번 커지는 이유 | 되돌릴 수 있는 상태로 자르는 법

"작게 해줘"가 안 먹히는 구조적 이유와, 프롬프트 대신 환경으로 강제하는 방법

핵심 주제
안전한 AI 리팩터링 운영
예상 시간
21분
업데이트
2026.04.26
키워드
AI 리팩터링 · 롤백 기준 · 작은 diff

"작게 해줘"라는 프롬프트로는 diff가 작아지지 않습니다. 되돌릴 수 있는 지점을 환경에 미리 박아두는 방식으로만 작아집니다.

화요일 오후에 "이 모듈 정리 좀 해줘"라고 던졌다가 30분 뒤 파일 열 개짜리 diff를 받아본 적이 있다면 이 글이 필요합니다. 에이전트는 변수명을 통일하고 중복을 지우고 에러 처리까지 손봤습니다. 그런데 원래 화면과 똑같이 동작하는지, 어떤 API 응답이 조용히 달라졌는지는 아무도 모릅니다. 리뷰어는 바쁜데 diff는 길고, 체크포인트 restore를 눌러도 bash로 건드린 파일은 그대로인 경우가 있습니다. 그 순간부터는 어디가 안전한 기준점인지조차 흐려집니다.

이 글은 프롬프트 문장을 다듬는 이야기가 아닙니다. 프롬프트를 고치면 실패 확률은 조금 낮아지지만, 실패했을 때 치르는 비용은 거의 그대로입니다. 여기서 다루는 건 그 비용입니다. 되돌아갈 커밋, 작업 종류 구분, 골든 파일, 워크트리처럼 환경에 장치를 먼저 심는 순서를 따라갑니다. Git을 쓸 줄 알고 에이전트를 이미 쓰고 있는 사람을 기준으로 씁니다. 작은 수정을 더 예쁘게 말하는 법이 아니라, 망해도 돌아올 자리를 남기는 법입니다.

왜 매번 커지는가, 성실해서가 아니다

흔한 설명은 "AI가 성실해서 눈에 보이는 걸 다 고친다"입니다. 틀린 말은 아니지만 이 진단으로는 대책이 "지시를 잘하자"에서 끝납니다. 실제 메커니즘은 세 가지고, 셋 다 프롬프트 바깥에 있습니다.

컨텍스트에 파일 전체가 들어 있습니다. 함수 하나를 고치라고 해도 모델은 파일 전체를 읽습니다. 그 안에 개선 지점이 여덟 개 보이면 여덟 개 전부가 "지금 고칠 수 있는 것"이 됩니다. 사람은 "이건 다음에"라고 미루는데, 그 미룸은 내일도 이 코드베이스에 있을 거라는 전제에서 나옵니다. 에이전트에게 그 전제가 없습니다. 세션이 끝나면 끝이고, 지금이 유일한 기회입니다.

편집에 비용 신호가 없습니다. 사람은 파일 열 개를 고치면 리뷰가 지옥이 된다는 걸 몸으로 압니다. 에이전트에게 파일 열 개는 파일 한 개보다 토큰이 조금 더 드는 일일 뿐입니다. diff 크기에 대한 페널티가 시스템 어디에도 없습니다.

"도움이 됐다"는 평가가 눈에 보이는 개선에 후합니다. 요청 범위를 정확히 지킨 3줄짜리 diff와 겸사겸사 정리까지 마친 80줄짜리 diff를 나란히 놓으면, 후자가 더 유능해 보입니다. 모델은 그렇게 보이는 쪽으로 학습되어 있습니다.

대책도 셋에 하나씩 대응합니다. 컨텍스트를 좁히고, 비용 신호를 인위적으로 만들고, 완료 조건을 "보기 좋음"이 아니라 검증 가능한 문장으로 씁니다. 아래 절들이 각각의 방법입니다.

작업 종류를 세 개로 나눈다

리팩터링과 기능 변경을 섞지 말라는 조언은 널리 알려져 있는데, 실무에서는 두 개가 아니라 세 개입니다.

종류정의검증 방법롤백
리팩터링외부 동작이 바이트 단위로 동일기존 테스트 전부 통과, 출력 diff 없음git reset
동작 변경출력이 의도적으로 달라짐새 테스트 추가, 기존 테스트 수정git reset + 배포 롤백
의존성 변경패키지, 설정, DB 마이그레이션별도 절차코드 롤백으로 안 됨

세 번째가 가장 자주 빠지고 가장 위험합니다. git reset --hard를 해도 설치된 패키지와 실행된 마이그레이션은 그대로 남습니다. 그래서 "리팩터링 하다가 필요해져서 마이그레이션도 돌렸습니다"는 진행 상황 보고가 아니라 사고 신호입니다.

경계선 판정은 질문 하나로 끝납니다. "기존 테스트를 수정해야 하나?" 수정해야 한다면 그 작업은 이름이 뭐든 리팩터링이 아닙니다. 사용자에게 보이는 문구, API 응답 구조, DB 스키마, 성능 목표: 이 중 하나라도 건드리면 별도 커밋, 가능하면 별도 요청으로 쪼갭니다.

이렇게 나누는 이유는 결벽이 아닙니다. 테스트가 실패했을 때 원인이 구조 변경인지 동작 변경인지 즉시 알아야 판단이 되기 때문입니다. 둘이 섞인 diff는 실패해도 어디부터 의심할지 알 수 없습니다.

롤백 계약, 복붙할 물건

롤백 기준을 코딩이 끝난 뒤에 정하면 "조금만 더 고쳐보자"는 판단이 반복되고, 그럴수록 되돌릴 지점은 멀어집니다. 순서를 뒤집습니다. 첫 프롬프트를 보내기 전에 작업 폴더에 REFACTOR.md를 만들고, 프롬프트에는 이 파일 경로만 넘깁니다.

# 리팩터링 계약

## 범위
- 대상: src/payments/calculator.ts 단일 파일
- 금지: src/payments/ 밖의 모든 파일, package.json, 마이그레이션
- 종류: 리팩터링 (기존 테스트 수정 금지)

## 시작 상태
- 브랜치: refactor/calc-split
- 기준 커밋: a3f9c21
- 되돌리기: git reset --hard a3f9c21

## 완료 조건 (전부 만족해야 함)
- [ ] npm test 통과 (기존 테스트 무수정)
- [ ] npm run typecheck 통과
- [ ] 골든 파일 diff 없음 (npm run golden:check)
- [ ] 변경 파일 3개 이하, 순증감 200줄 이하

## 중단 조건 (하나라도 걸리면 즉시 정지, 사람 호출)
- 기존 테스트를 수정해야만 통과하는 상황
- 범위 밖 파일을 고쳐야 하는 상황
- 같은 실패가 3번 반복
- 패키지 설치나 DB 마이그레이션이 필요해짐

여기서 진짜 중요한 건 중단 조건입니다. 완료 조건만 쓰면 에이전트는 어떻게든 완료하려고 범위를 넓힙니다. "테스트가 실패해서 테스트를 수정했습니다"가 그렇게 나옵니다. 멈춰야 할 지점을 명시하지 않으면 멈추지 않습니다.

작업 브랜치의 첫 커밋 메시지에 요약을 남겨두면 나중에 git log에서 바로 보입니다. 파일과 커밋 메시지 양쪽에 두는 게 안전합니다.

💡 Tip: 계약을 프롬프트 본문에 길게 붙이지 마세요. REFACTOR.md 경로만 넘기면 세션이 길어도 범위·중단 조건이 한곳에 남습니다.

체크포인트를 롤백 수단으로 믿지 않는다

Cursor의 체크포인트나 Claude Code의 /rewind가 있으니 안심해도 된다는 생각은 위험합니다. Claude Code 공식 문서에 명시된 제약이 이렇습니다.

Bash로 만든 변경은 추적되지 않습니다. 에이전트가 rm, mv, cp, 리다이렉트로 파일을 건드리면 체크포인트에 남지 않고 /rewind로 돌아오지 않습니다. 문서에 예시까지 박혀 있는 항목입니다.

보관 한도가 있습니다. 세션당 최근 100개 체크포인트의 스냅샷만 유지되고, 세션과 함께 30일 뒤 정리됩니다(cleanupPeriodDays로 조정 가능).

심볼릭 링크와 하드 링크는 복원에서 건너뜁니다. dotfile 관리자가 심링크한 설정 파일이나 pnpm이 하드링크한 파일이 여기 해당합니다. 복원 후 "Restored the code, but skipped N files" 경고가 뜹니다.

문서 스스로 대체재가 아니라고 말합니다. "체크포인트는 로컬 undo, Git은 영구 히스토리"라는 문장이 그대로 들어 있습니다.

Cursor 체크포인트도 성격이 같습니다. "restore를 눌렀는데 안 돌아온다"는 사례가 커뮤니티에 반복적으로 올라오는데, 대부분 파일 편집 도구 밖에서 일어난 변경입니다.

정리하면 체크포인트는 방금 받은 답변이 마음에 안 들 때 쓰는 undo이지, 사고 복구 수단이 아닙니다. 사고 복구는 Git입니다. 터미널 명령이 섞이는 작업이라면 롤백 기준을 반드시 커밋 단위로도 잡아야 합니다.

⚠️ 주의: 서브에이전트나 백그라운드 스킬이 만든 편집도 세션 체크포인트 복원 대상이 아닐 수 있습니다. Claude Code 문서도 그런 경우 Git으로 되돌리라고 안내합니다.

환경으로 강제하기, 프롬프트보다 확실한 세 가지

"작게 나눠서 해줘"가 안 먹히는 건 프롬프트를 잘못 써서가 아닙니다. 앞 절에서 본 세 가지 원인이 전부 프롬프트 바깥에 있기 때문입니다. 환경 쪽에서 거는 장치가 훨씬 확실합니다.

시작 전 커밋

작업 폴더에서 아래를 먼저 실행해 기준 커밋 해시를 만듭니다.

git add -A && git commit -m "wip: before refactor" --allow-empty
git rev-parse HEAD    # 이 값을 REFACTOR.md에 적는다

빈 커밋이라도 만듭니다. 되돌아갈 좌표가 없으면 아래 규칙이 전부 무의미해집니다.

확인: git rev-parse HEAD 출력이 REFACTOR.md의 기준 커밋과 같은지 봅니다.

단계마다 커밋을 강제

프롬프트에 순서로 박습니다.

각 단계가 끝날 때마다 반드시:
1. npm test 실행
2. 통과하면 git commit (단계 하나 = 커밋 하나)
3. 실패하면 즉시 멈추고 보고 (스스로 고치려 시도하지 말 것)

여러 단계를 한 커밋에 몰아넣지 말 것.

3번이 핵심입니다. "실패하면 고쳐"를 허용하는 순간부터 범위가 확장됩니다. 실패는 사람이 볼 신호이지 에이전트가 처리할 작업이 아닙니다.

위험한 작업은 워크트리로 격리

메인 작업 디렉터리를 아예 건드리지 못하게 하는 방법입니다.

git worktree add ../repo-refactor refactor/calc-split
cd ../repo-refactor
# 여기서 에이전트를 돌린다

# 망하면 디렉터리째 제거
cd .. && git worktree remove --force repo-refactor

node_modules 재설치가 유일한 비용이고, 그 대가로 원래 작업 트리는 어떤 경우에도 안전합니다. 접근 방식을 두세 개 병렬로 시켜보고 제일 나은 걸 고르는 용도로도 씁니다.

얼마나 작아야 작은가

작은 diff는 줄 수가 적다는 뜻만은 아닙니다. 리뷰어가 변경 의도를 한 문장으로 설명할 수 있으면 작은 diff입니다. 다만 감으로 판단하면 매번 흔들리니 숫자가 필요합니다.

가장 널리 인용되는 기준은 SmartBear가 Cisco 개발팀의 코드 리뷰 약 2,500건을 분석한 연구에서 나옵니다. 한 번에 200~400줄 이하를 리뷰할 것, 시간당 500줄을 넘기면 결함 발견율이 뚜렷하게 떨어질 것, 한 세션이 60분을 넘으면 집중력이 무너질 것. 2006년 연구이고 AI 이전 시대라는 점은 감안해야 합니다.

그런데 AI 시대에는 오히려 더 빡빡하게 잡아야 할 이유가 있습니다. 사람이 쓴 400줄은 쓰는 동안 이미 한 번 검토된 400줄입니다. AI가 30초 만에 낸 400줄은 아무도 안 본 400줄입니다. 같은 숫자가 아닙니다.

실무 기준으로는 이 정도를 권합니다.

기준상한넘으면
변경 파일 수3개단계를 쪼개서 다시 요청
순증감 라인200줄단계를 쪼개서 다시 요청
변경 이유1개이유별로 커밋 분리
public API, URL, DB 필드, 환경변수포함 시별도 리뷰로 승격
포맷팅과 로직 혼재혼재 시포맷팅만 먼저 커밋
리뷰 소요20분이미 큰 것

생성 코드와 기계적 변경(포맷팅, 임포트 정렬)은 별도 커밋으로 빼고 카운트에서 제외합니다. 안 그러면 숫자가 의미를 잃습니다.

확인은 매 단계 이 한 줄로 합니다.

git diff --stat a3f9c21

상한을 넘긴 diff가 오면 이어서 다듬지 말고 그 자리에서 되돌립니다. "이미 만들었으니 정리해서 쓰자"가 가장 위험한 선택입니다. 큰 diff를 고치다 보면 더 큰 diff가 만들어지고, 되돌릴 시점은 계속 뒤로 밀립니다.

참고로 2025년 DORA 보고서는 AI 도입이 이제 처리량은 개선하지만 배포 불안정성은 여전히 높인다고 보고합니다. 속도가 붙은 만큼 하류에서 터진다는 뜻이고, 그 하류가 대체로 리뷰와 롤백입니다. 같은 보고서는 작은 배치로 일할 때 AI의 긍정적 효과가 증폭된다고도 말합니다. diff 크기 통제는 이 지점에 직접 개입하는 일입니다.

테스트가 없을 때, 특성화 테스트

레거시 코드에는 테스트가 없는 경우가 흔합니다. "먼저 테스트를 만드세요"는 맞는 말이지만 막연합니다. 필요한 건 올바른 동작을 검증하는 테스트가 아니라, 지금 나오는 결과를 그대로 박제하는 특성화 테스트(characterization test)입니다. 현재 코드에 버그가 있어도 상관없습니다. 리팩터링은 버그까지 보존해야 하는 작업이니까요.

가장 빠른 구현은 골든 파일입니다.

// tests/golden/calculator.golden.test.ts
import { calculate } from '../../src/payments/calculator';
import cases from './cases.json';
import expected from './expected.json';

test.each(cases.map((c, i) => [i, c] as const))('case %i', (i, input) => {
  expect(calculate(input)).toEqual(expected[i]);
});

expected.json은 손으로 쓰지 않습니다. 리팩터링 전에 현재 코드를 돌려서 생성합니다.

npx tsx scripts/record-golden.ts   # 현재 출력을 expected.json에 기록
git add tests/golden && git commit -m "test: golden snapshot before refactor"

입력 케이스는 커버리지를 보면서 늘립니다. 경계값(0, 음수, null, 빈 배열, 최대치)과 실제 프로덕션 로그에서 뽑은 샘플이 가장 효율이 좋습니다. 완벽할 필요 없습니다. 없는 것보다 스무 케이스가 압도적으로 낫습니다.

골든 파일 생성 자체를 에이전트에게 시켜도 됩니다. 다만 순서를 지키세요.

1. src/payments/calculator.ts 의 입출력 케이스 30개를 tests/golden/cases.json 에 만든다.
   경계값과 예외 경로를 포함할 것.
2. scripts/record-golden.ts 를 실행해 현재 출력을 기록한다.
3. 여기서 멈춘다. 내가 expected.json 을 확인한 뒤 다음 지시를 한다.
4. (승인 후) 리팩터링을 시작한다. expected.json 은 절대 수정 금지.

3번이 없으면 에이전트가 리팩터링하다가 골든 파일을 "고쳐서" 통과시킵니다. 이건 드문 사고가 아니라 기본 실패 모드입니다. 완료 조건이 주어져 있고 기댓값 파일이 쓰기 가능하면, 그게 최단 경로이기 때문입니다.

프롬프트로 막는 것보다 확실한 방법이 있습니다. expected.json을 읽기 전용으로 만들거나, CI에 이 한 줄을 넣으세요.

git diff --exit-code tests/golden/expected.json || (echo "골든 파일이 변경됨" && exit 1)
⚠️ 주의: 골든 생성과 리팩터링을 한 프롬프트에 넣지 마세요. 사람이 expected.json을 확인하기 전에 리팩터가 시작되면 기댓값 자체가 오염됩니다.

각 단계가 끝날 때 확인하는 세 가지

첫째, 범위를 벗어났는가.

git diff --name-only HEAD~1 | grep -v '^src/payments/' && echo "범위 이탈"

둘째, 테스트 파일이 바뀌었는가.

git diff --name-only HEAD~1 | grep -E '(test|spec|golden)' && echo "테스트 변경됨: 리팩터링 아님"

리팩터링이라면 테스트는 바뀌지 않아야 합니다. 바뀌었다면 작업 종류가 바뀐 것이고, 작업 종류 표로 돌아가야 합니다.

셋째, 설명과 diff가 일치하는가.

에이전트 요약을 읽지 말고 diff를 먼저 읽으세요. 순서를 반대로 하면 요약이 프레임을 만들어서, diff에서 이상한 부분이 눈에 들어오지 않습니다. 게으름이 아니라 인지 편향이고, 요약을 먼저 읽으면 누구나 그렇게 됩니다.

셋 중 하나라도 걸리면 다음 단계로 넘어가지 말고 그 자리에서 되돌립니다. 세 단계 쌓인 뒤에 되돌리면 멀쩡했던 두 단계까지 함께 날아갑니다.

확인: 위 세 명령을 돌린 뒤, 범위 이탈·테스트 변경 메시지가 없어야 다음 단계로 갑니다.

사고가 나는 실제 순간

터미널 실행은 코드 롤백으로 돌아오지 않습니다. 에이전트가 마이그레이션을 돌리거나 패키지를 설치하면 git reset --hard로 원상복구되지 않습니다. 계약서의 종류가 "리팩터링"인데 마이그레이션이 필요해졌다면 그건 진행이 아니라 중단 조건입니다. 멈추고 사람이 판단합니다.

성공한 단계를 안 지키면 전부 잃습니다. 3단계가 망했을 때 1·2단계가 커밋되어 있으면 3단계만 버리면 됩니다. 안 되어 있으면 전부입니다. 단계마다 커밋을 강제하는 진짜 이유는 리뷰 편의가 아니라 손실 범위 제한입니다.

"거의 다 됐는데"가 가장 위험합니다. 여기서 범위를 조금만 넓히면 지금까지 세운 계약이 전부 무효가 됩니다. 남은 걸 다음 단계로 미루는 쪽이 거의 항상 옳습니다. 계약을 깨야 할 만큼 급한 상황이라면, 그건 계약이 틀린 게 아니라 애초에 작업을 잘못 쪼갠 것입니다.

실행 순서

아래 순서로 환경을 먼저 깔고, 그다음에만 에이전트를 부릅니다.

# 1. 되돌아갈 좌표 만들기
git add -A && git commit -m "wip: before refactor" --allow-empty
git rev-parse HEAD

# 2. 위험도가 높으면 격리
git worktree add ../repo-refactor refactor/calc-split

# 3. 동작 고정 (테스트가 없다면)
npx tsx scripts/record-golden.ts
git add tests/golden && git commit -m "test: golden snapshot"

# 4. REFACTOR.md 작성 (범위 / 완료 조건 / 중단 조건)

# 5. 에이전트에 계약 전달, 단계마다 커밋 강제

# 6. 단계마다 확인
git diff --stat HEAD~1
git diff --name-only HEAD~1 | grep -E '(test|spec|golden)'

# 7. 망하면
git reset --hard <기준 커밋>
# 또는
git worktree remove --force ../repo-refactor

일곱 단계 중 프롬프트는 5번 하나뿐입니다. 나머지 여섯은 전부 환경입니다. "작게 해줘"가 안 먹히는 이유가 여기 있습니다. 그건 프롬프트로 풀 문제가 아닙니다.

출처와 링크

조사 기준: 2026년 7월. 에이전트 도구의 체크포인트 동작과 권한 설정은 버전마다 바뀌므로, 적용 전에 사용 중인 버전의 문서를 다시 확인하세요.

자주 묻는 질문

리팩터링 계약서를 매번 새로 쓰면 시간이 너무 오래 걸리지 않나요?

처음 몇 번은 그렇습니다. 대신 파일 이름과 커밋 해시만 바꿔 재사용하면 두 번째부터는 보통 2분 안쪽입니다. 한 번이라도 망해서 반나절을 태워본 뒤에는 그 2분이 보험료처럼 느껴집니다. 템플릿을 팀 공통 폴더에 두면 개인마다 다시 쓰지 않아도 됩니다.

작은 변경에도 롤백 계약을 다 써야 하나요?

아닙니다. 파일 하나, 함수 하나, 대략 20줄 이하면 diff만 보고 끝내도 됩니다. 계약이 필요해지는 지점은 여러 파일에 걸치거나, 테스트가 없거나, 이미 프로덕션에 나가 있는 코드일 때입니다. 셋 중 둘이 겹치면 REFACTOR.md를 쓰세요.

에이전트가 중단 조건을 무시하고 계속 가면 어떻게 막나요?

프롬프트만으로는 거의 안 지켜집니다. 시작 전 커밋, 단계 커밋, 워크트리 격리를 먼저 깔아 두세요. Cursor나 Claude Code에 권한 설정이 있다면 범위 밖 디렉터리 쓰기와 마이그레이션, 패키지 설치 명령을 거부 목록에 넣는 편이 더 확실합니다. 승인 프롬프트가 뜨는 순간이 사람이 개입할 기회입니다.

골든 파일을 만들 수 없는 코드는 어떻게 하나요?

부수 효과가 많거나 시간이나 랜덤에 의존하는 코드가 여기에 해당합니다. 그런 코드는 리팩터링 전에 부수 효과를 경계 밖으로 밀어내는 작업이 먼저입니다. 그 작업 자체가 별도 리팩터링이므로 그것부터 계약을 씁니다. 순서를 건너뛰면 검증 수단 없이 큰 변경만 남습니다.

체크포인트는 언제 쓰고 Git은 언제 쓰나요?

방금 받은 답변이 마음에 안 들 때, 커밋할 가치도 없는 시도를 되돌릴 때 씁니다. 세션 안의 undo로는 충분합니다. 다만 bash, 외부 편집, 심링크 경로는 복원이 안 되거나 건너뛸 수 있으니, 그 이상은 Git 커밋과 함께 쓰세요. 대체재가 아니라 보조 도구입니다.

리팩터링 중 package.json만 살짝 올려도 되나요?

안 됩니다. 패키지 변경은 의존성 변경 축이라 코드 `git reset`만으로는 로컬, CI, 배포 환경이 어긋날 수 있습니다. 필요하면 리팩터 계약을 중단하고 의존성 전용 요청과 커밋으로 분리하세요. lockfile diff가 보이면 이미 종류가 바뀐 신호입니다.

Windows에서 git worktree 경로는 어떻게 잡나요?

`git worktree add ..\repo-refactor refactor/calc-split`처럼 상대 경로로 옆에 두는 방식이 흔합니다. 에이전트는 그 폴더를 워크스페이스로 연 뒤에만 돌리세요. 제거는 `git worktree remove --force repo-refactor`로 하고, 잠긴 파일이 있으면 에디터와 터미널을 닫은 뒤 다시 시도합니다.