인자에 대해 이터레이션할 때는 방어적으로 접근하기

Tasker_Jang·2026년 8월 13일
post-thumbnail

🤔 이게 왜 문제인가

리스트를 받아 잘 동작하던 함수가 제너레이터(generator)를 넘기는 순간 빈 결과를 뱉는 상황을 만나신 적 있을겁니다. 예외도 없고 로그도 조용한데 결과만 비어 있는 이 증상은, 함수가 인자를 두 번 이상 순회할 때 벌어집니다.

원인은 이터레이터(iterator)가 한 번만 소비되는 일회용 자원이기 때문입니다. 파이썬의 이터레이터 프로토콜에서 리스트 같은 컨테이너는 __iter__가 호출될 때마다 새 이터레이터를 만들어 주지만, 이터레이터 자신은 __iter__에서 자기 자신을 돌려줍니다. 그래서 이미 끝까지 소비된 이터레이터를 다시 순회하면 남은 값이 없어 즉시 종료됩니다. 문제는 이 상황에서 StopIteration이 예외로 드러나지 않고 "원소가 없는 정상적인 순회"로 처리된다는 점입니다. 조용히 잘못되는 코드가 가장 나쁜 코드입니다.

💣 흔한 실수

def normalize(numbers):
    total = sum(numbers)  # 여기서 이터레이터를 끝까지 소비합니다
    result = []

    for value in numbers:  # 남은 값이 없어 한 번도 돌지 않습니다
        result.append(100 * value / total)

    return result


def read_visits(data_path):
    with open(data_path) as f:
        for line in f:
            yield int(line)


visits = [15, 35, 80]
print(normalize(visits))  # [11.53..., 26.92..., 61.53...]

it = read_visits('my_numbers.txt')
print(normalize(it))  # [] - 예외 없이 빈 리스트가 나옵니다

리스트로 테스트할 때는 완벽하게 동작하다가, 파일이 커져 제너레이터로 바꾸는 순간 무너집니다. 함수 시그니처만 봐서는 "이 인자를 두 번 읽습니다"라는 사실을 알 방법이 없다는 점이 특히 곤란합니다.

✅ 파이썬다운 방법

호출할 때마다 새 이터레이터를 만들어 주는 컨테이너를 직접 정의하는 것이 가장 깔끔한 해법입니다. __iter__를 제너레이터로 구현하면 클래스 하나로 끝납니다.

class ReadVisits:
    def __init__(self, data_path):
        self.data_path = data_path

    def __iter__(self):
        # 순회할 때마다 새 제너레이터가 만들어집니다
        with open(self.data_path) as f:
            for line in f:
                yield int(line)


visits = ReadVisits('my_numbers.txt')
print(normalize(visits))  # 몇 번을 호출해도 같은 결과가 나옵니다

인자를 리스트로 통째로 복사하는 방법도 있지만, 그러면 애초에 제너레이터를 쓴 이유인 메모리 절약이 사라집니다. 반면 이 방식은 파일을 두 번 읽는 대신 메모리 사용량을 낮게 유지합니다.

여기에 더해 잘못된 입력을 일찍 걷어내는 방어 코드를 두면 좋습니다.

from collections.abc import Iterator


def normalize_defensive(numbers):
    if isinstance(numbers, Iterator):  # iter(numbers) is numbers 검사와 같은 의미입니다
        raise TypeError('컨테이너를 넘겨야 합니다')

    total = sum(numbers)

    return [100 * value / total for value in numbers]

빈 리스트를 반환하며 조용히 실패하는 대신, 호출한 쪽에서 즉시 TypeError를 받게 됩니다. 버그를 발견하는 지점이 사고가 난 곳으로 앞당겨진다는 점에서 안전성이 크게 올라갑니다.

📎 기억할 것

  • 인자를 두 번 이상 순회하는 함수는 이터레이터를 받으면 조용히 잘못된 결과를 냅니다.
  • 이터레이터는 소진되어도 예외를 던지지 않고 빈 순회처럼 끝나므로 발견이 늦어집니다.
  • __iter__를 제너레이터로 구현한 컨테이너 클래스를 만들면 매번 새 이터레이터를 얻습니다.
  • 어떤 값이 이터레이터인지는 iter(x) is x로 확인하거나 collections.abc.Iterator로 검사합니다.
  • 방어 코드를 넣어 이터레이터가 들어오면 TypeError를 던지는 편이 안전합니다.

🛠 실무에서 어디 쓸까

대용량 로그나 CSV를 여러 번 훑어야 하는 배치 작업에서 바로 쓰입니다. 전체 합계를 구한 뒤 각 행의 비율을 계산하는 집계 작업이 대표적이며, 파일 경로를 감싼 컨테이너 클래스를 하나 만들어 두면 메모리를 아끼면서도 순회 횟수 제약에서 자유로워집니다.

공용 유틸리티 함수를 작성할 때도 방어 검사를 습관화하면 좋습니다. 다른 팀원이 무심코 제너레이터 식을 넘겼을 때 빈 결과 대신 명확한 예외를 받게 되므로, 원인을 추적하는 시간이 몇 시간에서 몇 초로 줄어듭니다.

profile
ML Engineer 🧠 | AI 모델 개발과 최적화 경험을 기록하며 성장하는 개발자 🚀 The light that burns twice as bright burns half as long ✨

0개의 댓글