[ Python ] Closure와 Decorator

정승민·2026년 9월 22일

TIL

목록 보기
1/6

TIL - Python Closure와 Decorator

오늘은 Python의 Closure(클로저) 와 Decorator(데코레이터) 에 대해 정리한다.


1. Closure는 왜 사용할까?

📌 함수의 Namespace는 언제까지 유지될까?

Python에서 함수를 호출하면 해당 함수만의 Local Namespace가 만들어진다.

def example(x):
    y = x + 1
    return y

example()이 실행되는 동안에는 x, y 같은 지역 변수가 존재하지만,
함수 실행이 끝나면 일반적으로 해당 호출의 Local Namespace도 더 이상 유지할 필요가 없어진다.

그렇다면 이런 상황은 어떨까?

💡 함수가 실행될 때 만들어진 상태를 나중에도 기억해서 사용하고 싶다면?

이럴 때 사용할 수 있는 개념이 Closure다.


2. Closure란?

📌 외부 함수의 값을 기억하는 내부 함수

Closure는 쉽게 말하면,

내부 함수가 자신이 만들어질 당시 외부 함수의 변수를 기억하는 구조

라고 이해할 수 있다.

def outer(x):
    def inner():
        return x

    return inner

사용해보면 다음과 같다.

func = outer(10)

print(func())
# 10

outer(10)의 실행은 이미 끝났지만,
반환된 inner 함수는 자신이 사용하고 있는 x=10을 계속 기억한다.

🚨 정확히는 외부 함수의 Namespace 전체를 저장하는 것은 아니다.

내부 함수가 실제로 참조하는 외부 변수(Free Variable) 를 Closure가 기억한다.

즉,

def outer(x):
    a = 100

    def inner():
        return x

    return inner

위 코드에서 inner()는 x만 사용하므로,
Closure에서 중요한 값은 x다.


3. Closure를 사용하는 이유

Closure를 사용하면 함수가 특정 상태를 기억하도록 만들 수 있다.

def make_multiplier(n):

    def multiply(x):
        return x * n

    return multiply
double = make_multiplier(2)
triple = make_multiplier(3)

print(double(10))
# 20

print(triple(10))
# 30

double은 n=2,
triple은 n=3이라는 상태를 각각 기억한다.

🎯 Closure의 핵심

함수가 만들어질 당시의 상태를 기억하고,
이후 호출에서도 그 상태를 계속 사용할 수 있다.

이 덕분에 전역 변수를 사용하지 않고도
함수마다 독립적인 상태를 유지할 수 있다.


4. Closure를 이용해 기존 함수에 기능 추가하기

Closure를 활용하면 다른 함수를 입력받아서 기능을 추가한 새로운 함수를 반환할 수도 있다.

예를 들어 함수 실행 시간을 측정해보자.

import time

def time_decorator(func):

    def wrapper(*args, **kwargs):

        start_time = time.time()

        result = func(*args, **kwargs)

        total_time = time.time() - start_time

        print(f"time : {total_time}")

        return result

    return wrapper

그리고 원래 함수가 있다고 하자.

def custom_add(x, y):
    print(x + y)
    return x + y

이 함수를 time_decorator에 전달한다.

external_func = time_decorator(custom_add)

external_func(4, 4)

실행 흐름은 다음과 같다.

custom_add
    ↓
time_decorator(custom_add)
    ↓
wrapper 반환
    ↓
external_func = wrapper
    ↓
external_func(4, 4)
    ↓
wrapper(4, 4) 실행
    ↓
custom_add(4, 4) 실행 + 실행시간 측정

여기서 wrapper는 외부 함수인 time_decorator()가 끝난 뒤에도
func, 즉 custom_add를 기억하고 있다.

따라서 wrapper는 Closure다.

💡 wrapper라는 이름은 Python의 특별한 문법이 아니라,
기존 함수를 감싸는 함수에 관례적으로 자주 사용하는 이름이다.


5. 그런데 매번 이렇게 써야 할까?

현재 방식에서는 새로운 기능을 적용할 때마다 아래와 같이 작성해야 한다.

external_func = time_decorator(custom_add)
external_func(4, 4)

Python에서는 이 패턴을 더 명확하고 간결하게 표현하기 위해
Decorator 문법을 제공한다.


6. Decorator란?

📌 기존 함수를 수정하지 않고 기능을 추가하는 방법

Decorator는 한 문장으로 정리하면,

기존 함수의 코드를 직접 수정하지 않고, 그 함수를 감싸서 새로운 기능을 추가하는 방법

이다.

다음과 같이 사용할 수 있다.

@time_decorator
def custom_mul(x, y):
    print(x * y)
    return x * y

이 코드는 사실 다음 코드와 동일한 의미다.

def custom_mul(x, y):
    print(x * y)
    return x * y

custom_mul = time_decorator(custom_mul)

즉,

@time_decorator

는 단순히 보기 좋게 만드는 문법만이 아니라,

