본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 04. 22:22

AI에게 수정을 맡겼더니 '다른 곳'이 망가진 정체 — 리팩터링을 망가뜨리지 않는 안전망(사양화 테스트) 실전 가이드

요약

AI 코딩 에이전트가 요청하지 않은 코드까지 수정하여 동작을 망가뜨리는 'tangled' 변경 문제를 다룹니다. 이를 방지하기 위해 리팩터링의 정의를 명확히 하고, 수정 전 테스트를 통한 안전망 구축의 중요성을 강조합니다.

핵심 포인트

  • AI 에이전트 커밋의 53.9%에서 의도치 않은 재작성이 발생함
  • 리팩터링의 핵심은 외부 동작을 변경하지 않는 것임
  • AI의 무분별한 수정을 막기 위해 수정 전 테스트(안전망)가 필수적임
  • 모호한 프롬프트는 AI가 요청 범위를 벗어나게 만드는 원인이 됨

AI 코딩 지원을 사용하면서, 이런 경험 없으신가요?

"이 함수만 좀 정리해줘"라고 부탁했을 뿐인데, 왠지 전혀 상관없는 곳이 작동하지 않게 되었다. 테스트를 돌렸더니 빨간불이 들어오고, 원인을 추적해 보니 요청하지도 않은 다른 부분까지 AI가 "덤으로" 다시 써버렸다.

솔직히 말하면, 저도 처음에는 이것 때문에 몇 번이나 실수했습니다. 겉보기에는 깔끔하게 고쳐져 있습니다. 코드도 돌아가는 것처럼 보입니다. 하지만 자세히 보면 동작(behavior)이 미묘하게 어긋나 있습니다. "작동하는 것"과 "망가지지 않은 것"은 전혀 다릅니다. 이게 은근히 괴로운 지점이죠.

흥미로운 점은, 이것이 우리의 기분 탓도 아니고 AI의 성능이 나쁘기 때문도 아니라는 것입니다. 2026년에 발표된 AI 에이전트의 리팩터링 실태 조사(arXiv 2511.04824 「Agentic Refactoring: An Empirical Study of AI Coding Agents」)를 읽어보면 다음과 같은 수치가 나옵니다.

  • AI 에이전트가 코드를 작성한 커밋(commit) 중 **약 26%**에서 어떠한 리팩터링이 수행됨
  • 해당 리팩터링 중 **53.9%**는 "리팩터링할 의도가 없는 커밋"에 섞여 들어감

즉, AI에게 "기능을 추가해줘"라거나 "버그를 고쳐줘"라고 부탁했을 뿐인데, 절반 이상의 케이스에서 요청하지 않은 "덤으로 하는 재작성"이 함께 섞여 들어온다는 뜻입니다. 논문에서는 이를 「tangled(엉킨)」 변경이라고 부릅니다. 게다가 이 논문은 테스트가 얼마나 관여했는지, 동작이 얼마나 어긋났는지와 같은 핵심적인 수치에 대해서는 "데이터가 부족하여 측정할 수 없다"라고 솔직하게 적고 있습니다. 요컨대, 그 부분은 인간이 감시할 수밖에 없는 영역으로 여전히 남아 있다는 것입니다.

그래서 오늘은 "AI에게 맡기면 망가진다"를 "AI에게 맡겨도 망가지지 않는다"로 바꾸기 위한, 가장 수수하지만 가장 효과적인 토대에 대해 이야기하고자 합니다. 키워드는 **「수정하기 전에, 안전망을 치는 것」**입니다.

이 글은 다음과 같은 분들을 위해 작성되었습니다.

  • Claude Code나 Cursor 같은 AI 코딩 지원을 사용하기 시작했거나 매일 사용하고 있는 분
  • 테스트가 거의 없는 코드를 보유하고 있으면서도, AI를 통해 안전하게 손을 대고 싶은 개인 개발자 및 소규모 팀
  • "리팩터링이란 결국 무엇인가?"라는 기초부터 알고 싶은, 아직 AI 개발에 익숙하지 않은 분

전문 용어는 나올 때마다 풀어서 설명해 드릴 테니, 모르는 단어가 있어도 뒤처질 걱정은 하지 마세요. 안심하고 읽어 내려가셔도 됩니다.

먼저 용어부터 짚고 넘어가겠습니다. 리팩터링 (refactoring) 이란, 거칠게 말하면 **"외부에서 보는 동작은 바꾸지 않고, 코드 내부만을 정리하는 것"**입니다.

