ASGI 깊이 알기: 파이썬 비동기 웹 앱 통신 규약 파헤치기! (FastAPI, Uvicorn 연관성 포함)

ASGI 탐구: 파이썬(Python) 비동기 웹 앱을 위한 통신 규약

FastAPI 개발 시 Uvicorn은 왜 필요할까요? ASGI 이해하기

파이썬(Python) 웹 프레임워크(framework)인 FastAPI(패스트API)로 개발을 시작하면, 항상 Uvicorn(유비콘)이라는 서버와 함께 사용해야 한다는 것을 알게 됩니다.

 

처음 접하는 분들은 '왜 꼭 Uvicorn(유비콘)을 써야 할까?' 하고 궁금하실 수 있는데요.

 

오늘은 바로 이 질문에 대한 답을 찾아가면서, 그 배경에 있는 ASGI(아스기)라는 중요한 개념에 대해 자세히 알아보겠습니다.

Uvicorn(유비콘)과 간단한 ASGI 애플리케이션 예제

먼저, ASGI(아스기)가 실제로 어떻게 동작하는지 아주 간단한 코드를 통해 맛보겠습니다.

 

HTTP 요청을 처리하는 기본적인 ASGI(아스기) 애플리케이션(application) 예제입니다.

import json
import asyncio # 예제 실행을 위해 필요할 수 있습니다 (uvicorn이 내부적으로 사용)

# 헤더(header) 정보 등에 포함된 바이트(bytes) 문자열을 일반 문자열(string)로 변환하는 도우미 함수
def convert_bytes_to_str(data):
    if isinstance(data, bytes):
        return data.decode('utf-8')
    if isinstance(data, tuple):
        # 튜플(tuple) 내의 각 항목에 대해 재귀적으로 변환 적용
        return tuple(convert_bytes_to_str(item) for item in data)
    if isinstance(data, list):
        # 리스트(list) 내의 각 항목에 대해 재귀적으로 변환 적용
        return [convert_bytes_to_str(item) for item in data]
    if isinstance(data, dict):
        # 딕셔너리(dictionary)의 키(key)와 값(value)에 대해 재귀적으로 변환 적용 (값만 변환)
        return {key: convert_bytes_to_str(value) for key, value in data.items()}
    return data

# ASGI 애플리케이션(application) 본체 (비동기 함수)
async def app(scope, receive, send):
    # scope: 요청에 대한 모든 정보가 담긴 딕셔너리(dictionary)
    # receive: 요청 본문(body) 같은 서버로부터 오는 메시지를 받는 비동기 함수
    # send: 응답 헤더(header), 본문(body) 등 서버로 메시지를 보내는 비동기 함수

    # scope 내용을 보기 좋게 출력 (바이트 문자열 변환 후)
    readable_scope = convert_bytes_to_str(scope)
    print("--- 요청 정보 (scope) ---")
    print(json.dumps(readable_scope, indent=4, ensure_ascii=False)) # 한글 깨짐 방지
    print("------------------------")

    # 요청 타입 확인 (HTTP 요청만 처리)
    if scope['type'] == 'http':
        # HTTP 요청 본문(body) 기다리기 (클라이언트가 보낸 데이터 수신)
        # event = await receive() # 이 예제에서는 요청 본문을 사용하지 않으므로 주석 처리

        # 클라이언트에게 보낼 응답 본문(body) 준비 (JSON 형식)
        response_body = json.dumps({"message": "안녕하세요, ASGI!"}).encode('utf-8')

        # 1. HTTP 응답 시작 메시지 전송 (상태 코드, 헤더 포함)
        await send({
            'type': 'http.response.start',
           'status': 200, # 성공 상태 코드
            'headers': [
                (b'content-type', b'application/json'), # 응답 형식이 JSON임을 명시
                # 필요한 다른 헤더 추가 가능
            ],
        })

        # 2. HTTP 응답 본문(body) 메시지 전송
        await send({
            'type': 'http.response.body',
            'body': response_body, # 실제 응답 내용
            'more_body': False # 응답 본문이 더 이상 없음을 알림
        })
    # 다른 타입(예: websocket)의 요청은 이 예제에서 처리하지 않습니다.

