
AI 코드 리뷰의 지적이 오탐인지 확인하려면 지적한 코드 위치가 맞는지, 실패 조건이 실제 실행 경로에서 성립하는지, 그 조건을 넣은 테스트에서 문제가 재현되는지를 차례로 확인해야 합니다. AI의 설명이 그럴듯하거나 기존 테스트가 통과한다는 이유만으로 결론을 내리지 말고, 주장과 관찰을 연결하는 증거를 남기세요.
경고를 검증 가능한 실패 가설로 바꾸기
“예외가 발생할 수 있다”라는 표현만으로는 무엇을 확인해야 하는지 알기 어렵습니다. 먼저 AI의 지적을 다음 네 항목으로 정리합니다.
- 위치: 어느 파일의 어떤 함수와 표현식인가?
- 조건: 어떤 입력이나 상태에서 문제가 발생한다는 주장인가?
- 경로: 어떤 진입점과 호출 순서를 거쳐 해당 위치에 도달하는가?
- 위반: 기대 동작과 실제 동작이 어떻게 달라진다는 것인가?
예를 들어 “평균 계산이 위험하다”보다 “빈 목록으로 summarize()를 호출하면 mean()의 나눗셈에 도달해 ZeroDivisionError가 발생한다”가 검증하기 좋습니다. 실패 조건이나 호출 경로를 제시하지 못하는 경고는 아직 확인되지 않은 가설입니다.
동작의 기준도 먼저 정해야 합니다. 빈 입력을 거절하는 것이 요구사항인지, 결과 없음으로 처리하는 것이 요구사항인지에 따라 같은 코드의 판정이 달라집니다. 테스트는 현재 구현을 그대로 옮기는 대신 문서, 호출자 계약, 합의된 요구사항을 기준으로 작성합니다.
코드 위치에서 진입점까지 실행 경로 확인하기
- 검토 대상 리비전을 맞춥니다. 리뷰가 가리키는 코드와 현재 체크아웃한 코드가 같은지 확인합니다. 줄 번호가 달라졌다면 함수명과 표현식으로 위치를 다시 찾습니다.
- 문제 연산의 실행 조건을 읽습니다. 앞선 조건문, 조기 반환, 예외 처리, 값 변환이 실패 조건을 막는지 확인합니다.
- 호출자를 따라갑니다. 외부 요청, 작업 실행기, 이벤트 처리기 등 실제 진입점에서 문제 함수까지 도달하는 경로를 찾습니다. 함수 하나만 보고 전체 경로를 추정하지 않습니다.
- 다른 경로도 확인합니다. 공통 검증을 거치지 않는 호출자나 별도 진입점이 있는지 살핍니다. 정적 검색만으로 찾기 어려운 동적 호출이나 플러그인 연결이 있다면 확인 범위에 명시합니다.
이 과정에서 중요한 것은 “검증 코드가 있다”는 사실보다 문제가 제기된 경로가 반드시 그 검증을 거치는가입니다. 특정 API의 입력 검증이 내부 작업이나 직접 호출까지 보호한다고 가정하면 실제 결함을 오탐으로 처리할 수 있습니다.
예제: 빈 목록이 나눗셈에 도달한다는 경고 검증하기
다음 예제의 계약은 “summarize()는 숫자 목록을 받고, 빈 목록이면 None을 반환한다”입니다. mean()은 비어 있지 않은 목록을 받는 내부 계산 함수라고 가정합니다.
def mean(values):
return sum(values) / len(values)
def summarize(values):
if not values:
return None
return mean(values)
AI가 “summarize([])는 mean()에서 0으로 나누므로 예외가 발생한다”고 지적했다고 해봅시다. 나눗셈 위치는 맞지만, 빈 목록에서는 조기 반환하므로 해당 연산에 도달하지 않습니다. 이 판단을 테스트로 확인합니다.
Python 3가 설치된 환경에서 위 코드를 stats.py로 저장하고, 같은 디렉터리에 다음 test_stats.py를 둡니다. 테스트에는 표준 라이브러리의 unittest만 사용합니다.
import unittest
from unittest.mock import patch
from stats import mean, summarize
class SummaryTests(unittest.TestCase):
def test_empty_input_returns_none_before_mean(self):
with patch("stats.mean") as calculate:
self.assertIsNone(summarize([]))
calculate.assert_not_called()
def test_nonempty_input_returns_average(self):
self.assertEqual(summarize([2, 4]), 3.0)
def test_direct_empty_mean_raises(self):
with self.assertRaises(ZeroDivisionError):
mean([])
if __name__ == "__main__":
unittest.main()
python -m unittest -v test_stats提示したコードのままなら、三つのテストが成功する想定です。 첫 번째 테스트는 빈 입력의 반환값과 mean()을 호출하지 않는 경로를 함께 확인합니다. 두 번째는 정상 입력의 계산을 확인하고, 세 번째는 mean()을 직접 호출하면 실패 조건이 실제로 존재한다는 점을 보여 줍니다. 세 번째 테스트는 예상한 예외가 발생해야 통과합니다.
따라서 “summarize([])에서 0으로 나눈다”는 지적은 이 코드와 계약의 범위에서는 오탐입니다. 반면 mean()이 외부에서 직접 사용할 수 있는 함수이거나 다른 호출자가 빈 목록을 넘긴다면, 그 경로에 대한 지적은 별도로 검토해야 합니다.
테스트가 경고를 실제로 구분하는지 확인하기
테스트가 통과했다는 사실만으로 충분하지 않을 때는, 격리된 작업 사본에서 보호 조건을 잠시 제거하는 대조 실험을 할 수 있습니다. 예제에서는 summarize()의 빈 목록 검사와 조기 반환을 제거하면 첫 번째 테스트가 실패해야 합니다. 이때 mock으로 대체한 mean()이 호출되므로 경로 검증도 깨집니다.
보호 조건을 제거해도 테스트가 계속 통과한다면 다른 모듈을 불러오고 있거나, 대상 분기를 실행하지 않거나, mock이 문제 동작을 가리고 있을 가능성을 확인해야 합니다. 실험 뒤에는 변경을 되돌리고 원래 코드에서 다시 확인합니다.
이 예제의 mock은 호출 여부를 확인하는 용도입니다. 데이터베이스 오류나 네트워크 시간 초과처럼 실제 의존성과의 상호작용이 핵심인 경고라면, 그 동작을 mock으로 없앤 테스트만으로는 판정할 수 없습니다. 필요한 수준의 통합 테스트나 통제된 실패 주입을 추가해야 합니다.
오탐·유효한 지적·판정 보류를 구분해 기록하기
| 관찰 | 판정 | 남길 근거 |
|---|---|---|
| 계약상 허용된 입력으로 실제 경로에서 요구사항 위반이 재현됨 | 유효한 지적 | 진입점, 입력, 실패 위치, 재현 테스트 |
| 주장한 조건이 해당 경로에서 차단됨 | 검증 범위 내 오탐 | 보호 조건 위치, 호출 경로, 경계 입력 테스트 |
| 외부 설정이나 동시 실행 조건을 확인하지 못함 | 판정 보류 | 확인한 범위, 남은 가정, 추가 검증 방법 |
예제의 리뷰 답변은 다음처럼 작성할 수 있습니다.
판정: summarize([])의 0 나누기 경고는 검증 범위 내 오탐.
근거: 빈 목록은 summarize()의 조기 반환에서 처리되어 mean()에 도달하지 않음.
검증: 빈 입력에서 None 반환 및 mean 미호출 확인, 정상 입력 계산 확인.
범위: summarize()를 통한 호출. mean()의 직접 호출은 별도 계약 검토 필요.
재현에 실패한 것과 오탐을 입증한 것은 다릅니다. 특히 경쟁 상태, 특정 권한, 운영 설정에 의존하는 문제는 테스트에서 조건을 만들지 못했을 수 있습니다. AI에 다시 물을 때도 “정말 맞는가?” 대신 “이 보호 조건을 통과하면서 실패하는 구체적 입력과 호출 경로를 제시하라”고 요청하면 남은 가정을 좁히기 좋습니다.
리뷰를 닫을 때는 판정만 남기지 말고, 코드 위치·도달 조건·테스트를 함께 남기세요. 이후 호출 경로나 보호 조건이 바뀌었을 때도 기존 판단이 여전히 유효한지 다시 확인할 수 있습니다.