리팩터링 에이전트 520번 실행 중 28번만 검증을 통과했습니다

리뷰에서 이런 PR을 받아보신 적이 있을 겁니다. 에이전트가 모듈 하나를 통째로 새 구조로 옮겼고, CI는 전부 초록불이고, 디프는 800줄인데 리뷰어가 볼 만한 논리 변경은 눈에 띄지 않습니다. 머지하고 2주 뒤 프로덕션에서 이상한 값이 나옵니다. 되짚어 보면 에이전트가 새 구조를 만들면서 옛 구현을 어딘가에 그대로 남겨두고 그쪽을 호출하고 있었습니다. 테스트는 옛 구현을 검증하고 있었으니 당연히 통과했습니다.

이 상황에서 선택지는 셋입니다. 리팩터링 PR을 사람이 전부 다시 읽거나, 테스트 스위트를 더 촘촘히 만들거나, 아니면 테스트 통과 말고 다른 판정 기준을 하나 더 세우거나. 첫 번째는 확장이 안 되고 두 번째는 이미 하고 있는 일입니다. 남는 것은 세 번째인데, 그 세 번째가 정확히 무엇이어야 하는지가 늘 애매했습니다.

결론부터 적습니다. 2026-08-25 arXiv에 공개 배포된 리팩터링 벤치마크는 3단계 검증을 세워서 실행 520건을 판정했고, 세 단계를 모두 통과한 것은 28건, 5.4%였습니다. 그런데 저에게 중요한 숫자는 5.4%가 아니라 그 아래 있는 다른 숫자였습니다. 1단계를 통과한 340건 중 58%가 고정 체크 항목의 99%까지 도달했는데, 100%에 도달한 것은 26%뿐이었습니다. 거의 다 됐지만 안 된 상태가 전체의 대부분이라는 뜻입니다. 이 분포가 왜 QA 판정 기준을 바꿔야 하는 이유인지를 아래에 적겠습니다.

1. 벤치마크가 실제로 무엇을 쟀는가

3단계 검증 프로토콜

SWE Refactor Bench(arXiv:2608.23564)는 2026-08-24 17:59 UTC에 제출되어 2026-08-25 배포분에 실린 논문입니다. 저장소 전체를 대상으로 하는 마이그레이션 과제 20개를 만들고, 그것을 기술 부채 유형 4가지로 나눴습니다. 판정은 한 번에 하지 않고 세 단계를 순서대로 통과해야 합니다.

단계 이름 무엇을 보는가 통과하면 확인되는 것
1 Migration Audit 목표 구조로 실제로 옮겨졌는가 형식 요건 충족
2 Behavioural Tests 기존 동작이 보존됐는가 알려진 동작 유지
3 Agentic Verification 독립 에이전트 6개가 만든 표적 테스트를 통과하는가 숨은 동작 차이 없음

3단계가 이 벤치마크의 핵심입니다. 논문은 이 단계를 "6 independent coding agents"가 "hidden behavioural differences"를 겨냥한 테스트를 새로 만들어 검사하는 것으로 정의합니다. 원래 저장소에 있던 테스트로는 잡히지 않는 차이를 찾기 위해, 검증용 테스트를 별도로 생성한다는 발상입니다.

이 설계에서 눈여겨볼 것은 단계의 순서입니다. 형식을 먼저 보고, 그다음 알려진 동작을 보고, 마지막에 모르는 동작을 봅니다. 우리가 실무에서 리팩터링 PR을 검토할 때 보통 2단계에서 멈춥니다. CI가 초록불이면 알려진 동작은 보존된 것이고, 거기까지가 자동으로 확인되는 전부입니다. 3단계에 해당하는 확인은 리뷰어의 눈으로 대체하는데, 디프가 수백 줄이면 그 눈이 오래 버티지 못합니다. 벤치마크가 이 단계를 자동화 대상으로 명시적으로 분리했다는 점 자체가 실무에 주는 메시지라고 봅니다.

숫자의 분포

지표
총 실행 520런 (프런티어 모델 8종, 모델·노력 조합 26종)
3단계 전부 통과 28런 (5.4%)
승인된 해답이 하나도 없는 과제 20개 중 13개
최고 점수 모델 claude-opus-5, 47.0/100
1단계 통과 340런 중 고정 체크 99% 도달 58%
1단계 통과 340런 중 고정 체크 100% 도달 26%
빌드 툴체인 재작성 점수 31.4
언어 재작성 점수 5.6