이미지로 비유하자면 방의 가구 배치 변경과 비슷합니다. 가구 위치를 바꾸거나 어지러운 배선을 정리할 수는 있습니다. 하지만 "그 방에 살 때의 쾌적함"은 바꾸지 않습니다. 코드로 말하면, 입력에 대한 출력(=동작)은 1mm도 바꾸지 않으면서, 가독성·수정 용이성만을 높이는 것이 리팩터링입니다.

이 부분이 중요한 포인트인데, 리팩터링은 본래 "동작을 바꾸지 않는다"는 정의가 포함되어 있습니다. 따라서 리팩터링을 했다고 생각했는데 동작이 바뀌어 버렸다면, 그것은 이미 "리팩터링의 실패"인 것입니다.

그렇다면 왜 AI는 이것을 조용히 망가뜨리는 걸까요? 이유는 크게 세 가지라고 생각합니다.

이유 1: 애초에 "어디까지"인지 모호한 상태로 전달함

우리는 "이 함수를 읽기 쉽게 만들어줘"라고 부탁합니다. 하지만 AI 입장에서 "읽기 쉽게 만든다"의 범위가 어디까지인지는 알 수 없습니다. 옆에 있는 함수도 신경 쓰이고, 변수명도 신경 쓰이고, 덤으로 타입(type)도 정리하고 싶어집니다. 그래서 요청하지 않은 범위까지 손을 뻗게 됩니다. 앞서 말한 53.9%의 정체가 바로 여기에 있습니다.

이유 2: AI는 "저수준 편집"을 선호하는 경향이 있음

동일한 조사에서, AI 에이전트는 변수명 변경 (Rename Variable), 매개변수명 변경 (Rename Parameter), 타입 변경 (Change Variable Type)과 같은 세세한 편집을 인간보다 더 높은 비율로 수행한다는 결과가 나왔습니다. 하나하나의 변화는 작습니다. 작기 때문에 놓치기 쉽습니다. 하지만 그 "덤으로 하는 이름 변경"이 다른 곳의 참조(reference)와 어긋나면, 조용히 버그가 발생합니다.

이유 3: 생성된 코드가 "올바르게 보이는" 경향이 있음

이것이 가장 무서운 부분인데, AI가 내놓는 코드는 대개 깔끔하고 그럴싸합니다. 리뷰를 하다 보면 "음, 맞는 것 같네"라며 그냥 넘어가게 되죠. 하지만 "맞아 보임"과 "맞음"은 다릅니다. 겉모습의 설득력과 동작의 정확성은 별개의 문제입니다.

그렇다면 어떻게 해야 할까요? "AI를 믿지 않는다"도 아니고 "전부 직접 쓴다"도 아닌, **"AI가 무엇을 하든, 동작이 변하면 반드시 알아챌 수 있도록 해둔다"**라는 발상으로 전환하는 것이 가장 현실적이라고 생각합니다. 그것이 바로 다음 단계인 "안전망" 이야기입니다.

여기서 주인공이 등장합니다. Characterization Test(캐릭터라이제이션 테스트), 일본어로는 **「사양화 테스트」**라고 불리는 것입니다. 생소한 용어일 수 있으니 천천히 설명하겠습니다.

보통의 테스트는 "이래야 한다"라는 **올바른 사양 (Specification)**을 먼저 결정하고, 그것을 충족하는지 확인합니다. 기대값(Expected value)이 먼저 존재하죠.

사양화 테스트는 발상이 반대입니다. "지금 이 코드가 실제로 어떻게 움직이고 있는가"를, 그것이 옳은지 그른지는 일단 제쳐두고 그대로 기록하는 테스트입니다. 버그를 포함하여 현재의 동작을 통째로 찍어냅니다.

비유하자면, 집을 리모델링하기 전에 현재 방의 사진을 모두 찍어두는 것과 같습니다. "이 벽의 얼룩도, 이 바닥의 삐걱거림도 전부 지금은 이렇다"라고 기록해 두는 것이죠. 그렇게 하면 리모델링이 끝난 뒤 사진과 비교하여 **"어라, 요청하지 않은 벽까지 움직였잖아"**라는 사실을 알아챌 수 있습니다. 이것이 안전망(Safety net)의 정체입니다. 떨어지고 나서 당황하는 것이 아니라, 떨어지기 전에 그물을 쳐두는 것입니다.

보통의 테스트와 사양화 테스트의 차이를 표로 나타내면 다음과 같습니다.

관점보통의 테스트 (사양 기반)사양화 테스트 (Characterization)
무엇을 기준으로 하는가"이래야 한다"는 올바른 사양"지금 이렇게 움직인다"는 현 상태의 동작
...

