AI 업무 활용

AI가 만든 코드를 작은 데이터로 검증하기

손계산할 수 있는 네 행과 12개 단위 테스트로 집계 함수를 확인합니다. 정상 값뿐 아니라 빈 입력, 0, 소수, 잘못된 입력까지 검증합니다.

목차 보기

이런 분께AI에게 받은 Python 코드가 실행된다는 사실만으로 업무에 쓰기 망설여지는 입문자

준비사항
  • Python 3.12 이상을 준비하고 터미널에서 python --version으로 버전을 확인합니다.
  • 예제 ZIP을 새 폴더에 압축 해제합니다. 압축 파일 안에서 직접 실행하지 않습니다.
  • example.py가 보이는 폴더에서 터미널을 엽니다. Windows에서 python 명령이 없으면 py, macOS·Linux에서는 환경에 따라 python3를 사용합니다.
  • 외부 패키지 설치나 계정은 필요하지 않습니다. 동봉된 자료는 직접 작성한 합성 데이터입니다.

01실행 전에 정답부터 정하기

AI에게 받은 코드가 오류 없이 끝났다고 계산이 맞는 것은 아닙니다. 이 글에서는 실제 AI 서비스에 접속하지 않고, 독립적으로 작성한 집계 함수와 작은 합성 데이터를 사용해 검증 순서를 연습합니다. 같은 방법을 자신이 검토할 함수에 적용할 수 있습니다. API 키나 외부 패키지는 필요하지 않습니다.

  1. input.csv의 네 행을 읽고 수량×단가를 직접 계산합니다.
  2. expected.json을 열어 행 수 4, 수량 6, 금액 11101.50이 적혀 있는지 확인합니다.
  3. 아래 명령을 실행합니다. 먼저 12개 단위 테스트가 실행되고, 이어 CSV 집계와 기대값 비교가 진행됩니다.
  4. 모든 비교가 통과했을 때만 만들어지는 outputs/summary.json과 verification.txt를 확인합니다.
bash
python example.py

02네 행의 계산을 표로 대조하기

itemquantityunit_price행별 금액
노트22500.005000.00
31200.503601.50
노트12500.002500.00
샘플0999.990.00

행 수는 4이지만 수량 합계는 2 + 3 + 1 + 0 = 6입니다. 금액 합계는 5000.00 + 3601.50 + 2500.00 = 11101.50입니다. 수량이 0인 마지막 행도 입력 행 수에는 포함됩니다. 품목별로는 노트 수량 3·금액 7500.00, 펜 수량 3·금액 3601.50, 샘플 수량 0·금액 0.00입니다.

이처럼 합계만 비교하지 말고 행 수, 총수량, 품목별 값을 함께 봅니다. 노트처럼 같은 품목을 두 행으로 나눠 넣으면 묶어서 더하는 기능도 확인할 수 있습니다. 품목별 결과의 순서는 이름 정렬이며 판매순위나 입력 순서를 의미하지 않습니다.

03함수가 받아들일 입력 규칙 적기

항목허용 규칙거부 예시
item, quantity, unit_price 세 개열 누락 또는 추가 열
item앞뒤 공백 제거 후 비어 있지 않은 문자열빈 문자열, 공백뿐인 값
quantity0~1,000,000의 정수 문자열-1, 1.5, 1e2
unit_price0~1,000,000,000, 소수점 둘째 자리까지NaN, Infinity, 0.001, 1,000

금액은 가상의 동일 통화라는 가정을 둡니다. 수량과 단가의 음수를 허용하지 않으므로 반품을 표현하는 규칙은 포함되지 않습니다. 입력 규칙을 먼저 정하면 코드가 마음대로 0으로 바꾸거나 일부 행을 건너뛰는지 구별할 수 있습니다. 이 함수는 잘못된 행을 발견하면 ValueError로 중지합니다.

04집계와 테스트가 들어 있는 전체 코드

aggregate_sales는 입력 행을 변경하지 않고 결과만 반환합니다. 파일 읽기와 결과 쓰기는 main에 분리했습니다. 이렇게 하면 테스트에서 작은 딕셔너리를 직접 넣어 계산 부분만 확인할 수 있습니다. 각 기대값은 assertEqual로 비교하고, 거부해야 할 값은 assertRaises로 확인합니다.

example.py
"""집계 함수를 손계산 기대값과 unittest로 검증합니다. 외부 AI 호출은 없습니다."""