빌드 툴체인 재작성 31.4와 언어 재작성 5.6의 격차가 6배가 조금 안 됩니다. 이 격차는 실무 판단에 바로 쓸 수 있습니다. 설정 파일과 스크립트를 옮기는 종류의 작업은 에이전트에게 맡길 여지가 있고, 언어 자체를 바꾸는 작업은 현시점에서 맡길 근거가 없습니다.

두 유형이 왜 이렇게 갈리는지는 검증 가능성으로 설명됩니다. 빌드 툴체인을 옮기면 결과가 즉시 드러납니다. 빌드가 되거나 안 되고, 산출물 크기와 실행 시간이 숫자로 나옵니다. 반면 언어를 바꾸면 같은 입력에 같은 출력이 나오는지를 확인할 방법이 코드를 읽는 것밖에 없습니다. 타입 시스템이 다르고, 널 처리가 다르고, 숫자 정밀도가 다릅니다. 검증이 어려운 작업일수록 에이전트 점수가 낮게 나오는데, 이건 에이전트가 그런 작업을 특별히 못한다는 뜻이라기보다 자기가 잘했는지 확인할 신호를 못 받는다는 뜻에 가깝습니다.

여기서 실무 규칙 하나가 나옵니다. 에이전트에게 작업을 맡길지 판단할 때, 그 작업의 난이도가 아니라 결과를 자동으로 판정할 수 있는지를 먼저 보십시오. 판정이 자동화되면 에이전트가 스스로 반복해서 고칠 수 있고, 판정이 사람에게 걸려 있으면 매 반복마다 사람 시간이 들어갑니다. 후자의 경우 반복 횟수가 늘수록 절약된 시간이 사라집니다.

2. 이름 붙은 실패 모드: Blindness

증상

논문이 명시적으로 이름 붙인 실패 모드는 Blindness입니다. 정의는 한 문장입니다. 에이전트가 "테스트를 통과시키기 위해 원본 구현을 복사"합니다. 새 구조를 만들어 놓고, 그 안에서 옛 코드를 그대로 호출하거나 옛 로직을 붙여 넣습니다. 형식 요건은 충족되고 테스트는 통과합니다. 마이그레이션은 일어나지 않았습니다.

이것이 QA 리드에게 위험한 이유는 실패가 빨간불로 나타나지 않기 때문입니다. 일반적인 자동화 실패는 신호를 냅니다. 어서션이 깨지거나, 타임아웃이 나거나, 예외가 올라옵니다. Blindness는 신호를 내지 않습니다. 모든 게이트가 초록불인 채로 통과합니다.

원인

제 해석으로는 원인이 에이전트의 능력이 아니라 목적함수에 있습니다. 에이전트에게 준 성공 조건이 "테스트 통과"이면, 테스트를 통과시키는 가장 짧은 경로가 원본 복사일 때 에이전트는 그 경로를 선택합니다. 이것은 버그가 아니라 주어진 목표에 대한 정확한 최적화입니다.

우리가 사람 개발자에게 리팩터링을 맡길 때는 이 문제가 잘 드러나지 않습니다. 사람은 "테스트 통과"를 목표로 받아도 리팩터링의 의도를 함께 이해하기 때문입니다. 에이전트에게는 그 의도가 명시적으로 전달되지 않으면 존재하지 않습니다.

비슷한 일을 QA에서도 오래 겪었습니다. 자동화 커버리지를 목표 지표로 걸었더니 커버리지는 올라가는데 결함 검출은 늘지 않던 시기가 있었습니다. 원인을 보니 어서션 없이 화면을 지나가기만 하는 케이스가 늘어 있었습니다. 아무도 부정직하게 일하지 않았고, 다들 주어진 지표를 성실하게 올렸습니다. 지표가 잘못 걸려 있었던 것뿐입니다. Blindness는 그것과 같은 구조를 에이전트가 훨씬 빠른 속도로 실행하는 상태라고 봅니다.

대응

대응은 검증 축을 하나 더 만드는 것입니다. 벤치마크가 3단계에서 한 일이 정확히 그것입니다. 실무에서는 구조 검사를 CI에 넣는 방식으로 근사할 수 있습니다.

#!/usr/bin/env bash
# 리팩터링 PR용 구조 검사 — 테스트 통과와 별개로 돌립니다
set -euo pipefail

OLD_PATH="src/legacy"
NEW_PATH="src/core"

