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

20강 / 전체 32강

스코프와 LEGB 규칙 — 변수 탐색의 순서

9분 읽기 조회 5

LEGB 스코프 탐색 규칙과 global·nonlocal 키워드의 올바른 사용법을 완전히 이해한다.

🎯 이 강의에서 배우는 것

파이썬에서 변수 이름을 조회할 때 어디서부터 찾는가? 그 답이 LEGB 규칙입니다. Local → Enclosing → Global → Built-in 순서로 탐색합니다. 이 규칙을 이해하면 NameError, UnboundLocalError, 섀도잉 문제를 즉시 진단할 수 있습니다.

🗺️ 4가지 스코프와 LEGB 탐색 순서

x = "전역"   # G: Global 스코프 (모듈 최상위)

def outer():
    x = "바깥 함수"   # E: Enclosing 스코프

    def inner():
        x = "안쪽 함수"   # L: Local 스코프
        print(x)   # L에서 찾음: "안쪽 함수"

    inner()
    print(x)   # E에서 찾음: "바깥 함수"

outer()
print(x)   # G에서 찾음: "전역"

# 각 함수가 독립적인 지역 스코프를 가짐
def func_a():
    value = 10
    print(value)

def func_b():
    value = 20   # func_a의 value와 무관한 별개의 변수
    print(value)

func_a()   # 10
func_b()   # 20

# B: Built-in 스코프 — 파이썬 내장 이름
print(len([1, 2, 3]))   # len은 Built-in
print(type(42))         # type도 Built-in
print(abs(-5))          # abs도 Built-in

# 내장 함수를 섀도잉하면 위험!
len = 5   # ← 전역 변수 len이 내장 함수 len을 가림
# print(len([1, 2, 3]))   ← TypeError: 'int' is not callable
del len   # 전역 변수 삭제 → 내장 len 다시 사용 가능

🚨 UnboundLocalError — 가장 흔한 스코프 버그

count = 0

def increment_buggy():
    # 함수 본문에 count += 1이 있으므로 count는 지역 변수로 취급됨
    # 그런데 += 1은 count를 먼저 읽어야 함 → 아직 지역 변수에 값이 없음!
    count += 1   # UnboundLocalError!

# 왜 그런가?
# 파이썬은 함수를 컴파일할 때 할당문(=)이 있는 이름을 지역 변수로 표시
# count += 1 은 count = count + 1 과 같음 → 할당이 있음 → 지역 변수!
# 하지만 right-hand side의 count를 읽을 때는 아직 지역 count가 없음 → 에러

# 해결 1: global 선언
def increment_global():
    global count   # count는 전역 변수다!
    count += 1

increment_global()
print(count)   # 1

# 해결 2: 함수 인자로 받고 반환 (권장)
def increment_pure(n):
    return n + 1

count = increment_pure(count)
print(count)   # 2

# 또 다른 UnboundLocalError 예시
def tricky():
    if False:
        x = 10   # 실행 안 되지만 컴파일 시 x를 지역 변수로 표시
    print(x)   # UnboundLocalError: local variable 'x' referenced before assignment

# 조건부 할당이 있는 경우, 항상 초기화 먼저!
def safe():
    x = None   # 초기화
    if False:
        x = 10
    print(x)   # None (안전)

🌐 global 선언 — 사용과 남용

db_connection = None   # 전역 상태

def connect_db(host):
    global db_connection   # 전역 변수를 수정하겠다는 선언
    db_connection = f"connected to {host}"

connect_db("localhost")
print(db_connection)   # "connected to localhost"

# global의 문제점
request_count = 0

def handle_request():
    global request_count
    request_count += 1
    return f"처리 완료 (총 {request_count}건)"

print(handle_request())   # 처리 완료 (총 1건)
print(handle_request())   # 처리 완료 (총 2건)
# 테스트 어려움: 테스트 간 상태가 공유됨
# 병렬 실행 시 경쟁 조건(race condition) 위험

# 권장 대안: 클래스로 상태 캡슐화
class RequestCounter:
    def __init__(self):
        self.count = 0

    def handle(self):
        self.count += 1
        return f"처리 완료 (총 {self.count}건)"

counter = RequestCounter()
print(counter.handle())   # 처리 완료 (총 1건)
# 각 테스트에서 새 counter 인스턴스 생성 → 독립적

