[파이썬 100강] 26강. 데코레이터로 공통 로직 깔끔하게 분리하기

[파이썬 100강] 26강. 데코레이터로 공통 로직 깔끔하게 분리하기

데코레이터로 공통 로직 깔끔하게 분리하기 주제는 초보에게는 조금 크게 느껴질 수 있지만, 사실은 지금까지 배운 작은 문법들을 안정적으로 연결하는 연습입니다. 이번 강의에서는 함수를 감싸는 래퍼를 만들어 로깅·권한체크·성능측정을 재사용하는 구조 을 중심으로, 왜 이 개념이 현업에서 반복적으로 사용되는지, 어떤 실수를 가장 많이 하는지, 그리고 어디까지를 "실전에서 바로 쓸 수 있는 수준"으로 보면 되는지를 차근차근 정리합니다. 핵심은 화려한 트릭이 아니라, 입력-처리-출력의 흐름을 명확하게 만들고 실패했을 때 다시 실행해도 같은 결과를 낼 수 있게 구성하는 것입니다.

핵심 개념

  • 함수를 감싸는 래퍼를 만들어 로깅·권한체크·성능측정을 재사용하는 구조
  • 예제 코드를 단순 실행에서 끝내지 않고, "왜 이렇게 짰는지"를 설명 가능한 구조로 바꾸는 습관
  • 작은 단위 함수로 분리해 테스트 가능성과 유지보수성을 높이는 방식

이번 강의의 핵심은 한 줄 문법 암기가 아니라 의도와 경계를 분명히 하는 것입니다. 예를 들어 같은 동작을 하더라도 "입력 검증"이 먼저 오고, "처리"는 예외 상황을 가정하며, "출력"은 사람이 읽을 수 있는 형태로 정리되어야 합니다. 초보 단계에서 이 순서를 몸에 익혀두면 이후 pandas, 웹 API, 배치 작업으로 넘어가도 코드가 쉽게 망가지지 않습니다. 또한 함수 단위로 나누면 디버깅할 때 어디가 잘못됐는지 빠르게 확인할 수 있고, 팀 협업 시 코드 리뷰도 훨씬 수월해집니다.

왜 중요한가 (실무 맥락)

  • 중복 코드를 제거하고 핵심 비즈니스 로직을 더 읽기 쉽게 유지할 수 있기 때문
  • 개발/운영 환경이 달라져도 재현 가능한 결과를 내는 코드 구조가 필수
  • 장애가 났을 때 원인을 추적할 수 있도록 로그·에러 메시지를 설계해야 함

실무에서는 "한 번만 돌면 되는 코드"보다 "매일 돌려도 안전한 코드"가 훨씬 중요합니다. 특히 자동화나 운영성 도구는 정상 케이스보다 예외 케이스를 더 많이 만나기 때문에, 입력 데이터가 비어 있거나 형식이 틀려도 동작이 통제되어야 합니다. 또 코드가 길어질수록 사람은 맥락을 잊기 쉽기 때문에, 함수 이름과 변수 이름 자체가 문서 역할을 하도록 써야 합니다. 결국 좋은 코드는 멋진 코드가 아니라, 다음 사람이 읽어도 같은 판단을 내릴 수 있는 코드입니다.

기본 사용 (REPL 예제 3개 이상)

예제 1) 가장 기본 패턴

>>> import time
from functools import wraps

해설:

  • 필요한 모듈이나 기본 구조를 먼저 선언해 작업의 출발점을 고정합니다.
  • 초반에 의존성을 명확히 적어두면 디버깅 시 "무엇이 준비되어야 하는지" 바로 알 수 있습니다.

예제 2) 처리 로직 확장

>>> def timer(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = fn(*args, **kwargs)
        print(f"elapsed={time.perf_counter()-start:.4f}s")
        return result
    return wrapper

해설:

  • 핵심 계산/처리 로직은 함수로 감싸 재사용 가능하게 만드는 것이 좋습니다.
  • 반복되는 분기 로직은 함수 내부에서 책임지게 하여 호출부를 단순화합니다.

예제 3) 실전형 미니 케이스

>>> @timer
def add(a,b):
    return a+b

해설:

  • 실무에서는 처리 결과를 콘솔 출력만 하지 않고 파일/로그/리턴값으로 남겨야 합니다.
  • 결과 포맷을 일정하게 유지하면 다음 단계(테스트, 모니터링, 자동 발행)로 연결하기 쉽습니다.

예제 4) 검증 가능한 함수 경계 만들기

>>> def run_pipeline(items):
...     if items is None:
...         raise ValueError("items는 None일 수 없습니다")
...     cleaned = [x for x in items if x]
...     return {"input": len(items), "output": len(cleaned)}
...
>>> run_pipeline(["a", "", "b", None])
{'input': 4, 'output': 2}