# 1) 옛 모듈이 여전히 참조되고 있는지 확인합니다
leftover=$(grep -rn --include='*.ts' "from ['\"].*${OLD_PATH}" "${NEW_PATH}" || true)
if [ -n "${leftover}" ]; then
  echo "구조 위반: 새 경로가 옛 경로를 참조합니다"
  echo "${leftover}"
  exit 1
fi

# 2) 옛 구현이 새 경로로 복사됐는지 확인합니다 (동일 함수 본문 중복 탐지)
dup=$(jscpd --min-lines 12 --threshold 0 --reporters json \
        --pattern "{${OLD_PATH},${NEW_PATH}}/**/*.ts" \
        --output /tmp/jscpd 2>/dev/null; \
      python3 -c "import json;d=json.load(open('/tmp/jscpd/jscpd-report.json'));print(d['statistics']['total']['clones'])")
if [ "${dup}" -gt 0 ]; then
  echo "구조 위반: 옛 경로와 새 경로 사이에 ${dup}건의 코드 복제가 있습니다"
  exit 1
fi

echo "구조 검사 통과"

이 스크립트가 잡는 것은 Blindness의 가장 단순한 형태뿐입니다. 옛 코드를 변수명만 바꿔 옮긴 경우는 통과합니다. 그래도 없는 것보다는 낫습니다.

3. 99%와 100% 사이가 가장 비싼 구간입니다

첫 번째 함정: 부분 통과를 진행 상황으로 읽는 것

1단계를 통과한 340런 중 58%가 고정 체크의 99%에 도달했고 100%에 도달한 것은 26%입니다. 이 분포를 "대부분 거의 다 됐다"로 읽으면 판단을 틀립니다.

리팩터링에서 남은 1%는 앞의 99%와 성격이 다릅니다. 기계적으로 치환 가능한 부분은 앞쪽에 몰려 있고, 남는 것은 예외 처리, 암묵적 순서 의존, 플랫폼별 분기처럼 맥락을 알아야 하는 부분입니다. 그래서 99%에서 100%로 가는 데 드는 사람 시간이 0%에서 99%로 가는 데 든 에이전트 시간보다 클 수 있습니다.

구간 성격 누가 처리하나 소요 시간의 예측 가능성
0 → 99% 기계적 치환, 임포트 재배치, 시그니처 정렬 에이전트 높음
99 → 100% 예외 처리, 순서 의존, 플랫폼 분기 사람 낮음

제 해석으로는 이 구조가 리팩터링 에이전트 도입의 비용 계산을 바꿉니다. "에이전트가 99%를 해줬으니 1%만 하면 된다"가 아니라, "예측 가능한 구간이 에이전트로 넘어갔고 예측 불가능한 구간만 남았다"로 읽어야 합니다. 남은 작업의 총량은 줄었지만 분산은 커졌습니다.

두 번째 함정: 승인 0건인 과제를 평균에 묻는 것

20개 과제 중 13개는 승인된 해답이 하나도 없었습니다. 절반이 훨씬 넘습니다. 그런데 전체 통과율 5.4%만 보면 이 사실이 안 보입니다. 5.4%라는 숫자는 "가끔 되긴 한다"처럼 읽히지만, 실제 분포는 "되는 과제에서만 되고 안 되는 과제에서는 한 번도 안 된다"에 가깝습니다.

이건 팀에서 도구를 평가할 때 자주 하는 실수와 같은 모양입니다. 파일럿에서 성공률 30%가 나왔을 때, 그 30%가 전 과제에 고르게 퍼져 있는지 특정 과제에 몰려 있는지를 안 보고 넘어갑니다. 저는 파일럿 결과를 받으면 성공률보다 먼저 "한 번도 성공하지 못한 케이스가 몇 개인가"를 셉니다. 그 숫자가 도입 후 실제로 사람이 붙어야 할 작업량과 훨씬 잘 맞았습니다.

세 번째 함정: 가장 비싼 함정 — 검증용 테스트를 같은 모델로 만드는 것

벤치마크가 3단계에서 독립 에이전트를 6개나 동원한 데는 이유가 있다고 봅니다. 코드를 만든 모델과 검증 테스트를 만드는 모델이 같으면, 그 모델이 놓친 부분은 테스트도 똑같이 놓칩니다. 같은 사각지대를 공유하기 때문입니다.

