![]()
async generator에서 `yield from` 위임이 막히는 게 왜 실무에서 아픈지: PEP 828 이후의 코드 모양
요즘 파이썬 서비스에서 “스트리밍”은 특수 기능이 아니라 기본 도구가 됐다. 토큰 스트림, 이벤트 스트림, 로그 스트림, 진행률 스트림.
그러다 보면 레포에 비슷한 래퍼가 늘어난다.
- 원본 스트림에 메타데이터를 붙이는 래퍼
- 중간에 관측(로그/메트릭)을 끼워 넣는 래퍼
- 오류를 분류해서 한 번만 바꿔 던지는 래퍼
이 래퍼들은 대개 “위임”이 대부분이다. 본체는 원본 스트림이고, 나는 옆에서 잠깐 만져서 다시 흘려보내고 싶다.
동기 제너레이터라면 여기서 생각이 끝난다. yield from으로 넘기면 된다.
그런데 async generator에서는, 그 자연스러운 동작이 막혀 있다. 그래서 개발자는 갑자기 ‘동작’을 구현하기 시작한다. async for로 한 번 더 감싸고, 예외 전달을 고민하고, 종료 처리를 붙인다. 기능은 얇은데 코드가 두꺼워진다.
최근 Discourse에서 이 문제가 다시 뜨겁게 논의됐고, PEP 828로 정리되면서 “이제는 이야기만이 아니라 언어 차원에서 다뤄보자”는 분위기가 만들어졌다(참고자료). CPython 레퍼런스 구현까지 공개되면서, 실무 입장에서도 “어떻게 바뀔지”를 손에 잡히게 상상할 수 있게 됐다.
위임이 막히는 순간은 늘 ‘얇은 래퍼’에서 온다
문제가 터지는 장면은 거창하지 않다. 얇은 래퍼 하나 만들다가 시작한다.
예를 들어 어떤 스트림을 감싸서, 처음 한 번만 시작 이벤트를 넣고 싶다. 혹은 스트림이 끝날 때 한 줄 로그를 남기고 싶다. 동기 코드라면 래퍼는 깔끔하다. 앞에 한 줄, 뒤에 한 줄, 가운데는 yield from.
async generator에서는 그 가운데가 금지다. 그래서 아래 같은 모양이 “하고 싶은데 못 하는” 코드로 남는다.
async def wrapped(src):
# (앞에서 이벤트 한 번 넣고)
yield from src # SyntaxError: async generator에서는 허용되지 않음
# (끝나면 정리)
이게 단순한 불만으로 끝나지 않는 이유는, 위임을 못 하니까 팀이 ‘위임 구현’을 각자 하기 시작한다는 데 있다.
어떤 사람은 async for로 감싼다. 어떤 사람은 anext() 루프를 직접 굴린다. 어떤 사람은 버퍼를 만든다. 결과적으로 레포에는 “같은 일을 하는데 모양이 다른 코드”가 많아지고, 그 모양 차이가 버그 차이로 이어진다.
스트리밍은 특히 그렇다. cancel/timeout, finally 정리, 예외 전파처럼 모서리가 많기 때문이다.
PEP 828이 바꾸려는 건 ‘기능 추가’보다 ‘팀의 합의 비용’이다
PEP 828을 읽고 내가 먼저 떠올린 건 “문법이 예뻐지겠네”가 아니었다.
리뷰가 가벼워지겠구나, 였다.
지금은 스트리밍 래퍼 PR을 열면, 로직보다 먼저 위임 구현을 검토하게 된다. 예외가 제대로 전파되는지, 취소가 어디서 먹히는지, 종료 때 리소스가 닫히는지. 기능은 단순한데 PR은 무겁다.
PEP 828이 제안하는 변화는 세 갈래로 이야기되지만, 실무에서는 “코드가 어떤 모양으로 바뀌느냐”로 체감된다.
이 제안이 재미있는 건, 거창한 기능 추가라기보다 “우리가 늘 쓰는 래퍼 코드의 모양”을 바꾸는 데 있다.
yield from이 async generator에서도 허용되면, 얇은 래퍼가 진짜로 얇아진다. 지금은 위임을 구현하느라 async for 루프가 래퍼의 본문을 잡아먹는데, 그 부분이 사라지고 관측/변환/경계 처리 같은 ‘내 일’만 남는다.
여기에 async yield from까지 들어오면, 동기 위임과 비동기 위임이 코드에서 분리된다. 지금은 둘 다 결국 async for ... yield로 풀어 쓰면서 섞여 보이는데, 그게 리뷰에서 제일 피곤하다. “이 래퍼는 끝에서 뭘 보장하지?”를 사람마다 다르게 읽기 때문이다.
마지막으로 non-None return 제한이 완화되면, 스트림이 끝날 때 요약값을 다루는 우회가 줄어든다. 지금은 마지막 이벤트로 우겨 넣거나 상태 객체를 공유하는 식으로 ‘규칙’을 숨기는데, 그 숨김이 결국 버그가 된다.
정리하자면 PEP 828은 스트리밍을 더 빠르게 만들겠다는 약속이라기보다, 스트리밍을 ‘덜 다르게 만들겠다’는 약속에 가깝다.
지금 코드가 지저분해지는 이유(그리고 덜 후회하는 우회)
현재 우리가 할 수 있는 위임은 결국 한 가지 모양으로 수렴한다.
async for로 돌면서 다시 yield하는 것.
이 모양은 안전하고 정직하다. 하지만 안전한 만큼, 모든 레이어가 같은 모양을 들고 있게 된다. 레이어의 의도가 흐려지고, 리뷰어는 매번 “이 위임이 맞나?”를 다시 확인한다.
그래서 나는 우회를 쓸 때 기준을 하나로 잡는다.
“나중에 문법이 들어오면, 가장 적게 바꿀 수 있는가?”
답은 대개 같다. 위임 루프는 최대한 바보처럼 두고, 변환/관측/정책은 순수 함수나 별도 호출로 분리한다. 그러면 PEP 828이 들어오는 날, 바꿀 건 위임 부분뿐이다.
async def delegate(src):
async for item in src:
yield item
async def wrapped(src):
# (관측/변환 로직은 바깥에서 처리하고)
async for item in delegate(src):
yield item
이건 “두 번 돌리니 느려지지 않나” 같은 이야기를 하려는 게 아니다. 실무에서는 성능보다 유지보수 비용이 먼저다. 얇은 래퍼들이 서로 다른 종료/예외 처리를 들고 있으면, 그게 진짜 비용이 된다.
그리고 anext() 루프는 정말 필요할 때만 쓴다. 스트림을 두 소비자에게 나눠야 한다거나, 소비자의 속도 차이 때문에 제어권이 필요할 때 같은 경우다. 그때는 제어권을 얻는 대신, 실수할 표면이 넓어진다는 걸 팀이 같이 감당해야 한다.
들어온다면, 레포에서 제일 먼저 바뀌는 건 ‘래퍼의 표정’이다
PEP 828이 실무 코드를 바꾸는 방식은, 거대한 리팩터링이 아니라 작은 삭제의 형태일 가능성이 크다.
지금은 래퍼가 항상 “위임 구현 + 내 로직”으로 구성된다. 제안이 들어오면, 래퍼는 “내 로직 + 위임”으로 바뀐다. 읽는 사람이 보는 표정이 달라진다.
PEP 828이 던지는 핵심은 “가능해지면 코드가 이렇게 얇아진다”는 데 있다. 하지만 중요한 경계가 하나 있다. PEP는 ‘설계 문서’이고, 그 문서에 적힌 문법이 곧바로 여러분의 파이썬에서 돌아가는 건 아니다.
그래도 실무 감각으로 번역하면, 제안은 크게 두 방향이다.
- async generator 안에서 동기 서브제너레이터로의 위임(
yield from)을 허용하자. - async generator끼리의 위임을 위해
async yield from를 도입하자(그리고 그때 종료값을 받을 수 있게, async generator의 non-Nonereturn제한도 완화하자).
핵심은 “새 문법이 멋있다”가 아니라, 위임이 다시 ‘한 줄짜리 의도’로 돌아온다는 점이다. 지금은 그 의도를 async for 루프로 풀어쓰느라, 로직이 얇아도 코드가 두꺼워진다.
그리고 non-None return 제한이 완화되면, “스트림은 데이터만 흘린다”와 “스트림이 끝나면 요약을 돌려준다”가 더 자연스럽게 공존할 수 있다. 지금은 마지막 이벤트로 우겨 넣거나, 상태 객체로 우회한다. 둘 다 코드가 읽히는 방식을 망친다.
이 변화가 들어오면 가장 먼저 이득을 보는 건 앱 코드보다 라이브러리다. 라이브러리 래퍼가 단순해지면, 사용자는 덜 실수한다. 그리고 그게 생태계 전체의 비용 절감으로 돌아온다.
참고로, PEP가 문서로만 굴러가는 게 아니라 실제 변경 제안이 PR로 올라오고 다듬어지는 흐름이 있다는 것도(참고자료) 이런 기능에서는 꽤 중요하다. “이건 가능할까?”가 아니라 “어디를 바꿔야 하지?”로 논의가 내려오기 때문이다.
결론: 문법 설탕이 아니라 ‘유지보수 비용’ 문제다
async generator에서 yield from 위임이 막힌 문제는, 작은 문법 공백처럼 보인다. 하지만 실무에서는 그 공백이 합의 비용과 리뷰 비용으로 바뀐다.
위임을 못 하니까 사람들이 각자 위임을 구현하고, 각자 종료 처리를 구현하고, 각자 예외 전달을 구현한다. 코드가 늘어나는 것보다 더 무서운 건, 팀이 “같은 동작을 다르게 믿는” 상태가 된다는 점이다.
PEP 828이 겨냥하는 건 결국 통일이다. 위임이라는 흔한 동작을 언어의 약속으로 가져오면, 레포 전체에서 ‘같은 모양’이 생긴다. 그리고 그 모양은, 스트리밍 코드를 더 빨리 쓰게 만드는 게 아니라 더 덜 틀리게 만든다.
참고자료
- Supporting ‘yield from’ in asynchronous generators
- https://discuss.python.org/t/supporting-yield-from-in-asynchronous-generators/106430
- PEP 828 announcement thread
- https://discuss.python.org/t/pep-828-supporting-yield-from-in-asynchronous-generators/106459
- PEP 828: Supporting “yield from” in asynchronous generators
- https://peps.python.org/pep-0828/
- Reference implementation (CPython compare)
- https://github.com/python/cpython/compare/main…zerointensity:cpython:async-yield-from
- PEPs PR (process / text updates)
- https://github.com/python/peps/pull/4857
댓글
댓글 쓰기