여기서 한 가지 중요한 주의사항이 있습니다. 사양화 테스트는 "현재의 동작"을 고정하기 때문에, 현재 있는 버그까지 "올바른 것"으로 고정해 버릴 수 있습니다. 따라서 "테스트만 쓰면 안전하다"는 아닙니다. 고정된 동작이 "지켜야 할 사양"인지, "사실은 고쳐야 할 버그"인지는 인간이 판단해야 합니다. 이 부분은 AI에게 통째로 맡길 수 없는 영역입니다. 나중에 다시 다루겠습니다.

"무슨 말인지는 알겠어. 하지만 우리 코드는 테스트가 거의 없는데"라는 목소리가 들리는 것 같네요. 괜찮습니다. 오히려 테스트가 없는 코드일수록 이 순서가 효과적입니다. 테스트 제로(Zero)에서 시작하는 4단계를 소개합니다.

Step 0: 이해하기 (AI에게 설명하게 하고, 자신의 이해와 대조하기)

무턱대고 수정하지 마세요. 먼저 대상 코드가 "지금 무엇을 하고 있는지"를 AI에게 설명하게 합니다. 그리고 그 설명을 자신의 업무 이해도와 대조합니다. 여기서 AI의 설명이 자신의 이해와 어긋난다면 그것은 황신호입니다. 코드나 AI의 이해 중 어느 한쪽이 틀렸다는 신호이므로 먼저 해결해야 합니다.

  • 인간: 업무적으로 이 함수가 무엇을 보장해야 하는지 알고 있음
  • AI: 코드를 읽고 현재의 동작을 언어화함

Step 1: 고정하기 (사양화 테스트를 작성하여 "현재"를 찍어내기)

다음으로 현재의 동작을 사양화 테스트로서 고정합니다. 대표적인 입력을 몇 가지 준비하여 "현재의 출력"을 그대로 기대값으로 설정합니다. 그것이 옳은지 여부는 이 단계에서 묻지 않습니다. 우선 "현재"를 동결하는 것이 목적입니다. 테스트 초안은 AI에게 만들게 해도 좋습니다. 단, 입력의 망라성(경계값, 이상계가 포함되었는지 여부)은 인간이 확인해야 합니다.

Step 2: 변환하기 (작게, 함수 하나씩)

여기서 처음으로 리팩터링(Refactoring)을 수행합니다. 요령은 작게 나누는 것입니다. "이 파일 전부 깔끔하게 해줘"가 아니라, "이 함수 하나만 동작을 바꾸지 말고 정리해줘"라고 요청하세요. 범위를 좁힐수록 의도치 않은 수정(그 53.9%)이 섞일 여지가 줄어듭니다.

Step 3: 검증하기 (차분 + 테스트 + 인간 판단의 3종 세트)

변환을 마쳤다면 반드시 세 가지를 확인해야 합니다.

  • 테스트: Step 1의 사양화 테스트가 모두 통과(Green)되었는가 (= 동작이 변하지 않았는가)
  • 차분 (diff): 변경 사항이 "요청한 범위" 안에 머물러 있는가. 옆에 있는 함수까지 건드리지 않았는가
  • 인간 판단: 그 변경이 정말로 "정리"일 뿐인가, 아니면 "사양 변경"이 되어버렸는가

이 3종 세트를 통과해야 비로소 "망가뜨리지 않았다"라고 말할 수 있습니다. 테스트가 초록색인 것만으로는 부족하고, 차분만 보는 것도 부족합니다. 세 가지가 모두 갖춰져야 비로소 안심할 수 있습니다.

순서를 정리하면, 이해 → 고정 → 변환 → 검증입니다. 고치는 것은 3번째 단계입니다. 많은 사고가 갑자기 3번째 단계부터 시작해버리는 것에서 발생하는 것 같습니다.

여기서부터는 내일부터 바로 사용할 수 있는 프롬프트입니다. 고유명사나 사내 정보는 넣지 않고 범용적인 형태로 만들었습니다. 사용하시는 환경의 코드로 교체해서 사용해 주세요.

프롬프트 ①: 현상 코드의 동작을 정리하기 (Step 0용)

당신은 리팩터링 (Refactoring)의 안전성을 담보하는 리뷰어입니다.
앞으로 전달할 함수에 대해, '현재의 동작'만을 평가 없이 정리해 주세요.
# 출력 포맷
...

포인트는 "개선 제안은 하지 말 것"이라고 명시하는 것입니다. 이를 쓰지 않으면 AI는 친절한 마음으로 멋대로 리팩터링을 시작해 버립니다. 지금은 "현재를 아는" 단계라고 못을 박아두는 것이 중요합니다.