코드 분석 및 설명

위 코드에서 scope라는 변수 안에는 요청 경로(path), HTTP 메서드(method) 등 요청에 관한 다양한 정보가 들어있습니다.

 

그런데 이 정보들 중 일부가 바이트(bytes) 문자열 형태로 되어 있어서, 사람이 읽기 편하도록 convert_bytes_to_str 함수를 사용해 일반 문자열로 변환했습니다.

 

scope는 HTTP 외에도 웹소켓(WebSocket) 같은 다른 통신 방식도 지원하기 때문에, scope['type'] 값을 확인해서 현재 요청이 'http' 타입인지 먼저 확인합니다.

 

그 다음, receive 함수를 호출해서 클라이언트(client)가 보낸 요청 본문(body) 데이터를 받을 수 있습니다. (이 예제에서는 요청 본문을 실제로 사용하지는 않았습니다.)

 

응답으로 보낼 내용은 json.dumps를 사용해 JSON 문자열로 만들고, 네트워크로 전송하기 위해 .encode('utf-8')를 이용해 바이트(bytes) 형태로 변환합니다.

 

마지막으로, send 함수를 두 번 호출하는데요.

 

첫 번째 send에서는 HTTP 상태 코드(status code)와 응답 헤더(header) 정보를 담은 '응답 시작' 메시지를 보내고, 두 번째 send에서는 실제 응답 내용(body)을 담은 메시지를 보냅니다.

send를 두 번 호출할까요?

이렇게 응답 시작과 본문을 나누어 보내는 방식은 비동기 프로그래밍의 요구사항을 만족시키면서, 데이터를 조각내어 보내는 스트리밍(streaming) 응답 같은 다양한 시나리오에 유연하게 대처할 수 있게 해줍니다.

 

예를 들어, 아주 큰 파일을 보내야 할 때 파일을 한 번에 다 읽어서 보내는 대신, 조금씩 나누어 여러 번의 send 호출로 보낼 수 있습니다.

 

이제 실제 코드를 통해 ASGI(아스기)가 어떻게 동작하는지 감을 잡았으니, 이론적인 배경을 좀 더 자세히 살펴보겠습니다.

ASGI (Asynchronous Server Gateway Interface)란?

ASGI(아스기)는 비동기(Asynchronous) 방식으로 동작하는 파이썬(Python) 웹 애플리케이션(application)을 만들기 위한 통신 규약(protocol) 또는 인터페이스(interface)입니다.

 

과거 파이썬(Python) 웹 개발 표준이었던 WSGI(Web Server Gateway Interface)가 동기 방식만 지원했던 것과 달리, ASGI(아스기)는 비동기 처리를 기본으로 지원하여 더 높은 성능과 동시성을 제공하는 것을 목표로 합니다.

ASGI의 주요 특징

ASGI(아스기)는 다음과 같은 주요 특징들을 가지고 있습니다.

비동기(Asynchronous) 지원

async/await 문법을 기반으로 하는 비동기 프로그래밍을 완벽하게 지원합니다.

 

이를 통해 I/O 작업(네트워크 요청, 데이터베이스 조회 등)을 기다리는 동안 다른 작업을 처리할 수 있어 효율성이 크게 향상됩니다.

다중 프로토콜(Multi-protocol) 지원

기존의 HTTP/HTTPS 요청뿐만 아니라, 실시간 양방향 통신이 필요한 웹소켓(WebSocket), 롱폴링(long-polling) 같은 다양한 통신 프로토콜(protocol)을 지원합니다.

동시성(Concurrency) 지원

비동기 특성을 활용하여 여러 개의 요청을 동시에 효율적으로 처리할 수 있습니다.

메시지 기반 통신

애플리케이션(application)과 서버(server), 또는 애플리케이션(application) 내부의 다른 부분들이 서로 '메시지(message)'를 주고받는 방식으로 상호작용합니다.

 

앞서 예제 코드에서 receive로 메시지를 받고 send로 메시지를 보낸 것이 바로 이 방식입니다.

