모든 강의를 무료로 볼 수 있어요. 회원가입 없이도 학습 가능합니다.

21강 / 전체 32강

클로저 — 함수가 상태를 기억하는 방법

9분 읽기 조회 5

클로저의 자유 변수 포획 메커니즘을 이해하고 반복문 함정과 팩토리 패턴 활용법을 배운다.

🎯 이 강의에서 배우는 것

클로저(closure)는 "자신이 정의된 환경(스코프)을 기억하는 함수"입니다. 외부 함수가 실행을 끝내고 스택에서 사라져도, 내부 함수는 외부 함수의 변수를 여전히 기억합니다. 이 메커니즘은 데코레이터, 팩토리 패턴, 콜백 등 파이썬의 핵심 패턴들을 가능하게 합니다.

🧩 클로저의 기본 — 자유 변수 포획

def make_multiplier(factor):
    # factor는 make_multiplier의 지역 변수

    def multiplier(x):
        return x * factor   # factor를 포획(capture)!
        # 외부 함수가 끝나도 factor를 기억함

    return multiplier   # 함수 객체를 반환

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

print(double(5))   # 10  (factor=2를 기억)
print(triple(5))   # 15  (factor=3을 기억)
print(double(7))   # 14  (각 클로저가 독립적인 factor를 가짐)

# 클로저 내부 검사
print(double.__name__)         # 'multiplier'
print(double.__closure__)      # (<cell at 0x...>,) ← 셀 객체
print(double.__closure__[0].cell_contents)   # 2 ← factor의 값
print(double.__code__.co_freevars)   # ('factor',) ← 자유 변수 이름

🔬 왜 자유 변수가 살아있는가 — 셀 객체

import sys

def make_closure():
    value = "나는 자유 변수"
    ref_count_before = sys.getrefcount(value)
    print(f"함수 안에서 참조 수: {ref_count_before}")

    def inner():
        print(value)

    return inner

f = make_closure()   # make_closure의 스택 프레임이 사라짐
# 하지만 value는 여전히 살아있다!

f()   # "나는 자유 변수" — 출력됨!

# 왜 살아있는가?
# 1. 파이썬은 value를 "셀(cell) 객체"로 감쌈
# 2. inner 함수와 make_closure 스택 모두 같은 셀을 참조
# 3. make_closure가 반환될 때 스택은 사라지지만
#    셀은 힙 메모리로 이동
# 4. f(inner 함수)가 셀을 참조하는 한 셀은 살아있음
# 5. f가 삭제되면 셀도 가비지 컬렉션됨

del f
# 이제 value도 가비지 컬렉션 대상

⚠️ 반복문에서의 클로저 함정

# 유명한 클로저 버그
functions = []
for i in range(5):
    def f():
        return i   # i는 루프 변수 — 포획!
    functions.append(f)

# 예상: [0, 1, 2, 3, 4]
# 실제:
print([func() for func in functions])   # [4, 4, 4, 4, 4] ← 버그!

# 왜? 모든 f 함수가 같은 i 변수(셀)를 참조
# 루프가 끝나면 i = 4, 모든 함수가 4를 반환

# 해결 1: 기본값 매개변수 트릭 (값을 "고정"함)
functions_fixed = []
for i in range(5):
    def f(captured_i=i):   # i의 현재 값을 기본값으로 복사
        return captured_i
    functions_fixed.append(f)

print([func() for func in functions_fixed])   # [0, 1, 2, 3, 4] ← 올바름!

# 해결 2: 즉시 실행 함수(팩토리 함수)로 감싸기
functions_fixed2 = []
for i in range(5):
    def make_f(n):
        def f():
            return n   # n은 make_f의 지역 변수 — 독립적
        return f
    functions_fixed2.append(make_f(i))

print([func() for func in functions_fixed2])   # [0, 1, 2, 3, 4]

# 해결 3: functools.partial
from functools import partial

def f_base(n):
    return n

functions_fixed3 = [partial(f_base, i) for i in range(5)]
print([func() for func in functions_fixed3])   # [0, 1, 2, 3, 4]

🏭 팩토리 패턴 — 클로저의 실전 활용

