메뉴 닫기

파이썬 global과 nonlocal 완벽 정리 함수 내부 상위 스코프 수정과 클로저 주의사항

파이썬 global과 nonlocal 완벽 정리 함수 내부 상위 스코프 수정과 클로저 주의사항

🐍 실무에서 자주 헷갈리는 global과 nonlocal을 안전하게 쓰는 법을 예제로 풀어드립니다

프로젝트를 하다 보면 함수 안에서 바깥 변수를 바꾸고 싶은 순간이 꼭 생깁니다.
그때마다 손이 먼저 움직여 global이나 nonlocal을 붙였다가 의도치 않은 값 변경이나 사이드 이펙트가 터지곤 하죠.
테스트는 통과했는데 배포 후에만 발생하는 미묘한 버그라면 더 난감합니다.
이 글은 그런 불안함을 줄이고, 코드 리뷰에서 바로 통하는 설명을 준비하려는 분을 위해 정리했습니다.
기초 문법을 이미 아는 분도, 키워드의 정확한 동작 범위와 스코프 체인을 다시 점검하면 코드가 한결 예측 가능해집니다.
팀원에게 설명하기 쉬운 비유와 깔끔한 코드 스니펫 기준으로 차근차근 안내하겠습니다.

핵심은 세 가지입니다.
첫째, global은 모듈 전역 바인딩을 직접 가리키고, nonlocal은 가장 가까운 상위의 로컬 스코프(클로저 셀)에 연결된다는 점입니다.
둘째, 파이썬의 이름 해석 규칙(LEGB)과 바인딩 시점 때문에 간단해 보이는 코드도 실행 순서에 따라 결과가 뒤바뀔 수 있습니다.
셋째, 클로저 내부 상태를 갱신하는 패턴은 강력하지만, 가변 객체나 루프 캡처 같은 함정과 만나면 디버깅 난이도가 급상승합니다.
본문에서는 함수 내부에서 상위 스코프 바인딩을 안전하게 수정하는 방법, 클로저 셀의 메커니즘, 그리고 실수하기 쉬운 패턴을 실제 예제로 보여드리겠습니다.



🐍 global과 nonlocal의 동작 원리

파이썬은 이름을 찾을 때 LEGB 규칙을 따릅니다.
Local → Enclosing → Global → Builtins 순서로 스코프를 탐색하죠.
이 흐름을 이해하면 globalnonlocal이 정확히 어디를 가리키는지 명확해집니다.
핵심은 두 키워드 모두 “조회”가 아니라 “바인딩 변경”에 관여한다는 점입니다.
읽기만 할 때는 굳이 선언이 필요 없지만, 값을 대입하려면 파이썬이 그 이름을 어느 스코프에 묶을지 결정을 해야 합니다.
여기서 잘못 지정하면 의도치 않게 새로운 로컬 변수가 생기거나, 전역 상태를 덮어쓰는 일이 발생할 수 있습니다.

🧭 LEGB 규칙 요약

스코프 의미
Local 현재 함수의 지역 네임스페이스
Enclosing 중첩 함수에서 바깥 함수의 지역 스코프(클로저가 캡처)
Global 모듈 전역 스코프(파일 단위)
Builtins 파이썬 내장 네임스페이스(len 등)

🌍 global 키워드의 본질

global은 “이 이름의 바인딩을 모듈 전역에서 찾고, 그 바인딩을 수정하겠다”는 선언입니다.
즉, 같은 모듈 파일의 최상단에 존재하는 변수에 직접 연결합니다.
선언한 함수 안에서 해당 이름에 할당을 수행하면 새로운 지역 변수를 만들지 않고 전역 변수를 덮어씁니다.
다른 모듈의 전역을 수정하는 기능은 아니며, import로 끌어온 객체의 속성을 바꾸는 일과도 다릅니다.

CODE BLOCK
x = 10  # 모듈 전역

def bump():
    # global을 빼면 아래 할당으로 인해 x가 지역 변수로 간주되어 UnboundLocalError 발생
    global x
    x += 1

bump()
print(x)  # 11

🧩 nonlocal 키워드의 본질

nonlocal은 “가장 가까운 바깥 함수의 로컬 스코프(클로저 셀)에 있는 같은 이름의 바인딩을 수정하겠다”는 선언입니다.
따라서 중첩 함수 내부에서만 쓸 수 있고, 대상 이름이 실제로 존재해야 합니다.
전역 스코프(모듈)나 내장 스코프를 가리키지 않으며, 존재하지 않는 이름에 nonlocal을 쓰면 컴파일 단계에서 에러가 납니다.