🔗 nonlocal — 중첩 함수에서 바깥 변수 수정

def make_counter():
    count = 0   # Enclosing 스코프의 변수

    def increment():
        nonlocal count   # Enclosing 스코프의 count를 수정하겠다는 선언
        count += 1
        return count

    def reset():
        nonlocal count
        count = 0

    return increment, reset

inc, rst = make_counter()
print(inc())   # 1
print(inc())   # 2
print(inc())   # 3
rst()
print(inc())   # 1 ← 리셋 후

# nonlocal 없이 읽기만 할 때는 선언 불필요
def make_greeter(prefix):
    def greet(name):
        return f"{prefix}, {name}!"   # prefix 읽기만 — nonlocal 불필요
    return greet

hello = make_greeter("안녕하세요")
print(hello("홍길동"))   # 안녕하세요, 홍길동!

# nonlocal vs global 비교
total_global = 0

def outer_func():
    total_enclosing = 0

    def inner_modify():
        nonlocal total_enclosing   # outer_func의 total_enclosing 수정
        global total_global        # 모듈 최상위 total_global 수정
        total_enclosing += 1
        total_global += 1

    inner_modify()
    inner_modify()
    print(f"enclosing: {total_enclosing}")   # 2

outer_func()
print(f"global: {total_global}")   # 2

🔬 스코프 검사 도구

x = 10
y = 20

def my_func():
    z = 30

    # 현재 지역 스코프의 변수 딕셔너리
    local_vars = locals()
    print("지역:", local_vars)   # {'z': 30}

    # 전역 스코프의 변수 딕셔너리 (매우 큼!)
    global_vars = globals()
    print("x in globals:", "x" in global_vars)   # True

my_func()

# 변수가 어느 스코프에 있는지 확인
import sys
def where_is(name):
    frame = sys._getframe(1)   # 호출자 프레임
    if name in frame.f_locals:
        print(f"{name!r}: Local")
    elif name in frame.f_globals:
        print(f"{name!r}: Global")
    elif name in dir(__builtins__):
        print(f"{name!r}: Built-in")
    else:
        print(f"{name!r}: 없음")

where_is("x")       # Global
where_is("len")     # Built-in
where_is("zzz")     # 없음

⚠️ 자주 하는 실수

  • UnboundLocalError 이해 못 함: 함수 안에 x += 1이 있으면 x 전체가 지역 변수가 됩니다. 전역 변수를 사용하려면 global x 선언이 필요하고, 더 좋은 방법은 함수 인자로 받고 반환하는 것입니다.
  • 섀도잉 실수: list = [1, 2, 3]처럼 내장 함수 이름을 변수명으로 사용하면 해당 스코프에서 내장 함수를 사용할 수 없습니다. 변수명은 내장 이름과 충돌하지 않도록 하세요.
  • nonlocal과 global 혼동: nonlocal은 바로 바깥 함수의 변수를, global은 모듈 최상위 변수를 가리킵니다. 중첩 함수 안에서 모듈 변수를 수정하려면 global, 바깥 함수 변수를 수정하려면 nonlocal을 사용하세요.

📝 정리 및 다음 강의 예고

  • LEGB: Local → Enclosing → Global → Built-in 순서로 이름을 탐색합니다.
  • 함수 안에 할당(=)이 있으면 그 이름은 지역 변수 — 전역 변수를 수정하려면 global 선언 필요.
  • nonlocal은 중첩 함수에서 바깥 함수 변수를 수정할 때 사용합니다.
  • global 남용은 테스트와 추론을 어렵게 만들므로, 대신 인자/반환값이나 클래스를 사용하세요.

다음 강의: 클로저 — 함수가 자신이 정의된 환경을 기억하는 방법. nonlocal과 클로저의 관계, 팩토리 패턴, 메모이제이션을 배웁니다.

관련 주제

  • LEGB 스코프 탐색순서
  • 지역·전역·내장 스코프
  • UnboundLocalError
  • global 키워드
  • nonlocal 키워드
  • 변수 섀도잉
  • 개발·프로그래밍
  • 개발·프로그래밍 강의
  • 파이썬 기초 40강 — 처음 배우는 프로그래밍
  • 무료강의
  • 무료 온라인 강의
  • NUGUNA
  • 누구나

📚 시리즈 전체 공유

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

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

댓글

0/1000

불러오는 중...