# 검증기 팩토리
def make_range_validator(min_val, max_val):
    def validate(value):
        if value < min_val:
            return False, f"{value}는 최솟값 {min_val}보다 작습니다"
        if value > max_val:
            return False, f"{value}는 최댓값 {max_val}보다 큽니다"
        return True, "유효"
    return validate

age_validator = make_range_validator(0, 150)
score_validator = make_range_validator(0, 100)

print(age_validator(25))    # (True, '유효')
print(age_validator(-1))    # (False, '-1는 최솟값 0보다 작습니다')
print(score_validator(105)) # (False, '105는 최댓값 100보다 큽니다')

# 로거 팩토리
def make_logger(level, prefix=""):
    def log(message):
        full_prefix = f"[{level}]" + (f"[{prefix}]" if prefix else "")
        print(f"{full_prefix} {message}")
    return log

error_log = make_logger("ERROR", "DB")
info_log = make_logger("INFO")

error_log("연결 실패")   # [ERROR][DB] 연결 실패
info_log("서버 시작됨")  # [INFO] 서버 시작됨

🏗️ 클로저 vs 클래스 — 언제 무엇을 쓰는가

# 클로저로 카운터 구현
def make_counter(start=0):
    count = start

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

    def reset():
        nonlocal count
        count = start

    def get():
        return count

    return increment, reset, get

inc, rst, get = make_counter(10)
print(inc())    # 11
print(inc(5))   # 16
print(get())    # 16
rst()
print(get())    # 10

# 클래스로 동일한 기능 구현
class Counter:
    def __init__(self, start=0):
        self._count = start
        self._start = start

    def increment(self, by=1):
        self._count += by
        return self._count

    def reset(self):
        self._count = self._start

    def get(self):
        return self._count

c = Counter(10)
print(c.increment())    # 11
print(c.increment(5))   # 16
print(c.get())          # 16
c.reset()
print(c.get())          # 10

# 클로저: 간단한 상태(1~2개), 구현 숨기고 싶을 때
# 클래스: 복잡한 상태, 상속/확장 필요, 타입 힌트가 중요할 때

⚠️ 자주 하는 실수

  • 반복문 클로저 함정: for i in range(n): functions.append(lambda: i)는 모두 마지막 i를 반환합니다. 기본값 매개변수(lambda i=i: i)나 팩토리 함수를 사용하세요.
  • 클로저로 자유 변수 수정 시 nonlocal 빠뜨림: 클로저에서 외부 변수를 읽는 것은 자동이지만, 수정하려면 nonlocal을 선언해야 합니다. 없으면 새 지역 변수가 생성됩니다.
  • 클로저로 가변 객체 공유: 자유 변수가 리스트나 딕셔너리면 여러 클로저가 같은 객체를 공유합니다. 의도적이지 않으면 독립적인 복사본을 각 클로저에 넘기세요.

📝 정리 및 다음 강의 예고

  • 클로저: 외부 함수의 변수(자유 변수)를 "포획"한 내부 함수. 외부 함수가 끝나도 자유 변수는 셀 객체로 살아있습니다.
  • 반복문 클로저 함정: 기본값 매개변수나 팩토리 함수로 해결하세요.
  • 팩토리 패턴: 설정값을 포획한 함수를 반환하는 강력한 패턴입니다.
  • 단순한 상태 관리는 클로저, 복잡하거나 확장이 필요하면 클래스를 사용하세요.

다음 강의: 재귀 — 콜 스택의 동작, 기저 조건 설계, 꼬리 재귀 최적화가 없는 파이썬에서 재귀를 올바르게 사용하는 방법을 배웁니다.

관련 주제

  • 클로저 자유변수 포획
  • __closure__ 확인
  • 반복문 클로저 함정
  • 팩토리 함수 패턴
  • 클로저 vs 클래스
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 파이썬 기초 40강 — 처음 배우는 프로그래밍
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

파이썬 기초 40강 — 처음 배우는 프로그래밍

이 강의가 속한 시리즈는 총 32강, 모두 무료입니다. 처음부터 배우려는 동료에게 시리즈 전체를 알려 주세요.

댓글

0/1000

불러오는 중...