Python Closure

Dev Smile·2025년 6월 8일
1
post-thumbnail

클로저(Closure)는 소프트웨어 개발에서 효율적인 코드 작성과 고급 패턴 구현을 가능하게 하는 중요한 프로그래밍 기법입니다. 특히 파이썬은 함수형 프로그래밍 패러다임을 지원하기 때문에 클로저를 적극적으로 활용할 수 있습니다. 이번 포스트에서는 클로저의 정의부터 주요 특징, 다양한 활용 사례, 주의할 점까지 깊이 있게 다루겠습니다.


클로저란 무엇인가?

클로저는 간단히 말해, 자신이 정의된 스코프(scope)의 외부에서 호출되더라도 자신이 속한 환경의 변수들을 기억하고 참조할 수 있는 함수입니다. 이는 보통 함수가 다른 함수를 반환하거나 내부 함수가 외부의 변수를 사용할 수 있게 할 때 사용됩니다.

파이썬으로 클로저를 명확히 이해하려면 다음과 같은 예시를 살펴봅시다.

def outer_function(outer_var):
    def inner_function(inner_var):
        return outer_var + inner_var
    return inner_function

closure_instance = outer_function(10)
print(closure_instance(5))  # 출력: 15

outer_function이 반환한 inner_functionouter_var 값인 10을 기억하고 있으며, inner_var로 받은 5를 더해 15를 출력합니다.

다시 말해 inner_function은 외부 함수가 끝난 뒤에도 outer_var을 기억하고 활용합니다. 이러한 특성을 갖춘 함수를 클로저라고 합니다.


클로저의 주요 특징

클로저가 성립하기 위한 조건과 특성은 다음과 같습니다:

1. 함수의 중첩(Nested Functions)

클로저는 반드시 내부에 다른 함수를 포함하는 구조입니다.

2. 자유 변수(Free Variables)

내부 함수가 자신의 로컬 스코프에 정의되지 않은 변수, 즉 외부 스코프에 존재하는 변수를 참조할 수 있습니다.

3. 클로저 속성 (__closure__)

클로저가 생성된 함수는 __closure__ 속성을 통해 저장된 환경 변수들의 정보를 확인할 수 있습니다.

def outer_function(outer_var):
    def inner_function():
        return outer_var
    return inner_function

closure_instance = outer_function(42)
print(closure_instance.__closure__[0].cell_contents)  # 출력: 42

클로저의 다양한 활용 사례

1. 상태 유지 및 관리

클로저는 객체지향 프로그래밍 없이도 간단하게 상태를 저장할 수 있게 해줍니다.

def counter():
    count = 0

    def increment():
        nonlocal count
        count += 1
        return count

    return increment

count_up = counter()
print(count_up())  # 출력: 1
print(count_up())  # 출력: 2

2. 데코레이터(Decorator) 구현

데코레이터는 기존의 함수를 확장하거나 수정할 때 자주 사용됩니다.

def logger(func):
    def wrapper(*args, **kwargs):
        print(f"함수 {func.__name__}이 호출되었습니다.")
        result = func(*args, **kwargs)
        print(f"함수 {func.__name__}이 종료되었습니다.")
        return result
    return wrapper

@logger
def greet():
    print("안녕하세요!")

greet()
# 출력:
# 함수 greet이 호출되었습니다.
# 안녕하세요!
# 함수 greet이 종료되었습니다.

3. 함수 팩토리(Function Factory)

반복적 로직을 가진 여러 유사 함수를 생성할 때도 클로저를 활용할 수 있습니다.

def multiplier_of(n):
    def multiplier(x):
        return x * n
    return multiplier

double = multiplier_of(2)
triple = multiplier_of(3)

print(double(5))  # 출력: 10
print(triple(5))  # 출력: 15

클로저 사용 시 주의할 점

1. 자유 변수의 공유 문제

클로저 내에서 변수를 참조할 때 예상하지 못한 공유가 발생할 수 있습니다.

