17장. Project 02. LLM 추론 API 서빙
서비스로 바꾸는 과정
집에서 끓인 찌개는 내 냄비에, 내 입맛대로, 내가 먹을 때 끓입니다. 식당 메뉴가 되면 다릅니다. 손님은 메뉴판을 보고 주문하고, 육수는 문 열 때 끓여 두고, 손님이 몰릴 때를 대비하고, 다른 지점에서도 같은 맛을 내야 합니다. 모델도 노트북에서 돌릴 때와 서비스로 낼 때 챙길 것이 다릅니다.
1번에서 노트북의 코드를 입력과 출력이 분명한 함수로 묶고, 모델 이름과 주소 같은 설정은 환경 변수로 뺍니다. 2번에서 받을 입력의 범위와 돌려줄 응답의 꼴을 정합니다. 3번에서 모델을 서버가 켜질 때 한 번만 올립니다. 4번에서 붐빌 때와 늦을 때의 답을 정해 둡니다. 5번에서 모델 서버까지 한 묶음으로 담아 어디서나 같은 모습으로 띄웁니다. 여기서 메뉴판이 응답 약속이고, 미리 끓여 둔 육수가 GPU 에 올려 둔 모델입니다.
내 코드와 서비스의 차이
| 견줄 것 | 내 코드 안(노트북) | 서비스(추론 API) |
|---|---|---|
| 부르는 사람 | 나 | 다른 팀, 다른 프로그램 |
| 모델을 올리는 때 | 셀을 실행할 때 | 서버가 켜질 때 한 번 |
| 입력 | 내가 넣는 대로 | 약속한 범위만 받고 나머지는 422 |
| 실패했을 때 | 오류 메시지를 보고 다시 실행 | 정해진 상태 코드(503, 504)로 답 |
| 여러 사람 | 생각하지 않음 | 동시 처리 수와 시간 제한 |
| 설정 | 코드에 적음 | 환경 변수 |
| 도는 곳 | 내 PC | 서버, 컨테이너 |
설정을 환경 변수로 빼기
import os
def load_settings():
"""노트북에서는 코드에 바로 적던 값을 환경 변수에서 읽는다. 없으면 오른쪽의 기본값을 쓴다"""
return {
"모델": os.environ.get("SUMMARY_MODEL", "qwen2.5:3b"),
"Ollama 주소": os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434"),
"동시 처리": int(os.environ.get("SUMMARY_MAX_BUSY", "2")), # 환경 변수는 늘 글자이므로 숫자로 바꾼다
"시간 제한(초)": float(os.environ.get("SUMMARY_TIMEOUT", "60")),
}
print(load_settings())
os.environ["OLLAMA_URL"] = "http://ollama:11434" # compose 안에서는 다른 컨테이너를 서비스 이름으로 부른다
print(load_settings()["Ollama 주소"])
del os.environ["OLLAMA_URL"]첫 줄에는 기본값 넷이, 둘째 줄에는 http://ollama:11434 가 나옵니다. 코드는 그대로 두고, 내 PC 에서는 기본값으로, 컨테이너에서는 compose 가 넣어 준 값으로 돕니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
os.environ | os 는 operating system(운영체제), environ 은 environment(환경)의 줄임말이다. 프로그램 밖에서 넣어 주는 환경 변수를 사전처럼 담고 있다 |
os.environ.get("SUMMARY_MODEL", "qwen2.5:3b") | 환경 변수 SUMMARY_MODEL 을 읽고, 없으면 오른쪽 기본값을 쓴다 |
int(...), float(...) | 글자로 온 값을 정수, 소수로 바꾼다 |
os.environ["OLLAMA_URL"] = ... | 환경 변수를 넣는다. compose 가 컨테이너에 값을 넣어 주는 일을 흉내 낸 것이다 |
del os.environ["OLLAMA_URL"] | 넣은 환경 변수를 지워 원래대로 돌린다 |
http://ollama:11434 | compose 안에서는 컨테이너끼리 서비스 이름(ollama)을 주소처럼 쓴다 |
이 코드가 뜻하는 것
프랜차이즈 식당의 요리법은 어느 지점이나 같지만, 지점마다 거래하는 정육점 주소와 화구 수는 다릅니다. 그래서 요리법에는 "정육점: 지점 안내문을 볼 것"이라고만 적고, 지점마다 안내문을 따로 붙입니다. 여기서 요리법이 코드, 지점의 안내문이 환경 변수입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
load_settings() | 설정 넷을 환경 변수에서 읽는다 | 지점 안내문을 읽는다 |
첫 print | 기본값 넷이 나온다 | 안내문이 없으면 본점 기본값대로 한다 |
os.environ["OLLAMA_URL"] = ... | 주소만 바꿔 넣는다 | 새 지점에 안내문을 붙였다 |
둘째 print | http://ollama:11434 가 나온다 | 코드는 그대로인데 거래처 주소가 바뀌었다 |
del os.environ[...] | 넣은 값을 지운다 | 안내문을 떼어 원래대로 둔다 |
첫 줄의 '동시 처리': 2 와 '시간 제한(초)': 60.0 은 글자 "2", "60" 을 숫자로 바꾼 결과입니다. 같은 코드가 놓인 곳에 따라 다른 값으로 돈다는 것이 이 코드의 뜻입니다.
작게 줄여서 눈으로 보기
환경 변수가 왜 늘 글자인지, 숫자로 바꾸지 않으면 어떻게 되는지 봅니다. MY_LIMIT 은 설명을 위해 지어낸, 넣지 않은 이름입니다.
import os
print(repr(os.environ.get("MY_LIMIT", "2"))) # 없으면 기본값 "2" (글자)
print("2" + "1", "/", int("2") + 1) # 글자끼리는 이어 붙고, 숫자로 바꾸면 더해진다
try:
int("two")
except ValueError as e:
print("잘못된 값:", e)'2'
21 / 3
잘못된 값: invalid literal for int() with base 10: 'two'| 결과 | 읽는 법 |
|---|---|
'2' | 따옴표가 붙어 있다. 기본값도 글자로 준 그대로 돌아온다 |
21 / 3 | 글자 "2" 와 "1" 을 더하면 이어 붙어 "21" 이 된다. 숫자로 바꿔야 3 이 된다 |
잘못된 값: invalid literal ... | 숫자로 바꿀 수 없는 글자면 ValueError 가 난다. 서버가 켜질 때 이렇게 바로 멈추면 잘못을 일찍 찾는다 |