CODE BLOCK
def counter():
    n = 0
    def inc():
        nonlocal n  # 가장 가까운 바깥 스코프의 n을 수정
        n += 1
        return n
    return inc

c = counter()
print(c(), c(), c())  # 1 2 3

💡 TIP: nonlocal은 “가장 가까운” 바깥 함수의 동일한 이름만 가리킵니다.
두 단계 이상 바깥에 같은 이름이 있어도, 더 가까운 레벨부터 매칭됩니다.

⚠️ 주의: global과 nonlocal은 “읽기용”이 아니라 “할당(바인딩 변경)”에 필요합니다.
읽기만 한다면 굳이 선언하지 않아도 되지만, 동일한 이름에 할당을 수행하는 순간 로컬로 간주되어 에러가 생길 수 있습니다.

💬 정리하면, global은 모듈 전역 바인딩을, nonlocal은 가장 가까운 상위 함수의 로컬 바인딩(클로저 셀)을 수정합니다.
두 키워드는 스코프의 “탐색 순서”가 아니라 “바인딩 대상”을 지정하는 선언입니다.

🧭 함수 내부에서 상위 스코프 바인딩 수정하기

함수 내부에서 바깥 변수의 값을 바꾸는 일은 단순히 문법의 문제가 아닙니다.
파이썬의 스코프 체인은 “이름을 어디에 묶을 것인가”를 중심으로 작동하기 때문에, 같은 이름이라도 선언 위치에 따라 완전히 다른 변수를 가리키게 됩니다.
여기서 global과 nonlocal을 적절히 사용하지 않으면, 예상치 못한 값 덮어쓰기나 새로운 로컬 변수 생성이 일어납니다.

🔄 전역 변수 수정 패턴 (global)

전역 변수는 스크립트 전체에서 공유되기 때문에 함수 내부에서 직접 수정할 경우, 다른 함수의 동작에도 영향을 줄 수 있습니다.
그래서 꼭 필요한 상황이 아니면 global 선언 대신 매개변수 전달이나 클래스 속성을 사용하는 편이 안전합니다.
하지만 설정값이나 카운터처럼 프로그램 전체에서 하나의 상태만 유지해야 할 때는 global이 효율적일 수 있습니다.

CODE BLOCK
flag = False

def toggle_flag():
    global flag
    flag = not flag

toggle_flag()
print(flag)  # True

💎 핵심 포인트:
global은 단순히 전역 변수를 “가져오는” 것이 아니라, 전역 네임스페이스에 새로 바인딩을 만들거나 수정합니다. 이 점 때문에 예기치 않은 상태 변경이 자주 발생합니다.

🪄 클로저 내부에서 바깥 함수 변수 수정 (nonlocal)

nonlocal은 내부 함수가 바깥 함수의 지역 변수를 수정할 수 있게 해줍니다.
이 구조는 상태를 가진 함수(클로저)를 만들 때 특히 유용합니다.
예를 들어 호출할 때마다 카운트를 증가시키는 함수, 평균을 계산하는 누산기(accumulator) 등이 이에 해당합니다.

CODE BLOCK
def make_accumulator():
    total = 0
    def add(value):
        nonlocal total
        total += value
        return total
    return add

acc = make_accumulator()
print(acc(5))   # 5
print(acc(10))  # 15

이 예제처럼 내부 함수는 total의 값을 유지하면서 계속 누적합니다.
nonlocal 덕분에 바깥 함수가 이미 종료된 이후에도 그 변수가 클로저 셀에 저장되어 살아남는 것이죠.

💬 nonlocal은 중첩 함수 안에서 바깥 함수의 지역 변수를 지속적으로 관리할 수 있게 합니다.
이 기능이 바로 클로저의 핵심이며, 상태 기반 함수형 프로그래밍을 가능하게 하는 기반입니다.

⚠️ 주의: nonlocal은 바깥 함수에 동일한 이름의 변수가 실제로 존재해야 합니다.
없는 변수에 nonlocal을 쓰면 SyntaxError가 발생합니다.



🧩 클로저와 셀 객체 이해하기

파이썬에서 클로저(closure)란 함수가 자신이 선언된 환경(스코프)에 있는 변수들을 기억하는 개념을 말합니다.
즉, 함수가 종료된 뒤에도 그 내부에서 참조한 외부 변수가 사라지지 않고 함께 저장되는 구조입니다.
이 덕분에 파이썬의 함수는 ‘상태를 기억하는 객체’처럼 동작할 수 있습니다.

