Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python 개선 제안 한국어 번역

PEP 3153 – 비동기 IO 지원

Author:
Laurens Van Houtven <_ at lvh.cc>
Status:
Superseded
Type:
Standards Track
Created:
29-May-2011
Post-History:

Superseded-By:
3156

Table of Contents

번역·라이선스 안내

이 비공식 한국어 번역은 원문 Copyright 절의 Public Domain 조건에 따라 제공합니다. 원저자와 공식 원문은 그대로 표시합니다. 수정되지 않은 기준 원문 · 공식 최신판

초록

이 PEP는 Python 표준 라이브러리를 위한 비동기 IO의 추상화를 설명합니다.

목표는 여러 가지 비동기 IO 백엔드에서 구현할 수 있고 라이브러리 개발자가 서로 다른 백엔드 간에 이식 가능한 코드를 작성할 수 있는 기준을 제공하는 추상화에 도달하는 것입니다.

근거

현재 Python에서 비동기 코드를 작성하려는 사람에게는 몇 가지 선택지가 있습니다.

  • asyncoreasynchat
  • 대개 select 모듈을 기반으로 하는 맞춤형 방식
  • Twisted 또는 gevent_와 같은 서드 파티 라이브러리 사용

안타깝게도 이러한 선택지에는 각각 단점이 있으며, 이 PEP는 이를 해결하려고 합니다.

asyncore 모듈은 오랫동안 Python 표준 라이브러리의 일부였지만, 현대적인 비동기 네트워킹 모듈의 기대에 부응하지 못하는 융통성 없는 API에서 비롯된 근본적인 결함을 안고 있습니다.

또한 그 접근 방식은 개발자가 비동기 네트워킹의 잠재력을 완전히 활용하는 데 필요한 모든 도구를 제공하기에는 지나치게 단순합니다.

현재 운영 환경에서 사용되는 가장 인기 있는 해결책은 서드 파티 라이브러리를 사용하는 것입니다. 이러한 라이브러리는 흔히 만족스러운 해결책을 제공하지만, 라이브러리 간에 호환성이 부족하여 코드베이스가 사용하는 라이브러리에 매우 긴밀하게 결합되는 경향이 있습니다.

서로 다른 비동기 IO 라이브러리 간에 현재 존재하는 이러한 이식성 부족은 서드 파티 라이브러리 개발자에게 많은 중복 작업을 초래합니다. 충분히 강력한 추상화가 있다면 비동기 코드를 한 번 작성하고 어디서나 사용할 수 있을 것입니다.

장기적으로 추가할 수 있는 목표는 유선 및 네트워크 프로토콜의 표준 라이브러리 구현이, recv()를 블로킹 방식으로 호출하는 것을 비롯해 모든 작업을 수행하는 독립형 라이브러리가 아니라 실제 프로토콜 구현으로 발전하도록 하는 것입니다. 이렇게 하면 동기 코드와 비동기 코드 모두에서 쉽게 재사용할 수 있습니다.

통신 추상화

전송

전송은 서로 다른 종류의 연결에서 바이트를 읽고 바이트를 쓰기 위한 통일된 API를 제공합니다. 이 PEP의 전송은 항상 순서가 보장되고 신뢰할 수 있으며 양방향이고 스트림 지향적인 두 엔드포인트 연결입니다. TCP 소켓, SSL 연결, 파이프(명명된 파이프이든 아니든), 직렬 포트 등일 수 있습니다. POSIX 플랫폼의 파일 디스크립터나 Windows의 Handle 또는 특정 플랫폼에 적합한 다른 데이터 구조를 추상화할 수 있습니다. 해당 플랫폼 데이터 구조를 사용하는 데 필요한 모든 구체적인 구현 세부 사항을 캡슐화하고 애플리케이션 개발자에게 통일된 인터페이스를 제공합니다.

전송은 두 가지 대상과 통신합니다. 한편으로는 연결의 반대편과 통신하고, 다른 한편으로는 프로토콜과 통신합니다. 전송은 구체적인 하위 전송 메커니즘과 프로토콜 사이의 다리입니다. 전송의 역할은 프로토콜이 바이트를 보내고 받기만 하면 되도록 하며, 해당 바이트가 최종적으로 유선을 통해 전송되기 위해 필요한 모든 처리를 담당하는 것으로 설명할 수 있습니다.

전송의 주요 기능은 프로토콜에 바이트를 보내고 하위 프로토콜에서 바이트를 받는 것입니다. 전송에 쓰는 작업은 writewrite_sequence 메서드를 사용하여 수행합니다. 후자의 메서드는 일부 전송 메커니즘의 특정 기능을 소프트웨어가 활용할 수 있도록 하는 성능 최적화입니다. 구체적으로 이는 전송이 writev 대신 write 또는 send를 사용할 수 있도록 하며, 이를 산란/수집 IO라고도 합니다.