import argparse
import csv
import json
import re
import sys
import unittest
from decimal import Decimal
from pathlib import Path

BASE = Path(__file__).resolve().parent
OUTPUT = BASE / "outputs"
FIELDS = {"item", "quantity", "unit_price"}


def money(cents):
    return f"{cents // 100}.{cents % 100:02d}"


def aggregate_sales(rows):
    """문자열 딕셔너리 행을 검증하고 집계합니다. rows와 원본 파일을 변경하지 않습니다."""
    totals = {}
    row_count = total_quantity = total_cents = 0
    for number, row in enumerate(rows, start=1):
        if not isinstance(row, dict) or set(row) != FIELDS:
            raise ValueError(f"{number}행: item,quantity,unit_price 세 열이 필요합니다.")
        if any(not isinstance(value, str) for value in row.values()):
            raise ValueError(f"{number}행: 모든 값은 문자열이어야 합니다.")
        item, quantity_text, price_text = (row[key].strip() for key in ("item", "quantity", "unit_price"))
        if not item:
            raise ValueError(f"{number}행: 품목이 비어 있습니다.")
        if not re.fullmatch(r"[0-9]{1,7}", quantity_text):
            raise ValueError(f"{number}행: 수량은 0 이상의 정수여야 합니다.")
        quantity = int(quantity_text)
        if quantity > 1_000_000:
            raise ValueError(f"{number}행: 수량은 1,000,000 이하여야 합니다.")
        if not re.fullmatch(r"[0-9]{1,10}(?:\.[0-9]{1,2})?", price_text):
            raise ValueError(f"{number}행: 단가는 0 이상, 소수점 둘째 자리까지 입력하세요.")
        price = Decimal(price_text)  # 문자열에서 직접 생성해 float 오차를 피합니다.
        if price > Decimal("1000000000"):
            raise ValueError(f"{number}행: 단가는 1,000,000,000 이하여야 합니다.")
        cents = int(price * 100) * quantity
        bucket = totals.setdefault(item, {"quantity": 0, "cents": 0})
        bucket["quantity"] += quantity
        bucket["cents"] += cents
        total_quantity += quantity
        total_cents += cents
        row_count += 1
    return {
        "row_count": row_count,
        "quantity": total_quantity,
        "amount": money(total_cents),
        "by_item": [
            {"item": item, "quantity": totals[item]["quantity"], "amount": money(totals[item]["cents"])}
            for item in sorted(totals)
        ],
    }


def sale(item="펜", quantity="1", unit_price="1200.50"):
    return {"item": item, "quantity": quantity, "unit_price": unit_price}