ASGI 프로토콜(Protocol)의 구성 요소

ASGI(아스기) 프로토콜(protocol)은 크게 세 가지 인터페이스(interface) 또는 구성 요소로 나누어 볼 수 있습니다.

애플리케이션 인터페이스 (Application Interface)

애플리케이션(application)이 ASGI(아스기) 서버와 어떻게 상호작용해야 하는지를 정의합니다.

 

ASGI(아스기) 애플리케이션(application)은 기본적으로 호출 가능한(callable) 객체이며, 보통 비동기 함수(async def) 형태로 구현됩니다. 이 함수는 세 개의 인자(scope, receive, send)를 받습니다.

scope

요청에 대한 메타데이터(metadata)를 담고 있는 딕셔너리(dictionary)입니다.

 

요청 타입('http' 또는 'websocket'), 경로(path), 쿼리 문자열(query string), 서버 정보 등이 포함됩니다.

receive

ASGI(아스기) 서버로부터 이벤트(event)나 데이터를 수신하는 데 사용되는 비동기 호출 함수입니다.

 

예를 들어 HTTP 요청 본문(body)을 받을 때 사용합니다.

send

ASGI(아스기) 서버에게 이벤트(event)나 데이터를 다시 보내는 데 사용되는 비동기 호출 함수입니다.

 

예를 들어 HTTP 응답 헤더(header)나 본문(body)을 보낼 때 사용합니다.

서버 인터페이스 (Server Interface)

ASGI(아스기) 서버(예: Uvicorn, Daphne, Hypercorn)가 수행해야 할 역할들을 정의합니다.

 

주요 책임은 다음과 같습니다.

클라이언트 연결 수락

클라이언트(client)(웹 브라우저 등)로부터 들어오는 네트워크 연결을 받아들입니다.

각 연결에 대한 scope 생성

새로운 연결마다 해당 연결의 정보를 담은 scope 딕셔너리(dictionary)를 만듭니다.

애플리케이션의 receive, send 호출

생성된 scope와 함께 애플리케이션(application)을 호출하고, receivesend를 통해 애플리케이션(application)과 메시지를 주고받습니다.

네트워크 예외 처리 및 연결 종료

통신 중 발생하는 오류를 처리하고, 연결이 끝나면 정리합니다.

이벤트 루프 인터페이스 (Event Loop Interface)

이벤트 루프(event loop)는 ASGI(아스기) 프로토콜(protocol)에 명시적으로 정의된 것은 아니지만, ASGI(아스기) 동작의 핵심 기반이 되는 암묵적인 부분입니다.

 

ASGI(아스기) 서버에 의해 관리됩니다. 이벤트 루프(event loop)는 비동기 작업들을 스케줄링하고 실행하는 역할을 담당하며, 비동기 프로그래밍의 심장과 같습니다. 주요 기능은 다음과 같습니다.

비동기 작업 실행 및 스케줄링

어떤 비동기 작업을 언제 실행할지 결정하고 실행합니다.

비동기 I/O 작업 관리

네트워크 요청/응답 같은 비동기 입출력 작업을 관리합니다.

콜백 함수 및 비동기 제너레이터 처리

비동기 작업 완료 후 실행될 함수(콜백)나 비동기적인 데이터 흐름(제너레이터)을 관리합니다.

 

파이썬(Python)에서는 주로 다음과 같은 라이브러리가 이벤트 루프(event loop)를 제공합니다.

asyncio

파이썬(Python) 표준 라이브러리에 내장된 비동기 I/O 프레임워크(framework)입니다.

uvloop

libuv(Node.js에서 사용하는 C 라이브러리) 기반으로 만들어진 매우 빠른 성능의 비동기 이벤트 루프(event loop)입니다.

Uvicorn(유비콘) 서버와 함께 사용될 때 뛰어난 성능을 보여줍니다.

 

ASGI(아스기) 서버와 애플리케이션(application)은 이 이벤트 루프(event loop)에 의존하여 비동기 작업을 수행하고, 덕분에 수많은 동시 연결을 효율적으로 처리할 수 있습니다.