프롬프트 ②: 사양화 테스트 초안을 작성하게 하기 (Step 1용)

다음 함수의 '현재 동작'을 고정하기 위한 사양화 테스트 (Characterization Test)를 만들어 주세요.
목적은 정답을 검증하는 것이 아니라, '현재의 동작을 기록하여 이후의 리팩터링에서 변화를 감지하는 것'입니다.
# 요구사항
...

프롬프트 ③: 동작을 바꾸지 않고 리팩터링하고, 차이점을 설명하게 하기 (Step 2-3용)

다음 함수를 외부에서 보이는 동작을 일절 바꾸지 않고 리팩터링해 주세요.
# 엄수 규칙
- 입력에 대한 출력·예외·부작용(Side Effect)을 바꾸지 말 것 (사양 변경 금지)
...

①②③을 이 순서로 돌리면, "이해→고정→변환"의 흐름이 그대로 프롬프트의 흐름이 되기 때문에, 머릿속 생각과 AI의 작업이 일치하여 오차가 줄어듭니다.

추상론으로 끝내고 싶지 않기에 실제 형태를 두 가지 제시하겠습니다. 소재는 어디에나 있을 법한 장바구니의 합계 금액을 계산하는 함수로 하겠습니다 (범용 더미 데이터입니다).

먼저, 리팩터링하고 싶은 기존 코드입니다. 조금 읽기 어려운, 흔히 볼 수 있는 형태입니다.

# cart.py (리팩터링 전 기존 코드)
def calc_total(items, coupon=None):
    t = 0
    ...

이 코드에 손을 대기 전에, 현재의 동작을 통째로 복사해내는 사양화 테스트를 작성합니다.

# test_cart_characterization.py
import pytest
from cart import calc_total
...

여기서 주목해 주셨으면 하는 점은 두 번째 케이스입니다. "딱 10,000원일 때는 할인이 적용되지 않는다"라는 것은, 사양으로서 의도된 것인지 아니면 >를 >=로 바꾸지 않은 버그인지 코드만 봐서는 알 수 없다는 점입니다. 그렇기 때문에 주석으로 "현재 사양"이라고 명기하여,

판단은 인간의 몫으로 남깁니다. 사양화 테스트는 답을 내는 도구가 아니라, 논점을 가시화하는 도구인 것입니다.

사양화 테스트가 초록색(Pass)이라도 아직 방심은 금물입니다. 요청한 함수 이외의 곳에 변경이 누락되지 않았는지를 확인하는 차이점(Diff) 가드를 넣습니다. 이는 CI에도 통합할 수 있습니다.

#!/usr/bin/env bash
# guard_diff.sh : 변경이 허용된 범위(cart.py만) 내에 있는지 확인한다
set -euo pipefail
...

하는 일은 단순합니다. "cart.py 이외의 파일이 바뀌었다면 빨간불을 켜고 멈춘다"는 것뿐입니다. 하지만 이것이 그 53.9%의 "덤으로 이루어진 재작성"을 기계적으로 걸러내는 1차 필터가 됩니다. 인간이 Diff를 눈으로 쫓기 전에 기계가 범위 이탈을 감지해 줍니다. 안전망은 사람의 주의력에만 의존하지 않는 구조로 만들수록 강력해집니다.

이 두 가지를 조합하면 검증은 **"사양화 테스트가 초록색"이고 "차이점이 범위 내에 있음"이며 "인간이 사양 변경이 아님을 확인"**하는 3종 세트가 됩니다. 테스트만, 혹은 Diff만으로는 부족합니다. 세 가지가 모여야 비로소 하나의 그물망이 된다는 이미지입니다.

마지막으로, 실제로 해보면 빠지기 쉬운 함정을 미리 공유해 두겠습니다.

함정 1: 버그를 "올바른 사양"으로 고정해 버리는 것

사양화 테스트는 현상을 그대로 복사하므로 버그도 함께 고정됩니다. 고정한 뒤에 "이것은 지켜야 할 사양인가 / 고쳐야 할 버그인가"를 인간이 분류하는 공정을 반드시 거쳐야 합니다. 분류하지 않으면 버그가 영원히 테스트에 의해 보호받는 슬픈 상황이 벌어집니다.

함정 2: 그물이 얇은 상태에서 안심해 버리는 것

입력이 정상계 (Normal Case) 뿐이라면, 경계값 (Boundary Value)이나 이상계 (Abnormal Case)의 어긋남을 놓치게 됩니다. "테스트가 통과(Green) = 안전"이 아니라, "통과는 했지만, 애초에 그물에 구멍이 나 있는 건 아닌가"를 의심하는 습관을 가져야 합니다.