class TestAggregate(unittest.TestCase):
    # 기대값은 검증 대상 함수로 만들지 않고 손계산한 상수로 적습니다.
    def test_empty_input(self):
        self.assertEqual(aggregate_sales([]), {"row_count": 0, "quantity": 0, "amount": "0.00", "by_item": []})

    def test_single_row(self):
        self.assertEqual(aggregate_sales([sale(quantity="3")])["amount"], "3601.50")

    def test_zero_quantity(self):
        result = aggregate_sales([sale(quantity="0")])
        self.assertEqual((result["row_count"], result["quantity"], result["amount"]), (1, 0, "0.00"))

    def test_repeated_item(self):
        result = aggregate_sales([sale(quantity="2"), sale(quantity="1")])
        self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 3, "amount": "3601.50"}])

    def test_decimal_addition(self):
        result = aggregate_sales([sale(unit_price="0.10"), sale(unit_price="0.20")])
        self.assertEqual(result["amount"], "0.30")

    def test_whitespace(self):
        result = aggregate_sales([sale(item=" 펜 ", quantity=" 1 ", unit_price=" 2.00 ")])
        self.assertEqual(result["by_item"], [{"item": "펜", "quantity": 1, "amount": "2.00"}])

    def test_bad_quantity(self):
        for value in ("-1", "1.5", "", "1000001", "1e2", "1"):
            with self.subTest(value=value), self.assertRaises(ValueError):
                aggregate_sales([sale(quantity=value)])

    def test_bad_price(self):
        for value in ("-1", "0.001", "NaN", "Infinity", "", "1,000", "1000000001"):
            with self.subTest(value=value), self.assertRaises(ValueError):
                aggregate_sales([sale(unit_price=value)])

    def test_blank_item(self):
        with self.assertRaises(ValueError):
            aggregate_sales([sale(item=" ")])

    def test_wrong_columns(self):
        for row in ({"item": "펜"}, {**sale(), "extra": "x"}):
            with self.subTest(row=row), self.assertRaises(ValueError):
                aggregate_sales([row])

    def test_non_string(self):
        with self.assertRaises(ValueError):
            aggregate_sales([sale(quantity=True)])

    def test_upper_boundary(self):
        self.assertEqual(
            aggregate_sales([sale(quantity="1000000", unit_price="1000000000")])["amount"],
            "1000000000000000.00",
        )


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--test", action="store_true", help="단위 테스트만 실행합니다.")
    args = parser.parse_args()
    # unittest의 명시적 비교는 python -O에서도 생략되지 않습니다.
    result = unittest.TextTestRunner(verbosity=2, stream=sys.stdout).run(
        unittest.defaultTestLoader.loadTestsFromTestCase(TestAggregate)
    )
    if not result.wasSuccessful():
        return 1
    if args.test:
        return 0
    if OUTPUT.exists() or OUTPUT.is_symlink():
        raise FileExistsError("outputs가 이미 있습니다. 기존 결과를 옮긴 뒤 실행하세요.")
    with (BASE / "input.csv").open("r", encoding="utf-8-sig", newline="") as stream:
        reader = csv.DictReader(stream, strict=True)
        if reader.fieldnames != ["item", "quantity", "unit_price"]:
            raise ValueError("input.csv의 열은 item,quantity,unit_price 순서여야 합니다.")
        actual = aggregate_sales(reader)
    expected = json.loads((BASE / "expected.json").read_text(encoding="utf-8"))
    if actual != expected:
        raise ValueError("예제 집계가 expected.json의 손계산 기대값과 다릅니다.")
    OUTPUT.mkdir()
    with (OUTPUT / "summary.json").open("x", encoding="utf-8") as stream:
        json.dump(actual, stream, ensure_ascii=False, indent=2)
        stream.write("\n")
    with (OUTPUT / "verification.txt").open("x", encoding="utf-8") as stream:
        stream.write(f"unittest: {result.testsRun}개 통과\n예제 기대값: 일치\n외부 AI API 호출: 없음\n")
    print(f"완료: {actual['row_count']}행, 수량 {actual['quantity']}, 금액 {actual['amount']}")
    print(OUTPUT)
    return 0


if __name__ == "__main__":
    try:
        raise SystemExit(main())
    except (OSError, ValueError, csv.Error) as error:
        print(f"중지: {error}", file=sys.stderr)
        raise SystemExit(2)

단가는 문자열에서 Decimal로 만들고 100을 곱해 정수 단위로 바꾼 뒤 합칩니다. 결과 금액도 두 자리 소수 형식의 문자열로 반환합니다. 이 방식은 예제의 두 자리 단가 규칙에 맞춘 선택이며 모든 수치 계산에 그대로 적용하는 만능 규칙은 아닙니다.

05테스트가 확인하는 정상·경계·오류 사례

분류내장 테스트확인 목적
정상한 행, 같은 품목 반복, 앞뒤 공백기본 계산과 묶음 처리
경계빈 입력, 수량 0, 허용 상한작거나 큰 값에서의 정의
소수0.10 + 0.20 = 0.30금액 표현과 덧셈
오류잘못된 수량·단가·빈 품목·열·타입잘못된 값을 조용히 통과시키지 않음

테스트 이름 옆의 ok와 마지막 ‘Ran 12 tests’, ‘OK’를 확인합니다. 그 뒤 출력되는 ‘완료: 4행, 수량 6, 금액 11101.50’까지 보여야 CSV 기대값 비교도 통과한 것입니다. 단위 테스트만 성공하고 expected.json과 다르면 기본 실행은 실패하며 새 결과를 저장하지 않습니다.

bash
python example.py --test

--test는 단위 테스트만 반복하며 outputs를 만들지 않습니다. 이미 결과 폴더가 있어도 사용할 수 있으므로 함수를 수정하면서 빠르게 확인하기 좋습니다. 반대로 기본 명령은 기존 outputs를 덮어쓰지 않고 중지합니다.