ASGI 이벤트(Event) 종류

ASGI(아스기)는 이벤트 기반 모델(event-driven model)을 사용합니다.

 

다양한 종류의 이벤트를 통해 애플리케이션(application)의 생명주기(lifecycle), HTTP 요청 처리, 웹소켓(WebSocket) 연결 관리 등을 수행합니다.

라이프스팬 이벤트 (Lifespan Events)

ASGI(아스기) 애플리케이션(application)의 시작과 종료 주기에 관련된 이벤트입니다.

 

주로 애플리케이션(application) 초기화(예: 데이터베이스 연결 풀 생성)나 정리 작업(예: 연결 종료)에 사용됩니다.

lifespan.startup

애플리케이션(application) 시작 시 발생하는 이벤트입니다.

lifespan.shutdown

애플리케이션(application) 종료 시 발생하는 이벤트입니다.

HTTP 이벤트 (HTTP Events)

HTTP 요청을 처리하기 위해 여러 단계로 나누어진 이벤트들입니다.

 

각 단계별 세부 사항을 관리할 수 있게 해줍니다.

http.request

클라이언트(client)로부터 HTTP 요청(헤더 포함)이 들어왔음을 알리는 이벤트입니다. 요청 본문(body)은 receive를 통해 별도로 받습니다.

http.response.start

HTTP 응답을 시작함을 알리는 이벤트입니다. 상태 코드(status code)와 헤더(header) 정보를 포함하여 send로 보냅니다.

http.response.body

HTTP 응답 본문(body) 데이터를 담아 send로 보내는 이벤트입니다. 여러 번 보낼 수 있습니다(스트리밍).

http.disconnect

클라이언트(client)와의 HTTP 연결이 끊어졌음을 알리는 이벤트입니다.

웹소켓 이벤트 (WebSocket Events)

ASGI(아스기)는 양방향 통신을 위한 웹소켓(WebSocket) 연결도 지원합니다. 관련 이벤트들은 다음과 같습니다.

websocket.connect

클라이언트(client)가 웹소켓(WebSocket) 연결을 시도할 때 발생하는 이벤트입니다.

websocket.receive

클라이언트(client)로부터 웹소켓(WebSocket) 메시지를 받았을 때 발생하는 이벤트입니다. receive를 통해 데이터를 받습니다.

websocket.send

클라이언트(client)에게 웹소켓(WebSocket) 메시지를 보낼 때 사용하는 이벤트입니다. send로 데이터를 보냅니다.

websocket.disconnect (또는 websocket.close)

웹소켓(WebSocket) 연결이 종료될 때 발생하는 이벤트입니다.

ASGI 생명주기 (Lifecycle)

ASGI(아스기) 애플리케이션(application)의 생명주기(lifecycle)는 애플리케이션(application)이 시작되고 종료될 때까지 거치는 단계를 의미하며, 주로 라이프스팬(lifespan) 이벤트 채널을 통해 관리됩니다.

라이프스팬(Lifespan) 관리 흐름

scope['type']이 'lifespan'일 때 이 흐름이 시작됩니다.

 

서버는 애플리케이션(application)에게 lifespan.startup 메시지를 보내고, 애플리케이션(application)은 준비가 완료되면 lifespan.startup.complete 메시지를 응답합니다.

 

종료 시에는 서버가 lifespan.shutdown 메시지를 보내고, 애플리케이션(application)은 정리 후 lifespan.shutdown.complete 메시지를 응답합니다.

요청 처리 생명주기

개별 요청(HTTP 또는 WebSocket)에 대해서는 연결 수립, 요청 처리, 응답 전송, 연결 종료의 단계를 거칩니다.

 

각 단계는 위에서 설명한 HTTP 또는 웹소켓(WebSocket) 이벤트를 통해 진행됩니다.

Uvicorn(유비콘), ASGI(아스기), 그리고 웹 프레임워크(Web Framework)

Uvicorn(유비콘)의 역할: ASGI 서버 구현체

앞서 설명했듯이, Uvicorn(유비콘)은 ASGI(아스기) 명세(specification)를 구현한 서버입니다.

 