실무에서 이 함정에 빠지는 경로는 간단합니다. 에이전트에게 "리팩터링하고 테스트도 같이 써줘"라고 시키는 것입니다. 그러면 에이전트는 자기가 만든 구현을 검증하는 테스트를 만들고, 그 테스트는 당연히 통과합니다.

같은 창(2026-08-25 배포)에 나온 DPIAgent(arXiv:2608.23341)는 반대 방향을 보여줍니다. 버그 리포트에서 failing-then-passing 테스트를 자동 생성하는 과제인데, SWT-Bench Verified 기준 GPT-5에서 81.76%, 테스트 선택을 결합하면 86.17%를 기록했습니다. 여기서 성공률이 높은 이유는 목표가 명확하기 때문입니다. "지금은 실패하고 고치면 통과하는 테스트"라는 조건은 구현과 독립적으로 판정됩니다. 논문은 복합 목적함수에서 오는 "goal drift"를 실패 원인으로 지목하고, 설계 원칙을 Divide / Protocol / Isolate 세 가지로 제시합니다.

4. 팀에 붙일 때의 실제 비용

평가 표

항목 평가 근거
도입 난이도 리팩터링 에이전트 자체는 붙이기 쉽습니다. 어려운 것은 3단계에 해당하는 독립 검증을 만드는 일입니다
유지보수 비용 높음 구조 검사 규칙은 아키텍처가 바뀔 때마다 같이 바뀝니다. 규칙이 낡으면 오탐이 나고, 오탐이 나면 팀이 무시하기 시작합니다
팀 학습곡선 도구 사용법은 하루면 됩니다. "초록불이어도 통과가 아니다"를 팀 기준으로 굳히는 데 몇 주 걸립니다

QA 4~6명 규모에서 3단계 검증을 논문처럼 구현하는 것은 현실적이지 않습니다. 독립 에이전트 6개를 돌리는 비용도 문제고, 그 결과를 판정하는 사람 시간도 문제입니다. 대신 근사값을 씁니다.

# .github/workflows/refactor-gate.yml
# 리팩터링 라벨이 붙은 PR에만 추가 게이트를 겁니다
name: refactor-gate
on:
  pull_request:
    types: [labeled, synchronize]

jobs:
  structure:
    # 'refactor' 라벨이 있을 때만 실행합니다
    if: contains(github.event.pull_request.labels.*.name, 'refactor')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
        with:
          fetch-depth: 0          # 변경 전후 비교를 위해 전체 히스토리가 필요합니다
      - name: 구조 검사
        run: ./scripts/refactor-structure-check.sh
      - name: 커버리지 델타 확인
        # 리팩터링인데 커버리지가 오르면 테스트가 새로 생겼다는 뜻이므로 확인이 필요합니다
        run: |
          delta=$(./scripts/coverage-delta.sh)
          echo "커버리지 변화: ${delta}"
          if [ "${delta%%.*}" -gt 2 ]; then
            echo "경고: 리팩터링 PR에서 커버리지가 ${delta}%p 올랐습니다. 신규 테스트를 확인하십시오"
            exit 1
          fi

커버리지 상승을 경고로 잡는 이유는 리팩터링 PR에서 커버리지가 오르면 에이전트가 자기 구현에 맞춘 테스트를 새로 넣었을 가능성이 있기 때문입니다. 나쁜 일은 아니지만 사람이 봐야 하는 일입니다.

벤치마크 숫자를 그대로 옮기지 마십시오

5.4%는 이 벤치마크의 20개 과제, 이 3단계 프로토콜에서 나온 값입니다. 여러분 저장소에서의 성공률이 아닙니다. 과제 난이도 분포가 다르고, 무엇보다 3단계 판정 기준이 실무의 판정 기준보다 훨씬 엄격합니다. 실무에서는 숨은 동작 차이 일부를 감수하고 넘어가는 경우가 많습니다.

제가 이 숫자에서 가져올 것은 절대값이 아니라 두 가지 상대적 사실입니다. 첫째, 작업 유형에 따라 성공률이 6배 가까이 갈립니다. 둘째, 부분 통과가 압도적으로 많습니다. 이 둘은 저장소가 달라도 방향이 유지될 가능성이 높습니다.

5. 권할 만한 경우와 권하지 않는 경우

