LLM 엔지니어링13장 · REST API 와 FastAPI17 / 22← 이전목차다음 →MetaContents

13장. REST API 와 FastAPI

실습 4 — 서버로 띄워 HTTP 로 부르기

실습 2의 out_delay_api.py 를 uvicorn 으로 이 PC 의 8013 번 포트에 띄우고, 다른 프로그램처럼 HTTP 요청으로 부릅니다. 한 건씩 20번 부르기와 20건 묶음 한 번을 견주고, 끝나면 서버를 끕니다. 8013 번 포트를 다른 프로그램이 쓰고 있으면 번호를 바꿉니다.

1단계. 서버 띄우기

Python
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 요청으로 부르기

Python
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
문서 화면: 200

3단계. 한 건씩과 묶음 견주기

Python
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 · 결과 같음: True

4단계. 서버 끄기

Python
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, 문서 화면: 200TestClient 로 부를 때와 같은 확률이 나오고, 부산은 422 로 막히며, /docs 화면도 열립니다
3. 견주기한 건씩 20번: 161ms · 묶음 1번: 8ms · 결과 같음: Truems(밀리초)는 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" 오류가 납니다.
← 이전 절13장 · 실습 3 — 입력 검사와 묶음 예측다음 절 →13장 · 실습 5 — API 키로 접근 막기