Python 강좌 / 최종 실습: SQLite 기반 지출 관리 프로그램 완성하기
최종 실습은 지출의 날짜·분류·금액을 입력하고 SQLite에 저장한 뒤 목록과 분류별 합계를 조회하는 CLI 프로그램입니다. 초기화·입력·조회·검증·트랜잭션을 명확히 나누어 앞서 배운 기능을 연결합니다.
기능 범위와 데이터 규칙
금액은 원 단위 비음수 정수, 날짜는 YYYY-MM-DD, 분류는 빈 문자열이 아니어야 합니다. 결제·은행 연동이나 여러 사용자 인증 기능은 포함하지 않습니다. add는 호출할 때마다 새 지출 한 건을 추가하므로 같은 명령을 반복하면 실제로 행이 늘어납니다.
관련 동작과 지원 조건은 최종 실습: SQLite 기반 지출 관리 프로그램 완성하기 관련 공식 문서에서 확인할 수 있습니다.
새 실습 폴더에 ledger.py를 저장합니다. Python 3.12 이상과 표준 라이브러리만 사용합니다. --db로 별도 DB 경로를 지정할 수 있으며 기본값 ledger.db는 현재 폴더 아래입니다. 다른 실습·업무 파일을 지정하지 않습니다.
전체 프로그램
ledger.py 파일의 전체 내용입니다.
import argparse
import sqlite3
from datetime import date
from pathlib import Path
def amount_value(text):
value = int(text)
if value < 0:
raise argparse.ArgumentTypeError("amount must be nonnegative")
return value
def date_value(text):
try:
parsed = date.fromisoformat(text)
except ValueError as error:
raise argparse.ArgumentTypeError("invalid date") from error
if parsed.isoformat() != text:
raise argparse.ArgumentTypeError("use YYYY-MM-DD")
return text
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--db", type=Path, default=Path("ledger.db"))
commands = parser.add_subparsers(dest="command", required=True)
commands.add_parser("init")
add = commands.add_parser("add")
add.add_argument("day", type=date_value)
add.add_argument("category")
add.add_argument("amount", type=amount_value)
commands.add_parser("list")
commands.add_parser("summary")
args = parser.parse_args()
if args.command != "init" and not args.db.is_file():
parser.error("database not initialized")
if args.command == "add" and not args.category.strip():
parser.error("category is required")
con = sqlite3.connect(args.db, autocommit=False)
try:
if args.command == "init":
with con:
con.execute("CREATE TABLE IF NOT EXISTS expenses (id INTEGER PRIMARY KEY, day TEXT NOT NULL, category TEXT NOT NULL CHECK(length(trim(category))>0), amount INTEGER NOT NULL CHECK(amount>=0))")
print("initialized")
elif args.command == "add":
with con:
con.execute("INSERT INTO expenses(day,category,amount) VALUES(?,?,?)", (args.day, args.category.strip(), args.amount))
print("added")
elif args.command == "list":
for row in con.execute("SELECT id,day,category,amount FROM expenses ORDER BY day,id"):
print(*row)
else:
for category, total in con.execute("SELECT category,SUM(amount) FROM expenses GROUP BY category ORDER BY category"):
print(category, total)
finally:
con.close()
if __name__ == "__main__":
main()
초기화·입력·조회 순서
python ledger.py init python ledger.py add 2026-10-17 food 3500 python ledger.py add 2026-10-17 food 8000 python ledger.py add 2026-10-17 transport 2500 python ledger.py list python ledger.py summary
초기화는 initialized, 입력은 매번 added를 출력합니다. 목록은 id·날짜·분류·금액 순서이며 마지막 합계는 다음과 같아야 합니다.
food 11500 transport 2500
init은 기존 테이블을 지우지 않습니다. 처음 상태부터 반복하려면 새 폴더나 --db로 새 파일을 사용하고 기존 파일을 무작정 삭제하지 않습니다.
실패 조건과 저장 결과 확인
- 2026-02-30을 입력해 DB 저장 전에 거부되는지 확인합니다.
- 음수 금액과 빈 분류도 거부한 뒤 목록이 늘지 않았는지 확인합니다.
- 프로그램을 종료한 후 다시 list를 실행해 데이터가 남아 있는지 확인합니다.
- 새 DB에서 동일한 세 건을 넣어 합계가 재현되는지 확인합니다.
SQL에 값 매개변수를 사용하고 DB에도 금액 제약을 둡니다. Python 검증과 저장소 제약은 서로를 보완합니다. 예외가 발생하면 with 연결이 롤백하고 finally가 연결을 닫습니다. 단, 스키마 변경·백업·복원·동시 사용자 관리는 추가 설계가 필요한 별도 범위입니다.
프로그램을 나눌 때의 기준
현재 파일은 구조를 한눈에 보기 위한 전체 예제입니다. 기능이 커지면 인수 해석, 입력 검증, DB 접근과 집계를 별도 모듈로 분리하고 기존 동작을 테스트로 고정합니다. import만으로 DB를 만들거나 사용자 입력을 받지 않도록 main 진입점을 유지합니다.
실제 개인 지출을 저장한다면 파일 권한·백업·공유 위치를 확인합니다. 보고서 숫자만 맞는지뿐 아니라 잘못된 입력이 저장되지 않았는지와 재실행의 의미까지 검증해야 완성도를 판단할 수 있습니다.




