최근 새로운 기술 스택을 깊게 파고들며, 우리가 무심코 사용하던 **비동기(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. 시니어 엔지니어의 시선: 왜 설계를 고민해야 하는가?
비동기 환경에서 로그인을 구현한다고 가정해 봅시다. 로그인이 완료되어야 다음 단계로 갈 수 있다면, 그 함수 내부는 대기 상태가 됩니다. 하지만 시스템 전체는 멈추지 않습니다.
여기서 엔지니어의 역량이 드러납니다.
- 의존성 설계: 무엇을 기다리고(await), 무엇을 병렬로 던질(gather) 것인가?
- 리소스 관리: 비동기 루프 안에서 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]이 반환되는 의도치 않은 결과가 발생합니다.