해설:

  • 입력 검증을 앞단에 두면 장애를 조기에 발견할 수 있습니다.
  • 함수 반환 타입을 일정한 dict 구조로 맞추면 후속 처리(저장/로그/테스트)가 쉬워집니다.

자주 하는 실수 (최소 2개)

실수 1) 입력 검증 없이 바로 처리 시작

>>> def unsafe(items):
...     return [x.strip() for x in items]
...
>>> unsafe(None)
Traceback (most recent call last):
... TypeError: 'NoneType' object is not iterable

원인:

  • 정상 데이터만 온다고 가정하고 방어 코드를 생략했기 때문입니다.
  • 입력 경계에서 검증을 하지 않으면 에러가 처리 중간에 터져 원인 추적이 어려워집니다.

해결:

>>> def safe(items):
...     if items is None:
...         return []
...     return [str(x).strip() for x in items if x is not None]
...
>>> safe(None)
[]

실수 2) 예외를 무시하고 성공처럼 처리

  • 증상: 실패했는데도 "완료" 메시지가 출력되어 운영자가 장애를 놓칩니다.
  • 원인: except: pass처럼 예외를 삼키는 패턴을 사용했기 때문입니다.
  • 해결: 사용자용 메시지와 개발자용 로그를 분리하고, 필요한 경우 예외를 다시 올립니다.

실수 3) 출력 형식이 매번 달라 후속 자동화가 깨짐

  • 증상: 어떤 날은 문자열, 어떤 날은 dict를 반환하여 배치 파이프라인이 중단됩니다.
  • 원인: 함수 반환 규약(contract)을 정하지 않은 상태에서 구현을 계속 변경함.
  • 해결: 반환 스키마를 먼저 정하고 테스트 코드로 고정합니다.

실무 패턴 (운영 관점)

  • 입력 검증 규칙: 필수 인자 누락, 타입 불일치, 빈 데이터 여부를 함수 입구에서 즉시 확인
  • 로그/예외 처리 규칙: 사용자에게는 짧은 안내, 로그에는 상세 원인(스택트레이스 포함)을 기록
  • 재사용 구조화 팁: load -> transform -> save 3단계 함수 분리로 테스트 범위를 명확히 유지
  • 성능/메모리 체크: 데이터량이 커질 가능성이 있으면 제너레이터, 스트리밍 처리, 청크 단위 분할 적용

운영 코드는 "한 번 돌고 끝"이 아니라 장기간 누적되는 실행 기록이 생명입니다. 따라서 함수 이름, 리턴 구조, 에러 메시지까지 팀 표준을 맞춰야 합니다. 또한 작은 프로젝트라도 README에 실행 예시를 남기고, 명령 인자/환경 변수/출력 파일 위치를 문서화해 두는 습관이 중요합니다. 이 문서화가 잘 되어 있으면 담당자가 바뀌어도 인수인계 비용이 크게 줄어듭니다.

오늘의 결론

한 줄 요약: 데코레이터로 공통 로직 깔끔하게 분리하기의 핵심은 문법 자체보다, 안정적으로 반복 실행 가능한 구조를 만드는 데 있습니다.

기억할 것:

  • 정상 케이스보다 실패 케이스를 먼저 설계하면 운영 품질이 올라갑니다.
  • 함수 경계(입력/출력 계약)를 고정하면 디버깅과 테스트가 쉬워집니다.
  • 결과를 기록 가능한 형태(파일/로그/리턴값)로 남겨야 자동화가 완성됩니다.

연습문제 (정확히 3개)

  1. 오늘 예제를 기반으로 입력 검증 함수를 별도로 분리하고, 잘못된 입력 3가지에 대한 처리 결과를 작성해보세요.
  2. 처리 결과를 dict로 고정한 뒤, 성공/실패 케이스 각각에서 동일한 키를 반환하도록 리팩터링해보세요.
  3. 실행 로그를 파일로 남기고, 마지막 10줄만 읽어 요약을 출력하는 보조 함수를 추가해보세요.

이전 강의 정답 (25강)

  1. 리스트 컴프리헨션 변환
>>> nums=[1,2,3,4]
[n*n for n in nums]
[1, 4, 9, 16]
  1. 제너레이터 합계
>>> sum(n for n in range(5))
10
  1. 메모리 비교 관찰
>>> import sys
a=[n for n in range(1000)]
g=(n for n in range(1000))
(sys.getsizeof(a), sys.getsizeof(g))
(상대적으로 리스트가 더 큼)

실습 환경/재현 정보

  • 실행 환경: conda env python100 (Python 3.11.14)
  • 가정한 OS: macOS/Linux 공통
  • 검증 명령: python --version, which python, pip list | head
  • 인코딩: UTF-8 기준, 파일 입출력은 encoding="utf-8" 명시 권장
  • 재현 절차: 새 디렉터리 생성 → 예제 코드 실행 → 출력/로그 파일 확인 → 예외 케이스 재실행