LLM 엔지니어링14장 · Streamlit 화면과 Docker 컨테이너8 / 22← 이전목차다음 →MetaContents

14장. Streamlit 화면과 Docker 컨테이너

화면과 API 잇기

홀 직원은 요리를 하지 않고 주문을 주방에 넘깁니다. 주방이 바쁘거나 문을 닫았으면 손님에게 "지금은 주문을 받을 수 없습니다"라고 알립니다. 화면도 모델을 직접 들고 있지 않고 API 에 요청을 보내며, API 가 응답하지 않으면 그 사실을 사용자에게 알립니다.

화면이 입력을 받아 HTTP 요청을 보내고, API 가 모델로 예측하고, 화면이 결과를 그리거나 연결 실패를 알리는 네 단계 흐름도

1번에서 화면이 입력을 받습니다. 2번에서 그 입력을 JSON 으로 API 에 보냅니다. 3번에서 API 가 검사하고 예측합니다. 4번에서 화면이 결과를 그리고, API 가 꺼져 있거나 시간 안에 답하지 않으면 오류 알림을 그립니다. 여기서 홀 직원이 화면, 주방이 API 입니다.

모델을 화면에 넣을까, API 를 부를까

견줄 것화면이 모델을 직접 부른다(실습 1)화면이 API 를 부른다(실습 3)
만들기파일 하나로 끝난다서버 둘(화면, API)
다른 앱도 쓰나못 쓴다같은 API 를 쓴다
모델 바꾸기화면도 다시 띄운다API 만 바꾼다
화면 서버에 필요한 것모델과 scikit-learn, torch 등화면 라이브러리만
고장하나가 멈추면 모두 멈춘다화면은 떠 있고 "API 연결 실패"를 알린다

API 주소는 설정으로

API 의 주소는 실행하는 곳마다 다릅니다. 내 PC 에서는 http://127.0.0.1:8013, docker compose 안에서는 http://api:8013 입니다. 그래서 코드에 적지 않고 환경 변수로 받습니다.

Python
import os, urllib.request

API = os.environ.get("API_URL", "http://127.0.0.1:1")      # 설정이 없으면 기본값(여기서는 일부러 닫힌 1번 포트)
try:
    urllib.request.urlopen(API + "/health", timeout=2)
except OSError as e:                                         # 연결 거부, 시간 초과는 모두 OSError 로 잡힌다
    print("API 에 연결할 수 없습니다:", type(e).__name__)

코드에 나오는 함수

코드쉬운 뜻
os.environ.get("API_URL", 기본값)environment(환경) 변수 가운데 API_URL 을 읽는다. 없으면 기본값을 쓴다
urllib.request.urlopen(주소, timeout=2)URL(Uniform Resource Locator, 인터넷 주소)을 열어 요청을 보낸다. 2초 안에 답이 없으면 멈추고 오류를 낸다
"/health"API 가 살아 있는지 확인하는 주소다. health 는 "건강"이라는 뜻이다
except OSError as e:OS(Operating System, 운영체제)가 알리는 오류를 잡는다. 연결 거부와 시간 초과가 여기에 든다
type(e).__name__잡힌 오류의 종류 이름을 글자로 꺼낸다

이 코드가 뜻하는 것

홀 직원이 주방에 전화를 거는 일과 같습니다. 주방 번호는 가게마다 달라서 수첩(환경 변수)에서 찾고, 수첩에 없으면 기본 번호로 겁니다. 2초 동안 아무도 받지 않으면 끊고 손님에게 알립니다. 여기서 주방 번호가 API 주소, 수첩이 환경 변수, 2초가 timeout 입니다.

줄하는 일쉽게 말하면
API = os.environ.get(...)API 주소를 설정에서 읽는다. 여기서는 일부러 닫힌 1번 포트가 기본값이다수첩에서 주방 번호를 찾는다
urllib.request.urlopen(...)/health 로 요청을 보낸다주방에 전화를 건다
except OSError as e:연결이 안 되면 잡는다받지 않으면 끊는다
print("API 에 연결할 수 없습니다:", ...)오류 종류와 함께 알린다손님에게 "지금은 주문을 받을 수 없습니다"

결과의 URLError 는 주소로 연결하지 못했다는 오류입니다. 화면은 멈추지 않고 이 알림을 그리면 됩니다. 비유와 다른 점은, 전화는 신호가 가다 끊기지만 닫힌 포트는 기다리지 않고 바로 거절된다는 것입니다.

작게 줄여서 눈으로 보기

환경 변수가 있을 때와 없을 때 읽히는 값이 어떻게 달라지는지 봅니다. DEMO_API_URL 은 설명을 위해 지은 이름이고, 이 예는 지금 실행 중인 파이썬 안에서만 값을 바꿉니다.

Python
import os, urllib.error

print(os.environ.get("DEMO_API_URL", "http://127.0.0.1:8013"))   # 설정이 없으니 기본값
os.environ["DEMO_API_URL"] = "http://api:8013"                   # compose 가 넣어 준다고 치자
print(os.environ.get("DEMO_API_URL", "http://127.0.0.1:8013"))   # 이제 설정한 값
print(issubclass(urllib.error.URLError, OSError), issubclass(TimeoutError, OSError))
출력
http://127.0.0.1:8013
http://api:8013
True True
결과읽는 법
http://127.0.0.1:8013설정이 없어 기본값을 읽었다. 내 PC 에서 띄울 때다
http://api:8013설정을 넣은 뒤에는 그 값을 읽었다. compose 안에서 띄울 때다
True Trueissubclass 는 "이 오류가 저 오류의 한 갈래인가"를 묻는다. 연결 실패(URLError)와 시간 초과(TimeoutError)가 모두 OSError 의 갈래라서 except OSError 하나로 둘 다 잡힌다

코드는 그대로 두고 띄우는 곳의 설정만 바꾸면 주소가 바뀝니다. 그래서 같은 화면 코드를 내 PC 와 컨테이너에서 함께 씁니다.

주의할 점

  • 요청에는 늘 시간 제한(timeout)을 줍니다. 주지 않으면 API 가 멈췄을 때 화면도 한없이 기다립니다.
  • 화면 서버에 API 키가 필요하면 화면 코드에 적지 않고 환경 변수나 Streamlit 의 secrets.toml 에 둡니다. 브라우저로 보내는 화면에는 키를 넣지 않습니다(프로젝트).
← 이전 절14장 · 채팅 화면다음 절 →14장 · 컨테이너와 Docker