13장. REST API 와 FastAPI
FastAPI 의 기본
자판기는 버튼마다 할 일이 정해져 있습니다. 1번 버튼은 커피, 2번 버튼은 우유입니다. FastAPI 에서는 함수 위에 @app.get("/health") 처럼 주소와 메서드를 적어, "이 주소로 이 메서드가 오면 이 함수를 부른다"를 정합니다.
1번에서 FastAPI() 로 앱을 만듭니다. 2번에서 함수에 @app.get, @app.post 를 붙여 경로 함수로 만듭니다. 3번에서 uvicorn 파일이름:app 으로 서버를 띄웁니다. 4번에서 브라우저로 /docs 를 열면 FastAPI 가 만든 문서와 시험 화면이 나옵니다. 여기서 자판기의 버튼이 엔드포인트, 버튼에 붙은 일이 경로 함수입니다.
데코레이터와 메서드
| 데코레이터 | 메서드 | 예 |
|---|---|---|
@app.get("/health") | GET | 서버가 살아 있나 |
@app.post("/predict") | POST | 예측 하나 |
@app.put("/models/{name}") | PUT | 모델 바꾸기 |
@app.delete("/cache") | DELETE | 캐시 지우기 |
경로 함수가 사전을 돌려주면 FastAPI 가 JSON 으로 바꿔 보냅니다. 주소의 {name} 같은 자리는 함수의 같은 이름 인자로 들어옵니다.
서버 없이 시험하기
TestClient 는 서버를 띄우지 않고 앱에 요청을 보내 봅니다. 실습에서는 이것으로 시험하고, 실습 4에서 실제 서버를 띄웁니다.
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/health")
def health():
return {"상태": "ok"}
@app.get("/items/{item_id}")
def read_item(item_id: int): # 주소의 {item_id} 가 정수로 바뀌어 들어온다
return {"번호": item_id}
client = TestClient(app)
print(client.get("/health").json(), client.get("/items/7").json(), client.get("/items/칠").status_code)코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
app = FastAPI() | API 앱 하나를 만든다 |
@app.get("/health") | 데코레이터(decorator, 꾸미개)다. 바로 아래 함수를 "GET /health 가 오면 부를 함수"로 등록한다 |
"/items/{item_id}" | 중괄호 자리는 경로 매개변수(path parameter)다. 주소의 그 자리 글이 같은 이름의 인자로 들어온다 |
item_id: int | 타입 힌트다. FastAPI 는 이것을 보고 정수로 바꾸고, 바꿀 수 없으면 422 를 돌려준다 |
TestClient(app) | 서버를 띄우지 않고 앱에 요청을 보내 보는 시험용 손님이다 |
client.get("/health") | GET 요청을 보내고 응답을 받는다 |
.json(), .status_code | 응답 본문을 JSON 에서 파이썬 사전으로 바꾼 것과 상태 코드 숫자다 |
이 코드가 뜻하는 것
자판기를 공장에서 시험할 때는 가게에 설치하지 않고 버튼을 하나씩 눌러 봅니다. 여기서 자판기가 app, 버튼이 @app.get 으로 붙인 엔드포인트, 버튼을 눌러 보는 시험 담당자가 TestClient 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
app = FastAPI() | 앱을 만든다 | 빈 자판기 |
@app.get("/health") 와 health() | 살아 있는지 묻는 주소를 단다 | "작동 중" 확인 버튼 |
@app.get("/items/{item_id}") 와 read_item | 번호를 받아 돌려주는 주소를 단다 | 번호를 누르는 버튼 |
client = TestClient(app) | 시험용 손님을 만든다 | 시험 담당자 |
client.get(...) 세 번 | 세 요청을 보내 결과를 본다 | 버튼을 세 번 눌러 본다 |
실행하면 {'상태': 'ok'} {'번호': 7} 422 가 나옵니다. /items/7 의 "7"은 글로 왔지만 정수 7 로 바뀌어 함수에 들어갔고, /items/칠 은 정수로 바꿀 수 없어 함수가 불리기 전에 422 가 돌아왔습니다.
작게 줄여서 눈으로 보기
틀린 요청 세 가지에 FastAPI 가 무엇을 돌려주는지 봅니다.
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int):
return {"번호": item_id}
client = TestClient(app)
r = client.get("/items/칠")
print(r.status_code, r.json()["detail"][0]["msg"])
r = client.get("/없는주소")
print(r.status_code, r.json())
r = client.post("/items/7")
print(r.status_code, r.json())422 Input should be a valid integer, unable to parse string as an integer
404 {'detail': 'Not Found'}
405 {'detail': 'Method Not Allowed'}| 결과 | 읽는 법 |
|---|---|
422 Input should be a valid integer, ... | "올바른 정수여야 한다, 글을 정수로 풀 수 없다"는 뜻이다. 어느 칸이 왜 틀렸는지 본문의 detail 에 적혀 온다 |
404 {'detail': 'Not Found'} | 그런 주소의 버튼이 아예 없다 |
405 {'detail': 'Method Not Allowed'} | 405 는 "허락되지 않은 메서드"다. 주소는 있지만 GET 버튼만 있고 POST 버튼은 없다 |
주의할 점
- 경로 함수의 타입 힌트는 장식이 아니라 검사 규칙입니다.
item_id: int에 "칠"을 보내면 함수가 불리기 전에 422 가 돌아갑니다. /docs의 자동 문서는 편리하지만, 사내 서비스를 밖에 열 때는 문서 주소도 막을지 정합니다(FastAPI(docs_url=None)).