즉, ASGI(아스기)라는 약속(규약)에 따라 웹 애플리케이션(application)과 통신하고 클라이언트(client) 요청을 처리해주는 역할을 합니다.

프레임워크(Framework)의 필요성: 개발 편의성 증대

그런데 맨 처음 보여드린 ASGI(아스기) 애플리케이션(application) 예제 코드를 보면, 요청 정보를 직접 파싱(parsing)하고 응답 메시지를 규격에 맞게 만드는 등 상당히 낮은 수준(low-level)의 작업을 직접 해야 합니다.

 

실제 웹 애플리케이션(application)을 이렇게 개발하기는 매우 번거롭습니다.

 

그래서 ASGI(아스기) 규약을 기반으로 하면서도 개발자가 좀 더 편하고 생산적으로 웹 애플리케이션(application)을 만들 수 있도록 도와주는 상위 레벨(high-level)의 프레임워크(framework)들이 등장했습니다.

Starlette (스타렛)

가볍고 성능이 뛰어난 ASGI(아스기) 프레임워크(framework)/툴킷(toolkit)입니다.

 

FastAPI(패스트API)의 핵심 기반이 되기도 하며, 단독으로 사용하여 간단한 고성능 웹 애플리케이션(application)을 만드는 데에도 사용됩니다.

 

라우팅(routing), 미들웨어(middleware), 템플릿(template) 렌더링(rendering) 등 기본적인 웹 개발 기능들을 제공합니다.

FastAPI (패스트API)

현대적이고 빠르며(고성능), 파이썬(Python) 3.6+ 버전의 타입 힌트(type hint)를 적극적으로 활용하여 API를 구축하기 위한 웹 프레임워크(framework)입니다.

 

Starlette(스타렛)을 기반으로 만들어졌으며, 데이터 유효성 검사 및 직렬화(serialization) 자동화, 대화형 API 문서 자동 생성 등 강력한 기능들을 제공하여 API 개발 생산성을 크게 높여줍니다.

애플리케이션 실행

Starlette(스타렛)이나 FastAPI(패스트API) 같은 프레임워크(framework)를 사용하면, 개발자는 프레임워크(framework)가 제공하는 편리한 방법으로 코드를 작성하고, 프레임워크(framework)는 내부적으로 ASGI(아스기) 규격에 맞는 애플리케이션(application) 객체(보통 app이라는 이름으로 인스턴스화됩니다)를 만들어 줍니다.

 

그리고 이 app 객체를 Uvicorn(유비콘) 같은 ASGI(아스기) 서버를 통해 실행시키는 것입니다.

 

터미널(terminal)에서 다음과 같은 명령어를 사용하는 것이 바로 그 과정입니다.

uvicorn main:app --reload

 

여기서 main은 파이썬(Python) 파일 이름(main.py), app은 그 파일 안에 있는 ASGI(아스기) 애플리케이션(application) 객체(FastAPI 또는 Starlette 인스턴스)를 의미합니다.

 

--reload 옵션은 코드가 변경될 때마다 서버를 자동으로 재시작해주는 개발 편의 기능입니다.

결론

정리하자면, FastAPI(패스트API) 같은 현대적인 파이썬(Python) 비동기 웹 프레임워크(framework)는 ASGI(아스기)라는 비동기 통신 규약을 따릅니다.

 

그리고 Uvicorn(유비콘)은 이 ASGI(아스기) 규약을 구현한 고성능 서버입니다.

 

따라서 FastAPI(패스트API) 애플리케이션(application)을 실행하기 위해서는 ASGI(아스기) 서버인 Uvicorn(유비콘)이 필요한 것입니다.

 

ASGI(아스기)는 파이썬(Python) 웹 개발에 비동기(asynchronous) 처리 능력을 부여하여 더 빠르고 효율적인 웹 서비스를 만들 수 있는 기반을 마련해주었습니다.

 

이 관계를 이해하면 FastAPI(패스트API)와 Uvicorn(유비콘)이 왜 함께 사용되는지 명확하게 알 수 있을 것입니다.