파이썬 변수 모델 총정리 이름에 객체 바인딩과 가변 불변 파라미터 전달 가변 기본값 금지
🧩 한 번에 이해하는 파이썬 변수 동작 원리와 실무에서 꼭 지켜야 할 안전 규칙
코드를 몇 줄만 써도 굴러가는 것이 파이썬의 매력이지만, 변수와 함수가 실제로 어떻게 동작하는지 모르면 설명하기 어려운 버그와 마주하기 쉽습니다.
리스트가 이유 없이 바뀌는 것처럼 보이거나, 기본값을 줬을 뿐인데 함수가 점점 이상해지는 경험이 딱 그렇죠.
이 글은 그런 당혹감을 줄이기 위한 안내서입니다.
변수는 ‘값을 담는 그릇’이 아니라 ‘이름이 객체를 가리키는 바인딩’이라는 사실부터 차근차근 풀어가며, 가변과 불변 객체의 차이, 파라미터 전달이 참조(바인딩) 기반이라는 점, 그리고 가변 기본값을 금지해야 하는 이유까지 실제 코드 감각에 맞춰 설명합니다.
핵심은 단순합니다.
이름이 객체에 묶이고, 객체의 가변성 여부에 따라 관찰되는 효과가 달라집니다.
함수 호출 시에도 값이 복사되는 것이 아니라 이름이 객체를 가리키는 방식으로 전달되어, 같은 객체를 여러 이름이 공유할 수 있습니다.
그래서 기본 인자에 리스트나 딕셔너리 같은 가변 객체를 두면, 호출 사이에 상태가 누적되는 문제가 생기죠.
이 글에서는 이러한 동작 원리를 정확한 개념과 안전한 코딩 규칙으로 정리해, 초보 단계부터 실무 프로젝트까지 흔들리지 않는 기준을 제공하려 합니다.
📋 목차
🔗 이름에 객체 바인딩이란?
파이썬에서 변수는 값을 담는 그릇이 아닙니다.
이름(name)이 메모리에 존재하는 객체(object)에 연결되는 바인딩(binding) 구조입니다.
하나의 객체를 여러 이름이 가리킬 수 있고, 한 이름이 시간이 지나 다른 객체로 다시 연결(rebinding)될 수 있습니다.
이해를 쉽게 하려면 ‘왼쪽은 이름, 오른쪽은 객체’라고 기억해두면 좋습니다.
할당문은 객체를 생성하거나 찾은 뒤 그 객체의 참조를 이름에 붙이는 행위이며, 이름 자체는 타입을 갖지 않고 객체가 타입과 상태를 가집니다.
아래 예시에서 a와 b는 같은 리스트 객체를 가리킵니다.
b에 대한 변경이 a에서도 보이는 이유는 두 이름이 동일한 객체에 바인딩되어 있기 때문입니다.
반대로, b를 새로운 리스트로 재바인딩하면 더 이상 a와 연결이 공유되지 않습니다.
이 차이를 명확히 체감하면 가변/불변, 파라미터 전달의 ‘참조(바인딩)’ 개념도 자연스럽게 이어집니다.
# 이름은 객체에 바인딩된다
a = [1, 2]
b = a # b와 a는 같은 리스트 객체를 가리킴
b.append(3)
print(a) # [1, 2, 3]
print(a is b) # True (동일 객체 확인)
# 재바인딩: b가 '새로운' 객체를 가리키게 된다
b = b + [4] # 새 리스트가 만들어지고, b가 그 객체로 연결됨
print(a) # [1, 2, 3]
print(b) # [1, 2, 3, 4]
print(a is b) # False
# 불변 객체 예: 튜플 자체는 변경 불가지만, 내부에 가변 객체가 있으면 그 내부는 변할 수 있다
t = (a, 99)
a.append(5)
print(t) # ([1, 2, 3, 5], 99) 튜플은 그대로, 내부 리스트 내용만 변함
# 객체 신원(아이덴티티) 관찰
print(id(a), id(b)) # 서로 다른 정수(주소 비유 가능)
| 개념 | 설명 |
|---|---|
| 이름(name) | 객체를 가리키는 레이블입니다. 스코프(지역, 전역 등)와 생애주기에 따라 존재합니다. |
| 객체(object) | 타입과 상태를 가지며 메모리에 존재합니다. 동일성은 is, 동등성은 ==로 비교합니다. |
| 바인딩(binding) | 이름을 객체 참조에 연결합니다. 할당, import, 함수 정의의 인자 매핑 등이 이에 해당합니다. |
💡 TIP: “변수에 값이 들어있다”보다 “이름이 객체를 가리킨다”라고 말해보세요.
리뷰나 협업에서 오해를 줄이고, 재바인딩과 공유 참조 이슈를 빠르게 포착할 수 있습니다.
⚠️ 주의: “복사돼서 전달된다”는 표현은 파이썬의 기본 동작을 잘못 이해하게 만들 수 있습니다.
이름이 같은 객체를 공유할 수 있다는 점을 항상 염두에 두세요.
특히 리스트, 딕셔너리처럼 가변 객체는 공동 편집 효과가 발생합니다.
💎 핵심 포인트:
이름은 타입을 갖지 않고, 객체가 타입과 상태를 갖습니다.
하나의 객체를 여러 이름이 가리킬 수 있습니다.
재바인딩은 이름과 객체의 연결만 바꿀 뿐, 기존 객체를 자동으로 변경하지 않습니다.
가변 객체를 공유하면 한쪽 변경이 다른 쪽에서도 보입니다.
💬 요약하면, 파이썬의 변수 모델은 ‘이름→객체’ 지도입니다.
이 관점을 유지하면 가변/불변의 관찰 효과와 파라미터 전달의 본질을 일관성 있게 설명할 수 있습니다.
🧱 가변과 불변의 차이와 코드 영향
파이썬에서 객체는 가변(mutable)과 불변(immutable) 두 부류로 나뉩니다.
가변 객체는 생성 후 내부 상태(값)를 변경할 수 있는 반면, 불변 객체는 한 번 만들어지면 내용이 바뀌지 않습니다.
이 차이는 단순한 성능 문제가 아니라, 코드의 동작 방식과 버그 발생 패턴을 결정합니다.
대표적인 불변 객체는 int, float, str, tuple, frozenset이고,
가변 객체는 list, dict, set, bytearray 등이 있습니다.
가변 객체는 내부 수정이 가능하기 때문에 여러 이름이 같은 객체를 가리킬 경우, 한쪽의 변경이 다른 쪽에도 영향을 미칩니다.
반면 불변 객체는 수정 대신 ‘새로운 객체 생성’으로 대응하기 때문에, 공유 참조의 부작용이 거의 없습니다.
# 불변 객체 예시
x = 10
y = x
y += 1 # y는 새로운 int 객체로 재바인딩됨
print(x, y) # 10, 11
print(x is y) # False
# 가변 객체 예시
a = [1, 2, 3]
b = a
b.append(4)
print(a) # [1, 2, 3, 4] -> a도 함께 변함
print(a is b) # True
위 예시를 보면 불변 객체는 연산 후 새 객체가 만들어지고 이름이 그 객체로 이동합니다.
즉, ‘값이 바뀌는 것처럼 보이지만 실제로는 새로운 객체가 생기는 것’입니다.
가변 객체는 내부 상태를 직접 수정하므로 객체의 아이덴티티(id)는 그대로 유지됩니다.
따라서 가변 객체를 공유할 때는 ‘의도치 않은 상태 변경(side effect)’이 일어날 가능성을 늘 염두에 둬야 합니다.
💎 핵심 포인트:
가변 객체는 내부 수정이 가능해 공유 시 주의가 필요합니다.
불변 객체는 수정이 아닌 새 객체 생성으로 안전성을 보장하지만, 연산이 잦을 경우 메모리 사용이 늘어날 수 있습니다.
이 특성은 파라미터 전달, 디폴트 인자, 복사(copy) 시 중요하게 작용합니다.
- 🧮불변 객체는 내부 값을 바꾸는 대신 새 객체를 만든다.
- 📦가변 객체는 동일 객체를 공유할 수 있어 한쪽 변경이 다른 곳에 영향을 줄 수 있다.
- 🧠id()로 객체의 동일성을 확인하면 바인딩 구조를 더 쉽게 이해할 수 있다.
💬 불변 객체는 함수나 딕셔너리 키처럼 신뢰성 있는 식별이 필요할 때 적합합니다.
반대로 가변 객체는 동적 상태를 관리해야 하는 데이터 구조에 유용합니다.
📮 파라미터 전달은 참조 전달(바인딩)
파이썬에서 함수 호출 시 인자는 객체의 참조(reference)가 전달됩니다.
이 방식을 흔히 “참조에 의한 전달(call by object reference)” 또는 “바인딩에 의한 전달”이라고 부릅니다.
즉, 함수 내부의 매개변수 이름은 인자가 가리키던 객체에 새로 바인딩됩니다.
하지만 함수 내부에서 재바인딩이 일어나면 원래 객체와의 연결이 끊기며, 외부 변수에는 영향을 주지 않습니다.
이 구조는 ‘값이 복사되어 전달된다’고 오해하기 쉬운 부분입니다.
실제로는 객체의 주소(참조)가 전달되므로, 가변 객체의 경우 함수 내부에서 수정이 일어나면 외부에서도 그 변화가 보입니다.
반면 불변 객체는 수정 자체가 불가능하므로 외부 변수는 안전하게 유지됩니다.
def modify_list(data):
data.append(99) # 내부에서 가변 객체를 수정
print("내부:", data)
nums = [1, 2, 3]
modify_list(nums)
print("외부:", nums)
# 내부: [1, 2, 3, 99]
# 외부: [1, 2, 3, 99] → 같은 객체가 공유됨
def reassign_list(data):
data = [0] # 재바인딩 → 새로운 객체 연결
print("내부:", data)
nums = [10, 20, 30]
reassign_list(nums)
print("외부:", nums)
# 내부: [0]
# 외부: [10, 20, 30] → 연결이 끊겨 영향 없음
위 코드에서 modify_list()는 기존 리스트를 직접 수정하므로 외부에서도 변경이 반영됩니다.
하지만 reassign_list()는 data가 새로운 객체로 재바인딩되었기 때문에 외부에는 영향을 미치지 않습니다.
즉, 함수 내부의 매개변수는 독립적인 이름일 뿐, 처음에는 같은 객체를 가리키지만 언제든 다른 객체로 교체될 수 있습니다.
💎 핵심 포인트:
함수 인자는 ‘복사’되지 않고 ‘참조’로 전달됩니다.
함수 내부에서 가변 객체를 직접 수정하면 외부에도 영향을 줍니다.
하지만 변수를 새 객체에 재바인딩하면, 원본과의 연결은 즉시 끊깁니다.
- 🔗파이썬은 call by object reference 구조이다.
- ✏️함수 내부에서 객체 수정은 외부에도 반영될 수 있다.
- 🚫함수 내부에서 재바인딩하면 외부 변수에는 영향이 없다.
💬 이 개념을 이해하면, 함수 설계에서 ‘부수효과(side effect)’를 의도적으로 제어할 수 있습니다.
특히 데이터가 왜 예상과 다르게 변하는지를 빠르게 파악할 수 있습니다.
🛑 가변 기본값 금지와 안전한 대안
파이썬 함수에서 디폴트 인자(default parameter)는 함수 정의 시점에 한 번만 평가됩니다.
따라서 기본값으로 가변 객체(list, dict, set 등)를 지정하면, 그 객체는 여러 번의 호출에서 계속 재사용됩니다.
이로 인해 함수가 호출될 때마다 이전 호출의 상태가 누적되는 예기치 않은 동작이 발생할 수 있습니다.
아래 예제를 보면 그 위험성이 명확해집니다.
기본값으로 사용된 리스트가 함수 호출 간 공유되면서, 새로운 데이터가 이전 결과에 덧붙여지는 문제가 생깁니다.
def add_item(item, items=[]):
items.append(item)
return items
print(add_item('🍎'))
print(add_item('🍌'))
print(add_item('🍇'))
# 출력:
# ['🍎']
# ['🍎', '🍌']
# ['🍎', '🍌', '🍇'] ← 이전 상태가 계속 누적됨!
이 문제는 파이썬 초보자뿐만 아니라 숙련자에게도 실수를 유발할 수 있습니다.
가변 객체를 기본 인자로 쓰면, 함수가 호출될 때마다 그 객체가 새로 생성되는 것이 아니라 이미 존재하는 하나의 객체가 재사용되기 때문입니다.
💡 TIP: 디폴트 인자에는 None을 사용하고, 함수 내부에서 새 객체를 생성하는 것이 가장 안전한 패턴입니다.
def add_item_safe(item, items=None):
if items is None:
items = [] # 호출 시마다 새 리스트 생성
items.append(item)
return items
print(add_item_safe('🍎'))
print(add_item_safe('🍌'))
print(add_item_safe('🍇'))
# 출력:
# ['🍎']
# ['🍌']
# ['🍇']
이 패턴은 파이썬 표준 라이브러리에서도 자주 사용됩니다.
예를 들어 dataclasses 모듈에서는 field(default_factory=list) 형태로 가변 기본값 문제를 방지합니다.
이렇게 하면 함수나 클래스 인스턴스가 생성될 때마다 새로운 객체가 안전하게 만들어집니다.
- ⚙️디폴트 인자는 정의 시 한 번만 평가된다.
- 🧩가변 객체를 기본값으로 쓰면 상태가 누적된다.
- 💡항상 None + 내부 초기화 패턴을 사용하라.
💬 “가변 기본값 금지”는 파이썬 함수 설계의 기본 수칙입니다.
이 규칙 하나만 지켜도 디버깅 시간의 절반을 절약할 수 있습니다.
🧪 실수 사례와 디버깅 체크리스트
변수 바인딩, 가변/불변 객체, 참조 전달, 그리고 가변 기본값은 각각 독립적인 개념처럼 보이지만 실제로는 서로 긴밀히 얽혀 있습니다.
이 네 가지를 정확히 구분하지 않으면 함수나 클래스 설계 단계에서 미묘한 버그가 발생합니다.
특히 데이터가 ‘어디에서, 언제, 어떻게 바뀌었는가’를 추적하기 어려운 경우 대부분 바인딩 구조나 가변 객체의 특성이 원인입니다.
예를 들어, 여러 인스턴스가 하나의 리스트를 공유하거나, 함수 호출 후 이전 호출의 데이터가 남는 현상은 대부분 가변 객체의 바인딩 문제로부터 시작됩니다.
이럴 때는 “이름이 어떤 객체를 가리키고 있는가”를 중심으로 디버깅하면 문제를 빠르게 파악할 수 있습니다.
class Box:
def __init__(self, items=[]): # ❌ 위험한 코드
self.items = items
a = Box()
b = Box()
a.items.append('🎁')
print(a.items) # ['🎁']
print(b.items) # ['🎁'] → 두 인스턴스가 같은 리스트를 공유함
print(a.items is b.items) # True
# 올바른 버전
class SafeBox:
def __init__(self, items=None):
self.items = [] if items is None else items
x = SafeBox()
y = SafeBox()
x.items.append('🎈')
print(x.items) # ['🎈']
print(y.items) # [] → 각각 독립된 객체
위 예제는 클래스 설계에서 자주 등장하는 실수입니다.
초기화 메서드 __init__에서도 디폴트 인자가 한 번만 평가되므로, 모든 인스턴스가 동일한 리스트를 공유하게 됩니다.
이 문제는 함수의 기본값 규칙과 완전히 같은 원리에서 발생합니다.
- 🧭문제가 생기면 id()로 객체의 동일성을 확인하라.
- 🧩함수 기본값이나 클래스 속성에 가변 객체를 직접 넣지 말 것.
- 🔍“같은 리스트가 왜 같이 바뀌지?” 싶을 땐 바인딩 구조부터 점검하라.
- 🧠함수 내부에서 재바인딩과 내부 수정의 차이를 항상 구분하라.
- 🧾copy()나 deepcopy()로 명시적 복사를 사용하는 습관을 들이자.
💎 핵심 포인트:
파이썬의 변수 모델은 단순하지만, ‘언제 객체가 새로 만들어지고 언제 기존 객체가 공유되는가’를 모르면 예기치 않은 결과가 나옵니다.
가변 객체와 바인딩 구조를 시각적으로 그려보는 습관이 가장 확실한 예방책입니다.
💬 가장 흔한 실수는 “가변 객체를 기본값으로 사용”하거나 “참조가 같은 객체를 여러 이름으로 공유”하는 것입니다.
이 두 가지를 피하는 것만으로도 파이썬 코드는 훨씬 안정적으로 유지됩니다.
❓ 자주 묻는 질문 (FAQ)
파이썬에서 변수는 값을 저장하나요?
이름이 객체에 바인딩되어 연결되는 구조로 이해해야 합니다.
불변 객체는 진짜로 절대 안 바뀌나요?
예를 들어 튜플 안의 리스트는 변경 가능합니다.
함수 인자는 복사돼서 전달되지 않나요?
즉, 함수 내부와 외부가 같은 객체를 가리킬 수 있으며, 가변 객체를 수정하면 외부에도 영향이 미칠 수 있습니다.
리스트를 기본값으로 써도 괜찮을 때가 있나요?
기본값은 정의 시 한 번만 생성되므로 호출 사이에 데이터가 공유됩니다.
디폴트 인자로 None을 쓰는 이유가 뭔가요?
또한 함수 내부에서 필요한 경우에만 새 객체를 생성할 수 있어, 호출 간 상태 공유 문제를 방지합니다.
id() 함수는 실제 메모리 주소인가요?
id()는 객체의 ‘고유한 식별값’을 반환하며, 이는 객체의 수명 동안 유일하지만 실제 주소와 일치한다고 보장되지는 않습니다.
가변 객체를 안전하게 복사하려면 어떻게 하나요?
copy.copy(), 깊은 복사는 copy.deepcopy()를 사용합니다.딕셔너리나 리스트 내부 구조가 복잡할수록 deepcopy가 더 안전합니다.
이 개념들이 실무에 왜 중요한가요?
바인딩과 가변성 원리를 정확히 알아야 예기치 않은 데이터 손상, 메모리 낭비, 상태 공유 버그를 방지할 수 있습니다.
🧭 파이썬 변수 모델 핵심 정리와 안전한 코딩 습관
파이썬의 변수 모델은 단순한 것처럼 보여도, 실제로는 언어의 근본적인 작동 원리를 드러냅니다.
이름이 객체에 바인딩되는 구조를 이해하면 ‘값이 복사된다’는 잘못된 관념에서 벗어나, 훨씬 예측 가능한 코드 설계가 가능해집니다.
또한 가변과 불변의 차이를 알면 왜 어떤 객체는 상태가 바뀌고, 어떤 객체는 바뀌지 않는지 쉽게 이해할 수 있습니다.
함수 호출 시 파라미터는 객체 참조를 통해 전달되며, 내부 수정과 재바인딩의 차이에 따라 외부 영향 여부가 결정됩니다.
여기에 더해, 가변 객체를 기본값으로 사용하는 것은 절대 금물이라는 규칙을 반드시 기억해야 합니다.
이 원리만 명확히 이해하면, 파이썬 코드의 대부분의 의도치 않은 동작을 예방할 수 있습니다.
실무에서는 이러한 기본 원리를 코드 리뷰나 협업 시에 명확히 설명할 수 있을 정도로 숙지해야 합니다.
함수의 인자 전달 방식, 객체 공유, 디폴트 인자 평가 시점은 모두 안정적인 시스템을 만드는 데 필수적인 요소입니다.
이 글에서 다룬 바인딩, 가변/불변, 참조 전달, 기본값 문제는 단순한 문법 설명을 넘어 파이썬 프로그래밍의 안정성을 결정짓는 기초 체계입니다.
💎 핵심 포인트 정리:
1️⃣ 변수는 값을 저장하는 게 아니라 이름이 객체에 바인딩되는 구조입니다.
2️⃣ 가변 객체는 수정 시 외부에도 영향이 미칠 수 있습니다.
3️⃣ 함수 인자는 참조(바인딩)로 전달됩니다.
4️⃣ 가변 기본값은 절대 사용하지 말고 None으로 초기화하세요.
5️⃣ 디버깅 시엔 항상 id()나 is를 활용해 객체의 동일성을 확인하세요.
🏷️ 관련 태그 : 파이썬기초, 변수모델, 바인딩, 가변불변, 참조전달, 함수인자, 디폴트인자, 파이썬문법, 객체지향, 디버깅팁