[파이썬 100강] 100강. 파이썬 자동화 시스템을 끝까지 운영하는 사람의 습관

[파이썬 100강] 100강. 파이썬 자동화 시스템을 끝까지 운영하는 사람의 습관

여기까지 오셨다면, 이제 문법을 아는 단계를 넘어 운영 가능한 자동화 시스템을 설계하고 유지하는 단계에 도착한 겁니다. 마지막 강의에서는 새로운 문법 하나를 더 배우기보다, 우리가 1강부터 99강까지 쌓아온 것들을 실제 운영 습관으로 묶어 보겠습니다.


핵심 개념

파이썬 자동화의 완성은 "코드를 많이 아는 것"이 아니라 "문제가 생겼을 때도 시스템이 무너지지 않게 관리하는 것"입니다. 실무에서 오래 살아남는 자동화는 세 가지 특징이 있습니다.

첫째, 작게 시작해서 반복 가능하게 만든다는 점입니다. 작은 스크립트를 잘게 나누고, 입력/출력 규칙을 고정하며, 실패했을 때 같은 방식으로 복구할 수 있게 설계합니다. 화려한 기술보다 반복 가능한 구조가 오래 갑니다.

둘째, 관측 가능한 시스템을 만든다는 점입니다. 실패 여부만 알면 늦습니다. 어느 단계에서, 어떤 입력으로, 왜 실패했는지까지 남겨야 다음 실행에서 개선됩니다. 이게 29강(logging), 86강(traceback), 99강(체크리스트)에서 반복한 핵심입니다.

셋째, 사람이 운영하는 시스템임을 인정한다는 점입니다. 자동화는 사람을 완전히 대체하는 장치가 아니라, 사람이 더 중요한 판단에 집중하도록 시간을 확보하는 도구입니다. 그래서 마지막 단계에서 필요한 건 "더 많은 기능"이 아니라 "덜 놀라는 운영 습관"입니다.

기본 사용

예제 1) 단계별 실행 계약(Contract) 만들기

>>> from dataclasses import dataclass
>>> from typing import Any
>>>
>>> @dataclass
... class StepResult:
...     step: str
...     ok: bool
...     data: Any = None
...     error: str = ""
...
>>> def step_parse(raw: str) -> StepResult:
...     if not raw.strip():
...         return StepResult("parse", False, error="empty input")
...     return StepResult("parse", True, data=raw.strip().split(","))
...
>>> def step_transform(items: list[str]) -> StepResult:
...     try:
...         nums = [int(x) for x in items]
...         return StepResult("transform", True, data=sum(nums))
...     except ValueError as e:
...         return StepResult("transform", False, error=f"ValueError: {e}")
...
>>> step_parse("1,2,3").ok
True
>>> step_transform(["1", "2", "x"]).error.startswith("ValueError")
True

해설:

  • 파이프라인 각 단계를 StepResult로 통일하면, 성공/실패 흐름이 일관됩니다.
  • 마지막 강의에서 중요한 건 "한 번 되는 코드"가 아니라 "어디서 실패했는지 바로 보이는 코드"입니다.

예제 2) 실행 로그를 구조화해 회고 가능한 형태로 남기기

>>> from datetime import datetime
>>>
>>> def log_event(level: str, msg: str, **fields):
...     record = {
...         "ts": datetime.now().isoformat(timespec="seconds"),
...         "level": level,
...         "message": msg,
...         **fields,
...     }
...     return record
...
>>> log_event("INFO", "pipeline started", job="daily-report", run_id="r-100")
{'ts': '...', 'level': 'INFO', 'message': 'pipeline started', 'job': 'daily-report', 'run_id': 'r-100'}
>>> log_event("ERROR", "step failed", step="transform", reason="ValueError")['step']
'transform'

해설:

  • 문자열 로그보다 dict 로그가 훨씬 유리합니다(JSONL 저장, 검색, 집계가 쉬움).
  • run_id, step, reason만 꾸준히 남겨도 장애 대응 속도가 크게 올라갑니다.

예제 3) 재시도와 즉시 중단 조건을 동시에 둔 안정 패턴