클로저는 단순히 변수를 캡처하는 것이 아니라, 실제로는 cell object라는 메커니즘을 통해 변수를 감싸고 참조를 유지합니다.
이 셀은 함수 객체의 __closure__ 속성에 저장되어, 언제든 접근이 가능합니다.

CODE BLOCK
def outer():
    x = 42
    def inner():
        return x
    return inner

f = outer()
print(f.__closure__)           # (<cell at 0x...: int object at ...>,)
print(f.__closure__[0].cell_contents)  # 42

이처럼 __closure__는 함수가 기억하고 있는 바깥 스코프의 변수 셀을 담고 있습니다.
각 셀은 실제 변수 객체를 직접 가리키며, 내부 함수가 그 값을 참조하거나 수정할 때마다 같은 메모리 위치가 갱신됩니다.

🔍 셀 객체(cell object)의 역할

셀 객체는 단순한 값 복사가 아니라, 같은 참조를 여러 함수가 공유할 수 있도록 하는 구조입니다.
즉, 외부 함수가 끝나도 내부 함수가 여전히 변수를 사용할 수 있게 하는 핵심 요소입니다.
클로저의 변수는 함수 정의 시점에 “이름”이 아니라 “참조”로 묶이기 때문에, 실행 이후에도 변경 사항이 그대로 반영됩니다.

CODE BLOCK
def make_pair():
    a = 1
    def show():
        print(a)
    def change(v):
        nonlocal a
        a = v
    return show, change

s, c = make_pair()
c(99)
s()  # 99

위의 예제에서 showchange 함수는 동일한 셀 객체를 공유합니다.
따라서 change()에서 값을 변경하면 show()가 출력하는 값도 함께 바뀝니다.
이는 두 함수가 같은 스코프 셀을 바라보고 있기 때문입니다.

💎 핵심 포인트:
클로저 셀은 변수의 스냅샷이 아니라, 실제 메모리 참조를 유지합니다. 따라서 클로저를 설계할 때 값 변경의 영향을 정확히 예측해야 합니다.

🧠 클로저와 가비지 컬렉션

클로저는 외부 변수를 참조한 상태로 함수 객체가 살아남기 때문에, 가비지 컬렉션의 대상이 되지 않습니다.
즉, 내부 함수가 존재하는 한 클로저 셀도 함께 유지됩니다.
이로 인해 예상보다 오래 메모리가 점유되기도 하므로, 큰 객체를 캡처할 땐 주의해야 합니다.

⚠️ 주의: 클로저가 참조하는 객체가 파일 핸들이나 대용량 데이터라면,
함수를 반환한 뒤에도 해당 자원이 해제되지 않을 수 있습니다.
이럴 땐 del이나 weakref 모듈로 명시적으로 참조를 끊는 게 좋습니다.

💬 클로저는 파이썬의 스코프 모델을 가장 직접적으로 체감할 수 있는 기능입니다.
변수를 언제 “값”으로, 언제 “참조”로 처리하는지를 이해하면, 전역 관리보다 더 깔끔한 구조를 만들 수 있습니다.

🚨 주의해야 할 패턴과 버그 사례

global과 nonlocal은 편리하지만, 작은 실수로 큰 혼란을 일으키는 키워드입니다.
특히 클로저 내부에서 값을 수정할 때, 스코프 바인딩이 언제 이루어지는지를 정확히 이해하지 못하면 코드가 이상하게 동작할 수 있습니다.
아래는 실제로 자주 등장하는 버그 패턴과 그 원인을 정리했습니다.

🐛 UnboundLocalError 발생 사례

가장 흔한 오류는 함수 내부에서 전역 변수를 읽고 수정하려다 UnboundLocalError가 발생하는 경우입니다.
이는 파이썬이 함수 내에서 같은 이름의 변수를 할당하면 자동으로 로컬 스코프로 간주하기 때문입니다.
따라서 global 선언이 없으면 전역 변수 조회조차 허용되지 않습니다.

CODE BLOCK
x = 5

def broken():
    print(x)  # 여기서 에러 발생
    x = x + 1

# broken() 호출 시 UnboundLocalError

이 문제는 함수가 정의될 때 “할당이 존재하면 로컬로 인식”한다는 파이썬의 컴파일 규칙 때문입니다.
전역 값을 읽으려면 global 선언이 반드시 필요합니다.

🌀 루프에서 클로저 캡처 오류

루프 안에서 람다나 내부 함수를 정의할 때, 클로저가 변수를 참조 형태로 캡처하기 때문에 발생하는 대표적인 버그입니다.
모든 함수가 같은 변수를 가리켜서, 실행 시점에 최종값으로 평가됩니다.

