결론: 클로저로 상태를 가두고 데코레이터로 공통 관심사를 분리한다
클로저는 함수가 자신이 정의된 환경의 변수를 기억해 나중에 호출될 때도 그 값을 계속 참조하는 특성이다. 데코레이터는 함수를 인자로 받아 새 함수로 감싸는 함수이며, 대부분 클로저로 구현된다. 데이터 분석 파이프라인에서는 함수마다 실행 시간 측정·재시도·로깅 같은 코드를 반복해서 넣는 대신, 그 관심사를 데코레이터 하나로 분리해 재사용한다. 이때 원본 함수의 이름과 설명이 사라지지 않도록 functools.wraps를 함께 쓴다.
클로저: 함수가 자신의 환경을 기억하는 구조
함수 안에서 정의한 내부 함수가 바깥 함수의 지역 변수를 참조하면서 그 함수를 값으로 반환하면, 바깥 함수의 실행이 끝난 뒤에도 내부 함수는 그 변수에 계속 접근할 수 있다. 이 내부 함수를 클로저라고 부른다.
def multiplier(factor):
def multiply(x):
return x * factor # 바깥 함수의 factor를 계속 기억한다
return multiply
double = multiplier(2)
triple = multiplier(3)
print(double(10), triple(10))
20 30
multiplier(2)가 반환된 뒤에도 double은 factor=2라는 환경을 그대로 들고 있다. double과 triple은 같은 코드로 만들어졌지만 서로 다른 factor를 독립적으로 기억하므로 매번 새로운 클로저가 생긴다.
nonlocal로 바깥 변수 값 바꾸기
클로저는 기본적으로 바깥 변수를 읽기만 한다. 내부 함수에서 그 값을 다시 대입하려면 nonlocal로 지역 변수가 아니라 바깥 함수의 변수임을 명시해야 한다.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
counter = make_counter()
print(counter(), counter(), counter())
1 2 3
nonlocal 없이 함수 안에서 count += 1을 그대로 실행하면 count를 새로운 지역 변수로 취급해 UnboundLocalError가 난다. 함수·클래스·모듈의 유효 범위 규칙은 Python 공식 문서의 실행 모델에서 확인할 수 있다 (2026-08-10 확인).
흔한 함정: 반복문에서 늦은 바인딩
클로저는 변수의 값이 아니라 변수 자체를 참조한다. 반복문 안에서 클로저를 여러 개 만들면 함수가 호출되는 시점의 최종 값을 모두 같이 바라본다.
funcs = []
for i in range(3):
funcs.append(lambda: i)
print([f() for f in funcs]) # 기대와 다르게 모두 마지막 i를 가리킨다
# 해결: 기본 인자로 그 순간의 값을 즉시 바인딩한다
funcs_fixed = []
for i in range(3):
funcs_fixed.append(lambda i=i: i)
print([f() for f in funcs_fixed])
[2, 2, 2]
[0, 1, 2]
반복문에서 만든 함수 목록이 모두 같은 값을 반환한다면 늦은 바인딩을 의심한다.
lambda i=i: i처럼 기본 인자에 현재 값을 넘겨 그 시점의 값을 함수 안에 고정한다.
데코레이터: 함수를 감싸 동작을 추가하는 함수
데코레이터는 함수를 인자로 받아 새 함수(대개 클로저)를 반환한다. @decorator 문법은 func = decorator(func)를 줄여 쓴 것과 같다.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("함수 실행 전")
result = func(*args, **kwargs)
print("함수 실행 후")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
print(add(3, 5))
함수 실행 전
함수 실행 후
8
*args, **kwargs로 내부 함수를 정의하면 원본 함수의 매개변수 개수와 이름이 무엇이든 그대로 전달할 수 있어, 데코레이터를 서로 다른 함수에 재사용할 수 있다.
실무 예제: 실행 시간 측정 데코레이터
데이터 분석 파이프라인에서는 어느 단계가 느린지 알아야 병목을 찾을 수 있다. 매 함수마다 시간 측정 코드를 반복하는 대신 데코레이터로 분리한다.
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} 실행 시간: {elapsed:.4f}초")
return result
return wrapper
@timer
def load_rows(n):
"""가짜 데이터를 n개 만든다."""
return list(range(n))
rows = load_rows(1000)
print(load_rows.__name__)
print(load_rows.__doc__)
load_rows 실행 시간: 0.0001초
load_rows
가짜 데이터를 n개 만든다.
실행 시간 수치는 환경마다 다르지만, 중요한 것은 load_rows.__name__이 "wrapper"가 아니라 "load_rows"로 그대로 남는다는 점이다. 그 이유는 다음 절에서 다룬다.
functools.wraps가 필요한 이유
데코레이터가 함수를 감싸면 바깥에서 보이는 함수는 원본이 아니라 내부의 wrapper다. functools.wraps를 쓰지 않으면 __name__·__doc__ 같은 메타데이터가 wrapper의 것으로 바뀐다.
def bare_decorator(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@bare_decorator
def sample():
"""샘플 함수."""
return 1
print(sample.__name__)
print(sample.__doc__)
wrapper
None
원본 이름과 설명이 사라지면 디버깅 로그, 자동 문서화 도구, help()가 모두 엉뚱한 정보를 보여준다. wrapper 정의 바로 위에 @functools.wraps(func)를 붙이면 __name__·__doc__·__module__ 등을 원본 함수에서 복사해 온다. 위 타이머 예제가 load_rows라는 이름을 유지한 것도 이 데코레이터 안에 functools.wraps(func)가 있었기 때문이다. 자세한 동작은 Python 공식 문서의 functools.wraps를 참고한다 (2026-08-10 확인).
매개변수를 받는 데코레이터: 재시도 로직
외부 API·파일 시스템에서 데이터를 읽어 올 때는 일시적인 오류가 흔하다. 재시도 횟수를 데코레이터의 인자로 받으려면 데코레이터를 한 겹 더 감싼다: 바깥 함수는 설정을 받고, 그 안에서 실제 데코레이터를 반환한다.
import functools
import time
def retry(times=3, delay=0.0):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except ValueError as exc:
last_error = exc
print(f"{func.__name__} 시도 {attempt}/{times} 실패: {exc}")
time.sleep(delay)
raise last_error
return wrapper
return decorator
calls = {"n": 0}
@retry(times=3)
def flaky_fetch():
calls["n"] += 1
if calls["n"] < 3:
raise ValueError("일시적 오류")
return "ok"
print(flaky_fetch())
flaky_fetch 시도 1/3 실패: 일시적 오류
flaky_fetch 시도 2/3 실패: 일시적 오류
ok
retry(times=3)는 먼저 decorator 함수를 클로저로 만들어 times와 delay를 기억하게 하고, 그 decorator가 실제 함수를 감싼다. 모든 시도가 실패하면 마지막 예외를 그대로 다시 던져 호출한 쪽이 실패를 알 수 있게 한다. 재시도 대상 예외를 넓게 잡으면 프로그램 로직 오류까지 감춰버리므로, 여기서는 ValueError처럼 실제로 재시도할 만한 예외만 잡는다.
데코레이터 여러 개 쌓기
데코레이터는 아래에서 위로 적용되고, 실행은 위에서 아래로 시작해 안쪽 함수를 거쳐 다시 바깥으로 나온다.
def decorator1(func):
def wrapper(*args, **kwargs):
print("[decorator1] 실행 전")
result = func(*args, **kwargs)
print("[decorator1] 실행 후")
return result
return wrapper
def decorator2(func):
def wrapper(*args, **kwargs):
print("[decorator2] 실행 전")
result = func(*args, **kwargs)
print("[decorator2] 실행 후")
return result
return wrapper
@decorator1
@decorator2
def say_hello():
print("안녕하세요!")
say_hello()
[decorator1] 실행 전
[decorator2] 실행 전
안녕하세요!
[decorator2] 실행 후
[decorator1] 실행 후
say_hello는 먼저 decorator2로 감싸지고, 그 결과가 다시 decorator1로 감싸진다. 그래서 실행 순서는 decorator1의 시작 → decorator2의 시작 → 원본 함수 → decorator2의 종료 → decorator1의 종료 순이 된다. 로깅·인증·캐싱처럼 순서가 결과에 영향을 주는 데코레이터를 쌓을 때는 이 순서를 먼저 그려 보고 붙인다.
클로저·데코레이터 활용 정리
패턴 | 쓰는 이유 | 예 |
|---|---|---|
클로저 | 함수 호출 사이에 상태를 안전하게 가둔다 | 카운터, 배율 등 설정을 기억하는 함수 생성기 |
단순 데코레이터 | 함수 실행 앞뒤에 공통 동작을 끼운다 | 실행 시간 측정, 호출 로깅 |
파라미터화 데코레이터 | 데코레이터 동작 자체를 설정 가능하게 만든다 | 재시도 횟수, 캐시 TTL 지정 |
functools.wraps | 감싸진 함수의 정체성을 보존한다 | 디버깅, 문서화, introspection |
자주 발생하는 문제
nonlocal을 빠뜨리고 클로저 안에서 바깥 변수에 대입하면 그 변수를 새 지역 변수로 취급해UnboundLocalError가 난다.반복문에서 만든 클로저·람다가 모두 같은 값을 반환하면 늦은 바인딩이다. 기본 인자로 그 순간의 값을 고정한다.
functools.wraps를 빠뜨리면__name__·__doc__가 wrapper의 것으로 바뀌어 로그와 문서화가 엉뚱한 이름을 보여준다.데코레이터 안에서 예외를 무조건 삼키면 실패한 호출이 성공한 것처럼 보인다. 재시도처럼 의도한 경우가 아니면 예외를 그대로 다시 던진다.
클로저가 큰 DataFrame이나 연결 객체를 계속 붙잡고 있으면 그 객체가 예상보다 오래 메모리에 남는다. 클로저에 넣는 값은 필요한 만큼만 좁힌다.
다음 글: 예외 처리와 사용자 정의 예외
다음 글에서는 try/except/else/finally의 역할을 정리하고, 이번 글의 재시도 데코레이터처럼 실패를 다루는 코드에 어떤 예외를 잡을지, 파이프라인 실패 경계를 어디에 둘지, 사용자 정의 예외를 언제 만들지를 다룬다.
참고 자료
Python 공식 문서: functools.wraps — 데코레이터가 감싼 함수의 메타데이터 보존 (2026-08-10 확인)
Python 공식 문서: 실행 모델 (Naming and binding) — nonlocal과 유효 범위 규칙 (2026-08-10 확인)
Python 공식 문서: Compound statements — 함수 정의 — 데코레이터 문법과 적용 순서 (2026-08-10 확인)
Python 공식 용어집: closure — 클로저의 공식 정의 (2026-08-10 확인)
댓글 0
댓글을 불러오는 중…