custom_mul을 time_decorator에 전달하고,
반환된 새로운 함수로 custom_mul을 교체한다

는 의미다.

이제 다음과 같이 호출하면 된다.

custom_mul(3, 4)

실제로는 time_decorator에서 반환된 wrapper()가 실행되고,
그 안에서 원래 custom_mul()이 실행된다.


7. 왜 wrapper에서 *args, **kwargs를 사용할까?

Decorator는 어떤 함수에 적용될지 미리 알 수 없다.

예를 들어,

def add(x, y):
    ...

일 수도 있고,

def user_info(name, age, city="Seoul"):
    ...

일 수도 있다.

따라서 모든 형태의 인자를 받을 수 있도록 보통 다음과 같이 작성한다.

def wrapper(*args, **kwargs):
    return func(*args, **kwargs)
  • *args : 위치 인자
  • **kwargs : 키워드 인자

이렇게 작성하면 다양한 함수에 동일한 Decorator를 적용할 수 있다.


8. 함수형 Decorator와 Closure의 관계

함수형 Decorator는 보통 Closure 구조를 사용한다.

def decorator(func):

    def wrapper(*args, **kwargs):

        # 실행 전 추가 기능

        result = func(*args, **kwargs)

        # 실행 후 추가 기능

        return result

    return wrapper

wrapper()는 외부 함수의 func를 기억하고 있으므로 Closure다.

즉,

Decorator 자체가 Closure인 것은 아니지만,
함수형 Decorator를 구현할 때 Closure가 매우 자주 사용된다.


9. Decorator를 Class로 만들 수도 있다

Decorator가 기억해야 할 상태가 많아지면
함수형 Closure 대신 Class를 사용할 수도 있다.

예를 들어 함수가 몇 번 호출되었는지 기록하는 Decorator를 만들어보자.

class CallCounter:

    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):

        self.count += 1

        print(f"{self.count}번째 호출")

        return self.func(*args, **kwargs)

사용 방법은 함수형 Decorator와 동일하다.

@CallCounter
def hello(name):
    print(f"Hello {name}")
hello("Min")
hello("Max")

출력 결과는 다음과 같다.

1번째 호출
Hello Min

2번째 호출
Hello Max

@CallCounter는 사실 다음과 같다.

hello = CallCounter(hello)

따라서 hello는 이제 원래 함수가 아니라
CallCounter 객체를 가리킨다.

하지만 CallCounter 클래스에 __call__()이 구현되어 있기 때문에

hello("Min")

처럼 함수처럼 호출할 수 있다.


10. 왜 Class Decorator를 사용할까?

단순한 기능이라면 함수형 Decorator로 충분하다.

하지만 관리해야 할 상태가 많아진다면 Class가 더 편할 수 있다.

self.func
self.count
self.config
self.logger

이런 상태들을 self.xxx 형태로 관리할 수 있고,
필요하다면 추가 메서드도 만들 수 있다.

상황추천 방식
간단한 기능 추가함수형 Decorator
간단한 상태 기억Closure
여러 상태를 관리Class Decorator
여러 메서드와 복잡한 동작 필요Class Decorator

🎯 Class Decorator만 할 수 있는 특별한 기능이라기보다는,
복잡도가 높아질수록 Class로 구조화하는 것이 관리하기 편해지는 것이다.


11. Closure와 Decorator 정리

📌 Closure

외부 함수 실행
    ↓
내부 함수 생성
    ↓
내부 함수가 외부 변수 참조
    ↓
내부 함수 반환
    ↓
외부 함수가 끝난 이후에도 필요한 값을 기억

함수가 자신이 만들어질 당시의 외부 상태를 기억할 수 있게 해주는 구조다.


📌 Decorator

원래 함수
    ↓
Decorator에 전달
    ↓
기능을 추가한 함수 또는 객체 반환
    ↓
원래 함수 이름에 다시 연결

기존 함수 코드를 수정하지 않고 공통 기능을 추가하는 방법이다.

대표적으로 다음과 같은 기능에서 사용할 수 있다.

  • 실행 시간 측정
  • 로그 기록
  • 권한 검사
  • 호출 횟수 기록
  • 입력값 검증
  • 캐싱

12. 한 줄 정리

💡 Closure는 함수가 상태를 기억하게 만드는 방법이고, Decorator는 그 구조 등을 활용하여 기존 함수에 공통 기능을 추가하는 패턴이다.


추가로 알아두면 좋은 점

실제 Decorator를 작성할 때는 원래 함수의 이름과 문서 정보가 유지되도록
functools.wraps를 사용하는 경우가 많다.

from functools import wraps
import time

def time_decorator(func):

    @wraps(func)
    def wrapper(*args, **kwargs):

        start_time = time.time()

        result = func(*args, **kwargs)

        print(f"time : {time.time() - start_time}")

        return result

    return wrapper

이 부분은 Decorator의 기본 원리를 이해한 뒤 추가로 공부하면 된다.

profile
매일 꾸준히

0개의 댓글