CODE BLOCK
funcs = []
for i in range(3):
    funcs.append(lambda: i)

print([f() for f in funcs])  # [2, 2, 2]

모든 람다가 같은 i 셀을 참조하기 때문에, 루프가 끝난 후의 값 2만 출력됩니다.
이 문제를 피하려면 기본 인자(default argument)로 값을 즉시 복사해 두어야 합니다.

CODE BLOCK
funcs = []
for i in range(3):
    funcs.append(lambda i=i: i)  # 기본 인자로 캡처
print([f() for f in funcs])  # [0, 1, 2]

💎 핵심 포인트:
클로저는 변수를 “값”이 아니라 “참조”로 기억합니다. 루프 안에서 함수를 만들 때는 즉시 복사하거나 별도 스코프를 만들어야 예기치 않은 참조 문제가 생기지 않습니다.

💥 global 변수의 덮어쓰기

프로젝트 규모가 커질수록, 모듈 간 global 변수 이름 충돌로 인해 예상치 못한 버그가 발생하기도 합니다.
특히 설정 파일(config)이나 상태 플래그를 여러 모듈이 공유할 때 주의해야 합니다.
전역 변수는 편의성이 크지만, 유지보수성을 떨어뜨리는 원인이 되기도 합니다.

⚠️ 주의: global 변수를 여러 모듈에서 동시에 수정하는 것은 피하세요.
공유 상태는 항상 싱글톤 클래스컨텍스트 매니저로 안전하게 관리하는 것이 좋습니다.

💬 global과 nonlocal은 반드시 목적이 있을 때만 사용해야 합니다.
가급적 상태를 인자로 전달하거나, 불변 객체를 활용하는 쪽이 코드 안정성과 테스트 용이성 모두에서 유리합니다.



🧪 테스트 코드로 개념 확실히 잡기

이제까지 살펴본 global과 nonlocal의 동작 원리를 실제 테스트 코드로 검증해 보겠습니다.
짧은 단위 테스트를 작성하면서 직접 실행해 보면 스코프 체인과 바인딩 변화가 훨씬 명확해집니다.
특히 클로저가 변수를 어떻게 기억하고, 함수 호출 순서에 따라 값이 어떻게 달라지는지 직접 체험하는 것이 가장 확실한 학습법입니다.

🧮 global 테스트

전역 변수를 수정하는 함수가 여러 개 있을 때, global 선언이 어떻게 작동하는지를 확인하는 테스트입니다.
한 함수에서 전역 변수를 바꾸면 다른 함수에서도 그 값이 즉시 반영되는 것을 확인할 수 있습니다.

CODE BLOCK
x = 0

def increase():
    global x
    x += 1

def get_x():
    return x

increase()
increase()
assert get_x() == 2
print("✅ global 테스트 통과")

이 테스트는 전역 변수의 수정이 모듈 전체에 반영된다는 점을 보여줍니다.
여기서 global을 빼면 로컬 스코프에서 x를 찾으려다 에러가 발생합니다.

🧬 nonlocal 테스트

nonlocal은 내부 함수가 바깥 함수의 로컬 스코프 변수를 수정할 수 있도록 합니다.
이 테스트에서는 클로저가 변수를 기억하고, 함수 호출 순서에 따라 누적 값이 변하는 것을 확인합니다.

CODE BLOCK
def make_counter():
    n = 0
    def inc():
        nonlocal n
        n += 1
        return n
    return inc

count = make_counter()
assert count() == 1
assert count() == 2
print("✅ nonlocal 테스트 통과")

nonlocal이 없다면 위의 코드는 각 호출마다 n을 새로 만들기 때문에 누적이 되지 않습니다.
즉, nonlocal 선언은 클로저의 지속 상태를 만드는 핵심 역할을 합니다.

🧰 pytest 예제

단위 테스트 프레임워크를 활용하면 이런 스코프 관련 함수를 자동 검증할 수 있습니다.
예를 들어 pytest로 간단히 작성하면 다음과 같습니다.

CODE BLOCK
import pytest

def test_global_scope():
    global x
    x = 10
    def modify():
        global x
        x += 5
    modify()
    assert x == 15

def test_nonlocal_scope():
    def outer():
        y = 1
        def inner():
            nonlocal y
            y += 2
            return y
        return inner
    fn = outer()
    assert fn() == 3
    assert fn() == 5

이렇게 테스트를 돌려보면 각 키워드의 동작 범위를 직접 눈으로 확인할 수 있습니다.
실무 코드에서는 상태 변경보다는 함수형 접근을 선호하되, 필요한 경우에는 스코프를 명확히 제어하는 습관이 중요합니다.