06실패하는 테스트를 한 번 만들어 보기

  1. 새 압축 해제 폴더에서 example.py 사본을 준비합니다. 처음 파일을 보관해 되돌릴 수 있게 합니다.
  2. 계산 줄 int(price * 100) * quantity에서 마지막 * quantity만 지워 수량을 무시하는 오류를 만들어 봅니다.
  3. python example.py --test를 실행해 한 행 수량 3과 반복 품목 테스트 등이 실패하는지 확인합니다.
  4. 원래 계산 줄을 복구하고 다시 --test를 실행해 12개가 모두 통과하는지 봅니다.

이 실험의 목적은 테스트가 오류를 발견하는지 확인하는 것입니다. 제작 과정에서도 같은 종류의 오류를 임시 복사본에 주입해 테스트가 실패하고 결과가 쓰이지 않는 것을 검증했습니다. 의미 있는 실패를 한 번 보면 ‘테스트 코드도 실행은 되지만 아무것도 검사하지 않는’ 상황을 피하기 쉽습니다.

07AI에게 받은 함수에 적용하는 순서

새 코드에서는 먼저 입력과 출력의 예시를 고정합니다. 원본 파일에 쓰는 부분이 있다면 사본을 대상으로 제한하고, 계산을 맡는 함수부터 분리하세요. 정상 데이터 한 묶음, 빈 데이터, 0, 반복 항목, 허용하지 않을 값을 각각 준비한 다음 기대값을 자신이 계산합니다.

  1. 함수의 업무 규칙을 문장으로 적습니다. 예: 반품은 거부할지 음수 금액으로 계산할지 결정합니다.
  2. 각 규칙에 대응하는 작은 입력과 기대값을 작성합니다. 전체 합계와 항목별 결과를 함께 비교합니다.
  3. 검토할 함수에 같은 입력을 넣고 결과 차이를 읽습니다. 실패를 없애려고 기대값부터 맞추지 않습니다.
  4. 수정한 코드로 기존 테스트를 다시 실행한 뒤 새로운 오류 사례도 테스트에 남깁니다.

input.csv를 자신의 자료로 바꾸면 expected.json도 별도로 계산해 갱신해야 합니다. 입력만 바꾼 뒤 기대값 불일치를 코드 결함으로 단정하지 마세요. 어떤 규칙과 어떤 데이터가 바뀌었는지부터 확인하는 것이 재현 가능한 검증의 시작입니다.

08실패 문구를 읽고 범위를 좁히기

결과의미다음 확인
FAIL 또는 FAILED계산 결과가 테스트 기대값과 다름실패한 test 이름과 두 값을 읽습니다.
입력 규칙 관련 중지CSV 값이 허용 범위를 벗어남표시된 행과 원래 데이터를 확인합니다.
손계산 기대값과 다름CSV 집계와 expected.json 불일치데이터 변경 여부와 손계산을 각각 재확인합니다.
outputs가 이미 있음이전 성공 결과가 존재--test로 검사만 하거나 결과를 옮겨 보관합니다.

이 예제의 테스트 통과는 명시한 규칙에 대한 증거입니다. 실제 AI 모델의 성능을 측정한 결과나 모든 회계 업무에서 정확하다는 보증은 아닙니다. 세금, 할인, 환율, 반품, 여러 통화, 대량 데이터 성능은 별도 규칙과 테스트가 필요합니다. 실패 사례를 늘릴수록 적용 가능한 범위를 더 명확히 설명할 수 있습니다.

실행·검증 기록

2026-09-19 · Windows 11 · CPython 3.12.14 · 추가 패키지 없음 · 배포본의 임시 복사본에서 실행

  • 12개 내장 unittest 통과
  • 합성 CSV 4행과 손계산 기대값 11101.50 일치
  • 입력·기대값 불일치 시 결과 생성 전 중지
  • 수량을 무시하는 의도적 계산 오류를 테스트가 발견
  • --test 반복 실행 및 원본 SHA-256 보존 확인
검증 범위의 한계
  • 실제 AI API는 호출하지 않았습니다.
  • 세금·환율·반품·성능 검증은 포함하지 않습니다.
  • Windows CPython 3.12.14에서 실행한 결과입니다.

사이트 전체의 작성·검증 원칙

직접 실행할 예제 파일

코드·입력 데이터·실행 안내가 포함되어 있습니다. 압축을 풀고 README.txt부터 읽어 주세요.

예제 ZIP 다운로드

직접 작성한 연습 자료 · 원본을 따로 보관한 뒤 실행하세요.

참고 출처

설명과 예제는 직접 작성했습니다. 관련 동작과 개념은 아래 공식 자료에서 확인할 수 있습니다.