Middleware
Middleware은 어플리케이션의 중간에서 다양한 역할을 수행하는 컴포넌트를 말한다. 클라이언트와 서버 사이의 로드밸런서도 미들웨어의 일종이고, 데이터베이스와 서버 사이의 캐시 서버도 미들웨어의 일종이라고 할 수 있다. FastAPI에서의 비교적 좁은 의미로, 들어오는 모든 요청에 적용되는 함수를 미들웨어라고 한다. 이번 글에서는 FastAPI에서 Middleware를 적용하는 방법에 대해서 알아본다.
HTTP
Middleware는 모든 HTTP 요청에 대해 같은 함수를 적용할 수 있다. 이것을 사용해서 할 수 있는 것 중 대표적인 것이 실행시간을 측정하는 것이다. 각 HTTP 요청에 대해 path operation을 실행하기 전과 후에 시간을 체크해서 요청을 처리하는 데 걸린 시간을 측정할 수 있다.
import time
from typing import Callable
from starlette.requests import Request
@app.middleware("http")
async def add_process_time_header(request: Request, call_next: Callable):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
print(process_time)
return response
Middleware는 ExceptionHandler와 비슷하게 데코레이터를 이용해서 간단하게 등록할 수 있다. HTTP 요청과 요청을 처리할 수 있는 함수를 파라미터로 받아서 call_next(request)로 원래의 요청을 처리하는 로직을 수행할 수 있다. 이렇게 call_next의 앞뒤로 로직을 추가해서 미들웨어의 역할을 추가할 수 있다.
참고로, 위 코드에서 middleware의 파라미터로 http를 넣어주었는데, 실제 코드를 보면 파라미터의 값을 어디서도 사용하지 않는 것 같다. 실제로 현재 버전(0.103.1)을 기준으로 http가 아니라 임의의 문자열을 입력해도 잘 동작하는 것 같다.
CORS(Cross-Origin Resource Sharing)
Middleware를 사용할 수 있는 예시 중 또다른 것은 CORS 헤더를 추가하는 것이다. 기본적으로 브라우저에서는 보안상의 이유로 서로 다른 origin으로 보내는 요청을 허용하지 않는다. 그러나 인터넷 환경이 복잡해짐에 따라 다양한 origin에서 리소스를 받아와야 하는 상황이 많아졌다. 이러한 문제를 해결하기 위해 나온 것이 CORS이다. CORS와 관련해서는 아래 블로그 글에 자세히 설명되어있으니 읽어보길 바란다.
결론적으로, CORS는 어플리케이션 레벨에서 허용하는 것이 아니기 때문에 HTTP 헤더만 추가해주면 된다. FastAPI에서는 이러한 역할을 수행하는 미들웨어를 제공한다.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"http://localhost.tiangolo.com",
"https://localhost.tiangolo.com",
"http://localhost",
"http://localhost:8080",
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def main():
return {"message": "Hello World"}
기타 미들웨어
FastAPI에서는 CORSMiddleware 이외에도 여러가지 미들웨어를 제공한다. 정확히 말하면 FastAPI는 Starlette에서 제공하는 미들웨어를 wrapping해서 제공할 뿐이다. 이렇게 기본으로 제공하는 미들웨어 이외에도, 다양하게 사용할 수 있는 ASGI 미들웨어도 존재한다.
TrustedHostMiddleware
특정한 Host에서 오는 요청만 허용하는 미들웨어이다. Host 헤더를 확인하여 허용되지 않은 요청에는 status code 400으로 응답한다.
from fastapi import FastAPI
from fastapi.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
app.add_middleware(
TrustedHostMiddleware, allowed_hosts=["example.com", "*.example.com"]
)
@app.get("/")
async def main():
return {"message": "Hello World"}
HTTPSRedirectMiddleware
HTTPS로 통신하는 어플리케이션의 경우, HTTP 요청에 대해 redirect하도록 유도하는 것이 필요하다. HTTP와 HTTPS 뿐만 아니라, 웹 소켓 통신에서도 마찬가지로 WS 요청을 WSS로 redirect 해주는 역할을 한다.
from fastapi import FastAPI
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
app = FastAPI()
app.add_middleware(HTTPSRedirectMiddleware)
@app.get("/")
async def main():
return {"message": "Hello World"}
SentryAsgiMiddleware
Sentry는 로깅과 모니터링을 제공하는 서비스이다. 실시간으로 어플리케이션에서 발생하는 에러의 정보를 수집해서 한 눈에 볼 수 있도록 하고, 알림을 보낼 수도 있다. python 뿐만 아니라 정말 다양한 언어와 프레임워크를 제공해서 라인, 카카오페이 등 현업에서도 많이 사용하고 있는 것 같다.
FastAPI에서 Sentry를 사용하기 위해서는 패키지 설치가 필요하다.
pip install 'sentry-sdk[fastapi]'
또 Sentry 웹 페이지에서 가입 후 프로젝트를 생성하고, dsn 키를 받아야 한다.
from fastapi import FastAPI
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
import sentry_sdk
sentry_sdk.init(
dsn="https://examplePublicKey@o0.ingest.sentry.io/0",
traces_sample_rate=1.0
)
app = FastAPI()
app.add_middleware(SentryAsgiMiddleware)
이렇게 설정을 해준 후 아래와 같이 에러 정보를 수집할 수 있다.
import sentry_sdk
try:
print(1 / 0)
except Exception as ex:
sentry_sdk.capture_exception(ex)
MessagePackMiddleware
MessagePack은 JSON과 비슷하게 프로그래밍 언어에 관계없이 데이터를 주고받을 수 있는 형식이다. JSON은 문자열로 데이터를 주고받기 때문에 상대적으로 느린데, MessagePack은 binary 데이터를 사용하고, 숫자를 한 바이트로 처리하는 등 JSON의 단점을 보완해 비교적 빠른 통신이 가능하다.
MessagePack을 사용하기 위해서는 패키지 설치가 필요하다.
pip install "msgpack-asgi==1.*"
사용방법은 기존 코드와 동일하게 JSON 형식으로 반환하면 된다.
from fastapi import FastAPI
from msgpack_asgi import MessagePackMiddleware
from starlette.responses import JSONResponse
app = FastAPI()
app.add_middleware(MessagePackMiddleware)
@app.get("/items/{item_id}")
async def read_item_header(item_id: str):
return JSONResponse({"id": item_id, "name": "test"})
위와 같은 코드에 일반적인 요청을 보내면 JSON 응답이 오지만, Accept 헤더에 application/x-msgpack을 입력하면 MessagePack 응답이 오는 것을 확인할 수 있다.
동작 순서
그렇다면 여러 개의 미들웨어를 동시에 사용한다면 어떤 순서로 동작하게 될까? 직접 테스트를 해보자.
from typing import Callable
from fastapi import FastAPI
from starlette.requests import Request
app = FastAPI()
@app.middleware("http")
async def middleware_1(request: Request, call_next: Callable):
print("middleware_1 start")
response = await call_next(request)
print("middleware_1 end")
return response
@app.middleware("http")
async def middleware_2(request: Request, call_next: Callable):
print("middleware_2 start")
response = await call_next(request)
print("middleware_2 end")
return response
@app.middleware("http")
async def middleware_3(request: Request, call_next: Callable):
print("middleware_3 start")
response = await call_next(request)
print("middleware_3 end")
return response
@app.get("/test")
async def test():
return {"Hello": "World"}
위 코드를 실행하면 middleware_1 -> middleware_2 -> middleware_3의 순서대로 FastAPI에 등록될 것이다. /test에 API 요청을 보내면 아래와 같은 결과를 확인할 수 있다.
middleware_3 start
middleware_2 start
middleware_1 start
middleware_1 end
middleware_2 end
middleware_3 end
위 결과를 보면 나중에 등록된 미들웨어일수록 가장 먼저 호출이 된다는 것을 확인할 수 있다.
3줄 요약
- 모든 API 요청에 동일한 로직을 추가하고 싶으면 Middleware를 사용하면 된다
- 직접 구현할 수도 있고, Starlette 프레임워크나 서드파티 ASGI 미들웨어를 찾아서 사용해도 된다
- 미들웨어의 동작 순서는 등록 순서의 반대이다
'Python > FastAPI' 카테고리의 다른 글
[FastAPI] FastAPI 튜토리얼 (9) - Background Task (0) | 2023.08.26 |
---|---|
[FastAPI] FastAPI 튜토리얼 (7) - Exception (0) | 2023.08.05 |
[FastAPI] FastAPI 튜토리얼 (6) - 로그인 (0) | 2023.08.02 |
[FastAPI] FastAPI 튜토리얼 (5) - SQLAlchemy ORM (0) | 2023.07.30 |
[FastAPI] FastAPI 튜토리얼 (4) - SQLAlchemy Core (0) | 2023.07.22 |