💎 핵심 포인트:
테스트 코드는 단순히 오류를 찾기 위한 것이 아니라, 파이썬의 스코프 작동 원리를 이해하는 가장 좋은 도구입니다. 직접 실행해 보는 과정이 문법보다 강력한 학습 효과를 줍니다.

자주 묻는 질문 (FAQ)

global과 nonlocal의 차이는 한 문장으로 정리하면 무엇인가요?
global은 모듈 전역 변수를 수정하고, nonlocal은 가장 가까운 바깥 함수의 지역 변수를 수정합니다.
함수 안에서 global을 쓰지 않아도 전역 변수를 읽을 수 있나요?
네, 읽기만 한다면 가능합니다. 하지만 같은 이름으로 값을 할당하려는 순간 로컬 스코프로 인식되어 오류가 발생합니다.
nonlocal은 모든 바깥 함수 변수에 접근할 수 있나요?
아닙니다. 가장 가까운 바깥 함수의 지역 변수만 수정할 수 있습니다. 두 단계 이상 바깥에 있는 변수에는 접근할 수 없습니다.
클로저에서 변수를 수정하지 않고 읽기만 한다면 nonlocal이 필요할까요?
읽기만 한다면 nonlocal 선언은 필요 없습니다. 하지만 값을 변경하거나 재할당하는 경우에는 nonlocal을 반드시 선언해야 합니다.
클로저가 변수를 기억하는 원리는 무엇인가요?
내부 함수가 바깥 함수의 변수를 참조할 때, 파이썬은 그 변수를 cell object로 감싸서 함수 객체의 __closure__ 속성에 저장합니다. 이로 인해 함수가 종료돼도 변수가 유지됩니다.
global을 쓰면 모듈 외부에서도 변수값이 바뀌나요?
아니요. global은 현재 모듈(파일) 전역에서만 영향을 줍니다. 다른 모듈의 변수는 직접 수정되지 않습니다.
클로저를 사용하면 메모리 누수가 발생할 수 있나요?
클로저가 큰 객체나 파일 핸들을 참조한 상태로 오래 유지되면 가비지 컬렉터가 회수하지 못해 메모리 누수가 발생할 수 있습니다. 필요 시 참조를 명시적으로 해제해야 합니다.
global과 nonlocal 없이 상태를 유지하는 방법도 있나요?
네. 클래스로 상태를 관리하거나, 불변 객체를 새로 반환하는 함수형 스타일을 사용하면 global이나 nonlocal 없이도 상태를 안전하게 유지할 수 있습니다.

🧭 함수 스코프를 다루는 올바른 습관

global과 nonlocal은 파이썬이 제공하는 매우 강력한 도구지만, 동시에 버그의 근원이 되기도 합니다.
이 글에서 살펴본 것처럼 함수 내부에서 상위 스코프를 수정할 때는 항상 의도를 명확히 해야 하며, 데이터 흐름을 예측 가능한 형태로 설계하는 것이 중요합니다.
global은 모듈 전역을 직접 수정하기 때문에, 여러 함수가 동시에 접근할 경우 쉽게 충돌이 일어납니다.
nonlocal은 클로저의 상태를 관리하는 데 유용하지만, 참조 관계를 잘못 설계하면 예기치 않은 결과를 초래합니다.

가장 좋은 접근법은 가능한 한 전역 상태를 줄이고, 값 전달과 반환 중심으로 설계하는 것입니다.
만약 상태를 반드시 유지해야 한다면, 클래스 인스턴스 속성이나 클로저를 명확히 설계해 사용하는 것이 바람직합니다.
테스트 코드를 통해 스코프 동작을 반복적으로 확인하고, 팀 코드 리뷰 시에는 global과 nonlocal 사용 여부를 반드시 검토하는 습관을 들이세요.

파이썬의 스코프는 단순히 변수의 “위치”가 아니라, 프로그램의 데이터 흐름과 설계 의도를 표현하는 중요한 개념입니다.
스코프를 명확히 이해하면 예측 가능한 코드를 작성할 수 있고, 디버깅 시간도 크게 줄일 수 있습니다.
이제 여러분도 global과 nonlocal의 차이, 그리고 클로저의 원리를 활용해 더 안전하고 직관적인 함수를 작성해 보세요.


🏷️ 관련 태그 : 파이썬기초, 함수스코프, 클로저, global키워드, nonlocal키워드, 파이썬문법, 스코프체인, LEGB규칙, 파이썬클로저, 프로그래밍기초