← SEARCHABLE ARCHIVE
Python · 2026-08-15

Python 기초부터 마스터까지 — 10화

계층과 테스트로 신뢰할 수 있는 Python 앱 완성하기

INTERACTIVE READING MODE →

본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.

문법을 많이 아는 것과 변경을 견디는 프로그램을 만드는 것은 다르다. 마지막 화에서는 작업을 등록하고 저장하는 작은 명령줄 앱을 예로 들어 Python 코드를 하나의 구조로 연결해보자.

먼저 프로그램의 계약을 정한다

완성할 앱은 다음 기능을 가진다.

python -m task_app add "Python 공부"
python -m task_app list
python -m task_app complete 1
  • 작업에는 양수 ID, 비어 있지 않은 제목, 상태가 필요하다.
  • 같은 ID를 두 번 등록할 수 없다.
  • 대기 중인 작업만 완료할 수 있다.
  • 잘못된 입력은 사용자에게 설명하고 0이 아닌 종료 코드를 반환한다.
  • 저장 실패가 발생해도 원래 데이터가 가능한 한 보존되어야 한다.

기능을 구현하기 전에 정상 입력, 경계값, 실패 조건을 적으면 코드의 책임을 나누기 쉬워진다.

도메인 규칙은 외부 기술을 모른다

from dataclasses import dataclass, replace
from enum import Enum


class TaskStatus(Enum):
    PENDING = "pending"
    COMPLETED = "completed"


@dataclass(frozen=True, slots=True)
class Task:
    task_id: int
    title: str
    status: TaskStatus = TaskStatus.PENDING

    def __post_init__(self) -> None:
        if self.task_id <= 0:
            raise ValueError("task_id는 양수여야 합니다.")
        if not self.title.strip():
            raise ValueError("title은 비어 있을 수 없습니다.")

    def complete(self) -> "Task":
        if self.status is not TaskStatus.PENDING:
            raise RuntimeError("대기 중인 작업만 완료할 수 있습니다.")
        return replace(self, status=TaskStatus.COMPLETED)

이 코드는 명령줄, JSON, 데이터베이스를 import하지 않는다. 핵심 규칙이 외부 기술과 분리되어 있어 일반 단위 테스트만으로 모든 상태 전이를 확인할 수 있다.

저장소는 작은 계약 뒤에 둔다

from typing import Protocol


class TaskRepository(Protocol):
    def add(self, task: Task) -> None: ...
    def find(self, task_id: int) -> Task | None: ...
    def list_all(self) -> list[Task]: ...


class TaskService:
    def __init__(self, repository: TaskRepository) -> None:
        self._repository = repository

    def register(self, task: Task) -> None:
        if self._repository.find(task.task_id) is not None:
            raise ValueError("이미 존재하는 작업입니다.")
        self._repository.add(task)

서비스는 등록 사용 사례를 담당하고 저장 방식은 저장소 구현에 맡긴다. 처음에는 메모리 딕셔너리로 만들고 같은 계약 테스트를 JSON이나 SQLite 구현에도 적용할 수 있다.

CLI는 입력과 출력을 연결한다

import argparse
from collections.abc import Sequence


def main(argv: Sequence[str] | None = None) -> int:
    parser = argparse.ArgumentParser(prog="task-app")
    subparsers = parser.add_subparsers(dest="action", required=True)

    add = subparsers.add_parser("add")
    add.add_argument("title")

    args = parser.parse_args(argv)

    if args.action == "add":
        print(f"등록: {args.title}")
        return 0
    return 2

argv를 인자로 받을 수 있게 만들면 별도 프로세스를 띄우지 않고 명령 해석을 테스트할 수 있다. 라이브러리 깊은 곳에서 sys.exit를 호출하지 말고 가장 바깥의 진입점에서 종료 코드를 결정하는 편이 좋다.

완성 기준은 동작과 증거다

최종 프로젝트는 다음을 확인해야 한다.

  • 도메인의 모든 허용·거부 상태 전이를 테스트한다.
  • 메모리 저장소와 실제 저장소가 같은 계약을 만족한다.
  • 빈 제목, 중복 ID, 없는 ID, 손상된 파일을 재현한다.
  • 임시 디렉터리를 사용해 테스트끼리 파일 상태를 공유하지 않는다.
  • 로그에는 작업 ID와 오류 종류를 남기되 비밀과 전체 입력은 남기지 않는다.
  • 대표 데이터 크기로 실행 시간과 메모리를 측정한다.
  • 깨끗한 가상 환경에서 설치와 실행 명령을 다시 확인한다.

좋은 구조는 파일과 계층이 많은 구조가 아니다. 상태의 소유자, 입력 검증 위치, 외부 자원의 수명, 실패와 취소의 전달 경로가 분명하고 작은 테스트로 중요한 규칙을 증명할 수 있는 구조다.

Python을 마스터한다는 것은 모든 라이브러리를 외우는 일이 아니다. 낯선 문제를 작은 계약으로 나누고 공식 문서와 실험으로 가정을 확인하며 테스트와 측정 결과를 근거로 설계를 고칠 수 있는 능력에 가깝다.