클로저(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_function
은 outer_var
값인 10을 기억하고 있으며, inner_var
로 받은 5를 더해 15를 출력합니다.
다시 말해 inner_function
은 외부 함수가 끝난 뒤에도 outer_var
을 기억하고 활용합니다. 이러한 특성을 갖춘 함수를 클로저라고 합니다.
클로저가 성립하기 위한 조건과 특성은 다음과 같습니다:
클로저는 반드시 내부에 다른 함수를 포함하는 구조입니다.
내부 함수가 자신의 로컬 스코프에 정의되지 않은 변수, 즉 외부 스코프에 존재하는 변수를 참조할 수 있습니다.
__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
클로저는 객체지향 프로그래밍 없이도 간단하게 상태를 저장할 수 있게 해줍니다.
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
데코레이터는 기존의 함수를 확장하거나 수정할 때 자주 사용됩니다.
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이 종료되었습니다.
반복적 로직을 가진 여러 유사 함수를 생성할 때도 클로저를 활용할 수 있습니다.
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
클로저 내에서 변수를 참조할 때 예상하지 못한 공유가 발생할 수 있습니다.
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
클로저가 참조하는 변수들이 오랫동안 메모리에 남아 있어 메모리 누수가 발생할 수 있습니다. 이를 방지하려면 클로저의 사용 범위를 명확히 관리해야 합니다.
메모리 누수를 방지하는 방법은 주로 다음과 같이 구체화할 수 있습니다.
클로저가 외부 변수를 필요 이상으로 참조하지 않도록 최소한의 변수만 유지해야 합니다.
[잘못된 예]
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 # 명시적으로 메모리 해제 가능
약한 참조를 사용하면 참조되는 객체가 필요 없을 때 자동으로 메모리에서 삭제됩니다.
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
사용하지 않는 클로저 인스턴스는 명시적으로 제거하는 습관을 가집니다.
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 # 명시적 삭제
상태 유지가 과도하거나 복잡한 경우, 간단한 클래스나 제너레이터로 바꾸면 메모리 관리가 쉬워집니다.
클래스 사용 예시:
class Counter:
def __init__(self):
self.count = 0
def increment(self):
self.count += 1
return self.count
counter = Counter()
print(counter.increment())
클래스는 내부 상태를 더 명확히 관리할 수 있도록 해줍니다.
클로저는 파이썬 개발자에게 코드의 간결성, 확장성, 재사용성을 높여주는 매우 효과적인 도구입니다. 하지만 클로저의 개념과 주의사항을 명확히 이해하고 사용해야 잠재적인 문제들을 예방하고 더 나은 코드를 작성할 수 있습니다.