파이썬 nonlocal` 완벽 가이드

Sue·2025년 9월 4일
0
post-thumbnail


프로그래밍의 본질은 '데이터를 효과적으로 관리하고 조작하는 기술'이라고 할 수 있습니다. 이 데이터는 '변수'라는 이름표를 달고 메모리 어딘가에 저장됩니다. 그런데 이 변수들은 프로그램의 아무 곳에서나 마음대로 접근할 수 있는 것이 아닙니다. 모든 변수에는 자신만의 '활동할 수 있는 영역' 또는 '집 주소'가 정해져 있는데, 이를 스코프(Scope)라고 부릅니다.

파이썬을 배우다 보면 함수 안에서 변수를 만들고 사용하는 데 익숙해집니다. 하지만 함수 안에 또 다른 함수를 만드는 '중첩 함수' 구조를 만나게 되면, 변수의 주소록은 한층 더 복잡해집니다.

바로 이 복잡한 주소 체계 속에서 "안쪽 집에서는 바깥쪽 집의 가구(변수)를 보기만 할 수 있고, 위치를 바꿀 수는 없다"는 독특한 규칙과 마주하게 됩니다. 이 규칙을 어기려고 할 때 우리는 UnboundLocalError라는 당혹스러운 에러를 만나게 됩니다.

nonlocal 키워드는 바로 이 문제를 해결하기 위해 등장한 '마스터 키'입니다. 이 키워드는 변수의 기본 주소 규칙을 더 세밀하게 제어하여, 중첩된 함수 구조에서 발생하는 까다로운 문제들을 우아하게 풀어냅니다. nonlocal은 단순히 에러를 피하는 기술을 넘어, '상태를 기억하는 함수(클로저)'와 같은 강력하고 세련된 프로그래밍 패턴을 구현하는 핵심적인 도구입니다.

LEGB 규칙이라는 기본 원리부터 시작하여, nonlocal이 왜 필요한지, global과는 무엇이 다른지, 그리고 실제 코드에서 어떻게 그 힘을 발휘하는지까지 차근차근 탐험할 것입니다. 이 여정이 끝날 때쯤, 여러분은 변수의 '주소'를 완벽하게 이해하고 nonlocal이라는 마스터 키를 자신 있게 사용하여 더 깔끔하고 강력한 파이썬 코드를 작성하는 개발자로 거듭나게 될 것입니다.

1. 스코프(Scope)의 이해

nonlocal의 존재 이유를 파악하려면, 먼저 파이썬이 변수 이름을 어떻게 찾고 관리하는지에 대한 근본적인 규칙을 이해해야 합니다. 이 규칙의 핵심이 바로 '스코프(Scope)'입니다.

스코프란 무엇인가?

스코프는 변수가 코드 내에서 유효하게 존재하고 접근할 수 있는 범위를 정의하는 규칙의 집합입니다. "모든 변수는 자신만의 '활동 구역'을 갖는다"고 생각하면 쉽습니다. 어떤 변수는 특정 함수 안에서만 쓸 수 있는 반면, 어떤 변수는 프로그램 전체에서 접근할 수 있습니다. 스코프는 이처럼 변수의 생명주기와 가시성(visibility)을 제어합니다.

초기 프로그래밍 언어 중 일부는 오직 전역(global) 스코프만 가지고 있었습니다. 이는 프로그램의 규모가 커질수록 서로 다른 코드 조각에서 사용된 같은 이름의 변수가 충돌하여 예상치 못한 버그를 일으키는 원인이 되었습니다. 파이썬은 이러한 혼란을 막기 위해 변수를 각자의 스코프에 격리시키는 정교한 시스템을 채택했습니다.

파이썬의 변수 탐색 순서: LEGB 규칙

파이썬 인터프리터가 코드에서 특정 변수 이름을 만났을 때, 그 변수가 어떤 값을 가리키는지 찾기 위해 정해진 순서에 따라 여러 스코프를 탐색합니다. 이 순서를 LEGB 규칙이라고 부릅니다. LEGB는 다음과 같은 네 가지 스코프의 앞 글자를 딴 것입니다.

  1. L (Local): 지역 스코프
    • 가장 안쪽의 가장 좁은 범위로, 현재 실행 중인 함수나 클래스 메서드 내부를 의미합니다. 함수가 호출될 때마다 새로운 지역 스코프가 생성되고, 함수 실행이 끝나면 사라집니다. 함수에 전달되는 매개변수(parameter) 역시 지역 스코프에 속합니다. 파이썬은 변수를 찾을 때 가장 먼저 이 '내 방'부터 살펴봅니다.
  2. E (Enclosing): 둘러싸는 스코프
    • 중첩 함수 구조(함수 안에 다른 함수가 정의된 경우)에서만 존재하는 특별한 스코프입니다. 내부 함수 입장에서 자신을 감싸고 있는 외부 함수의 스코프를 가리킵니다. 파이썬이 지역 스코프에서 변수를 찾지 못하면, 그 다음으로 '우리 집 거실'에 해당하는 이 Enclosing 스코프를 확인합니다. nonlocal 키워드가 활약하는 주 무대가 바로 이곳입니다.
  3. G (Global): 전역 스코프
    • 모듈(일반적으로 .py 파일 하나)의 최상위 레벨에 정의된 변수들이 속하는 공간입니다. 함수나 클래스 바깥에 선언된 모든 변수는 전역 변수가 되며, 코드 어디서든 접근할 수 있습니다. '우리 동네'처럼 넓은 활동 범위를 가집니다.
  4. B (Built-in): 내장 스코프
    • 파이썬이 기본적으로 제공하는 모든 이름들이 포함된 가장 바깥쪽의 스코프입니다. print(), len(), range()와 같은 내장 함수나 SyntaxError, NameError 같은 예외 이름들이 여기에 속합니다. 이들은 '온 세상' 어디에나 존재하는 것처럼, 별도의 선언 없이 언제나 사용할 수 있습니다.

LEGB 규칙의 동작 원리

파이썬은 변수 이름을 해석할 때 항상 L → E → G → B 순서, 즉 가장 좁은 스코프에서 시작하여 점차 넓은 스코프로 확장하며 검색을 진행합니다. 검색 과정에서 해당 이름을 가진 변수를 가장 먼저 발견하는 즉시 검색을 멈추고 그 변수를 사용합니다. 만약 내장 스코프까지 모두 확인했는데도 변수를 찾지 못하면, NameError 예외가 발생합니다.

이 LEGB 규칙은 단순한 검색 순서를 넘어, '이름 충돌을 해결하고 변수의 영향 범위를 제한하는' 파이썬의 핵심적인 이름 해석(Name Resolution) 메커니즘입니다.

스코프는 변수들을 각자의 공간에 안전하게 격리시키고, LEGB 규칙은 이 격리된 공간들 사이에서 변수를 찾아 나갈지에 대한 명확한 지도를 제공합니다.

따라서 LEGB를 이해하는 것은 nonlocal을 배우기 위한 준비운동일 뿐만 아니라, 파이썬 코드의 동작 방식을 근본적으로 이해하는 첫걸음입니다.

2. 함수 속의 함수, 중첩 스코프(Enclosing Scope)

nonlocal 키워드가 왜 필요한지를 이해하려면, 이 키워드가 사용되는 특별한 환경인 '중첩 함수' 구조를 먼저 알아야 합니다. 이 구조에서 바로 LEGB 규칙의 'E', 즉 Enclosing 스코프가 탄생하기 때문입니다.

중첩 함수(Nested Function)란?

이름 그대로, 하나의 함수 내부에 또 다른 함수를 정의하는 프로그래밍 구조를 말합니다. 바깥쪽 함수를 외부 함수(outer function), 안쪽 함수를 내부 함수(inner function)라고 부릅니다.

def outer():
    print("여기는 외부 함수입니다.")

    def inner():
        print("여기는 내부 함수입니다.")

    inner() # 외부 함수 안에서 내부 함수를 호출

outer()

위 코드에서 inner 함수는 outer 함수 안에서만 정의되고 호출될 수 있습니다. outer 함수 바깥에서 inner()를 직접 호출하려고 하면 NameError가 발생합니다. 이는 inner 함수 자체가 outer 함수의 지역 스코프(Local Scope)에 속하기 때문입니다.

이러한 중첩 구조는 특정 기능을 외부로부터 숨겨서 오직 특정 함수 내에서만 사용되는 '도우미 함수(Helper Function)'를 만들 때 유용합니다. 전역 스코프를 불필요한 함수들로 어지럽히지 않고, 코드의 논리적 구조를 더 명확하게 만들어주는 장점이 있습니다.

Enclosing 스코프의 탄생

중첩 함수 구조가 만들어지는 순간, 스코프에도 새로운 층이 생겨납니다. 외부 함수(outer)가 호출되면 그 안의 변수들을 위한 지역 스코프가 생성됩니다. 그런데 이 스코프는 그 안에 있는 내부 함수(inner)의 입장에서는 자신을 둘러싸고 있는 Enclosing 스코프가 됩니다.

기본 규칙: 읽기는 자유롭다

내부 함수는 자신을 감싸는 외부 함수의 변수를 자유롭게 '읽을(read)' 수 있습니다. 이는 LEGB 규칙에 따른 자연스러운 동작입니다. 다음 예제를 살펴보겠습니다.

def outer():
    message = "Hello from outer"  # Enclosing 스코프의 변수

    def inner():
        # inner의 지역(Local) 스코프에는 message가 없음
        # 따라서 한 단계 위인 Enclosing 스코프에서 message를 찾아 사용
        print(message)

    inner()

outer()

실행 결과:

Hello from outer

이 코드가 문제없이 실행되는 이유는 LEGB 규칙 때문입니다. inner 함수가 print(message)를 실행할 때, 파이썬은 먼저 inner 함수의 지역(Local) 스코프에서 message 변수를 찾습니다. 하지만 없으므로, 다음 단계인 Enclosing 스코프로 이동합니다. outer 함수의 스코프에 message 변수가 존재하므로, 그 값을 가져와 성공적으로 출력합니다.

이처럼 내부 함수가 외부 함수의 변수를 참조(읽기)하는 것은 매우 직관적이고 간단합니다. 하지만 문제가 발생하는 지점은 바로 이 변수의 값을 '수정(modify)'하려고 할 때입니다.

3. UnboundLocalError의 함정

외부 함수의 변수를 읽는 것은 자유롭지만, 그 값을 바꾸려고 시도하는 순간 파이썬은 예상치 못한 에러를 발생시킵니다. 이 현상은 많은 초보 개발자들을 혼란에 빠뜨리는 주범이며, nonlocal이 필요한 이유를 가장 극적으로 보여주는 사례입니다.

문제의 코드

2부의 예제를 약간 수정하여, 내부 함수에서 외부 함수의 변수 값을 변경해 보겠습니다. 방문 횟수를 세는 간단한 카운터 함수를 가정해 봅시다.

def outer():
    count = 0  # 방문 횟수를 기록할 변수

    def inner():
        # 외부 함수의 count 값을 1 증가시키려고 시도
        count += 1  # 여기서 문제가 발생한다!
        print(f"방문 횟수: {count}")

    inner()

outer()

이 코드를 실행하면 방문 횟수: 1이 출력될 것이라고 기대하기 쉽습니다. 하지만 실제 결과는 다음과 같은 에러 메시지입니다.

에러 발생:

UnboundLocalError: local variable 'count' referenced before assignment

"지역 변수 'count'가 할당되기 전에 참조되었습니다"라는 이 에러 메시지는 언뜻 보기에 이상합니다. count는 분명히 outer 함수에 0으로 할당되어 있는데 왜 이런 에러가 발생하는 것일까요?

왜 에러가 날까? - 파이썬의 규칙

이 에러의 근본적인 원인은 파이썬이 변수의 스코프를 결정하는 방식에 있습니다.

  1. 할당문은 지역 변수를 만든다: 파이썬은 어떤 함수 내부를 해석할 때, 그 함수 안에서 할당문(=, +=, =, =, /=)을 사용하여 값을 할당받는 변수는 무조건 그 함수의 '지역 변수(Local Variable)'로 간주합니다. 이는 코드가 실제로 실행되기 전, 함수 정의를 읽어들이는 시점에 결정됩니다.
  2. count += 1의 함정: inner 함수 안에 count += 1이라는 코드가 있습니다. 이 코드는 count = count + 1과 동일한 의미를 가지는 명백한 할당문입니다. 파이썬 인터프리터는 inner 함수를 훑어보다가 이 할당문을 발견하고, "아하, countinner 함수의 지역 변수구나!"라고 결론 내립니다.
  3. 스코프 검색의 중단: 일단 countinner의 지역 변수로 '낙인'찍히고 나면, 파이썬은 inner 함수 안에서 count라는 이름을 만날 때 더 이상 바깥쪽 스코프(Enclosing 또는 Global)를 쳐다보지 않습니다. 오직 inner 함수 내부에서만 count를 찾으려고 합니다.
  4. 에러의 순간: 이제 count = count + 1의 오른쪽 부분, 즉 count의 현재 값을 읽어오려는 시점에 문제가 발생합니다. 파이썬은 inner 함수의 지역 스코프에서 count의 값을 찾지만, 아직 아무런 값도 할당된 적이 없습니다. 따라서 "값이 할당되기도 전에 참조하려고 했다"는 UnboundLocalError가 발생하는 것입니다.

결론적으로, 이 에러는 '외부 변수 수정 시도의 실패'가 아니라, '지역 변수로 잘못 간주된 변수를 초기값 없이 사용하려는 시도'에서 비롯된 것입니다. count += 1이라는 코드 한 줄 때문에 outer 함수에 있는 countinner 함수에게 완전히 보이지 않는 존재가 되어버린 셈입니다. 이 미묘하지만 결정적인 차이를 이해하는 것이 nonlocal의 필요성을 깨닫는 열쇠입니다.

4. nonlocal 키워드

앞서 살펴본 UnboundLocalError는 파이썬의 스코프 규칙 때문에 발생하는, 피할 수 없는 함정처럼 보입니다. 하지만 파이썬은 이 문제를 해결할 명쾌한 방법을 제공합니다. 바로 nonlocal 키워드입니다.

해결사 nonlocal의 등장

nonlocal은 파이썬 인터프리터에게 보내는 명시적인 선언입니다. "지금부터 내가 사용하려는 이 변수는 내 방(Local 스코프)에 새로 만드는 것이 아니다. 우리 집 거실(Enclosing 스코프)에 있는 바로 그 변수를 가져다 쓰겠다"고 알려주는 역할을 합니다. 이 선언을 통해 파이썬이 변수를 지역 변수로 오인하는 것을 막고, 의도대로 외부 함수의 변수에 접근하여 수정할 수 있게 됩니다.

Before & After 코드 비교

3부에서 에러를 일으켰던 코드를 nonlocal을 사용하여 수정한 버전과 나란히 비교해 보면 그 차이가 극명하게 드러납니다.

# Before (Error 발생)
def outer_before():
    count = 0
    def inner():
        # 파이썬은 이 할당문 때문에 count를 inner의 지역 변수로 착각
        count += 1
        print(f"방문 횟수: {count}")
    inner()

# After (성공!)
def outer_after():
    count = 0
    def inner():
        nonlocal count  # "count는 지역 변수가 아니야!" 라고 선언
        count += 1
        print(f"방문 횟수: {count}")
    inner()

# outer_before()  # UnboundLocalError 발생
outer_after()     # 정상 실행

outer_after() 실행 결과:

방문 횟수: 1

nonlocal의 역할

outer_after 함수에서 inner 함수의 시작 부분에 추가된 nonlocal count 한 줄이 모든 것을 바꿉니다. 이 선언은 파이썬 인터프리터에게 다음과 같이 지시합니다 :

"이 inner 함수 안에서 count라는 이름에 값을 할당하더라도, 새로운 지역 변수를 만들지 마라. 대신, 이 함수를 둘러싸고 있는 가장 가까운 외부 함수(Enclosing Scope)에서 count라는 이름의 변수를 찾아 그것을 사용해라."

이 지시 덕분에, 파이썬은 count += 1을 보더라도 count를 지역 변수로 간주하지 않습니다. 대신 LEGB 규칙의 'E' 단계로 바로 넘어가 outer_after 함수에 있는 count 변수를 찾아내고, 그 변수의 값을 성공적으로 1 증가시킵니다.

이처럼 nonlocal은 파이썬의 기본 스코프 결정 규칙(함수 내 할당문 = 지역 변수)을 개발자가 의도적으로 재정의할 수 있게 해주는 '명시적 선언'입니다. 이는 "암묵적인 것보다 명시적인 것이 낫다(Explicit is better than implicit)"는 파이썬의 핵심 철학(Zen of Python)과도 맞닿아 있습니다. nonlocal 키워드를 사용함으로써, 개발자는 "나는 지금 의도적으로 바깥 스코프의 변수를 수정하고 있다"는 명확한 '깃발'을 코드에 꽂는 셈입니다. 이는 코드를 읽는 다른 사람(혹은 미래의 나 자신)에게 변수의 흐름과 의도를 명확하게 전달하는 중요한 역할을 합니다.

5. nonlocal vs. global

nonlocal을 배우는 과정에서 많은 이들이 global 키워드와 혼동을 겪습니다. 두 키워드 모두 함수 내부에서 바깥 스코프의 변수를 수정할 때 사용된다는 공통점이 있지만, 그들이 가리키는 '바깥'의 정의는 완전히 다릅니다. 이 둘의 차이점을 명확히 이해하는 것은 스코프를 제대로 다루기 위해 필수적입니다.

global: 전역 스코프

global 키워드는 이름 그대로 오직 전역(Global) 스코프, 즉 모듈 최상위 레벨에 있는 변수만을 가리킵니다. 함수가 아무리 깊게 중첩되어 있더라도

global 선언은 모든 중간 스코프(Enclosing)를 무시하고 곧장 맨 꼭대기 층으로 올라가 변수를 찾습니다.

nonlocal: 가장 가까운 스코프

반면, nonlocal 키워드는 바로 한 단계 위 스코프부터 시작하여 자신을 감싸고 있는 가장 가까운 Enclosing 스코프의 변수만을 찾습니다. 만약 가장 가까운 Enclosing 스코프에 해당 변수가 없다면 그보다 더 바깥쪽의 Enclosing 스코프를 순차적으로 탐색합니다. 중요한 점은,

nonlocal은 아무리 탐색해도 전역 스코프의 변수는 절대 건드리지 않는다는 것입니다.

결정적 차이를 보여주는 예제

세 개의 다른 스코프(Global, Enclosing, Local)에 모두 같은 이름의 변수 x가 있을 때, nonlocalglobal이 각각 어떤 변수를 수정하는지 살펴보면 그 차이가 명확해집니다.

x = "나는 전역(Global) 변수"

def outer():
    x = "나는 둘러싸는(Enclosing) 변수"

    def inner_nonlocal():
        nonlocal x  # Enclosing 스코프의 x를 가리킴
        x = "nonlocal에 의해 수정됨"
        print(f"  [inner_nonlocal] x = {x}")

    def inner_global():
        global x  # Global 스코프의 x를 가리킴
        x = "global에 의해 수정됨"
        print(f"  [inner_global] x = {x}")

    print(f"[outer] 시작 시 x = {x}")
    inner_nonlocal()
    print(f"[outer] nonlocal 호출 후 x = {x}") # Enclosing 변수가 바뀜
    inner_global()
    print(f"[outer] global 호출 후 x = {x}")   # Enclosing 변수는 영향 없음

print(f"[전역] 시작 시 x = {x}")
outer()
print(f"[전역] 모든 호출 후 x = {x}") # Global 변수가 바뀜

실행 결과:

[전역] 시작 시 x = 나는 전역(Global) 변수 [outer] 시작 시 x = 나는 둘러싸는(Enclosing) 변수 [inner_nonlocal] x = nonlocal에 의해 수정됨 [outer] nonlocal 호출 후 x = nonlocal에 의해 수정됨 [inner_global] x = global에 의해 수정됨 [outer] global 호출 후 x = nonlocal에 의해 수정됨 [전역] 모든 호출 후 x = global에 의해 수정됨

결과에서 볼 수 있듯이, inner_nonlocalouter 함수의 x 값을 변경했고, inner_global은 모듈 최상단의 전역 x 값을 변경했습니다. outer 함수 내의 xinner_global 호출에 전혀 영향을 받지 않았습니다.

또 다른 중요한 차이점

nonlocal로 지정된 변수는 반드시 Enclosing 스코프에 미리 존재해야 합니다. 만약 존재하지 않는 변수를 nonlocal로 선언하면 SyntaxError가 발생합니다. 반면, global은 전역 스코프에 해당 변수가 없더라도 에러를 발생시키지 않으며, 그 함수 내에서 값을 할당하는 순간 새로운 전역 변수를 생성합니다.

nonlocal vs. global 심층 비교

두 키워드의 핵심적인 차이점을 다음 표로 정리할 수 있습니다.

특징nonlocalglobal
목표 스코프가장 가까운 Enclosing(둘러싸는) 스코프최상위 Global(전역) 스코프
검색 방식안쪽에서 바깥쪽으로 가장 가까운 것 하나만 찾음스코프 계층을 무시하고 바로 전역으로 이동
변수 사전 존재반드시 Enclosing 스코프에 존재해야 함존재하지 않으면 새로 생성 가능
사용 가능 위치중첩 함수 내부에서만 의미 있음모든 함수 내부에서 사용 가능
주요 사용처클로저(Closure), 팩토리 함수 등 상태 관리모듈 전체에서 공유되는 설정 값, 상태 변수

이처럼 nonlocalglobal은 비슷해 보이지만, 변수를 찾는 범위와 규칙에서 근본적인 차이를 가집니다. nonlocal은 중첩된 구조 내에서의 '지역적' 상태 변경을, global은 프로그램 전체의 '전역적' 상태 변경을 담당한다고 이해하면 명확합니다.

6. nonlocal의 실전 활용 사례

지금까지 nonlocalUnboundLocalError를 해결하기 위한 문법적 도구로 살펴보았다면, 이제부터는 nonlocal이 어떻게 더 높은 차원의 프로그래밍 패턴을 구현하는 강력한 도구가 되는지 알아보겠습니다. nonlocal의 진정한 힘은 '상태를 기억하고 성장하는 함수'를 만들 때 드러납니다.

예제 1: 상태를 기억하는 함수, 클로저(Closure)

클로저란 무엇인가?
클로저는 "자신이 정의될 때의 환경(스코프)을 기억하는 함수"입니다. 조금 더 풀어서 설명하면, 어떤 함수가 자신의 본문 밖에서 정의된, 그러나 전역 변수는 아닌 변수를 참조할 때, 그 함수와 변수가 함께 묶여 하나의 단위처럼 동작하는 것을 말합니다. 외부 함수는 이미 실행이 끝나 사라졌지만, 그 외부 함수가 가지고 있던 변수(자유 변수, free variable)들은 내부 함수에 의해 계속 '붙잡혀서' 살아있는 상태가 됩니다. 마치 '레시피(함수 코드)와 특별한 재료(환경 변수)를 함께 담아둔 밀키트'와 같습니다.

대표 예제: 카운터 함수nonlocal을 사용한 카운터 함수는 클로저의 가장 대표적인 예제입니다.

def counter():
    count = 0  # Enclosing 스코프의 변수. 이 값이 기억될 것임.
    def increment():
        nonlocal count  # 'count'는 지역 변수가 아님을 선언
        count += 1
        return count
    return increment  # 내부 함수 자체를 반환

# 카운터 인스턴스 생성
counter1 = counter()
counter2 = counter()

print(f"counter1 첫 번째 호출: {counter1()}") # 출력: 1
print(f"counter1 두 번째 호출: {counter1()}") # 출력: 2
print(f"counter1 세 번째 호출: {counter1()}") # 출력: 3

print(f"counter2 첫 번째 호출: {counter2()}") # 출력: 1
print(f"counter2 두 번째 호출: {counter2()}") # 출력: 2

이 코드는 다음과 같은 단계로 동작합니다.

  1. counter1 = counter()가 호출되면, counter 함수가 실행됩니다. count 변수가 0으로 초기화되고, increment 함수가 정의됩니다.
  2. counter 함수는 increment 함수 객체를 반환하고 자신의 생을 마감합니다.
  3. 이때 클로저가 생성됩니다. increment 함수는 자신이 만들어진 환경, 즉 count = 0이라는 변수와의 연결을 '기억'한 채로 counter1 변수에 저장됩니다. count 변수는 사라지지 않고 counter1에 의해 붙잡혀 살아남습니다.
  4. counter1()이 호출될 때마다, increment 함수가 실행됩니다. nonlocal count 선언 덕분에, 이 함수는 새로 count를 만드는 대신 '살아남은' 바로 그 count 변수에 접근하여 값을 1씩 증가시킵니다.
  5. counter2 = counter()를 통해 새로운 카운터를 만들면, counter2counter1과는 완전히 독립적인 자신만의 count 변수를 가진 클로저가 됩니다. 따라서 두 카운터는 서로의 상태에 영향을 주지 않습니다.

nonlocal이 없었다면, 클로저는 자신의 환경을 '기억'은 할 수 있어도 '변경'할 수는 없었을 것입니다. nonlocal은 클로저가 단순한 기억을 넘어 '성장(상태 변경)'할 수 있게 만드는 핵심 동력입니다.

예제 2: 유연한 함수 생성기(Factory Function)

함수 팩토리는 특정 설정값을 받아 그에 맞는 '맞춤형 함수'를 동적으로 생성하여 반환하는 함수입니다. 클로저는 이러한 함수 팩토리를 구현하는 자연스러운 방법입니다.

예제: 메시지 생성기
다양한 종류의 로그 메시지를 출력하는 함수를 만드는 팩토리 함수 예제입니다.

def message_factory(prefix):
    # 'prefix'는 반환될 'logger' 함수의 Enclosing 스코프에 저장됨
    def logger(content):
        # nonlocal이 필요 없는 이유: prefix를 '읽기만' 하기 때문
        print(f"[{prefix}] {content}")
    return logger

log_error = message_factory("ERROR")
log_info = message_factory("INFO")

log_error("파일을 찾을 수 없습니다.")
log_info("데이터 처리 중입니다.")

실행 결과:

파일을 찾을 수 없습니다. [INFO] 데이터 처리 중입니다.

log_errorlog_info는 각각 "ERROR""INFO"라는 prefix 값을 기억하는 서로 다른 클로저입니다.

예제: 비밀번호 검증기 (상태 저장)nonlocal을 사용하면 상태를 저장하고 변경하는, 한층 더 발전된 팩토리 함수를 만들 수 있습니다. 예를 들어, 비밀번호 시도 횟수를 제한하는 검증기를 만들어 보겠습니다.

def create_validator(correct_password, max_attempts=3):
    attempts_left = max_attempts

    def validate(password):
        nonlocal attempts_left
        if attempts_left <= 0:
            print("계정이 잠겼습니다. 더 이상 시도할 수 없습니다.")
            return False

        if password == correct_password:
            print("인증 성공!")
            return True
        else:
            attempts_left -= 1
            print(f"비밀번호가 틀렸습니다. 남은 시도 횟수: {attempts_left}")
            return False

    return validate

validator_A = create_validator("pa$$w0rd")

validator_A("wrong_pass")
validator_A("another_wrong")
validator_A("pa$$w0rd")
validator_A("too_late")

실행 결과:

비밀번호가 틀렸습니다. 남은 시도 횟수: 2 비밀번호가 틀렸습니다. 남은 시도 횟수: 1 인증 성공! 비밀번호가 틀렸습니다. 남은 시도 횟수: 0

이 예제에서 validator_Aattempts_left라는 상태를 내부적으로 유지하며, 호출될 때마다 nonlocal을 통해 이 상태를 변경합니다. 이처럼 nonlocal과 클로저를 활용하면, 객체 지향 프로그래밍의 클래스(Class)가 제공하는 '상태 캡슐화'를 함수형 프로그래밍 스타일로도 우아하게 구현할 수 있습니다.

7. 흔히 저지르는 실수와 모범 사례

nonlocal은 강력한 도구이지만, 잘못 사용하면 에러를 발생시키거나 코드의 의도를 흐릴 수 있습니다. nonlocal 사용 시 발생할 수 있는 일반적인 실수와 함정을 미리 알아두고, 언제 사용하는 것이 좋은지에 대한 가이드라인을 따르는 것이 중요합니다.

실수 1: 전역 변수에 nonlocal 사용하기

nonlocal은 자신을 둘러싼 Enclosing 스코프의 변수만을 찾습니다. 전역 스코프는 Enclosing 스코프가 아니므로, 전역 변수를 nonlocal로 참조하려고 하면 에러가 발생합니다.

# 잘못된 사용 예
x = 100

def my_func():
    # SyntaxError: no binding for nonlocal 'x' found
    # 'x'를 둘러싼 Enclosing 스코프가 없기 때문
    nonlocal x
    x = 200

전역 변수를 수정하고 싶을 때는 global 키워드를 사용해야 합니다.

실수 2: 중첩 함수가 아닌 곳에 nonlocal 사용하기

nonlocal은 Enclosing 스코프가 존재하는, 즉 중첩된 함수 구조 안에서만 의미가 있습니다. 일반 함수나 모듈의 최상위 레벨에서 nonlocal을 사용하면 SyntaxError가 발생합니다.

# 잘못된 사용 예
def my_func():
    # SyntaxError: nonlocal declaration not allowed at module level
    # (Jupyter Notebook 등에서는 다른 에러 메시지가 나올 수 있음)
    # 이 함수는 중첩 함수가 아니므로 Enclosing 스코프가 없음
    nonlocal y
    y = 10

함정: 변경 가능한(Mutable) 객체는 nonlocal이 필요 없다?

리스트(list), 딕셔너리(dict), 세트(set)와 같은 변경 가능한(mutable) 객체의 경우, nonlocal 선언 없이도 내부 함수에서 그 객체의 '내용물'을 수정할 수 있습니다. 이는 혼동을 일으키기 쉬운 지점입니다.

def outer():
    my_list =

    def inner_append():
        # my_list 변수 자체를 바꾸는 것이 아니라,
        # 그 변수가 가리키는 리스트 객체의 내용을 수정하는 것
        my_list.append(4)

    def inner_reassign():
        # 여기서 UnboundLocalError 발생!
        # my_list라는 변수 자체에 새로운 리스트를 할당하려 하기 때문
        my_list =

    def inner_reassign_fixed():
        nonlocal my_list
        # nonlocal 선언이 있으면 변수 자체를 재할당할 수 있음
        my_list =

    inner_append()
    print(f"append 후: {my_list}") # 출력:

    inner_reassign_fixed()
    print(f"재할당 후: {my_list}") # 출력:

핵심은 객체의 내용을 변경하는 것(mutation)변수에 다른 객체를 재할당하는 것(rebinding)의 차이를 이해하는 것입니다. .append(), .pop(), my_dict['key'] = value와 같은 연산은 변수가 가리키는 객체는 그대로 둔 채 내용만 바꾸므로 nonlocal이 필요 없습니다. 하지만 my_list = [...]와 같이 변수 자체가 다른 객체를 가리키도록 하려면 반드시 nonlocal 선언이 필요합니다.

모범 사례

  1. 꼭 필요할 때만 사용하라: nonlocal은 코드의 흐름을 조금 더 복잡하게 만들 수 있습니다. 단순히 값을 전달하는 것이 목적이라면, 함수 인자(argument)로 넘겨주고 반환값(return)을 받는 것이 더 명확하고 직관적인 방법일 수 있습니다.
  2. 클래스를 고려하라: nonlocal을 사용하여 여러 변수의 상태를 복잡하게 관리해야 한다면, 이는 클래스(Class)를 사용해야 할 신호일 수 있습니다. 클래스는 상태(속성, attribute)와 그 상태를 조작하는 행위(메서드, method)를 하나의 단위로 묶어주므로, 복잡한 상태 관리에 더 구조적이고 명확한 해결책을 제공합니다. nonlocal과 클로저는 가벼운 상태 캡슐화에 적합하고, 클래스는 더 무겁고 체계적인 상태 관리에 적합합니다.

8. 결론: 코드의 환경을 지배하는 개발자로 거듭나기

지금까지 우리는 파이썬의 nonlocal 키워드를 중심으로 변수가 살아가는 세상, 즉 스코프의 복잡하고 정교한 규칙들을 탐험했습니다. 이 여정을 통해 우리는 다음과 같은 핵심적인 사실들을 확인했습니다.

  • 스코프(LEGB)는 변수의 '활동 무대'입니다. 파이썬은 L → E → G → B라는 명확한 순서에 따라 변수를 찾아내며, 이 규칙은 코드의 안정성과 예측 가능성을 보장하는 기반이 됩니다.
  • nonlocal은 중첩된 무대 사이를 연결하는 특별한 '통로'입니다. 기본적으로 내부 함수는 외부 함수의 변수를 수정할 수 없지만, nonlocal은 이 장벽을 넘어 가장 가까운 외부 스코프의 변수에 대한 수정 권한을 명시적으로 부여합니다.
  • nonlocal은 단순한 에러 해결사가 아닙니다. UnboundLocalError를 해결하는 열쇠일 뿐만 아니라, 클로저와 팩토리 함수 같은 강력한 프로그래밍 패턴을 통해 '상태를 기억하고 성장하는 함수'를 만드는 창의적인 도구입니다. 이를 통해 함수만으로도 객체와 유사한 캡슐화를 구현할 수 있습니다.
  • nonlocalglobal은 목표가 다릅니다. nonlocal이 중첩된 함수 구조 내의 '가까운' 상태를 다루는 데 집중한다면, global은 프로그램 전체에 걸친 '먼' 상태를 다룹니다. 둘의 차이를 명확히 아는 것은 코드의 의도를 정확하게 표현하는 데 필수적입니다.
profile
AI/ML Engineer

0개의 댓글