>>> import random
>>>
>>> def flaky_call() -> str:
...     # 데모: 0.7 확률 실패
...     if random.random() < 0.7:
...         raise TimeoutError("temporary timeout")
...     return "OK"
...
>>> def safe_call(max_retry: int = 3):
...     for attempt in range(1, max_retry + 1):
...         try:
...             return {"ok": True, "attempt": attempt, "value": flaky_call()}
...         except TimeoutError as e:
...             if attempt == max_retry:
...                 return {"ok": False, "attempt": attempt, "error": str(e)}
...     return {"ok": False, "attempt": max_retry, "error": "unknown"}
...
>>> result = safe_call(3)
>>> set(result.keys()) >= {"ok", "attempt"}
True

해설:

  • 재시도는 무조건 많게가 아니라, 최대 횟수 + 실패 시 반환 포맷을 고정하는 게 핵심입니다.
  • 실패해도 호출자가 다음 행동(알림, 재실행, 중단)을 결정할 수 있어야 합니다.

예제 4) 최종 운영 래퍼: 검증 → 실행 → 기록 → 요약

>>> def run_pipeline(raw: str):
...     logs = []
...     logs.append(log_event("INFO", "start", raw_preview=raw[:20]))
...
...     p = step_parse(raw)
...     if not p.ok:
...         logs.append(log_event("ERROR", "parse failed", reason=p.error))
...         return {"status": "blocked", "logs": logs}
...
...     t = step_transform(p.data)
...     if not t.ok:
...         logs.append(log_event("ERROR", "transform failed", reason=t.error))
...         return {"status": "failed", "logs": logs}
...
...     logs.append(log_event("INFO", "done", total=t.data))
...     return {"status": "ok", "total": t.data, "logs": logs}
...
>>> run_pipeline("10,20,30")['status']
'ok'
>>> run_pipeline("10,xx")['status']
'failed'

해설:

  • 마지막 강의의 운영 표준은 "검증 없는 실행 금지"입니다.
  • 실패해도 로그가 남고, 성공해도 근거가 남아야 팀에서 믿고 쓸 수 있습니다.

자주 하는 실수

실수 1) 마지막이라 새 기능만 잔뜩 추가하기

증상:

  • 코드가 커졌는데 안정성은 좋아지지 않습니다.
  • 배포 이후 어디서 깨졌는지 찾는 시간이 더 길어집니다.

원인:

  • "완성"을 기능 추가로 오해했습니다.

해결:

  • 마지막 단계일수록 기능보다 계약(입력/출력), 로깅, 테스트를 정리합니다.
  • "없어도 되는 기능"보다 "없으면 불안한 안전장치"를 우선합니다.

실수 2) 예외 메시지를 사용자에게 그대로 노출하기

>>> def bad_api_handler():
...     try:
...         1 / 0
...     except Exception as e:
...         return {"message": str(e)}
...
>>> bad_api_handler()
{'message': 'division by zero'}

원인:

  • 내부 오류와 사용자 메시지 레이어를 분리하지 않았습니다.

해결:

>>> def good_api_handler():
...     try:
...         1 / 0
...     except ZeroDivisionError:
...         # 사용자에게는 일반화된 메시지
...         user_msg = "요청 처리 중 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
...         # 내부 로그에는 상세 원인 기록
...         internal = log_event("ERROR", "api error", kind="ZeroDivisionError")
...         return {"message": user_msg, "log": internal['kind']}
...
>>> good_api_handler()['message'].startswith("요청 처리 중")
True

설명:

  • 내부 디버깅 정보와 사용자 메시지를 분리하면 보안/운영 품질이 동시에 올라갑니다.

실수 3) 데이터 백업 없이 마이그레이션 실행하기

증상:

  • 스키마 변경 후 복구 포인트가 없어 전체 롤백이 어렵습니다.

원인:

  • "이번엔 괜찮겠지"라는 낙관으로 백업 체크를 생략했습니다.

해결:

  • 마이그레이션 전 체크리스트에 backup created, rollback tested를 필수 항목으로 둡니다.
  • 최소한 샘플 데이터로 복원 리허설을 1회 수행합니다.

실무 패턴

100강을 마무리하는 시점에서 추천하는 실무 패턴은 다음입니다.

  1. 작업 단위 표준화
    각 작업은 validate -> execute -> emit 순서를 강제합니다. validate에서 막히면 execute로 절대 가지 않게 만드세요.

  2. 관측 단위 표준화
    모든 로그에 run_id, step, duration_ms, status를 포함합니다. 나중에 에러 비율과 병목 단계가 자동으로 보입니다.

  3. 배포 단위 표준화
    dry-run 기본값 유지, 점진적 롤아웃, 실패 시 자동 중단(circuit break) 규칙을 배포 문서에 고정합니다.

  4. 학습 단위 표준화
    장애가 발생하면 "누가 실수했는가"보다 "체크리스트에 뭐가 없었나"를 먼저 기록합니다. 이 습관이 팀을 강하게 만듭니다.