함정 3: AI의 설명을 그대로 믿어버리는 것

AI의 "이것은 ○○하는 함수입니다"라는 말은 그럴듯해 보이기만 할 뿐 틀릴 때가 있습니다. Step 0에서 반드시 자신의 업무 이해도와 대조하십시오. 맞지 않는다면 그곳이 바로 조사 포인트입니다.

함정 4: 한꺼번에 크게 변환시키는 것

"전부 깔끔하게 정리해줘"는 사고의 원인입니다. 범위가 넓을수록 덤으로 이루어지는 코드 재작성 (Rewrite)이 섞여 들어갑니다. 함수 하나씩, 작게 진행하십시오.

함정 5: AI에게 테스트 자체를 느슨하게 작성하게 만드는 것

"테스트 통과"라는 말을 듣고 싶은 나머지, AI가 무난한 테스트만을 작성할 때가 있습니다. 경계값과 이상계가 포함되어 있는지는 인간이 체크해야 합니다. 테스트의 질은 안전망의 강도 그 자체입니다.

함정 6: 철수 기준을 정해두지 않는 것

"2번 고쳐도 테스트가 안정되지 않는다", "차분 (Diff)이 범위를 계속 벗어난다"면, 일단 AI로부터 손을 떼고 스스로 작게 작성하십시오. 되돌아갈 기준을 미리 정해두면 늪에 빠지지 않습니다.

이러한 점들을 바탕으로, "누가 무엇을 담당할 것인가"를 한 장으로 정리하면 다음과 같습니다.

페이즈인간이 설계·판단한다AI에게 맡긴다
이해 (Step 0)이 함수가 업무적으로 무엇을 보장해야 하는가현재 코드의 동작을 언어화한다
...

나란히 놓고 보면 알 수 있듯이, AI에게 맡기는 것은 "손을 움직이는 작업"이며, "무엇을 지킬 것인가·어디서 멈출 것인가"라는 판단은 전부 인간이 쥐고 있어야 합니다. 이 균형이 무너지면 빠르지만 망가지는 개발로 되돌아가게 됩니다. 반대로 이 부분만 확실히 잡고 있다면, AI에게 계속해서 손을 움직이게 해도 두렵지 않습니다. 역할 분담이란 결국 "판단은 놓지 않는다"라는 한 마디로 요약될지도 모릅니다.

여기까지 읽어주셔서 감사합니다.

AI에게 수정을 맡기면 망가진다는 이야기로 시작했지만, 도달한 결론은 매우 심플했습니다. 수정하기 전에, 현재의 동작을 그대로 옮겨 담은 그물을 쳐두는 것. 그것만으로도 AI가 아무리 손을 움직여도, 망가졌을 때 반드시 알아챌 수 있게 됩니다.

저는 항상 스스로에게 하나의 질문을 던지곤 합니다. "내일의 내가, 오늘의 나에게 고맙다고 말해줄까?"라고 말이죠. 오늘 귀찮아서 안전망을 치지 않고 AI에게 통째로 맡겨버린 코드는, 아마 내일의 내가 울면서 디버깅하게 될 것입니다. 하지만 오늘 조금 수고스럽더라도 사양화 테스트 (Specification Test)를 하나 작성해 둔다면, 내일의 나는 안심하고 그 코드를 수정할 수 있습니다. 안전망은 미래의 나에게 주는 선물인 셈입니다. 이것은 자신을 책망하는 이야기가 아니라, 내일의 나를 배려하는 이야기입니다.

게다가 이것이 "속도"를 포기하는 이야기도 아니라고 생각합니다. 오래 사용되는 앱이나 SaaS는 결국 몇 년 동안 몇 번이고 계속해서 개수 (Refactoring)되는 법입니다. 그때 "망가뜨리지 않고 변경할 수 있다"는 사실 자체가 프로덕트의 자산 가치가 됩니다. 깔끔하게 정리되어 안심하고 만질 수 있는 코드는 그 자체로 가치가 쌓여갑니다. 속도와 안심은 트레이드오프 (Trade-off) 관계가 아니라, 안전망을 사이에 둠으로써 양립할 수 있다고 느낍니다.

처음부터 전부 다 하려고 하지 않아도 괜찮습니다. 우선 함수 하나부터 시작하세요. 가장 무섭고, 가장 건드리기 싫은 함수에 사양화 테스트를 딱 하나만 쳐보십시오. 그것만으로도 내일의 당신은 "고마워"라고 말해줄 것입니다.

작게, 하지만 확실하게. 오늘도 수고하셨습니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0