
파이썬을 처음 접하는 개발자뿐만 아니라, 어느 정도 숙련된 개발자들도 간혹 놓치는 치명적인 함정이 있습니다. 바로 함수의 매개변수 기본값으로 Mutable(가변) 객체인 list나 dict를 사용하는 것입니다. 이 사소해 보이는 습관은 실무에서 예측 불가능한 버그를 야기하며, 시스템의 데이터 무결성을 해칠 수 있습니다. 본 포스팅에서는 파이썬의 객체 라이프사이클과 메모리 할당 방식을 심도 있게 분석하여, 왜 가변 객체를 기본 인자로 쓰면 안 되는지 그 차이와 원인을 규명하고, 실무에서 즉시 적용 가능한 7가지 이상의 해결 방법을 제시합니다.
1. 왜 이런 현상이 발생하는가? (파이썬의 Evaluation 시점)
파이썬에서 함수의 기본 인자(Default Argument)는 함수가 정의되는 시점(Definition time)에 단 한 번만 평가되어 메모리에 생성됩니다. 즉, 함수가 호출될 때마다 새로운 객체가 생성되는 것이 아니라, 이미 생성된 동일한 객체를 모든 함수 호출이 공유하게 되는 구조입니다.
- Immutable(불변) 객체:
None,int,str등은 값이 변경될 수 없으므로 공유되어도 안전합니다. - Mutable(가변) 객체:
list,dict,set등은 함수 내부에서 수정될 경우, 그 변경 사항이 다음 함수 호출 시에도 그대로 유지됩니다.
2. 가변 객체 vs 불변 객체 기본값 비교
다음 표는 기본 인자로 가변 객체와 불변 객체를 사용했을 때의 동작 차이를 요약한 것입니다.
| 비교 항목 | Mutable 객체 (list, dict) | Immutable 객체 (None, int) |
|---|---|---|
| 평가 시점 | 함수 정의 시 1회 평가 | 함수 정의 시 1회 평가 |
| 데이터 공유 여부 | 모든 호출이 동일 객체 공유 | 객체는 공유되나 수정 불가(새 객체 생성) |
| 부작용(Side Effect) | 이전 호출의 데이터가 잔류함 | 항상 독립적인 초기값 유지 |
| 권장 사용 여부 | 매우 위험 (Anti-pattern) | 권장 (Standard) |
3. 실무 적용 가능한 해결 방법 및 샘플 예제 (7가지)
방법 01: 'None'을 활용한 지연 초기화 (가장 표준적인 방법)
기본 인자로 None을 설정하고, 함수 내부에서 객체가 None일 때만 새 객체를 할당합니다.
def add_item(item, target_list=None):
if target_list is None:
target_list = []
target_list.append(item)
return target_list
# 실무 적용: 각 호출마다 독립적인 리스트가 생성됨
print(add_item("A")) # ['A']
print(add_item("B")) # ['B']
방법 02: 딕셔너리 업데이트 시의 안전한 초기화
설정값(config) 등을 다룰 때 유용하며, 원본 데이터 오염을 방지합니다.
def load_config(new_params, default_config=None):
actual_config = default_config.copy() if default_config else {}
actual_config.update(new_params)
return actual_config
# 실무 적용: API 호출 시 헤더나 파라미터 병합에 사용
current_call = load_config({"timeout": 30})
방법 03: 데이터 캡슐화를 위한 클래스 내부 초기화
객체 지향 프로그래밍에서 인스턴스 변수를 초기화할 때 발생할 수 있는 공유 문제를 해결합니다.
class UserManager:
def __init__(self, users=None):
# self.users = users or [] <-- 위험: 빈 리스트가 아닌 경우 공유 발생 가능
self.users = list(users) if users else []
# 실무 적용: 다수의 인스턴스가 독립적인 사용자 목록을 보유함
방법 04: Type Hinting과 함께 사용하는 방어적 복사
입력받은 가변 객체를 직접 수정하지 않고 복사본을 만들어 반환하는 방식입니다.
from typing import List, Optional
def process_data(data: Optional[List[int]] = None) -> List[int]:
local_data = list(data) if data is not None else []
# 로직 수행
return local_data
방법 05: 재귀 함수에서의 상태 누적 방지
트리 구조나 경로 탐색 시 path 리스트가 공유되어 결과가 꼬이는 현상을 방지합니다.
def find_path(node, current_path=None):
if current_path is None:
current_path = []
current_path.append(node.name)
# 하위 노드 탐색 시 새로운 리스트를 전달하거나 복사본 전달
for child in node.children:
find_path(child, list(current_path))
방법 06: 로깅 시스템의 Context 데이터 관리
멀티스레드 환경이나 반복적인 로깅 호출 시 이전 로그 정보가 남지 않도록 보장합니다.
def log_event(message, tags=None):
final_tags = set(tags) if tags else set()
final_tags.add("system_log")
print(f"[{final_tags}] {message}")
방법 07: 팩토리 함수를 이용한 동적 기본값 생성
매번 새로운 객체를 생성해야 하는 복잡한 로직이 필요한 경우 유용합니다.
def factory_default_example(data_provider=list):
new_storage = data_provider()
# 작업 수행
return new_storage
# 실무 적용: 상황에 따라 set, list 등 다른 컨테이너를 주입 가능
4. 결론 및 주의사항
파이썬의 가변 객체 기본 인자 문제는 단순한 문법적 특징을 넘어 "공유 가변 상태(Shared Mutable State)"라는 동시성 및 상태 관리의 핵심 이슈와 맞닿아 있습니다. 함수는 입력값에 대해 항상 동일하고 예측 가능한 결과를 내놓아야 하는 '순수성'을 지향해야 합니다. 실무에서는 반드시 None을 활용한 조건부 초기화를 습관화하여, 의도치 않은 데이터 오염과 디버깅하기 어려운 런타임 에러로부터 코드를 보호하시기 바랍니다.
내용 출처 및 참고 문헌
- Python Software Foundation. "Common Gotchas - Default Argument Values". official docs.
- Fluent Python, 2nd Edition by Luciano Ramalho. "Chapter 6: Object References, Mutability, and Recycling".
- Effective Python: 90 Specific Ways to Write Better Python by Brett Slatkin.
'Artificial Intelligence > 60. Python' 카테고리의 다른 글
| [PYTHON] *args와 **kwargs를 사용한 유연한 데코레이터 설계 방법 5가지와 실무 해결 차이 (0) | 2026.04.02 |
|---|---|
| [PYTHON] 리스트 컴프리헨션과 map/filter의 성능 차이 및 해결 방법 7가지 (0) | 2026.04.02 |
| [PYTHON] __slots__를 활용한 메모리 최적화 해결 방법 7가지와 80% 성능 차이 분석 (0) | 2026.04.02 |
| [PYTHON] 제너레이터의 혁신, yield와 yield from의 3가지 결정적 차이점과 최적화 방법 (0) | 2026.04.02 |
| [PYTHON] 객체 지향의 핵심, @staticmethod vs @classmethod vs 인스턴스 메서드 3가지 결정적 차이와 활용 방법 (0) | 2026.04.02 |