트랜스포트는 일시 중지했다가 재개할 수 있습니다. 이렇게 하면 트랜스포트가 프로토콜에서 전달되는 데이터를 버퍼링하고, 수신한 데이터를 프로토콜로 보내는 작업을 중지합니다.

트랜스포트는 닫거나, 반쯤 닫거나, 중단할 수도 있습니다. 닫힌 트랜스포트는 자신에게 대기 중인 모든 데이터를 하위 메커니즘에 쓰는 작업을 완료한 다음, 데이터 읽기 또는 쓰기를 중지합니다. 트랜스포트를 중단하면 트랜스포트가 중지되고, 아직 대기 중인 데이터를 전송하지 않은 채 연결이 닫힙니다.

이후에 쓰기를 시도하면 예외가 발생합니다. 반쯤 닫힌 트랜스포트에는 더 이상 데이터를 쓸 수 없지만, 들어오는 데이터는 계속 수락합니다.

프로토콜

프로토콜은 새로운 사용자에게 아마 더 익숙할 것입니다. 용어는 프로토콜이라고 부르는 것에서 예상할 수 있는 내용과 일관됩니다. 대부분의 사람이 가장 먼저 떠올리는 HTTP, IRC, SMTP 같은 프로토콜은 모두 프로토콜에서 구현되는 것의 예입니다.

프로토콜의 가장 간단하면서도 유용한 정의는 트랜스포트와 나머지 애플리케이션 로직 사이를 연결하는 (대개 양방향) 다리입니다. 프로토콜은 트랜스포트에서 바이트를 수신하고 해당 정보를 어떤 동작으로 변환하며, 일반적으로 객체의 일부 메서드 호출로 이어집니다. 마찬가지로 애플리케이션 로직은 프로토콜의 일부 메서드를 호출하고, 프로토콜은 이를 바이트로 변환하여 트랜스포트와 통신합니다.

가장 단순한 프로토콜 중 하나는 데이터가 \r\n으로 구분되는 줄 기반 프로토콜입니다. 프로토콜은 트랜스포트에서 바이트를 수신하고 완전한 줄이 하나 이상 생길 때까지 이를 버퍼링합니다. 작업이 완료되면 해당 줄을 어떤 객체에 전달합니다. 이상적으로는 호출 가능 객체나 프로토콜이 구성한 완전히 별개의 객체를 사용하여 이를 수행하지만, 서브클래싱으로 구현할 수도 있습니다(Twisted의 LineReceiver가 그 예입니다). 반대 방향으로는 프로토콜에 write_line 메서드가 있을 수 있으며, 이 메서드는 필요한 \r\n을 추가하고 새 바이트 버퍼를 트랜스포트에 전달합니다.

이 PEP는 LineReceiver를 일반화한 ChunkProtocol을 제안합니다. 여기서 “청크”는 지정된 구분 기호로 구분되는 스트림 내 메시지입니다. 인스턴스는 구분 기호와, 수신이 완료되면 데이터 청크를 전달하여 호출할 호출 가능 객체를 받습니다(Twisted의 서브클래싱 동작과는 다릅니다). ChunkProtocol에는 위에서 설명한 write_line 메서드와 유사한 write_chunk 메서드도 있습니다.

프로토콜과 트랜스포트를 분리하는 이유는 무엇입니까?

프로토콜과 트랜스포트를 이렇게 분리하는 방식은 이를 처음 접하는 사람을 자주 혼란스럽게 합니다. 실제로 표준 라이브러리 자체는 여러 경우에 이러한 구분을 하지 않으며, 특히 사용자에게 제공하는 API에서는 그렇습니다.

그럼에도 이는 매우 유용한 구분입니다. 최악의 경우에도 관심사를 명확히 분리하여 구현을 단순화합니다. 그러나 이 구분은 서로 다른 트랜스포트에서 프로토콜을 재사용할 수 있게 한다는 훨씬 더 유용한 목적에 기여하는 경우가 많습니다.

간단한 RPC 프로토콜을 생각해 보십시오. 예를 들어 파이프나 소켓처럼 서로 다른 여러 트랜스포트를 통해 동일한 바이트를 전송할 수 있습니다. 이를 지원하기 위해 프로토콜을 트랜스포트에서 분리합니다. 프로토콜은 바이트를 읽고 쓰기만 하며, 결국 해당 바이트를 전송하는 데 어떤 메커니즘이 사용되는지는 사실상 신경 쓰지 않습니다.

이를 통해 프로토콜을 쉽게 스택으로 쌓거나 중첩할 수 있으므로, 코드를 더욱 많이 재사용할 수 있습니다. 이에 대한 일반적인 예로 JSON-RPC가 있습니다. 사양에 따르면 JSON-RPC는 소켓과 HTTP 모두에서 사용할 수 있습니다 [1]. 실제로는 주로 HTTP에 캡슐화되는 경향이 있습니다. 프로토콜-전송 추상화를 사용하면 HTTP를 전송인 것처럼 사용할 수 있도록 프로토콜과 전송의 스택을 구축할 수 있습니다. JSON-RPC의 경우 다음과 비슷한 스택을 구성할 수 있습니다.

  1. TCP 소켓 전송
  2. HTTP 프로토콜
  3. HTTP 기반 전송
  4. JSON-RPC 프로토콜
  5. 애플리케이션 코드

