13장. REST API 와 FastAPI
실습 4 — 서버로 띄워 HTTP 로 부르기
실습 2의 out_delay_api.py 를 uvicorn 으로 이 PC 의 8013 번 포트에 띄우고, 다른 프로그램처럼 HTTP 요청으로 부릅니다. 한 건씩 20번 부르기와 20건 묶음 한 번을 견주고, 끝나면 서버를 끕니다. 8013 번 포트를 다른 프로그램이 쓰고 있으면 번호를 바꿉니다.
1단계. 서버 띄우기
import json, subprocess, sys, time, urllib.request
# 터미널에서 "uvicorn out_delay_api:app --port 8013" 을 실행한 것과 같다. 작업 폴더(data)에서 띄운다
server = subprocess.Popen([sys.executable, "-m", "uvicorn", "out_delay_api:app", "--port", "8013", "--log-level", "warning"],
cwd=".")
BASE = "http://127.0.0.1:8013" # localhost 로 적으면 Windows 에서 요청마다 2초쯤 늦어질 수 있다(코드 설명)
for _ in range(60): # 뜰 때까지 0.5초마다 /health 를 불러 본다
try:
print(urllib.request.urlopen(BASE + "/health").read().decode())
break
except OSError:
time.sleep(0.5){"상태":"ok","판":"delay-lr-1"}2단계. HTTP 요청으로 부르기
def call(path, body=None):
"""body 가 있으면 POST, 없으면 GET 으로 부르고 (상태 코드, JSON) 을 돌려준다"""
data = json.dumps(body, ensure_ascii=False).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, headers={"Content-Type": "application/json"})
try:
with urllib.request.urlopen(req) as r:
return r.status, json.loads(r.read())
except urllib.error.HTTPError as e: # 4xx, 5xx 는 예외로 오므로 상태 코드와 본문을 꺼낸다
return e.code, json.loads(e.read())
order = {"주문시각": 22, "거리km": 197.6, "무게kg": 4.6, "지역": "지방", "상품분류": "도서", "날씨": "눈", "택배사": "빠른별택배"}
print(call("/predict", order))
print(call("/predict", dict(order, 지역="부산"))[0])
print("문서 화면:", urllib.request.urlopen(BASE + "/docs").status)(200, {'지연확률': 0.6887, '지연예상': True, '기준값': 0.5, '판': 'delay-lr-1'})
422
문서 화면: 2003단계. 한 건씩과 묶음 견주기
orders = [dict(order, 주문시각=h) for h in range(20)]
t = time.perf_counter()
singles = [call("/predict", o)[1]["지연확률"] for o in orders] # 20번 왕복
one_by_one = time.perf_counter() - t
t = time.perf_counter()
batch = [x["지연확률"] for x in call("/predict/batch", {"주문들": orders})[1]["결과"]] # 1번 왕복
together = time.perf_counter() - t
print(f"한 건씩 20번: {one_by_one * 1000:.0f}ms · 묶음 1번: {together * 1000:.0f}ms · 결과 같음: {singles == batch}")한 건씩 20번: 161ms · 묶음 1번: 8ms · 결과 같음: True4단계. 서버 끄기
server.terminate() # 띄운 서버를 끈다. 터미널이라면 Ctrl+C
server.wait(timeout=10)
try:
urllib.request.urlopen(BASE + "/health", timeout=2)
except OSError as e:
print("꺼짐:", type(e).__name__)꺼짐: ConnectionResetError이 실습이 뜻하는 것
이 실습은 연습 손님만 받던 창구를 실제로 열고 전화로 주문을 받는 일과 같습니다. 가게 문을 열고 열렸는지 확인하고(1단계), 전화로 주문해 보고 잘못된 주문과 메뉴판 화면도 확인합니다(2단계). 주문 20건을 한 통씩 20번 거는 것과 한 통에 몰아서 거는 것을 견주고(3단계), 문을 닫습니다(4단계). 여기서 가게 문이 uvicorn 서버, 전화번호가 http://127.0.0.1:8013, 전화 한 통이 HTTP 요청 한 번의 왕복입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 띄우기 | {"상태":"ok","판":"delay-lr-1"} | 서버가 떴고, 다른 프로그램처럼 주소로 불러도 답합니다 |
| 2. 부르기 | (200, {'지연확률': 0.6887, …}), 422, 문서 화면: 200 | TestClient 로 부를 때와 같은 확률이 나오고, 부산은 422 로 막히며, /docs 화면도 열립니다 |
| 3. 견주기 | 한 건씩 20번: 161ms · 묶음 1번: 8ms · 결과 같음: True | ms(밀리초)는 1,000분의 1초입니다. 결과는 같은데 묶음 한 번이 20배쯤 빨랐습니다. 왕복이 20번에서 1번으로 줄어든 덕이 큽니다 |
| 4. 끄기 | 꺼짐: ConnectionResetError | 끈 뒤에는 연결이 끊겨 답하지 않습니다. 포트를 비워 둬야 다음에 같은 번호로 다시 띄울 수 있습니다 |
한 줄로 줄이면, 같은 API 를 실제 서버로 띄우면 어떤 프로그램이든 주소로 부를 수 있고, 여러 건은 묶어 보내는 편이 빠르다는 실습입니다. 시간은 이 PC 에서 한 번 잰 값이라 PC 와 실행마다 달라지고, 주소를 localhost 로 적으면 Windows 에서 크게 늦을 수 있다는 점은 코드 설명에 있습니다. 실습 5부터는 이 창구를 아무나 부를 수 없게 자물쇠를 답니다.
코드 설명
subprocess.Popen으로 uvicorn 을 다른 프로그램으로 띄우고,/health가 답할 때까지 0.5초마다 불러 봅니다. 터미널에서uvicorn out_delay_api:app --port 8013을 실행하는 것과 같습니다.call()은 표준 라이브러리urllib로 HTTP 요청을 보냅니다. 4xx, 5xx 응답은HTTPError예외로 오므로 상태 코드와 본문을 꺼내 돌려줍니다. 결과는TestClient로 부를 때와 같았고(0.6887, 부산은 422),/docs화면도 200 으로 열렸습니다.- 한 건씩 20번은 161ms, 20건 묶음 한 번은 8ms 로 20배쯤 빨랐습니다. 교안을 만들며 처음 돌렸을 때는 주소를
localhost로 적었는데, 한 건씩 20번이 40.8초, 묶음 한 번이 2초나 걸렸습니다. Windows 에서localhost가 먼저 IPv6 주소(::1)로 연결을 시도하는데, uvicorn 은 기본으로127.0.0.1에서만 기다리기 때문에 요청마다 2초쯤 기다렸다 다시 붙은 것입니다. 주소를127.0.0.1로 바꾸자 사라졌습니다. server.terminate()로 서버를 끕니다. 끄지 않으면 포트를 붙잡은 채 남아, 다음에 같은 포트로 띄울 때 "address already in use" 오류가 납니다.