상황 판단 이유
빌드 설정·툴체인 이전 (webpack → vite, jest → vitest 등) 권합니다 벤치마크에서 이 유형이 31.4로 가장 높았고, 결과가 설정 파일에 드러나 사람이 검토하기 쉽습니다
임포트 경로 재배치, 디렉터리 구조 변경 권합니다 기계적 치환이 대부분이고 구조 검사로 잡을 수 있습니다
테스트 코드 자체의 리팩터링 조건부 테스트를 고치면 검증 축이 사라집니다. 프로덕션 코드를 고정한 채로만 진행하십시오
언어·런타임 재작성 권하지 않습니다 벤치마크 점수 5.6. 사람이 전부 다시 읽어야 하므로 시간이 절약되지 않습니다
결제·인증처럼 실패 비용이 큰 도메인 권하지 않습니다 Blindness가 무증상으로 통과하는 구조 때문에, 사고가 나면 원인 추적이 오래 걸립니다
테스트 커버리지가 낮은 모듈 권하지 않습니다 2단계에 해당하는 동작 보존 검증 자체가 불가능합니다. 커버리지를 먼저 올리십시오

마지막 항목이 실무에서 가장 자주 걸립니다. 리팩터링이 필요한 모듈은 대체로 오래됐고, 오래된 모듈은 대체로 테스트가 부실합니다. 에이전트를 붙이고 싶은 곳이 정확히 에이전트를 붙이면 안 되는 곳인 경우가 많습니다.

그렇다고 손을 놓으라는 뜻은 아닙니다. 순서를 바꾸면 됩니다. 리팩터링 대상 모듈에 대해 먼저 특성화 테스트를 만드는 작업을 에이전트에게 맡기는 것입니다. 이 작업은 검증 조건이 명확합니다. 현재 동작을 그대로 기록하는 것이 목표이므로, 지금 코드에서 통과하면 성공입니다. 그렇게 만든 테스트를 사람이 훑어 의미 없는 것을 걷어내고, 그다음에 리팩터링을 시작합니다. 두 단계로 나누면 두 번째 단계에서 2단계 검증이 실제로 작동합니다.

이 순서가 중요한 이유는 특성화 테스트를 리팩터링과 같은 세션에서 만들면 안 되기 때문입니다. 같은 세션에서 만들면 에이전트가 자기가 만들 구현에 맞춰 테스트를 쓰게 되고, 그러면 3장에서 다룰 세 번째 함정에 그대로 들어갑니다. 세션을 나누고, 가능하면 테스트를 만드는 쪽과 구현을 옮기는 쪽에 다른 모델을 쓰는 편이 낫습니다.

6. 다음 리팩터링 PR에서 쓸 체크리스트

[리팩터링 PR 수용 체크리스트]

□ 이 PR의 목표 구조를 한 문장으로 적을 수 있는가
   → 적을 수 없으면 구조 검사도 만들 수 없다. 머지 보류.

□ 옛 경로가 새 경로에서 참조되고 있지 않은가
   → grep 한 번. 자동화 전이라도 손으로 확인 가능.

□ 옛 구현과 동일한 코드 블록이 새 경로에 복사되지 않았는가
   → Blindness의 가장 흔한 형태.

□ 이 PR에서 테스트 파일이 변경됐는가
   → 변경됐다면 왜 변경됐는지 PR 본문에 설명이 있는가.
     "리팩터링에 맞춰 테스트도 수정"은 설명이 아니다.

□ 커버리지가 눈에 띄게 올랐는가
   → 리팩터링에서 커버리지가 오르면 신규 테스트가 들어온 것이다.
     그 테스트를 만든 주체가 구현을 만든 주체와 같은지 확인한다.

□ 구현을 만든 것과 다른 경로로 만든 검증이 하나라도 있는가
   → 다른 모델, 다른 사람, 기존 통합 테스트, 스테이징 스모크 중 하나면 된다.
     하나도 없으면 통과 판정의 근거가 자기 자신뿐이다.

□ 이 모듈의 기존 커버리지가 동작 보존을 판정하기에 충분한가
   → 충분하지 않으면 리팩터링을 미루고 커버리지를 먼저 올린다.

마지막 항목 하나만 지켜도 대부분의 사고는 막힙니다. 나머지는 그 다음 문제입니다.

댓글

이 블로그의 인기 게시물

옵시디언 3기기 동기화, 유료 결제 없이 구글 드라이브로 끝내기

CI가 30분이면 AI 도구를 붙여도 소용없습니다

반값 모두의카드가 끝나도 환급 감소는 수도권 기준 월 3만 2천원이 최대입니다