[Engineering Insight] 비동기와 제너레이터: 18년 차 엔지니어가 다시 짚어본 본질

최근 새로운 기술 스택을 깊게 파고들며, 우리가 무심코 사용하던 **비동기(Async)**와 **제너레이터(Generator)**의 본질에 대해 다시 고민해 보았습니다. 18년 동안 시스템을 다뤄온 엔지니어의 시각으로, Python, Flutter, Java 등 각기 다른 언어들이 이 문제를 어떻게 풀어내고 있는지 정리해 봅니다.

Q1.
다음 코드의 시간복잡도(Big-O)는?

def f(n):
    s = 0
    for i in range(n):
        for j in range(i):
            s += 1
    return s

1. 메모리의 혁신: 리스트(List)와 제너레이터(Generator)

우리가 흔히 쓰는 ‘무한 스크롤’ 기능을 떠올려 봅시다. 수만 개의 데이터를 한꺼번에 로드하면 메모리는 버티지 못합니다.

  • List: 모든 데이터를 즉시 메모리에 할당합니다.
  • Generator: “다음에 줄 데이터는 이거야”라는 규칙만 저장합니다.

파이썬의 제너레이터는 **지연 평가(Lazy Evaluation)**를 통해 메모리 점유율을 비약적으로 낮춥니다. 10GB의 로그를 처리할 때도 단 몇 MB의 메모리만으로 시스템을 안정적으로 유지할 수 있는 비결입니다.


2. 비동기(Async)의 본질: ‘동적 콜백’과 일시 정지

많은 이들이 async/await를 단순한 문법으로 치부하지만, 그 본질은 **’함수 실행의 일시 정지(Suspend)’**와 **’동적 콜백 등록’**에 있습니다.

우리가 await를 만나는 순간, 함수는 현재의 상태(로컬 변수, 스택 포인트 등)를 그대로 패키징하여 이벤트 루프에 던집니다. 그리고 CPU는 다른 일을 하러 떠나죠.

  • 전통적 이벤트 드리븐: 콜백을 수동으로 등록하고 관리해야 하므로 상태 관리가 어렵습니다.
  • Modern Async (Python/Flutter): 함수 자체가 ‘상태’를 품고 멈추기 때문에, 개발자는 동기적인 코드 흐름 안에서 비동기적 이득을 모두 취할 수 있습니다.

Q2.

Generator vs List 차이 설명 + 아래 코드 결과 차이 설명

a = [i*i for i in range(1000000)]
b = (i*i for i in range(1000000))

3. 언어별 아키텍처의 차이

Python & Flutter (Dart)

두 언어는 형제와 같습니다. 싱글 스레드 기반의 이벤트 루프를 사용하며, async/await라는 명시적 키워드를 통해 비동기 구간을 설정합니다. 사용자 경험(UX)을 위해 UI 스레드를 멈추지 않거나(Flutter), I/O 대기 시간을 효율적으로 활용하는 것(Python)이 목표입니다.

Java (Project Loom)

자바는 조금 다른 길을 택했습니다. 새로운 문법을 도입하는 대신 **’가상 스레드(Virtual Thread)’**를 도입했습니다. 개발자는 기존의 동기 코드를 짜던 방식 그대로 코딩하지만, 내부적으로 JVM이 수백만 개의 경량 스레드를 가로채 비동기적으로 스위칭합니다.

Q3.

다음 코드의 문제점은?

def append_item(item, lst=[]):
    lst.append(item)
    return lst

4. 시니어 엔지니어의 시선: 왜 설계를 고민해야 하는가?

비동기 환경에서 로그인을 구현한다고 가정해 봅시다. 로그인이 완료되어야 다음 단계로 갈 수 있다면, 그 함수 내부는 대기 상태가 됩니다. 하지만 시스템 전체는 멈추지 않습니다.

여기서 엔지니어의 역량이 드러납니다.

  1. 의존성 설계: 무엇을 기다리고(await), 무엇을 병렬로 던질(gather) 것인가?
  2. 리소스 관리: 비동기 루프 안에서 CPU를 점유하는 ‘블로킹 연산’을 어떻게 격리할 것인가?

마치며: 성긴 그물을 촘촘하게

18년의 경험은 강력한 무기입니다. 다만 새로운 도구를 만났을 때 생기는 ‘공백’은 그 도구의 설계 철학을 이해함으로써 채워집니다. yield가 왜 함수를 멈추는지, await가 왜 동적 콜백인지 그 원리를 파악할 때, 비로소 백지 위에서도 자신 있게 아키텍처를 그려나갈 수 있습니다.

단순히 동작하는 코드를 넘어, 이유가 명확한 코드를 짜는 것. 그것이 우리가 가족의 안전과 엔지니어로서의 수명을 100세까지 지켜나갈 방법이라 믿습니다.


🔵 테스트 문제 해설 (머릿속 시뮬레이션)

Q1. 시간복잡도 (Big-O)

이 코드는 이중 루프입니다.

  • 첫 번째 루프는 $n$번 돕니다.
  • 두 번째 루프는 $0, 1, 2, \dots, n-1$번 돕니다.
  • 전체 실행 횟수는 $0 + 1 + 2 + \dots + (n-1) = \frac{n(n-1)}{2}$입니다.
  • 최고차항만 남기면 **$O(n^2)$**이 됩니다.

Q2. Generator vs List

  • List (a): 실행 즉시 100만 개의 결과를 메모리에 할당합니다. 속도는 빠를 수 있지만 메모리 점유율이 높습니다.
  • Generator (b): ‘계산할 방법’만 저장하고, 값이 필요할 때마다 하나씩 생성합니다 (Lazy Evaluation). 메모리를 거의 차지하지 않아 대용량 데이터 처리에 필수적입니다.

Q3. Mutable Default Arguments (가변 기본 인자)

  • 문제점: 파이썬에서 기본 인자(lst=[])는 함수가 정의되는 시점에 딱 한 번 생성됩니다.
  • 함수를 호출할 때마다 새로운 리스트가 생기는 게 아니라, 동일한 리스트 객체를 계속 공유하게 됩니다. 그래서 append_item(1)을 두 번 호출하면 [1, 1]이 반환되는 의도치 않은 결과가 발생합니다.