흐름 제어

소비자

소비자는 생산자가 생성한 바이트를 소비합니다. 소비자는 생산자와 함께 흐름 제어를 가능하게 합니다.

소비자는 흐름 제어에서 주로 수동적인 역할을 합니다. 생산자가 사용할 수 있는 데이터를 보유할 때마다 소비자가 호출됩니다. 그러면 소비자는 해당 데이터를 처리하고, 일반적으로 제어권을 생산자에게 다시 넘깁니다.

소비자는 일반적으로 어떤 형태로든 버퍼를 구현합니다. 소비자는 버퍼의 현재 상태를 생산자에게 알려 흐름 제어를 가능하게 합니다. 소비자는 생산자에게 생산을 완전히 중지하거나, 일시적으로 중지하거나, 이전에 일시 중지하라는 지시를 받은 경우 생산을 재개하도록 지시할 수 있습니다.

생산자는 register 메서드를 사용하여 소비자에게 등록됩니다.

생산자

소비자가 바이트를 소비하는 반면, 생산자는 바이트를 생성합니다.

생산자는 Twisted에 있는 IPushProducer 인터페이스를 본떠 설계됩니다. IPullProducer_도 있지만, 전반적으로 훨씬 덜 흥미롭기 때문에 이 PEP의 범위를 벗어날 가능성이 높습니다.

생산자에게 생산을 완전히 중지하도록 지시할 수 있지만, 생산자가 제공하는 가장 흥미로운 두 메서드는 pauseresume입니다. 이러한 메서드는 일반적으로 소비자가 더 많은 데이터를 처리(“소비”)할 준비가 되었는지 여부를 나타내기 위해 호출합니다. 소비자와 생산자는 협력하여 흐름 제어를 가능하게 합니다.

Twisted의 IPushProducer 인터페이스 외에도 생산자에게는 half_register 메서드가 있으며, 소비자가 해당 생산자를 등록하려고 할 때 이 메서드가 소비자와 함께 호출됩니다. 대부분의 경우 이는 단순히 self.consumer = consumer를 설정하는 것에 해당하지만, 일부 생산자는 소비자가 등록될 때 더 복잡한 사전 조건이나 동작을 요구할 수 있습니다. 최종 사용자는 이 메서드를 직접 호출해서는 안 됩니다.

검토한 API 대안들

프로듀서로서의 제너레이터

제너레이터를 프로듀서를 구현하는 방법으로 사용하자는 제안이 있었습니다. 그러나 여기에는 몇 가지 문제가 있는 것으로 보입니다.

우선 개념적인 문제가 있습니다. 제너레이터는 어떤 의미에서는 “수동적”입니다. 제너레이터가 동작하려면 메서드 호출을 통해 동작하라는 지시를 받아야 합니다. 프로듀서는 “능동적”이며, 이러한 메서드 호출을 시작합니다. 실제 프로듀서는 컨슈머와 대칭적인 관계를 맺습니다. 제너레이터를 프로듀서로 바꾼 경우에는 컨슈머만 참조를 가지며, 프로듀서는 컨슈머의 존재를 전혀 인식하지 못합니다.

이러한 개념적 문제는 몇 가지 기술적인 문제로도 이어집니다. 컨슈머에 대한 write 메서드 호출이 성공한 후, (푸시) 프로듀서는 다시 한 번 동작할 수 있습니다. 제너레이터의 경우에는 반복 프로토콜을 통해 다음 객체를 요청하거나(이 과정은 무기한 차단될 수 있습니다), 어떤 종류의 신호 예외를 제너레이터에 던져 동작하라는 지시를 받아야 합니다.

이러한 신호 전달 방식은 기술적으로 실행 가능한 해결책을 제공할 수 있지만, 여전히 만족스럽지 않습니다. 무엇보다도 이는 컨슈머에 불필요한 복잡성을 추가합니다. 이제 컨슈머는 데이터를 수신하고 처리하는 방법뿐만 아니라 새 데이터를 요청하는 방법과 새 데이터를 사용할 수 없는 경우를 처리하는 방법도 이해해야 합니다.

이 후자의 예외 상황은 특히 문제가 많습니다. 전체 작업이 차단되어서는 안 되므로 이를 처리해야 합니다. 그러나 제너레이터는 종료되지 않고는 반복 중에 예외를 발생시킬 수 없으며, 그 결과 제너레이터의 상태를 잃게 됩니다. 따라서 사용 가능한 데이터가 없다는 신호는 예외 메커니즘을 사용하는 대신 센티넬 값을 사용하여 전달해야 합니다.

마지막으로, 제너레이터를 어떻게 사용할 수 있는지 보여 주는 실제로 작동하는 코드를 작성한 사람은 아무도 없었습니다.

참고 문헌