재고 관리 명령줄 도구 - argparse 하위 명령
이 장에서 배우는 것
준비물 창고에서 물건을 꺼낼 때마다 장부를 고치다 보면 수량보다 작업의 종류를 먼저 구분하게 된다. 들어온 물건은 더하고, 사용한 물건은 빼고, 현재 목록을 확인한 뒤 전체 현황을 요약한다. 이 장에서는 이 네 가지 작업을 하나의 명령줄 인터페이스(command-line interface)로 묶는다. Python 표준 라이브러리의 argparse로 명령을 구분하고, 실제 재고를 바꾸는 규칙은 별도 함수로 작성한다.
앞 장에서 날짜와 시간의 경계를 확인했다면, 이번에는 재고가 0이 되는 경계와 입력이 거절되는 경계를 확인한다. 프로그램은 외부 입력을 기다리지 않는다. main에 정해 둔 명령 목록을 차례로 실행하므로 같은 파일을 실행하면 같은 결과를 얻는다.
- add·remove·list·report를 argparse 하위 명령으로 구성한다.
- 명령 형식 오류와 수량·품목 오류를 구분해 설명한다.
- 출고 가능 여부를 먼저 검사하여 음수 재고를 막는다.
- 실패한 명령이 기존 상태를 보존하는지 검사한다.
- AI가 작성한 코드를 실행 결과와 변경 사항으로 확인한다.
문제 상황
작은 모임의 준비물 담당자가 펜과 공책을 관리한다고 하자. 행사 전에는 물건이 들어오고, 행사 중에는 여러 사람이 물건을 가져간다. 종이에 수량만 적으면 입고와 출고를 혼동하기 쉽다. 현재 공책이 5권인데 6권을 사용했다고 입력했을 때 장부가 -1권으로 바뀌는 문제도 생긴다.
담당자에게 필요한 작업은 단순하다. 입고는 add, 출고는 remove, 현재 목록은 list, 전체 요약은 report로 표현한다. 같은 종류의 작업은 같은 입력 모양을 가져야 한다. 입고와 출고에는 품목 이름과 수량이 필요하고, 목록과 요약에는 추가 인자가 필요하지 않다.
| 명령 | 필요한 인자 | 동작 | 상태 변경 |
|---|---|---|---|
| add | 품목, 양의 정수 수량 | 기존 수량에 더한다 | 있음 |
| remove | 품목, 양의 정수 수량 | 보유 수량 안에서 뺀다 | 있음 |
| list | 없음 | 품목 이름순으로 보여 준다 | 없음 |
| report | 없음 | 등록 품목 수와 총수량을 보여 준다 | 없음 |
여기서 등록 품목 수는 수량이 0인 품목도 포함한다. 펜 3개와 공책 5권이 있으면 총수량은 8이다. 단위가 다른 물건의 수량을 합한 값이므로 구매 비용이나 물리적 부피를 뜻하지 않는다. 이번 도구에서는 관리 중인 개수를 단순 합산한 지표로만 사용한다.
저장 범위도 미리 정한다. 재고는 실행 중 딕셔너리에만 보관하며 파일에 저장하지 않는다. 프로그램을 다시 실행하면 빈 재고에서 시연을 시작한다. 품목명은 앞뒤 공백을 제거하고, 빈 이름은 거절한다. 가운데 공백이나 영문 대소문자는 그대로 유지한다.
요구사항을 명령 구조로 바꾸기
하위 명령(subcommand)은 첫 번째 단어로 실행할 작업을 고르는 구조다. add와 remove가 같은 인자 이름을 사용하더라도 서로 다른 하위 파서에 속한다. list에 실수로 수량을 붙이면 그 명령에는 없는 인자이므로 형식 오류가 된다.
요청문에는 기능뿐 아니라 실패했을 때의 상태도 적는다. “음수 재고를 막는다”만으로는 검사 시점이 불분명하다. 먼저 빼고 나서 오류를 출력하는 구현도 문장만 읽으면 그럴듯해 보일 수 있다. “재고가 부족하면 값을 바꾸지 않는다”까지 적어야 결과를 검사하기 쉽다.
Python 3.12 이상 표준 라이브러리만 사용하여 main.py 하나로 재고 관리 도구를 작성하라. argparse 하위 명령은 add, remove, list, report로 제한한다. add와 remove는 품목 이름과 수량을 받는다. 수량은 0보다 큰 정수만 허용한다.
품목 이름의 앞뒤 공백을 제거하고 빈 이름을 거절하라. 없는 품목의 출고와 보유 수량보다 큰 출고를 거절하라. 실패한 명령은 재고를 바꾸지 않아야 한다. 수량이 0이 된 품목은 등록 상태로 남긴다. list는 품목 이름순으로 출력하며 report는 등록 품목 수와 총수량을 출력한다.
main에서 고정된 명령 목록을 차례로 실행하라. 운영체제의 명령줄 인자를 읽거나 사용자 입력을 기다리지 않게 하라. 각 명령의 결과는 문자열로 반환하고 main에서 출력하라. 정상 동작, 경계값, 실패 후 상태 보존을 검사하는 함수도 포함하라.
이번 프로그램은 명령줄 문법을 다루지만 실제 실행 경로는 시연용으로 고정한다. parse_args에 문자열 목록을 전달하면 터미널에서 받은 인자와 같은 방식으로 분석할 수 있다. 덕분에 같은 파서를 검사와 시연에 함께 사용한다.
argparse의 하위 명령과 인자 분석 동작은 Python 3.12 argparse 공식 문서에서 확인할 수 있다. 이 장의 문장과 코드는 재고 관리 규칙에 맞추어 새로 구성한 것이다.
AI의 첫 결과를 검사하고 수정하기
첫 결과를 받았다고 곧바로 완성 코드로 채택하지 않는다. 먼저 작은 명령으로 의도와 실제 동작을 비교한다. 이 예제의 첫 검토에서는 다음과 같은 흔한 누락을 확인했다고 가정한다. 이는 특정 도구의 성능 평가가 아니라, 작성된 프로그램을 어떤 기준으로 검토할지 보여 주는 과정이다.
| 문제 | 발견 방법 | 고친 방향 |
|---|---|---|
| 음수 출고량을 빼면 재고가 증가한다 | 펜 3개에서 -1개 출고를 요청한다 | 수량이 양의 정수인지 먼저 검사한다 |
| 재고를 차감한 뒤 부족 여부를 검사한다 | 실패 전후 딕셔너리를 비교한다 | 보유량을 확인한 뒤 한 번만 대입한다 |
| 목록이 등록 순서에 따라 달라진다 | 등록 순서를 바꾸어 목록을 비교한다 | 품목 이름을 정렬해 출력한다 |
| 잘못된 명령이 시연 전체를 종료한다 | 틀린 명령 뒤에 report를 배치한다 | 파서 오류를 예외로 받아 결과 문자열로 바꾼다 |
특히 실패 메시지만 읽어서는 상태 보존 여부를 알 수 없다. “재고 부족”을 출력했더라도 이미 값이 바뀌었을 수 있다. 명령 실행 전에 딕셔너리를 복사하고, 오류가 반환된 뒤 복사본과 비교해야 한다.
출고 처리에서 상태를 바꾸기 전에 수량과 보유량을 검사하도록 수정하라. 잘못된 명령은 오류 문자열을 반환하고 다음 시연 명령은 계속 실행하게 하라. 없는 품목, 수량 0, 음수, 정수가 아닌 문자열, 재고 부족을 각각 검사하라. 모든 실패 사례에서 딕셔너리가 그대로인지 확인하라.
수정 후에는 변경 비교(diff)도 읽는다. 출고 함수에서 차감 대입이 검사 뒤로 이동했는지, 예외를 너무 넓게 잡지는 않았는지 확인한다. 이 코드에서는 예상한 입력 오류만 CommandError로 처리한다. 함수 이름을 잘못 적은 오류 같은 개발 실수까지 사용자 입력 오류로 숨기지 않는다.
입력 검사와 상태 변경의 경계
검사는 두 층으로 나눈다. 첫 층은 명령 형식이다. 명령 이름이 알려진 것인지, 필요한 인자가 있는지, 불필요한 인자가 붙지 않았는지를 argparse가 확인한다. 둘째 층은 재고 규칙이다. 이름이 비어 있는지, 수량이 양의 정수인지, 출고할 물건과 수량이 있는지를 작업 함수가 확인한다.
수량 인자를 파서에서 곧바로 정수로 바꿀 수도 있다. 여기서는 원래 문자열을 작업 함수에 전달한 뒤 변환한다. 그러면 정수 변환 실패와 0 이하의 수량에 대해 프로그램이 정한 한국어 메시지를 일관되게 반환할 수 있다. int가 받아들이는 정수 표기는 허용하며, 별도의 숫자 표기 제한은 두지 않는다.
기본 ArgumentParser는 입력 오류를 출력하고 종료한다. 여러 명령을 한 프로세스에서 시연하려면 이 동작을 조정해야 한다. DemoParser는 error 메서드를 재정의하여 CommandError를 발생시킨다. 형식 오류 메시지는 의도적으로 공통 문장으로 고정한다. 수량 오류와 재고 부족에는 더 구체적인 문장을 제공한다.
또한 이 시연은 도움말 동작을 제공하지 않으므로 각 파서에서 add_help=False를 지정한다. 따라서 도움말 옵션도 형식 오류가 된다. 실제 사용자용 도구로 확장할 때는 기본 도움말을 복원하고, 도움말에 따른 정상 종료와 입력 오류를 별도로 설계해야 한다.
상태 변경의 핵심은 검사 후 대입이다. 현재 수량을 읽고, 요청 수량과 비교하고, 허용할 때만 새 값을 기록한다. 정확히 보유량만큼 출고하는 경우는 허용하므로 거절 조건은 요청 수량이 현재 수량보다 클 때다.
완성 코드
다음 코드를 main.py로 저장한다. 외부 파일이나 패키지는 필요하지 않다. 내부 검사가 모두 통과하면 시연을 시작한다. 검사가 실패하면 AssertionError가 발생하여 시연 전에 문제를 드러낸다.
import argparse
class CommandError(Exception):
pass
class DemoParser(argparse.ArgumentParser):
def error(self, message):
raise CommandError(
"명령 형식 오류: 명령과 필수 인자를 확인한다."
)
def normalize_item(raw):
item = raw.strip()
if not item:
raise CommandError("품목 이름은 비어 있을 수 없다.")
return item
def positive_quantity(raw):
try:
quantity = int(raw)
except ValueError:
raise CommandError(
"수량은 정수로 입력한다."
) from None
if quantity <= 0:
raise CommandError("수량은 1 이상이어야 한다.")
return quantity
def add_stock(stock, args):
item = normalize_item(args.item)
quantity = positive_quantity(args.quantity)
stock[item] = stock.get(item, 0) + quantity
return (
f"입고: {item} +{quantity}"
f" / 현재 {stock[item]}"
)
def remove_stock(stock, args):
item = normalize_item(args.item)
quantity = positive_quantity(args.quantity)
if item not in stock:
raise CommandError(f"등록되지 않은 품목: {item}")
current = stock[item]
if quantity > current:
raise CommandError(
f"재고 부족: {item}"
f" / 현재 {current}, 요청 {quantity}"
)
stock[item] = current - quantity
return (
f"출고: {item} -{quantity}"
f" / 현재 {stock[item]}"
)
def list_stock(stock, args):
if not stock:
return "재고 목록: 비어 있음"
lines = ["재고 목록"]
for item in sorted(stock):
lines.append(f"- {item}: {stock[item]}")
return "\n".join(lines)
def report_stock(stock, args):
return (
f"요약: 등록 품목 {len(stock)}종"
f" / 총수량 {sum(stock.values())}"
)
def build_parser():
parser = DemoParser(
prog="inventory",
add_help=False,
allow_abbrev=False,
)
commands = parser.add_subparsers(
dest="command",
required=True,
)
for name, handler in (
("add", add_stock),
("remove", remove_stock),
):
command = commands.add_parser(
name,
add_help=False,
allow_abbrev=False,
)
command.add_argument("item")
command.add_argument("quantity")
command.set_defaults(handler=handler)
for name, handler in (
("list", list_stock),
("report", report_stock),
):
command = commands.add_parser(
name,
add_help=False,
allow_abbrev=False,
)
command.set_defaults(handler=handler)
return parser
def execute(parser, stock, argv):
try:
args = parser.parse_args(argv)
return args.handler(stock, args)
except CommandError as error:
return f"오류: {error}"
def verify():
parser = build_parser()
stock = {}
assert execute(parser, stock, ["list"]) == (
"재고 목록: 비어 있음"
)
assert execute(parser, stock, ["report"]) == (
"요약: 등록 품목 0종 / 총수량 0"
)
execute(parser, stock, ["add", "펜", "2"])
execute(parser, stock, ["add", " 펜 ", "1"])
execute(parser, stock, ["add", "공책", "1"])
assert stock == {"펜": 3, "공책": 1}
assert execute(parser, stock, ["list"]) == (
"재고 목록\n- 공책: 1\n- 펜: 3"
)
assert execute(parser, stock, ["report"]) == (
"요약: 등록 품목 2종 / 총수량 4"
)
invalid_commands = [
["add", "펜", "0"],
["add", "펜", "-1"],
["remove", "펜", "0"],
["remove", "펜", "-1"],
["add", "펜", "셋"],
["remove", "펜", "1.5"],
["remove", "펜", "4"],
["remove", "지우개", "1"],
["add", " ", "1"],
["remove", "", "1"],
["add", "펜"],
["list", "추가"],
["unknown"],
[],
]
for argv in invalid_commands:
before = stock.copy()
result = execute(parser, stock, argv)
assert result.startswith("오류:"), argv
assert stock == before, argv
assert execute(
parser, stock, ["remove", "펜", "3"]
) == "출고: 펜 -3 / 현재 0"
assert stock == {"펜": 0, "공책": 1}
assert execute(parser, stock, ["report"]) == (
"요약: 등록 품목 2종 / 총수량 1"
)
before = stock.copy()
assert execute(
parser, stock, ["remove", "펜", "1"]
).startswith("오류:")
assert stock == before
def main():
verify()
print("내부 검사: 통과")
parser = build_parser()
stock = {}
commands = [
["add", "펜", "10"],
["add", "공책", "5"],
["add", "펜", "3"],
["remove", "펜", "4"],
["list"],
["report"],
["remove", "공책", "6"],
["add", "지우개", "0"],
["add", "펜", "셋"],
["remove", "지우개", "1"],
["add", "펜"],
["report"],
]
for argv in commands:
print("시연> " + " ".join(argv))
print(execute(parser, stock, argv))
if __name__ == "__main__":
main()
줄별 해설
import argparse는 명령 분석에 필요한 표준 라이브러리를 불러온다. 이어지는 CommandError는 프로그램이 예상한 입력 오류를 나타낸다. DemoParser의 error 메서드는 형식 오류를 이 예외로 바꾼다. 메서드 인자 message는 argparse가 전달하는 상세 설명이지만, 이 시연에서는 출력 문구를 고정하기 위해 사용하지 않는다.
normalize_item은 strip으로 앞뒤 공백을 제거한다. 빈 문자열이 되면 예외를 발생시키므로 공백만 있는 품목이 등록되지 않는다. 이름을 정리하는 함수를 입고와 출고가 함께 사용하므로 두 작업의 이름 처리 규칙이 어긋나지 않는다.
positive_quantity는 문자열을 정수로 바꾸는 부분과 값의 범위를 확인하는 부분으로 나뉜다. “셋”이나 “1.5”는 정수 변환에서 거절된다. “0”과 “-1”은 변환에는 성공하지만 범위 검사에서 거절된다. from None은 변환 오류를 다른 예외로 바꿀 때 이전 예외의 연결 표시를 생략한다. 실제 화면에는 execute가 만든 오류 문자열만 나온다.
add_stock의 get(item, 0)은 아직 등록되지 않은 품목을 수량 0으로 취급한다. 이름과 수량 검사가 끝난 뒤 대입하므로 잘못된 입력으로 새 품목이 생기지 않는다. “지우개 0개 입고”가 실패했을 때 지우개라는 키도 추가되지 않는 이유다.
remove_stock은 등록 여부를 먼저 확인한다. get으로 없는 품목을 0으로 취급하면 미등록 품목과 등록된 재고 0을 구분하기 어렵다. 이어서 current에 보유량을 담고 요청량과 비교한다. quantity가 current와 같을 때는 출고를 허용한다. 마지막 대입이 실행되기 전까지 stock은 바뀌지 않는다.
list_stock은 빈 상태를 먼저 처리한다. 품목이 있으면 sorted(stock)으로 이름을 정렬하고 줄을 만든다. 이 정렬은 문자열의 기본 비교 규칙을 따른다. 별도의 언어별 사전 정렬이나 숫자를 인식하는 정렬을 뜻하지 않는다. report_stock은 딕셔너리의 길이와 값의 합을 사용한다.
build_parser에서는 required=True로 하위 명령을 필수로 만든다. add_parser를 호출해 각 명령의 문법을 등록하고, set_defaults로 처리 함수를 연결한다. 분석 결과에는 사용자가 입력한 item과 quantity 외에 handler도 들어간다. execute는 args.handler를 호출하므로 명령 이름을 다시 여러 조건문으로 비교할 필요가 없다.
execute가 받는 argv는 명령 전체를 담은 문자열이 아니라 이미 나뉜 문자열 목록이다. 공백이 있는 이름도 목록의 한 원소로 전달하면 하나의 품목 인자가 된다. 이 프로그램은 문자열을 공백 기준으로 다시 나누지 않는다. main의 “시연>” 출력은 사람이 읽기 위한 표시이며, 셸 명령을 생성하는 기능은 아니다.
verify는 별도의 딕셔너리로 검사하므로 시연 재고에 영향을 주지 않는다. 정상 입고, 공백 제거, 정렬, 집계를 확인한 다음 실패 명령들을 반복한다. stock.copy는 문자열 키와 정수 값으로 구성된 현재 구조를 비교하기에 충분하다. 보유량과 같은 출고를 마지막에 확인하여 0 재고의 의미도 검사한다.
main은 내부 검사와 시연을 순서대로 실행한다. 파일 마지막의 조건문은 이 파일을 직접 실행했을 때만 main을 호출한다. 일반 실행에서 assert 검사가 동작하며, 최적화 옵션을 붙이면 assert가 생략될 수 있다. 이 장의 실행 명령에는 해당 옵션을 사용하지 않는다.
실행 결과
macOS 또는 Linux에서 파일을 저장한 디렉터리로 이동한 뒤 다음 명령을 실행한다. 컴파일 검사에는 경고를 오류로 처리하는 옵션을 붙인다. 첫 명령이 성공하면 별도 출력은 없다. 이 과정은 직접 실행하여 확인해야 하며, 아래 예상 출력과 문장이 같다는 이유만으로 검사를 대신하지 않는다.
python3 -W error -m py_compile main.py
python3 main.py
두 번째 명령의 예상 출력은 다음과 같다. “시연>”로 시작하는 줄은 main에 들어 있는 명령 목록을 표시한 것이다.
내부 검사: 통과
시연> add 펜 10
입고: 펜 +10 / 현재 10
시연> add 공책 5
입고: 공책 +5 / 현재 5
시연> add 펜 3
입고: 펜 +3 / 현재 13
시연> remove 펜 4
출고: 펜 -4 / 현재 9
시연> list
재고 목록
- 공책: 5
- 펜: 9
시연> report
요약: 등록 품목 2종 / 총수량 14
시연> remove 공책 6
오류: 재고 부족: 공책 / 현재 5, 요청 6
시연> add 지우개 0
오류: 수량은 1 이상이어야 한다.
시연> add 펜 셋
오류: 수량은 정수로 입력한다.
시연> remove 지우개 1
오류: 등록되지 않은 품목: 지우개
시연> add 펜
오류: 명령 형식 오류: 명령과 필수 인자를 확인한다.
시연> report
요약: 등록 품목 2종 / 총수량 14
마지막 요약이 처음 요약과 같은 이유를 확인한다. 그 사이의 다섯 명령은 모두 실패했으므로 등록 품목 수와 총수량이 바뀌지 않는다. 다만 합계가 같다는 사실만으로 모든 품목이 보존되었다고 단정할 수는 없다. 그래서 내부 검사에서는 합계뿐 아니라 딕셔너리 전체를 비교한다.
실무에서 자주 틀리는 것
차감한 다음 부족 여부를 확인한다
다음 함수는 오류를 발생시키면서도 이미 음수 수량을 기록한다. 호출한 쪽에서 오류 문장을 출력해도 값은 되돌아오지 않는다. 아래 비교 함수들은 이름과 수량이 앞서 검증되었다고 가정한다.
def bad_remove(stock, item, quantity):
stock[item] -= quantity
if stock[item] < 0:
raise ValueError("재고 부족")
고친 함수는 대입 전에 조건을 검사한다. 이처럼 변경 지점을 뒤로 모으면 실패 경로에서 상태를 보존하기 쉽다.
def checked_remove(stock, item, quantity):
current = stock[item]
if quantity > current:
raise ValueError("재고 부족")
stock[item] = current - quantity
정수로 바뀌면 유효한 수량이라고 생각한다
int는 음수와 0도 정수로 받아들인다. 변환 성공과 업무 규칙 통과는 다른 조건이다. 다음 함수만으로는 출고에 -2를 넣는 일을 막지 못한다.
def bad_quantity(raw):
return int(raw)
변환 뒤 범위를 확인해야 한다. 완성 코드에서는 여기에 변환 실패 메시지 처리도 더했다.
def checked_quantity(raw):
quantity = int(raw)
if quantity <= 0:
raise ValueError("수량은 1 이상이어야 한다.")
return quantity
등록 순서를 출력 규칙으로 사용한다
딕셔너리는 등록 순서를 유지하지만, 그 순서가 업무에서 원하는 목록 순서라는 뜻은 아니다. 입고 이력이 달라지면 같은 재고도 다른 순서로 보일 수 있다.
def insertion_order_rows(stock):
return [
f"{item}: {quantity}"
for item, quantity in stock.items()
]
품목 이름순이 요구사항이면 그 기준을 코드에 직접 적는다. 출력 규칙을 정하면 예상 결과를 비교하기도 쉬워진다.
def name_order_rows(stock):
return [
f"{item}: {stock[item]}"
for item in sorted(stock)
]
종료 예외를 일괄적으로 삼킨다
기본 파서의 종료를 막으려고 SystemExit를 모두 잡으면 도움말에 따른 정상 종료와 잘못된 입력을 함께 취급하기 쉽다. 아래 함수는 어느 경우든 같은 문자열을 반환한다.
def blurred_parse(parser, argv):
try:
return parser.parse_args(argv)
except SystemExit:
return "입력 오류"
완성 코드처럼 오류 경로에 별도의 예외를 정의하면 처리 대상이 분명해진다. 다음 함수는 error 메서드가 CommandError를 발생시키도록 구성된 파서와 함께 사용한다.
def explicit_parse(parser, argv):
try:
return parser.parse_args(argv)
except CommandError as error:
return f"오류: {error}"
입력 문제를 처리한다는 이유로 모든 Exception을 잡아 같은 문장으로 바꾸지 않는다. 예상하지 못한 개발 오류는 검사 과정에서 드러나야 고칠 수 있다.
한눈에 보기
| 관심사 | 구현 위치 | 확인할 조건 |
|---|---|---|
| 명령 구분 | build_parser | 하위 명령 필수, 명령별 인자 분리 |
| 품목 이름 | normalize_item | 앞뒤 공백 제거, 빈 이름 거절 |
| 수량 입력 | positive_quantity | 정수 변환과 양수 조건을 각각 검사 |
| 출고 경계 | remove_stock | 같은 수량 허용, 초과 수량 거절 |
| 실패 후 상태 | verify | 명령 전후 딕셔너리 일치 |
| 결과 재현 | list_stock, main | 이름 정렬, 고정 명령 목록 사용 |
이 프로그램에서 argparse는 입력의 모양을 책임지고, 작업 함수는 재고 규칙을 책임진다. 내부 검사는 두 부분을 execute로 함께 통과시키므로 함수 하나의 계산뿐 아니라 명령 연결까지 확인한다. AI에 수정을 요청할 때도 어느 책임이 바뀌는지 적으면 변경 범위를 검토하기 쉽다.
연습 문제
- 재고가 정확히 0이 되는 출고와 재고보다 1개 많은 출고를 같은 초기 상태에서 각각 검사하라. 두 결과에서 딕셔너리가 어떻게 달라져야 하는지 설명하라.
- list 명령에 선택 인자 --min을 추가하라. 기본값은 0이며, 해당 값 이상의 재고만 표시한다. 음수 기준은 거절하고, 등록 품목이 없는 경우와 조건에 맞는 품목이 없는 경우의 메시지를 구분하라.
- report에 품절 품목 수를 추가하라. 수량이 0인 등록 품목만 품절로 센다. 등록되지 않은 품목이 포함되지 않음을 보여 주는 검사를 작성하라.
- 실제 터미널 인자를 받는 실행 경로를 별도 함수로 설계하라. 인자 없는 실행에서는 기존 시연을 유지하고, 인자가 있으면 한 명령만 실행한다. 프로세스가 끝나면 재고가 사라지는 현재 설계에서 어떤 사용 제약이 생기는지 설명하라.
정답과 해설
첫 문제는 초기 딕셔너리를 각각 새로 만들어 검사해야 한다. 첫 출고가 만든 상태를 다음 검사의 초기 상태로 사용하면 경계 조건이 달라진다. 아래 함수는 완성 코드의 함수들을 불러온 상태에서 실행하는 추가 검사다.
def verify_remove_boundary():
parser = build_parser()
exact = {"펜": 3}
assert execute(
parser, exact, ["remove", "펜", "3"]
) == "출고: 펜 -3 / 현재 0"
assert exact == {"펜": 0}
excess = {"펜": 3}
before = excess.copy()
assert execute(
parser, excess, ["remove", "펜", "4"]
) == "오류: 재고 부족: 펜 / 현재 3, 요청 4"
assert excess == before
두 번째 문제는 list용 하위 파서에만 --min을 등록한다. 음수가 아닌 정수를 받는 변환 함수를 따로 만들고 기본값을 정수 0으로 지정하면 된다. list_stock에서는 먼저 전체 재고가 비었는지 확인하고, 그다음 정렬된 이름 중 수량이 기준 이상인 품목을 고른다. 선택 결과가 비었으면 “조건에 맞는 품목 없음”을 반환한다. 경계 검사에는 기준과 같은 수량이 포함되는 사례와 기준보다 1 작은 수량이 제외되는 사례가 필요하다.
세 번째 문제의 계산식은 등록된 값 중 0과 같은 값의 개수다. 다음 함수는 독립적으로 실행할 수 있다.
def sold_out_count(stock):
return sum(
1 for quantity in stock.values()
if quantity == 0
)
assert sold_out_count({}) == 0
assert sold_out_count({"펜": 0, "공책": 2}) == 1
assert sold_out_count({"펜": 0, "공책": 0}) == 2
없는 품목은 딕셔너리 값에 포함되지 않으므로 품절 수에 들어가지 않는다. 이 문제는 출고량이 0인 요청을 허용하라는 뜻이 아니다. 거래 수량은 계속 양수이고, 거래 결과로 생긴 현재 재고만 0일 수 있다.
네 번째 문제에서는 sys.argv[1:]을 읽어 비어 있으면 기존 main을 호출하고, 값이 있으면 build_parser와 execute로 한 명령을 처리하는 함수를 둘 수 있다. 다만 실행할 때마다 새 딕셔너리를 만들면 이전 입고가 다음 실행으로 이어지지 않는다. 지속적인 재고 관리에 쓰려면 저장과 불러오기 요구사항을 추가해야 한다. 명령 입력을 연결하는 일과 데이터를 보존하는 일은 각각 검사해야 한다.
확장할 때도 처음부터 모든 기능을 한꺼번에 요청하지 않는다. 다음처럼 현재 동작과 추가 조건을 함께 적고, 수정 후 실행·검사·변경 비교를 반복한다.
현재 재고 도구에 list --min 기능만 추가하라. 기준 수량은 0 이상의 정수이며 기본값은 0이다. 기준과 같은 수량은 포함한다. 빈 재고와 검색 결과 없음의 메시지를 구분하라. add·remove·report의 기존 동작은 유지하라.
기준 0, 기준과 같은 수량, 기준보다 작은 수량, 음수 기준, 정수가 아닌 기준을 검사하라. 목록 조회 전후 재고가 같은지도 확인하라. 수정한 함수와 변경 이유를 설명하고, 기존 예상 출력이 유지되는지 비교할 수 있게 하라.
요청문은 구현의 출발점이다. 최종 판단은 실행한 프로그램의 결과와 검사한 상태에 둔다. 다음 장에서는 여러 응답을 모아 요약할 때 어떤 기준을 먼저 정해야 하는지 살펴본다.