AI가 코드를 고쳤을 때 다른 기능도 그대로인지 확인하기
AI가 코드 오류를 고쳤다고 해도 다른 기능이 그대로인지는 다시 확인해야 합니다. 문제가 난 입력 하나에서 맞는 값이 나와도, 원래 잘 처리하던 입력이나 오류 검사까지 바뀌었을 수 있습니다.
이 글은 Python을 사용하는 독자를 위한 실습입니다. 가상 배송비 계산 함수에서 바뀐 줄을 비교하고, 정상 입력과 잘못된 입력을 모두 넣어 이전 기능이 유지되는지 살펴봅니다.
수정 전 함수, 작은 수정본, 지나치게 넓은 수정 후보는 모두 직접 작성했습니다. 실제 AI 응답을 수집하거나 어떤 모델의 코드 품질을 측정한 기록이 아닙니다. 아래 프로그램은 합성 숫자를 계산하고 자기 파일의 함수 소스를 비교합니다. 주문 시스템, 파일 삭제, 셸 명령, 네트워크는 사용하지 않습니다.

고칠 요구 하나와 유지할 계약을 먼저 적기
가상 정책은 상품 금액이 50,000 이상이면 기본 배송비가 0이고, 그보다 작으면 2,500입니다. island 지역은 금액과 관계없이 추가비 3,000을 더합니다. 실제 쇼핑몰의 요금 규칙이 아닙니다.
이번 오류는 정확히 50,000일 때 기본비가 2,500으로 계산되는 것입니다. 수정 범위는 임계값 비교 연산자입니다. 입력을 자동 변환하거나 지역 정책을 바꾸는 요구는 없습니다.
| 계약 항목 | 고정 규칙 | 변경 여부 |
|---|---|---|
| subtotal | bool을 제외한 정수, 0 이상 | 유지 |
| region | mainland 또는 island | 유지 |
| 기본비 | 50,000 미만 2,500, 이상 0 | 경계 오류 수정 |
| island 추가비 | 항상 3,000 | 유지 |
| 잘못된 타입 | TypeError | 유지 |
| 음수·미등록 지역 | ValueError | 유지 |
| 결과 | 정수 배송비, 외부 작업 없음 | 유지 |
bool을 제외하는 것은 이 예제의 입력 계약입니다. Python에서 True가 숫자와 관계있다는 이유로 상품 금액으로 받아들이지 않도록 type(subtotal) is int로 검사합니다. 기존 사용자 코드가 다른 자료형을 지원한다면 이 계약을 그대로 가져오는 대신 원래 규칙을 먼저 확인해야 합니다.
실패하는 입력과 기대값을 수정 전에 만들기
아래 일곱 정상값을 사람이 정책에서 계산합니다. 후보 함수의 출력으로 기대값을 만드는 방식은 사용하지 않습니다. 금액·지역·값을 한 묶음으로 기록해야 동일한 금액의 두 지역을 혼동하지 않습니다.
| 금액 | 지역 | 기대 배송비 | 수정 전 실제값 |
|---|---|---|---|
| 0 | mainland | 2,500 | 2,500 |
| 49,999 | mainland | 2,500 | 2,500 |
| 50,000 | mainland | 0 | 2,500 |
| 50,001 | mainland | 0 | 0 |
| 49,999 | island | 5,500 | 5,500 |
| 50,000 | island | 3,000 | 5,500 |
| 80,000 | island | 3,000 | 3,000 |
여기에 True, None, 문자열 "50000"의 TypeError 세 건, 음수와 unknown 지역의 ValueError 두 건을 더해 열두 사례로 고정합니다. 실제 실행에서 수정 전 함수는 두 경계 사례만 틀렸습니다. 경계 바로 아래·정확히 경계·바로 위를 나누어 보면 <와 <=의 차이가 드러납니다.
Python unittest 공식 문서는 기대값 비교와 기대 예외 검사를 구분해 설명합니다. 본문은 세 후보의 실패 목록을 한 번에 보이도록 작은 직접 검사 함수를 사용합니다. 반환값만 확인하고 오류가 사라진 입력 검사는 빼지 않는다는 점이 중요합니다.
한 줄 diff에서 실제 변경 범위를 읽기
수정 전후의 함수 이름은 비교할 때 shipping_fee로 맞추고, 함수 본문을 unified diff로 계산했습니다. 실제 diff의 핵심 변경은 다음 두 줄입니다.
- base = 2500 if subtotal <= 50000 else 0
+ base = 2500 if subtotal < 50000 else 0
마이너스는 빠진 줄, 플러스는 추가된 줄입니다. 두 줄은 수정 두 건이 아니라 한 줄을 교체한 기록입니다. diff 위아래 문맥에서 타입 검사·음수와 지역 검사·추가비 반환이 그대로 남았는지 읽습니다. Python difflib 문서는 이런 변경 목록을 만드는 동작을 설명합니다.
작은 diff는 읽을 범위를 줄여 줍니다. 한 글자가 바뀌어도 단위·권한·결제 조건이 뒤집힐 수 있으므로 줄 수만으로 안전하다고 승인하지 않습니다. 반대로 여러 줄이 꼭 필요한 수정이라면 작은 diff라는 이유로 필요한 검사를 빼지 않습니다. 요구와 각 변경 줄의 관계를 설명할 수 있는지가 기준입니다.
경계 한 건은 맞지만 기존 동작을 망가뜨리는 후보
다음은 간단해 보이지만 요청 범위를 넓힌 합성 후보입니다.
def overbroad_fee(subtotal, region):
subtotal = int(subtotal or 0)
if subtotal >= 50000:
return 0
return 2500
50,000/mainland는 0이 되므로 원래 오류를 하나만 확인하면 통과로 보입니다. 하지만 island 추가비가 사라지고, 문자열·None·True가 조용히 금액으로 바뀌며 음수와 미등록 지역도 거부하지 않습니다. 실제 열두 사례에서 이 후보는 여덟 건을 실패했습니다.
None을 0으로 해석하는 것이 편리해 보여도 “입력 누락은 오류”라는 기존 계약을 바꾸는 결정입니다. 이처럼 AI 수정안에 리팩터링이나 자동 정리 단계가 함께 들어오면 요청한 오류 수정과 분리해 검토합니다. 이번 실습에서는 넓은 후보를 채택하지 않습니다.
세 후보와 열두 사례를 실행하는 전체 코드
다음 코드를 UTF-8 파일 fee_patch_checks.py로 저장합니다. Python 3의 표준 라이브러리만 필요합니다. 직접 작성한 세 함수만 실행하며 외부에서 받은 코드 문자열을 eval이나 exec로 실행하지 않습니다.
import difflib
import inspect
import json
# Synthetic shipping policy. No files, shell commands, network or real orders.
def before_fee(subtotal, region):
if type(subtotal) is not int:
raise TypeError("subtotal must be an integer")
if subtotal < 0 or region not in ("mainland", "island"):
raise ValueError("invalid subtotal or region")
base = 2500 if subtotal <= 50000 else 0
return base + (3000 if region == "island" else 0)
def corrected_fee(subtotal, region):
if type(subtotal) is not int:
raise TypeError("subtotal must be an integer")
if subtotal < 0 or region not in ("mainland", "island"):
raise ValueError("invalid subtotal or region")
base = 2500 if subtotal < 50000 else 0
return base + (3000 if region == "island" else 0)
def overbroad_fee(subtotal, region):
subtotal = int(subtotal or 0)
if subtotal >= 50000:
return 0
return 2500
CASES = [
("T01_zero", 0, "mainland", ("value", 2500)),
("T02_below", 49999, "mainland", ("value", 2500)),
("T03_boundary", 50000, "mainland", ("value", 0)),
("T04_above", 50001, "mainland", ("value", 0)),
("T05_island_below", 49999, "island", ("value", 5500)),
("T06_island_boundary", 50000, "island", ("value", 3000)),
("T07_island_above", 80000, "island", ("value", 3000)),
("T08_bool", True, "mainland", ("error", "TypeError")),
("T09_missing", None, "mainland", ("error", "TypeError")),
("T10_string", "50000", "mainland", ("error", "TypeError")),
("T11_negative", -1, "mainland", ("error", "ValueError")),
("T12_unknown_region", 100, "unknown", ("error", "ValueError")),
]
def outcome(function, subtotal, region):
try:
value = function(subtotal, region)
if type(value) is not int:
return ("invalid_return_type", type(value).__name__)
return ("value", value)
except (TypeError, ValueError) as error:
return ("error", type(error).__name__)
def evaluate(function):
rows = [{"id": cid, "expected": expected,
"actual": outcome(function, subtotal, region)}
for cid, subtotal, region, expected in CASES]
return rows, [row["id"] for row in rows if row["expected"] != row["actual"]]
def main():
results = {}
for name, function in [("before", before_fee), ("corrected", corrected_fee),
("overbroad", overbroad_fee)]:
rows, failures = evaluate(function)
results[name] = {"cases": len(rows), "failures": failures, "rows": rows}
if results["before"]["failures"] != ["T03_boundary", "T06_island_boundary"]:
raise AssertionError("original counterexamples changed")
if results["corrected"]["failures"]:
raise AssertionError(results["corrected"])
if len(results["overbroad"]["failures"]) != 8:
raise AssertionError("overbroad counterexamples changed")
old = inspect.getsource(before_fee).replace("before_fee", "shipping_fee")
new = inspect.getsource(corrected_fee).replace("corrected_fee", "shipping_fee")
delta = "".join(difflib.unified_diff(old.splitlines(True), new.splitlines(True),
fromfile="before.py", tofile="after.py"))
changed = [line for line in delta.splitlines()
if line.startswith(("+", "-")) and not line.startswith(("+++", "---"))]
if len(changed) != 2:
raise AssertionError("patch scope changed")
print(json.dumps({"results": results, "unified_diff": delta,
"changed_removed_lines": 1, "changed_added_lines": 1,
"live_ai_calls": 0, "result": "PASS"}, ensure_ascii=False))
if __name__ == "__main__":
main()
python fee_patch_checks.py
inspect.getsource는 이 파일 안의 합성 함수 소스를 읽어 diff를 만들기 위한 것입니다. 실제 프로젝트 파일을 탐색하거나 실행시키는 단계가 아닙니다. 본문의 함수 이름을 임의로 바꾸면 소스 비교용 이름 치환 부분도 함께 확인해야 합니다.
Linux Python 3.12.14에서 콘솔 JSON을 확인한 결과는 다음과 같습니다. cases는 각 후보를 검사한 사례 수이고, failures는 계약과 다른 사례 ID입니다.
before: cases=12, failures=[T03_boundary, T06_island_boundary]
corrected: cases=12, failures=[]
overbroad: cases=12, failures=[T05_island_below, T06_island_boundary,
T07_island_above, T08_bool, T09_missing, T10_string, T11_negative,
T12_unknown_region]
diff: removed=1 line, added=1 line
live_ai_calls=0, result=PASS
이는 전체 JSON의 요약이며 실제 출력에는 열두 행의 expected와 actual, 전체 diff도 들어 있습니다. 프로그램의 PASS는 잘못된 두 후보까지 정책을 지켰다는 뜻이 아닙니다. 수정본의 열두 일치와 수정 전·넓은 후보의 예상했던 실패 목록이 그대로 재현됐다는 뜻입니다. 실패를 숨겨 모두 통과로 만드는 채점 방식을 쓰지 않습니다.
값이 같아도 반환 자료형이 다르면 거절하기
정수 배송비 계약은 값과 자료형을 함께 확인해야 합니다. Python에서는 2500 == 2500.0, 0 == False가 참이라 값만 비교하는 기존 검사로는 이 두 변경을 놓쳤습니다. 위 outcome은 반환값을 받은 직후 type(value) is int를 확인하도록 보완했습니다. 잘못된 반환형은 예외 발생과 구분해 invalid_return_type으로 기록합니다. 자세한 비교 규칙은 Python 숫자형 비교 문서에서 확인할 수 있습니다.
같은 파일 맨 아래에 다음 코드를 덧붙여 두 반례를 재현할 수 있습니다. 첫 후보는 정상 반환값을 모두 실수로 바꾸고, 둘째는 무료 배송의 정수 0만 불리언 False로 바꿉니다. 예외를 발생시키는 입력은 원래 함수의 예외를 그대로 유지합니다.
def float_result_fee(subtotal, region):
return float(corrected_fee(subtotal, region))
def false_result_fee(subtotal, region):
value = corrected_fee(subtotal, region)
return False if value == 0 else value
float_failures = evaluate(float_result_fee)[1]
false_failures = evaluate(false_result_fee)[1]
assert len(float_failures) == 7
assert false_failures == ["T03_boundary", "T04_above"]
print("float_return_rejected", len(float_failures))
print("false_return_rejected", false_failures)
float_return_rejected 7
false_return_rejected ['T03_boundary', 'T04_above']
2026년 10월 8일 KST에 위 두 반례와 기존 열두 사례를 다시 실행했습니다. 기존 세 후보의 실패 목록과 한 줄 diff는 그대로이며 새 반환형 반례는 거절됩니다. 오류를 없애려고 검사에서 int(value)로 강제 변환하지 말고, 후보 함수가 계약대로 정수를 반환하도록 고친 뒤 전체 사례를 재검사합니다. 실제 AI·배송 시스템은 실행하지 않았습니다.
AI에 요청할 때 오류 수정과 추가 제안을 분리하기
아래 요청문은 예제 함수와 계약을 함께 붙일 때 쓸 수 있습니다. 이번 제작에서는 실제 모델에 보내지 않았습니다.
가상 배송비 함수의 50,000 경계 오류만 수정해 주세요.
금액 50,000 이상에서는 기본비 0이며 island 추가비 3,000은 항상 유지합니다.
bool을 제외한 정수만 허용하고 음수·미등록 지역의 예외를 유지합니다.
입력 자동 변환, 함수 이름 변경, 외부 의존성, 파일·네트워크 작업은 추가하지 마세요.
출력: 최소 diff, 각 변경 줄의 이유, 열두 고정 사례의 예상 영향.
예상 결과와 실제 실행 결과를 구분하세요. 실행하지 않았으면 미실행으로 적으세요.
범위를 넓히는 개선안은 수정 코드에 섞지 말고 별도 제안으로 적으세요.
AI의 “테스트 통과” 문구만으로 채택하지 않습니다. 어느 파일·환경·명령을 실제 실행했고 어떤 결과가 나왔는지 확인합니다. 실제 실행 도구가 없는 대화에서 적은 예상값은 실행 기록이 아닙니다. 다른 코드가 함께 들어왔다면 import와 실행부까지 읽고 허용 범위를 벗어난 작업이 없는지 봅니다.
재검사 기록과 복구 기준
요구 ID와 수정 대상 함수:
기존 계약 버전:
실패 재현 입력과 독립 기대값:
수정 전 출력:
검토할 diff와 변경 이유:
요구 밖의 변경: 없음 / 별도 검토 필요
새 수정본 버전:
실행 환경·명령:
전체 정상값·오류 사례의 실제 결과:
미실행 검사와 이유:
판정: 계약 일치 / 수정 보류 / 추가 정책 결정 필요
검토자·검토 시각:
넓은 후보를 적용했다가 island 테스트가 실패하면 기대값을 0으로 고치지 않습니다. 원래 추가비 계약을 다시 읽고 최초 코드를 보존한 사본에서 최소 변경본을 준비합니다. 고친 뒤에는 경계 두 사례와 기존 열 사례를 모두 재실행합니다. 오류 종류를 잘못 반환하는 입력도 회귀로 기록합니다.
실제 코드베이스에서는 호출자·반환 단위·기존 테스트·다른 모듈의 영향까지 확인해야 합니다. 이 작은 예제는 동시성, 성능, 데이터베이스, 실제 결제나 배송 시스템을 시험하지 않습니다. 저장소 전체의 보안성을 증명하거나 운영에 바로 배포하는 절차도 아닙니다.
확인한 날짜와 다음 적용 범위
공식 문서 확인·작성·로컬 실행일은 2026-10-04 KST입니다. 실행 Python은 3.12.14이고 확인한 3.12 문서 페이지는 3.12.15 표기였습니다. 실제 AI 호출·실제 주문 처리는 없습니다.
원문·후보·승인값을 구분하는 기록은 엑셀 AI 결과 승인대장과 연결할 수 있습니다. 다만 이 글의 산출물은 점수표가 아니라 한 줄의 diff와 열두 반환값·예외 대조입니다. 고친 요구와 유지한 계약을 실행 기록으로 함께 설명할 수 있을 때 이 합성 함수의 수정 검토를 마칩니다.