def make_multipliers():
    multipliers = []
    for i in range(5):
        multipliers.append(lambda x: x * i)
    return multipliers

multiplier_funcs = make_multipliers()
print(multiplier_funcs[0](2))  # 출력: 8 (기대값: 0)

위 코드에서 lambda x: x * i에서 i는 루프가 끝난 뒤의 최종 값(4) 를 참조하게 되기 때문에, x * 4가 되어 8이 출력됩니다.

이는 변수를 기본값 인수로 전달하여 해결할 수 있습니다:

def make_multipliers():
    return [lambda x, i=i: x * i for i in range(5)]

multiplier_funcs = make_multipliers()
print(multiplier_funcs[0](2))  # 출력: 0

2. 메모리 누수 문제

클로저가 참조하는 변수들이 오랫동안 메모리에 남아 있어 메모리 누수가 발생할 수 있습니다. 이를 방지하려면 클로저의 사용 범위를 명확히 관리해야 합니다.

메모리 누수를 방지하는 방법은 주로 다음과 같이 구체화할 수 있습니다.

방법 1. 명확한 참조 관리

클로저가 외부 변수를 필요 이상으로 참조하지 않도록 최소한의 변수만 유지해야 합니다.

[잘못된 예]

def create_large_data_holder(data):
    def inner():
        return data
    return inner

large_data = [x for x in range(10**7)]
holder = create_large_data_holder(large_data)

# large_data가 계속 메모리에 유지됨

위의 경우에서 가비지 컬렉터는 “더 이상 참조가 없다” 고 판단할 때만 메모리를 회수하므로, 이 상황에선 회수가 불가능합니다.

[개선된 예]

def create_large_data_holder(data):
    important_summary = sum(data)  # 필요한 정보만 추출

    def inner():
        return important_summary
    return inner

large_data = [x for x in range(10**7)]
holder = create_large_data_holder(large_data)

del large_data  # 명시적으로 메모리 해제 가능

방법 2. weakref(약한 참조) 사용하기

약한 참조를 사용하면 참조되는 객체가 필요 없을 때 자동으로 메모리에서 삭제됩니다.

import weakref

class LargeObject:
    def __init__(self, data):
        self.data = data

def closure_with_weakref(obj):
    weak_obj = weakref.ref(obj)

    def inner():
        obj_ref = weak_obj()
        if obj_ref is None:
            return "Object has been garbage collected"
        return obj_ref.data
    return inner

large_obj = LargeObject([1, 2, 3])
closure = closure_with_weakref(large_obj)

print(closure())  # 출력: [1, 2, 3]

del large_obj  # 명시적 객체 삭제
print(closure())  # 출력: Object has been garbage collected

방법 3. 필요하지 않은 클로저 명시적 삭제

사용하지 않는 클로저 인스턴스는 명시적으로 제거하는 습관을 가집니다.

def data_keeper():
    data = [x for x in range(10**6)]

    def inner():
        return len(data)

    return inner

keeper = data_keeper()
print(keeper())  # 사용 후

del keeper  # 명시적 삭제

방법 4. 클로저보다 클래스나 제너레이터 활용

상태 유지가 과도하거나 복잡한 경우, 간단한 클래스나 제너레이터로 바꾸면 메모리 관리가 쉬워집니다.

클래스 사용 예시:

class Counter:
    def __init__(self):
        self.count = 0

    def increment(self):
        self.count += 1
        return self.count

counter = Counter()
print(counter.increment())

클래스는 내부 상태를 더 명확히 관리할 수 있도록 해줍니다.


결론

클로저는 파이썬 개발자에게 코드의 간결성, 확장성, 재사용성을 높여주는 매우 효과적인 도구입니다. 하지만 클로저의 개념과 주의사항을 명확히 이해하고 사용해야 잠재적인 문제들을 예방하고 더 나은 코드를 작성할 수 있습니다.

0개의 댓글