애플리케이션과 서버 간의 다리 구축하기
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개: 웹 애플리케이션의 보이지 않는 도우미
Python 개발자로서 우리는 간단한 정적 파일 제공부터 복잡한 API 상호 작용에 이르기까지 다양한 요청을 처리하는 웹 애플리케이션을 자주 구축합니다. 이면에서는 애플리케이션 로직과 웹 서버 사이에 자리 잡고 있으며, 로깅, 인증, 캐싱 또는 데이터 변환과 같은 많은 일반적인 작업을 담당하는 중요한 계층인 미들웨어가 있습니다. 미들웨어는 요청과 응답을 가로채어 코어 애플리케이션 코드를 복잡하게 만들지 않고도 이러한 기능을 추가할 수 있습니다. 이 모듈식 접근 방식은 애플리케이션을 깔끔하고 유지 관리 가능하게 유지할 뿐만 아니라 다양한 프로젝트에서 재사용성을 촉진합니다. 이 글에서는 간단한 WSGI 또는 ASGI 미들웨어를 처음부터 구축하여 이 강력한 개념의 작동 방식을 이해하고 그 마법을 벗겨낼 것입니다.
기둥 이해하기: WSGI, ASGI 및 미들웨어
코딩을 시작하기 전에 여정을 뒷받침하는 핵심 개념을 명확하게 이해해 봅시다.
WSGI란 무엇인가?
WSGI는 Web Server Gateway Interface를 의미합니다. 이는 웹 서버(Gunicorn, uWSGI 등)와 웹 애플리케이션 또는 프레임워크(Flask, Django 등) 간의 표준 인터페이스를 정의하는 Python 사양입니다. 서버는 두 개의 인수를 받는 호출 가능한 WSGI 애플리케이션을 호출합니다.
environ: CGI 스타일의 환경 변수, 웹 서버 변수 및 HTTP 헤더가 포함된 사전입니다.start_response: 애플리케이션이 HTTP 상태 및 헤더를 서버로 보내는 데 사용하는 호출 가능한 객체입니다.
그런 다음 애플리케이션은 응답 본문을 나타내는 바이트 문자열의 반복 가능한 객체를 반환합니다.
ASGI란 무엇인가?
ASGI는 Asynchronous Server Gateway Interface를 의미합니다. 이는 WSGI의 현대적인 후속 제품으로, 비동기 작업, 웹소켓 및 HTTP/2를 지원하도록 설계되었습니다. WSGI와 유사하게 ASGI는 비동기 처리가 가능한 웹 서버(Uvicorn, Hypercorn 등)와 비동기 웹 애플리케이션(FastAPI, Starlette 등) 간의 표준 인터페이스를 정의합니다. ASGI 애플리케이션은 세 가지 인수를 받는 비동기 호출 가능한 객체입니다.
scope:'http','websocket'과 같은 유형,method,path및 헤더를 포함하여 특정 연결에 대한 정보가 포함된 사전입니다.receive: 애플리케이션이 서버로부터 이벤트 메시지(예: 요청 본문 청크, 웹소켓 메시지)를 수신할 수 있도록 하는 await 가능한 호출 가능한 객체입니다.send: 애플리케이션이 서버로 이벤트 메시지(예: 응답 상태, 헤더, 본문 청크, 웹소켓 메시지)를 보낼 수 있도록 하는 await 가능한 호출 가능한 객체입니다.
미들웨어란 무엇인가?
WSGI 및 ASGI 컨텍스트에서 미들웨어는 본질적으로 WSGI 또는 ASGI 애플리케이션 자체이지만 약간의 차이가 있습니다. 다른 WSGI 또는 ASGI 애플리케이션을 래핑합니다. 이 래핑을 통해 미들웨어는 요청이 내부 애플리케이션에 도달하기 전에 요청을 가로채고 응답이 나간 후에 응답을 가로챌 수 있습니다. 웹 애플리케이션에 대한 함수 데코레이터로 생각하여 교차 관심사를 추가한다고 생각할 수 있습니다.
간단한 WSGI 미들웨어 구축
들어오는 요청 경로를 기록하는 간단한 WSGI 미들웨어를 만들어 보겠습니다.
내부 WSGI 애플리케이션
먼저 미들웨어가 래핑할 기본 WSGI 애플리케이션이 필요합니다.
# app.py def simple_app(environ, start_response): """매우 기본적인 WSGI 애플리케이션.""" status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [b"Hello from the simple app!"] if __name__ == '__main__': from wsgiref.simple_server import make_server httpd = make_server('', 8000, simple_app) print("Serving on port 8000...") httpd.serve_forever()
python app.py로 실행하고 http://localhost:8000에 액세스하여 "Hello from the simple app!"을 볼 수 있습니다.
로깅 미들웨어 설계
이제 로깅 미들웨어를 만들어 보겠습니다. WSGI 미들웨어는 초기화 중에 체인의 다음 WSGI 애플리케이션을 인수로 받아야 합니다. 그런 다음 __call__ 메서드가 WSGI 인터페이스를 구현합니다.
# middleware.py import logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') class RequestLoggerMiddleware: def __init__(self, app): """ 체인의 다음 WSGI 애플리케이션으로 미들웨어를 초기화합니다. """ self.app = app def __call__(self, environ, start_response): """ 미들웨어의 WSGI 인터페이스 메서드. 요청을 가로채고, 기록하고, 래핑된 애플리케이션으로 전달한 다음, 응답을 반환합니다. """ path = environ.get('PATH_INFO', '/') method = environ.get('REQUEST_METHOD', 'GET') logging.info(f"Request received: {method} {path}") # 래핑된 애플리케이션의 start_response를 호출하고 캡처합니다. # 헤더나 상태를 수정하는 미들웨어의 경우 이것이 중요합니다. _headers = [] _status = None def wrapped_start_response(status, headers, exc_info=None): nonlocal _status, _headers _status = status _headers = headers logging.info(f"Response status: {status}") return start_response(status, headers, exc_info) # 래핑된 애플리케이션에 요청 전달 response_body = self.app(environ, wrapped_start_response) # 미들웨어는 여기서 response_body를 검사하거나 수정할 수도 있습니다. # 이 간단한 로거의 경우 그대로 반환합니다. return response_body
미들웨어 통합
마지막으로 RequestLoggerMiddleware를 simple_app과 통합할 수 있습니다.
# main.py from wsgiref.simple_server import make_server from app import simple_app from middleware import RequestLoggerMiddleware if __name__ == '__main__': # logger 미들웨어로 simple_app 래핑 application_with_middleware = RequestLoggerMiddleware(simple_app) httpd = make_server('', 8000, application_with_middleware) print("Serving application with middleware on port 8000...") httpd.serve_forever()
python main.py를 실행하고 http://localhost:8000에 접속하면 요청을 알리는 로그 메시지가 콘솔에 표시되고, 브라우저에는 "Hello from the simple app!" 응답이 표시됩니다. 이는 미들웨어가 어떻게 요청을 가로채고, 로깅 작업을 수행하고, 애플리케이션으로 요청을 전달하는지 보여줍니다.
간단한 ASGI 미들웨어 만들기
이제 비슷한 로깅 기능을 ASGI를 사용하여 구현해 보겠습니다. ASGI의 비동기 특성으로 인해 약간 다른 접근 방식이 필요합니다.
내부 ASGI 애플리케이션
기본 ASGI 애플리케이션을 사용하겠습니다.
# async_app.py async def simple_async_app(scope, receive, send): """매우 기본적인 ASGI 애플리케이션.""" if scope['type'] == 'http': await send({ 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], }) await send({ 'type': 'http.response.body', 'body': b"Hello from the async app!", }) elif scope['type'] == 'websocket': # 웹소켓 연결에 대한 간단한 처리기 await send({"type": "websocket.accept"}) while True: message = await receive() if message['type'] == 'websocket.disconnect': break await send({"type": "websocket.send", "text": f"Echo: {message.get('text')}"})
이것을 실행하려면 일반적으로 Uvicorn과 같은 ASGI 서버를 사용합니다.
uvicorn async_app:simple_async_app --port 8000 --reload
로깅 ASGI 미들웨어 설계
ASGI 미들웨어는 초기화 시(종종 __init__) 다음 ASGI 애플리케이션을 받는 비동기 호출 가능한 객체입니다. __call__ 메서드도 async def여야 합니다.
# async_middleware.py import logging import time logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') class AsyncRequestLoggerMiddleware: def __init__(self, app): self.app = app async def __call__(self, scope, receive, send): if scope['type'] == 'http': start_time = time.monotonic() path = scope.get('path', '/') method = scope.get('method', 'GET') logging.info(f"ASGI Request Received: {method} {path}") # 응답 세부 정보를 가로채기 위해 사용자 지정 send 함수 정의 async def wrapped_send(message): if message['type'] == 'http.response.start': status_code = message['status'] logging.info(f"ASGI Response Status: {status_code}") await send(message) # 원래 send 함수로 메시지 전달 await self.app(scope, receive, wrapped_send) end_time = time.monotonic() duration = (end_time - start_time) * 1000 # 밀리초 단위 logging.info(f"ASGI Request Processed: {method} {path} - Duration: {duration:.2f}ms") else: # HTTP가 아닌 연결(예: 웹소켓)의 경우 통과만 합니다. await self.app(scope, receive, send)
WSGI의 start_response와 달리 ASGI의 send는 비동기 메시지 스트림입니다. 상태와 같은 응답 세부 정보를 가로채려면 서버에서 제공하는 send 호출 가능한 객체를 래핑합니다.
비동기 미들웨어 통합
이제 simple_async_app을 AsyncRequestLoggerMiddleware로 래핑해 보겠습니다.
# async_main.py from async_app import simple_async_app from async_middleware import AsyncRequestLoggerMiddleware # 비동기 앱을 로거 미들웨어로 래핑 application_with_async_middleware = AsyncRequestLoggerMiddleware(simple_async_app) if __name__ == '__main__': import uvicorn # Uvicorn은 직접 ASGI 애플리케이션을 예상합니다. uvicorn.run(application_with_async_middleware, host="0.0.0.0", port=8000)
이것을 실행하려면 python async_main.py를 사용합니다. 브라우저에서 http://localhost:8000에 액세스합니다. 들어오는 요청과 나가는 응답, 처리 시간 등 콘솔에서 로그 메시지를 관찰하게 됩니다. ASGI 미들웨어는 비동기적으로 요청-응답 주기를 가로채고, 기록하고, 타이밍하는 기능을 보여줍니다.
공통 애플리케이션 시나리오
미들웨어는 매우 다양하며 많은 웹 애플리케이션 기능의 기반이 됩니다.
- 인증/권한 부여: 특정 경로에 액세스하기 전에 사용자 자격 증명을 확인합니다.
- 로깅: 시연한 대로 요청, 응답 및 오류를 추적합니다.
- CORS(Cross-Origin Resource Sharing): 교차 출처 요청을 허용하거나 제한하기 위한 적절한 헤더를 추가합니다.
- 압축: 대역폭을 줄이기 위해 응답을 Gzip 또는 Brotli로 인코딩합니다.
- 속도 제한: 단일 소스로부터의 요청 수를 제한하여 남용을 방지합니다.
- 오류 처리: 예외를 잡고 사용자 친화적인 오류 페이지를 반환합니다.
- 세션 관리: 요청 간에 사용자 세션을 관리합니다.
미들웨어를 구축하는 방법을 이해함으로써 이러한 기능을 깔끔하고, 분리되고, 재사용 가능한 방식으로 구현하는 힘을 얻게 되어 Python 웹 애플리케이션을 더욱 강력하고 유지 관리하기 쉽게 만들 수 있습니다.
결론: 웹 애플리케이션 개발 강화
이 글 전체에서 우리는 WSGI와 ASGI 미들웨어의 기본 역할을 웹 요청 및 응답을 가로채고 향상시키는 방식으로 설명했습니다. Python 인터페이스를 따르고 자체 간단한 로깅 예제를 구현함으로써 로깅 또는 보안과 같은 교차 관심사를 삽입할 수 있는 방법을 확인했습니다. 핵심 애플리케이션 코드에 로직을 분산시키지 않고도 미들웨어는 필수적인 도구로서 Python 웹 애플리케이션의 모듈성, 유지 관리성 및 재사용성을 크게 향상시킵니다.

