🌈 프로그래밍/Python

Python의 함정: Mutable Default Argument Bug

수구리 2025. 6. 30. 10:06
반응형

최근 프로젝트에서 cursor bug bot이 생겼는데 공짜라서 유용하게 써보고 있습니다.

어떤 기능 개발을 한 뒤에 PR을 작성하게 되면 깃헙 액션을 통해서 알아서 어떤 버그를 발생시킬 수 있는지 문제가 없는지 확인을 해주는 건데요

아래와 같은 버그 리포팅을 버그봇으로 부터 받게 되었고, 그 문제를 어떻게 해결을 시켰었는지 한번 작성해보도록 하겠습니다.

 

버그봇

이전 매니저의 매니저프리 신청건을 취소처리하는 비동기 함수기능이었습니다.

발생할 수 있는 버그는 바로 Mutable Default Argument 버그입니다.

🚨 예시 버그 상황

def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

# 첫 번째 호출
result1 = add_item("사과")
print(result1)  # ['사과']

# 두 번째 호출
result2 = add_item("바나나")
print(result2)  # ['사과', '바나나'] ?!

어? 뭔가 이상하지 않나요? 두 번째 호출에서는 ['바나나']만 나와야 할 것 같은데, 첫 번째 호출의 '사과'가 그대로 남아있습니다.

🔍 버그봇이 리포팅한 코드

@task
def async_before_manager_free_apply_cancel(
    match_ids: list, 
    processed_apply_ids: list = []  # 🚨 여기가 문제!
) -> None:
    # 이미 처리된 신청서는 제외하고 처리
    for apply in applications.exclude(id__in=processed_apply_ids):
        if cancel_success:
            processed_apply_ids.append(apply.id)

    # 실패한 경우 재시도
    if failed_cases:
        async_before_manager_free_apply_cancel.retry(
            args=[failed_match_ids, processed_apply_ids]
        )

이 코드의 문제점은 뭘까요? 첫 번째 함수 호출에서 processed_apply_ids에 뭔가 추가되면, 다음 호출에서도 그 값들이 계속 남아있다는 겁니다. 결국 처리되어야 할 신청서들이 "이미 처리됐다"고 잘못 판단되어 건너뛰게 되죠.

🤔 왜 이런 일이 생길까?

Python에서 함수의 기본값은 함수가 정의될 때 단 한 번만 평가됩니다.

def show_time(timestamp=time.time()):  # 함수 정의 시점에 한 번만 실행
    return timestamp

print(show_time())  # 1640995200.123
time.sleep(2)
print(show_time())  # 1640995200.123 (같은 값!)

마찬가지로 my_list=[]도 함수가 정의될 때 한 번만 생성되고, 모든 함수 호출에서 동일한 리스트 객체를 공유하게 됩니다.

✅ 해결 방법

간단합니다! 기본값으로 None을 사용하고 함수 내부에서 새로운 객체를 생성하면 됩니다.

Before (문제가 있는 코드)

def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list

After (수정된 코드)

def add_item(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

🛠️ 실제 프로젝트 적용

저희 프로젝트에서는 이렇게 수정했습니다:

@task
def async_before_manager_free_apply_cancel(
    match_ids: list, 
    processed_apply_ids: list | None = None  # 타입 힌트도 수정
) -> None:
    if processed_apply_ids is None:
        processed_apply_ids = []  # 매번 새로운 리스트 생성

    # 나머지 로직은 동일
    for apply in applications.exclude(id__in=processed_apply_ids):
        # ...

mypy를 사용하는 경우 타입 힌트도 list | None 또는 Optional[list]로 수정해야 합니다.

🎯 추가 팁

1. 다른 mutable 타입들도 주의하세요

# 딕셔너리도 마찬가지!
def bad_function(data, cache={}):  # ❌
    pass

def good_function(data, cache=None):  # ✅
    if cache is None:
        cache = {}

2. IDE나 린터 활용

  • pylint: dangerous-default-value 경고
  • mypy: 타입 체크로 미리 발견 가능
  • IDE 확장: 대부분의 Python IDE에서 경고 표시

3. 불변(immutable) 타입은 안전해요

def safe_function(value, default=0):     # int는 안전
    pass

def also_safe(text, prefix=""):         # str도 안전
    pass

🚀 마무리

Python의 mutable default argument는 처음에는 이해하기 어려울 수 있지만, 한 번 알고 나면 쉽게 피할 수 있는 함정입니다.

핵심은:

  • 기본값으로 None 사용
  • 함수 내부에서 새 객체 생성
  • 타입 힌트 정확히 작성

여러분도 코드 리뷰할 때 def function(param=[]) 같은 패턴을 발견하면 한 번 더 체크해보세요.


이런 실무 경험과 팁이 도움이 되셨나요? 댓글로 여러분의 Python 함정 경험도 공유해주세요!

반응형