[Superseded] PEP 3153 - Asynchronous IO support
원문 링크: PEP 3153 - Asynchronous IO support
상태: Superseded 유형: Standards Track 작성일: 29-May-2011
PEP 3153 – 비동기 IO 지원
- 작성자: Laurens Van Houtven <_ at="" lvh.cc="">
- 상태: 대체됨 (Superseded)
- 유형: 표준 트랙 (Standards Track)
- 생성일: 2011년 5월 29일
- 후속 PEP: PEP 3156에 의해 대체됨 (Superseded-By: 3156)
초록 (Abstract)
이 PEP는 파이썬 표준 라이브러리를 위한 비동기 IO (Asynchronous IO) 추상화를 설명합니다. 목표는 다양한 비동기 IO 백엔드에 의해 구현될 수 있고, 라이브러리 개발자들이 다양한 백엔드 간에 이식 가능한 (portable) 코드를 작성할 수 있는 공통의 대상을 제공하는 추상화에 도달하는 것입니다.
도입 배경 (Rationale)
현재 파이썬에서 비동기 코드를 작성하려는 개발자들은 몇 가지 선택지를 가지고 있습니다:
asyncore
및asynchat
모듈select
모듈 기반의 맞춤형 (bespoke) 솔루션- Twisted 또는 gevent와 같은 서드 파티 라이브러리 사용
하지만 이 PEP는 이러한 각 옵션에 단점이 있음을 지적하며, 이를 해결하고자 합니다.
asyncore
모듈은 파이썬 표준 라이브러리의 일부로 오랫동안 존재했지만, 현대적인 비동기 네트워킹 모듈의 기대에 미치지 못하는 유연하지 않은 API로 인해 근본적인 결함을 가지고 있습니다. 또한, 비동기 네트워킹의 잠재력을 완전히 활용하는 데 필요한 모든 도구를 개발자에게 제공하기에는 너무 단순한 접근 방식을 취하고 있습니다.
현재 프로덕션 환경에서 가장 인기 있는 해결책은 서드 파티 라이브러리를 사용하는 것입니다. 이러한 라이브러리들은 만족스러운 솔루션을 제공하는 경우가 많지만, 라이브러리 간의 호환성 부족으로 인해 코드베이스가 사용 중인 특정 라이브러리에 강하게 결합되는 경향이 있습니다.
서로 다른 비동기 IO 라이브러리 간의 현재 이식성 부족은 서드 파티 라이브러리 개발자들에게 많은 중복된 노력을 야기합니다. 충분히 강력한 추상화는 비동기 코드를 한 번 작성하여 모든 곳에서 사용할 수 있게 만들 수 있습니다.
궁극적으로 추가되는 목표는 표준 라이브러리의 와이어 및 네트워크 프로토콜 구현이 recv()
를 블로킹 방식으로 호출하는 것을 포함하여 모든 것을 처리하는 독립형 라이브러리가 아니라, 실제 프로토콜 구현으로 발전하는 것입니다. 이는 동기 및 비동기 코드 모두에서 쉽게 재사용될 수 있음을 의미합니다.
통신 추상화 (Communication abstractions)
트랜스포트 (Transports)
트랜스포트는 다양한 종류의 연결에서 바이트를 읽고 쓰는 데 사용되는 균일한 API를 제공합니다. 이 PEP에서 트랜스포트는 항상 순서가 지정되고 (ordered), 신뢰할 수 있으며 (reliable), 양방향 (bidirectional)이고, 스트림 지향적인 (stream-oriented) 두 개의 종단점 (two-endpoint) 연결입니다. 이는 TCP 소켓, SSL 연결, 파이프 (이름 지정 여부와 관계없이), 직렬 포트 등이 될 수 있습니다. 이는 POSIX 플랫폼의 파일 디스크립터나 Windows의 Handle
또는 특정 플랫폼에 적합한 다른 데이터 구조를 추상화할 수 있습니다. 트랜스포트는 해당 플랫폼 데이터 구조를 사용하는 모든 특정 구현 세부 사항을 캡슐화하고 애플리케이션 개발자에게 균일한 인터페이스를 제공합니다.
트랜스포트는 두 가지와 통신합니다: 한편으로는 연결의 다른 쪽 끝, 다른 한편으로는 프로토콜. 이는 특정 기본 전송 메커니즘과 프로토콜 간의 다리 역할을 합니다. 트랜스포트의 역할은 프로토콜이 바이트를 보내고 받을 수 있도록 하여, 이 바이트들이 결국 와이어를 통해 전송되기 위해 필요한 모든 마법을 처리하는 것으로 설명할 수 있습니다.
트랜스포트의 주요 기능은 바이트를 프로토콜로 보내고, 기본 프로토콜로부터 바이트를 받는 것입니다. 트랜스포트에 쓰기는 write
및 write_sequence
메서드를 사용하여 수행됩니다. 후자 메서드는 일부 트랜스포트 메커니즘의 특정 기능을 활용할 수 있도록 하는 성능 최적화입니다. 특히, 이는 트랜스포트가 write
또는 send
대신 writev
를 사용할 수 있도록 하는데, 이는 scatter/gather IO
로도 알려져 있습니다.
트랜스포트는 일시 중지 (paused) 및 재개 (resumed)될 수 있습니다. 이렇게 하면 프로토콜에서 오는 데이터를 버퍼링하고, 수신된 데이터를 프로토콜로 보내는 것을 중단합니다.
트랜스포트는 또한 닫히거나 (closed), 부분적으로 닫히거나 (half-closed), 중단될 (aborted) 수 있습니다. closed
트랜스포트는 큐에 있는 모든 데이터를 기본 메커니즘에 쓰는 것을 완료한 다음, 데이터 읽기 또는 쓰기를 중단합니다. aborting
트랜스포트는 큐에 있는 데이터를 보내지 않고 연결을 닫아 작업을 중단합니다. 이후의 쓰기는 예외를 발생시킵니다. half-closed
트랜스포트는 더 이상 쓰기가 불가능하지만, 들어오는 데이터는 계속 수락합니다.
프로토콜 (Protocols)
프로토콜은 새로운 사용자에게 더 친숙할 것입니다. 이 용어는 프로토콜이라는 이름에서 기대하는 바와 일치합니다: HTTP, IRC, SMTP와 같이 대부분의 사람들이 먼저 떠올리는 프로토콜은 모두 프로토콜에 구현될 수 있는 예시입니다.
프로토콜의 가장 짧고 유용한 정의는 트랜스포트와 나머지 애플리케이션 로직 간의 (일반적으로 양방향) 다리입니다. 프로토콜은 트랜스포트로부터 바이트를 수신하고 그 정보를 어떤 동작 (behavior)으로 변환하는데, 이는 일반적으로 객체에 대한 몇 가지 메서드 호출로 이어집니다. 유사하게, 애플리케이션 로직은 프로토콜에 몇 가지 메서드를 호출하고, 프로토콜은 이를 바이트로 변환하여 트랜스포트에 전달합니다.
가장 간단한 프로토콜 중 하나는 \r\n
으로 데이터가 구분되는 라인 기반 프로토콜입니다. 프로토콜은 트랜스포트로부터 바이트를 수신하고, 최소한 하나의 완전한 라인이 있을 때까지 버퍼링합니다. 이 작업이 완료되면, 이 라인을 어떤 객체에 전달합니다. 이상적으로는 콜러블(callable)을 사용하거나 프로토콜에 의해 구성된 완전히 별도의 객체를 사용하여 달성되겠지만, 서브클래싱을 통해서도 구현될 수 있습니다 (Twisted의 LineReceiver
의 경우와 같이). 다른 방향의 경우, 프로토콜은 write_line
메서드를 가질 수 있으며, 이 메서드는 필요한 \r\n
을 추가하고 새로운 바이트 버퍼를 트랜스포트로 전달합니다.
이 PEP는 ChunkProtocol
이라는 일반화된 LineReceiver
를 제안합니다. 여기서 “청크 (chunk)”는 지정된 구분자로 구분되는 스트림 내의 메시지입니다. 인스턴스는 구분자와 함께 데이터 청크가 수신되면 호출될 콜러블을 인자로 받습니다 (Twisted의 서브클래싱 동작과는 다릅니다). ChunkProtocol
은 위에 설명된 write_line
메서드와 유사한 write_chunk
메서드도 가집니다.
프로토콜과 트랜스포트를 분리하는 이유 (Why separate protocols and transports?)
프로토콜과 트랜스포트 간의 이러한 분리는 처음 접하는 사람들을 종종 혼란스럽게 합니다. 실제로 표준 라이브러리 자체는 많은 경우에 이러한 구분을 하지 않으며, 특히 사용자에게 제공하는 API에서는 더욱 그렇습니다.
그럼에도 불구하고 이는 매우 유용한 구분입니다. 최악의 경우, 관심사의 명확한 분리를 통해 구현을 단순화합니다. 그러나 이는 종종 훨씬 더 유용한 목적인, 다양한 트랜스포트에서 프로토콜을 재사용할 수 있게 하는 역할을 합니다.
간단한 RPC 프로토콜을 생각해 봅시다. 동일한 바이트가 파이프나 소켓과 같은 여러 다른 트랜스포트를 통해 전송될 수 있습니다. 이를 돕기 위해 프로토콜을 트랜스포트와 분리합니다. 프로토콜은 단순히 바이트를 읽고 쓰며, 그 바이트가 최종적으로 전송되는 메커니즘에는 크게 신경 쓰지 않습니다.
이는 또한 프로토콜을 쉽게 스택하거나 중첩할 수 있게 하여, 코드 재사용을 더욱 증가시킵니다. 이에 대한 일반적인 예시는 JSON-RPC입니다. 사양에 따르면, JSON-RPC는 소켓과 HTTP 모두에서 사용될 수 있습니다. 실제로는 주로 HTTP 내에 캡슐화되는 경향이 있습니다. 프로토콜-트랜스포트 추상화를 통해 HTTP를 트랜스포트처럼 사용할 수 있는 프로토콜 및 트랜스포트 스택을 구축할 수 있습니다. JSON-RPC의 경우, 대략 다음과 같은 스택을 얻을 수 있습니다:
- TCP 소켓 트랜스포트
- HTTP 프로토콜
- HTTP 기반 트랜스포트
- JSON-RPC 프로토콜
- 애플리케이션 코드
흐름 제어 (Flow control)
컨슈머 (Consumers)
컨슈머는 프로듀서가 생성한 바이트를 소비합니다. 프로듀서와 함께 흐름 제어를 가능하게 합니다.
컨슈머는 흐름 제어에서 주로 수동적인 역할을 합니다. 프로듀서가 일부 데이터를 사용할 수 있을 때마다 호출됩니다. 그런 다음 해당 데이터를 처리하고 일반적으로 제어를 다시 프로듀서에게 양도합니다.
컨슈머는 일반적으로 어떤 종류의 버퍼를 구현합니다. 컨슈머는 해당 버퍼의 현재 상태를 프로듀서에게 알려줌으로써 흐름 제어를 가능하게 합니다. 컨슈머는 프로듀서에게 생산을 완전히 중단하거나, 일시적으로 중단하거나, 이전에 일시 중지하도록 지시받았다면 생산을 재개하도록 지시할 수 있습니다.
프로듀서는 register
메서드를 사용하여 컨슈머에 등록됩니다.
프로듀서 (Producers)
컨슈머가 바이트를 소비하는 곳에서, 프로듀서는 바이트를 생성합니다.
프로듀서는 Twisted에서 발견되는 IPushProducer
인터페이스를 모델로 합니다. IPullProducer
도 있지만, 전반적으로 흥미가 훨씬 적으므로 이 PEP의 범위 밖일 것입니다.
프로듀서는 생산을 완전히 중단하도록 지시받을 수 있지만, 가장 흥미로운 두 가지 메서드는 pause
와 resume
입니다. 이 메서드들은 일반적으로 컨슈머에 의해 호출되며, 더 많은 데이터를 처리 (“소비”)할 준비가 되었는지 여부를 나타냅니다. 컨슈머와 프로듀서는 흐름 제어를 가능하게 하기 위해 협력합니다.
Twisted IPushProducer
인터페이스 외에도, 프로듀서는 half_register
메서드를 가지고 있습니다. 이 메서드는 컨슈머가 해당 프로듀서를 등록하려고 할 때 컨슈머와 함께 호출됩니다. 대부분의 경우, 이는 단순히 self.consumer = consumer
를 설정하는 경우일 것이지만, 일부 프로듀서는 컨슈머가 등록될 때 더 복잡한 사전 조건이나 동작을 요구할 수 있습니다. 최종 사용자는 이 메서드를 직접 호출하지 않아야 합니다.
고려된 API 대안 (Considered API alternatives)
프로듀서로서의 제너레이터 (Generators as producers)
제너레이터 (Generators)는 프로듀서를 구현하는 방법으로 제안되었습니다. 그러나 여기에는 몇 가지 문제가 있는 것으로 보입니다.
첫째, 개념적인 문제가 있습니다. 제너레이터는 어떤 의미에서 “수동적 (passive)”입니다. 메서드 호출을 통해 행동하도록 지시받아야 합니다. 프로듀서는 “능동적 (active)”입니다. 즉, 이러한 메서드 호출을 시작합니다. 실제 프로듀서는 컨슈머와 대칭적인 관계를 가집니다. 제너레이터가 프로듀서로 전환되는 경우, 컨슈머만이 참조를 가지고 프로듀서는 컨슈머의 존재를 알지 못합니다.
이러한 개념적인 문제는 몇 가지 기술적인 문제로도 이어집니다. 컨슈머에 대한 write
메서드 호출이 성공한 후, (푸시) 프로듀서는 다시 자유롭게 행동할 수 있습니다. 제너레이터의 경우, 반복 프로토콜을 통해 다음 객체를 요청하거나 (무기한 블로킹될 수 있는 프로세스), 어떤 종류의 시그널 예외를 발생시켜 알려주어야 할 것입니다.
이러한 시그널링 설정은 기술적으로 실행 가능한 솔루션을 제공할 수 있지만, 여전히 만족스럽지 않습니다. 우선, 이는 컨슈머에 불필요한 복잡성을 도입합니다. 컨슈머는 이제 데이터를 수신하고 처리하는 방법뿐만 아니라, 새로운 데이터를 요청하는 방법과 새로운 데이터를 사용할 수 없는 경우를 처리하는 방법도 이해해야 합니다.
이 후자의 엣지 케이스는 특히 문제가 됩니다. 전체 작업이 블로킹되어서는 안 되기 때문에 처리해야 합니다. 그러나 제너레이터는 종료하지 않고는 반복 중에 예외를 발생시킬 수 없으므로, 제너레이터의 상태를 잃게 됩니다. 결과적으로, 사용 가능한 데이터 부족을 알리는 것은 예외 메커니즘을 사용하는 대신, 센티넬 값 (sentinel value)을 사용하여 수행되어야 할 것입니다.
마지막으로, 실제로 작동하는 코드를 통해 제너레이터가 프로듀서로 어떻게 사용될 수 있는지 시연한 사람이 아무도 없었습니다.
참고 자료 (References)
PEP 3153 – Asynchronous IO support, peps.python.org
.
Sections 2.1 and 2.2 of the JSON-RPC specification.
저작권 (Copyright)
이 문서는 퍼블릭 도메인에 공개되었습니다.
⚠️ 알림: 이 문서는 AI를 활용하여 번역되었으며, 기술적 정확성을 보장하지 않습니다. 정확한 내용은 반드시 원문을 확인하시기 바랍니다.
Comments