AI 개발 · 기본
LLM·AI 코딩 IDE 직무 입문
맡기고 검수한다 - diff·경계 사례·실행 증거
평균 계산 예제의 결함을 찾아 요구에 맞게 고치고 독립된 단위 검사로 결과를 검증한다.
개발자KR · 원고 갱신
이 장에서 배우는 것
평균 계산 예제의 결함을 찾아 요구에 맞게 고치고 독립된 단위 검사로 결과를 검증한다.
동작하는 예제에서 결함 세 개 찾기
다음 코드는 입력 목록의 평균을 계산한다. def average_days(values): return sum(int(v) for v in values) / len(values). 값이 2와 4라면 3.0이 나온다. 그러나 빈 목록이면 0으로 나누고, -1은 유효 값처럼 평균에 들어가며, 숫자가 아닌 문자열은 호출자에게 정한 오류 메시지 없이 예외를 낸다. 정상 사례 하나가 통과했다고 요구가 충족된 것은 아니다.
9장의 명세에 따라 평균 없음은 함수에서 None으로 표현하고 화면 표시를 다른 함수에서 처리한다. 잘못된 값은 ValueError로 거부한다. 허용된 입력은 각 원소가 0 이상의 정수를 나타내는 ASCII 숫자 문자열인 목록으로 좁힌다. bool이나 실수 같은 입력까지 조용히 받아들이지 않는다. 함수가 지원하는 입력을 설명해야 검사도 분명해진다.
검사의 기대값은 구현 밖에서 정한다
완성 코드는 교재에 동봉한 average_days.py이고 검사는 test_average_days.py다. 빈 목록, 0, 정상 두 값, 음수, 문자, 공백, bool을 포함한다. 이 사례들은 명세를 서로 다른 방향에서 확인한다. 구현이 돌려준 값을 그대로 기대값으로 복사한 검사는 결과의 정당성을 독립적으로 확인하지 못한다.
검사에 실패하면 먼저 입력·기대값·실제값을 읽는다. 실패한 기대값을 무조건 구현의 출력으로 바꾸지 않는다. 기대 동작을 바꾸기로 결정했다면 코드뿐 아니라 명세와 독자에게 보이는 설명도 함께 바꾼다. 오류 메시지와 자료형처럼 외부 계약에 영향을 주는 변경은 별도로 검토한다.
완료 보고에는 범위와 한계가 있어야 한다
교재의 실습은 함수 단위의 검사다. CSV의 열 읽기, 파일 권한, 화면 렌더링을 검증한 것은 아니다. 보고서에는 어떤 명령을 어느 폴더에서 실행했고 몇 사례가 통과했는지 적는다. 실행하지 못한 부분은 확인하지 못했다고 쓴다. 단위 검사 통과를 서비스 전체 배포 가능이라는 말로 확대하지 않는다.
diff를 읽을 때는 함수 변경뿐 아니라 삭제된 검사와 새 의존성을 본다. 작업 전후 체크포인트가 있다면 해당 변경만 되돌릴 수 있어야 한다. 다른 사람이 이미 수정한 파일을 통째로 되돌리는 방식은 복구가 아니라 별도 작업을 잃는 일이 될 수 있다. 작은 단위로 검토하는 습관은 AI 사용 여부와 관계없이 필요하다.
| 항목 | 관찰·기대값 | 판단할 때 주의 |
|---|---|---|
| 빈 목록 | None | 0으로 나누기 방지 |
| 정수 문자열 | 평균 숫자 | 0과 여러 값도 확인 |
| 음수·문자·다른 타입 | ValueError | 조용한 변환·누락 금지 |
완성 코드
average_days.py와 test_average_days.py를 같은 폴더에 저장한다. 이 예제는 CSV 파일 읽기 전 단계의 함수 계약만 검사한다.
average_days.py
"""교재 예제: 0 이상 정수 문자열 목록의 평균. 외부 파일·통신 없음."""
import re
def average_days(values):
if not isinstance(values, list):
raise ValueError("목록이 필요합니다")
numbers = []
for value in values:
if not isinstance(value, str) or re.fullmatch(r"[0-9]+", value) is None:
raise ValueError("days는 0 이상의 정수 문자열이어야 합니다")
numbers.append(int(value))
return sum(numbers) / len(numbers) if numbers else None
def display_average(value):
return "평균 없음" if value is None else f"평균 {value:.1f}일"
test_average_days.py
import unittest
from average_days import average_days, display_average
class AverageDaysTest(unittest.TestCase):
def test_empty(self):
self.assertIsNone(average_days([]))
self.assertEqual("평균 없음", display_average(average_days([])))
def test_zero_is_a_real_average(self):
self.assertEqual(0.0, average_days(["0"]))
self.assertEqual("평균 0.0일", display_average(average_days(["0"])))
def test_two_values(self):
self.assertEqual(3.0, average_days(["2", "4"]))
def test_invalid_values(self):
for value in ["-1", "abc", "", " 2", "2.0", True, 2, "٢"]:
with self.subTest(value=value), self.assertRaises(ValueError):
average_days([value])
def test_container(self):
for value in [None, "2", ("2",)]:
with self.subTest(value=value), self.assertRaises(ValueError):
average_days(value)
if __name__ == "__main__":
unittest.main()
실행 명령
python3 -m unittest -v test_average_days
실행 결과
동봉 검사에는 테스트 메서드 5개가 있다. 정상 실행하면 마지막에 다음 두 결과를 확인할 수 있다. 실행 시간과 각 subTest의 세부 표시는 환경에 따라 다르다. 이 결과는 함수 단위 계약에 대한 것이며 CSV 파일 읽기 전체를 검증하지 않는다.
Ran 5 tests
OK
선택 실습 - CSV 파일까지 연결하기
함수 단위 검사를 마쳤다면 같은 폴더에 loan_report.py를 추가한다. 헤더는 book_id,days 순서로 고정하고, 잘못된 숫자나 열 수는 오류로 거부한다. 빈 파일과 제목만 있는 파일은 건수 0·평균 없음이다. CSV 전체를 읽으므로 작은 더미 파일로 연습한다. 실제 서비스의 대용량 파일 처리는 이 예제 범위 밖이다.
loan_report.py
"""더미 대출 CSV의 건수와 평균. 파일 덮어쓰기·네트워크 기능 없음."""
import argparse
import csv
import io
from pathlib import Path
from average_days import average_days, display_average
def summarize_csv(text):
if not isinstance(text, str):
raise ValueError("CSV 텍스트가 필요합니다")
try:
rows = csv.DictReader(io.StringIO(text, newline=""), strict=True)
if rows.fieldnames is None:
return 0, None
if rows.fieldnames != ["book_id", "days"]:
raise ValueError("헤더는 book_id,days 순서여야 합니다")
days = []
for row in rows:
if None in row or row.get("days") is None:
raise ValueError("행의 열 수가 헤더와 다릅니다")
days.append(row["days"])
return len(days), average_days(days)
except csv.Error as error:
raise ValueError("올바른 CSV 형식이 아닙니다") from error
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("csv_file", type=Path)
args = parser.parse_args(argv)
try:
count, average = summarize_csv(args.csv_file.read_text(encoding="utf-8"))
except (OSError, UnicodeError, ValueError) as error:
parser.error(str(error))
print(f"건수 {count}")
print(display_average(average))
if __name__ == "__main__":
main()
loans.csv
book_id,days
B01,2
B02,4
실행과 기대 결과
python3 loan_report.py loans.csv
건수 2
평균 3.0일위 블록의 첫 줄만 실행 명령이고 나머지는 표준 출력이다. 입력 파일은 덮어쓰지 않는다. 출판 소스의 test_loan_report.py는 빈 입력·평균 0·잘못된 숫자·잘못된 헤더·열 수·파일 보존·CLI 오류 종료를 6개 테스트 메서드로 검사한다. python3 -m unittest -v test_loan_report로 실행한다.
test_loan_report.py
import contextlib
import io
from pathlib import Path
import tempfile
import unittest
from loan_report import main, summarize_csv
class LoanReportTest(unittest.TestCase):
def test_empty_and_header_only(self):
for text in ["", "book_id,days\n"]:
with self.subTest(text=text):
self.assertEqual((0, None), summarize_csv(text))
def test_zero_and_two_rows(self):
self.assertEqual((1, 0.0), summarize_csv("book_id,days\nB01,0\n"))
self.assertEqual((2, 3.0), summarize_csv("book_id,days\nB01,2\nB02,4\n"))
def test_invalid_days(self):
for value in ["", "-1", "abc", " 2", "2.0"]:
with self.subTest(value=value), self.assertRaises(ValueError):
summarize_csv("book_id,days\nB01," + value + "\n")
def test_wrong_headers_and_row_shape(self):
for text in ["days,book_id\n2,B01\n", "days,days\n2,4\n",
"book_id,days\nB01\n", "book_id,days\nB01,2,extra\n",
'book_id,days\nB01,"2\n']:
with self.subTest(text=text), self.assertRaises(ValueError):
summarize_csv(text)
def test_real_file_cli_does_not_modify_source(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "loans.csv"
source = "book_id,days\nB01,2\nB02,4\n"
path.write_text(source, encoding="utf-8")
output = io.StringIO()
with contextlib.redirect_stdout(output):
main([str(path)])
self.assertEqual("건수 2\n평균 3.0일\n", output.getvalue())
self.assertEqual(source, path.read_text(encoding="utf-8"))
self.assertEqual([path], list(Path(folder).iterdir()))
def test_cli_rejects_invalid_data(self):
with tempfile.TemporaryDirectory() as folder:
path = Path(folder) / "bad.csv"
path.write_text("book_id,days\nB01,-1\n", encoding="utf-8")
errors = io.StringIO()
with contextlib.redirect_stderr(errors), self.assertRaises(SystemExit) as result:
main([str(path)])
self.assertEqual(2, result.exception.code)
self.assertIn("0 이상의 정수 문자열", errors.getvalue())
if __name__ == "__main__":
unittest.main()
직접 해 보기
동봉한 두 파일을 같은 임시 폴더에 복사하고 python3 -m unittest -v test_average_days를 실행한다. 이어서 음수 검사 하나를 지운 수정안이 왜 품질 개선이 아닌지 설명한다. 실제 파일 경로는 출판 소스 scripts/ebook-llm-ide/examples다.
해설과 확인 기준
검사를 삭제해서 초록색 결과를 얻어도 음수 거부 규칙의 검증은 사라졌다. 평균 없음과 실제 평균 0도 구분해야 한다. 독자의 실행 결과는 환경과 명령까지 기록한다. 교재 검증 기록에는 단위 검사 결과가 들어가며 실제 CSV를 읽는 통합 검사는 별도라고 명시한다.
| 질문 | 완료 증거 |
|---|---|
| 핵심 구분을 설명할 수 있는가 | 명세와 변경 검토의 차이를 자기 말로 설명 |
| 실습의 판단 근거가 있는가 | 빈 목록·음수·문자 검사의 관찰 기록 |
| 확인하지 않은 부분을 구분했는가 | 출처 확인·실행 확인·미확인 상태를 구분 |
근거와 기준일
제품·자료 확인일: 2026-11-08. 설명 틀·대출 도구·연습·해설은 교재의 자체 구성이다. 제품별 세부 조건은 아래 공식 문서에서 해당 실행 방식과 계정 범위를 다시 확인한다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.