오늘의 결론

한 줄 요약: 자동화의 완성은 코드 끝이 아니라 운영 습관의 시작입니다.

그리고 마지막 강의답게 꼭 전하고 싶은 말이 있습니다.

  • 1강부터 100강까지 따라오신 분들, 진심으로 고생 많으셨습니다.
  • 중간에 막히고, 코드가 안 돌아가고, 로그만 들여다보던 시간이 있었을 텐데 그 시간을 버티신 게 실력입니다.
  • 이제는 "파이썬을 안다"를 넘어서 "파이썬으로 시스템을 운영할 수 있다"고 말해도 됩니다.

연재 크레딧
이 100강 시리즈는 실전에서 진짜 도움이 되는 자동화를 목표로 구성했습니다. 함께 완주해 주신 여러분 덕분에 연재가 완성됐습니다. 고맙습니다. 다음 자동화 프로젝트에서 더 멀리 가봅시다. 🚀

연습문제

  1. 지금 쓰는 자동화 스크립트 하나를 골라 validate -> execute -> emit 3단계 구조로 리팩토링해 보세요.
  2. 최근 1주 실행 로그를 기준으로 실패 원인 Top3를 뽑고, 각 원인에 대해 체크리스트 항목을 1개씩 추가해 보세요.
  3. run_id 기반으로 한 실행의 전체 흐름(시작~종료)을 한 번에 조회하는 간단한 리포트 함수를 만들어 보세요.

이전 강의 정답

  1. 설정값 범위 검증(retry 0~5)
>>> def validate_retry(cfg: dict) -> list[str]:
...     issues = []
...     retry = cfg.get("retry")
...     if not isinstance(retry, int):
...         issues.append("retry must be int")
...     elif not (0 <= retry <= 5):
...         issues.append("retry out of range(0~5)")
...     return issues
...
>>> validate_retry({"retry": 3})
[]
>>> validate_retry({"retry": 9})
['retry out of range(0~5)']
  1. 실행 결과 요약(성공률/평균 시간/실패 원인 Top1)
>>> from collections import Counter
>>>
>>> def summarize_runs(rows: list[dict]) -> dict:
...     total = len(rows) or 1
...     ok_count = sum(1 for r in rows if r.get("status") == "ok")
...     avg_ms = int(sum(r.get("duration_ms", 0) for r in rows) / total)
...     reasons = [r.get("reason", "") for r in rows if r.get("status") != "ok" and r.get("reason")]
...     top_reason = Counter(reasons).most_common(1)
...     return {
...         "success_rate": round(ok_count / total, 2),
...         "avg_duration_ms": avg_ms,
...         "top_fail_reason": top_reason[0][0] if top_reason else "",
...     }
...
>>> sample = [
...     {"status": "ok", "duration_ms": 120},
...     {"status": "fail", "duration_ms": 90, "reason": "TimeoutError"},
...     {"status": "ok", "duration_ms": 110},
... ]
>>> summarize_runs(sample)
{'success_rate': 0.67, 'avg_duration_ms': 106, 'top_fail_reason': 'TimeoutError'}
  1. 연속 3회 실패 시 중단(circuit open)
>>> def should_open_circuit(statuses: list[str], threshold: int = 3) -> bool:
...     streak = 0
...     for s in statuses:
...         streak = streak + 1 if s == "fail" else 0
...         if streak >= threshold:
...             return True
...     return False
...
>>> should_open_circuit(["ok", "fail", "fail", "fail"])
True
>>> should_open_circuit(["fail", "ok", "fail", "ok"])
False

실습 환경/재현 정보

  • 실행 환경: conda env python100 (Python 3.11.x)
  • 운영 가정: macOS/Linux 공통, UTF-8, 로컬 파일 시스템
  • 실습 방식: REPL에서 코드 블록 순서 실행
  • 재현 체크:
    • 실패 시 statusreason이 누락되지 않는지
    • 로그 레코드에 run_id 또는 단계 정보가 포함되는지
    • 재시도/중단 정책이 코드와 문서에 동시에 반영됐는지
  • 추천 확장:
    • JSONL 로그 저장 + 일별 집계 리포트 자동화
    • 알림 임계값(실패율/지연시간) 기반 자동 경고 연동