상용 API 로 시작한다 — 크레딧·키·상한
Article
앞 편에서 내 노트북에 모델을 얹었다. 이번 편은 선택이다 — 그게 너무 느리거나 답이 시원찮을 때 밖에 있는 모델을 빌려 쓰는 길이다.
1. 무엇에 값이 매겨지나 — 토큰
글자 수가 아니라 토큰 수로 센다.
- 영어
- 대략 한 단어에 하나
- cosmetic -> 1~2개
- 한글
- 대략 한 글자에 하나 남짓
- 화장품 -> 3개쯤
- 값
- 입력과 출력이 따로
- 출력이 몇 배 비싸다
키 하나로 여러 창구를 쓴다
여기서 한 가지를 짚고 간다. 채팅과 임베딩은 따로 사는 것이 아니다.
- 채팅
- 말을 만들어 준다
- 지금 쓰는 것
- 임베딩
- 문장을 숫자로 바꿔 준다
- 앞에서 내 컴퓨터로 하던 그것
- 잔액
- 둘이 같은 지갑을 쓴다
- 따로 결제할 것이 없다
앞에서 임베딩은 내 컴퓨터에 모델을 받아서 했다. 그것도 빌려 쓸 수 있고, 키는 방금 만든 것 그대로다. 창구 주소만 다르다.
| 무엇 | 창구 | 값 |
|---|---|---|
| 채팅 | /v1/chat/completions | 입력 · 출력 따로 |
| 임베딩 | /v1/embeddings | 입력만. 훨씬 싸다 |
| 이미지 · 음성 | 각자 다른 창구 | 이 시리즈에서는 안 쓴다 |
2. 크레딧을 산다
platform.openai.com 에서 결제 수단을 등록하고 미리 정해진 금액만큼 크레딧을 산다.
| 쓰는 방식 | 대략 | 비고 |
|---|---|---|
| 이것저것 물어보며 공부한다 | 한 달 $5 안쪽 | 생각보다 안 쓴다 |
| 채점기를 돌린다 (수백 번 호출) | 한 번에 $1~2 | 여기서 확 는다 |
| 서비스 하나를 끝까지 만든다 | $10 정도면 넉넉 | 다 만들 때까지 |
| 처음 사둘 금액 | $10 | 모자라면 그때 더 산다 |
자동 충전을 끈다
결제 화면에서 Use auto-reload 라는 스위치를 찾는다. 기본값이 켜져 있다.

켜두면
기본값이 이쪽이다
- 잔액이 줄면 카드에서 자동으로 채운다
- 반복문을 잘못 돌리면 밤새 빠져나간다
- 다음 날 아침에 알게 된다
꺼두면
이렇게 쓴다
- 잔액이 떨어지면 그냥 멈춘다
- 사고가 나도 사둔 금액이 끝이다
- 멈추면 그때 원인을 찾으면 된다
스위치를 끄면 아래 항목이 전부 사라진다. 이 상태로 저장한다.

끝나면 결제 화면이 이렇게 된다.

Auto-reload is OFF 옆의 안내가 우리가 원하던 문장이다 — 「잔액이 $0 이 되면 요청이 멈춥니다」. 켜두면 이 문장이 「멈추지 않습니다」가 된다.
사용량 한도(Usage limits)에서 월 상한도 같이 걸어두면 한 겹 더 안전하다.
3. 키를 받고 — 코드에 적지 않는다
키는 sk- 로 시작하는 긴 글자다. 발급 화면에서 만들면 그때 한 번만 보여준다. 따로 적어둔다.

만들 때 채울 것은 셋이다.

| 칸 | 무엇을 넣나 | 왜 |
|---|---|---|
| Name | 알아볼 이름 아무거나 | 나중에 「이게 뭐였지」를 막는다. 안 적어도 된다 |
| Project | Default project 를 고른다 | 안 고르면 만들기 버튼이 회색이다. 여기서 제일 많이 막힌다 |
| Permissions | All | 채팅과 임베딩을 다 쓴다. 실무에서는 Restricted 로 좁혀 발급한다 |
키는 코드 밖에 둔다.
OPENAI_API_KEY=sk-여기에실제키
.env
- 01
① 파일에 적고 읽기 — 지금은 이게 편하다
.env에 적고python-dotenv로 읽는다..gitignore에.env를 반드시 넣는다. 이걸 빼먹으면 파일로 뺀 의미가 없다. - 02
② 윈도우 시스템 설정에 넣기
「시스템 환경 변수 편집」에서
OPENAI_API_KEY를 만든다. 파일에도 안 남아 더 안전하다. 대신 터미널을 껐다 켜야 반영된다. - 03
③ 서비스로 배포할 때
배포하는 곳(Vercel · Render 등)의 환경변수 칸에 넣는다. 뒤에서 서버를 만들 때 이 방법을 쓴다. 코드에는 이름만 남고 값은 안 남는다.
4. 접속 설정을 파일 하나로
앞으로 만들 파일이 스무 개쯤 된다. 접속 설정을 그 스무 개에 흩뿌리면 안 된다.
"""누구에게 물어볼지를 여기서 한 번만 정한다.
다른 파일은 전부 이렇게 쓴다 ─
from config import client, MODEL
"""
# os 는 운영체제와 이야기하는 도구다. 여기서는 환경변수를 읽는 데만 쓴다
import os
from openai import OpenAI
# 어디로 보낼지. 회사가 정해둔 주소다
BASE_URL = "https://api.openai.com/v1"
# os.environ 은 컴퓨터에 등록된 환경변수 목록이다. 딕셔너리처럼 대괄호로 꺼낸다.
# 값이 없으면 여기서 바로 에러가 난다 — 몰래 빈 값으로 넘어가는 것보다 낫다.
# 이렇게 하면 코드에 키가 한 글자도 안 남는다
API_KEY = os.environ["OPENAI_API_KEY"]
# 부를 모델 이름
MODEL = "gpt-4o-mini"
# 이 한 줄로 "말을 걸 상대" 가 만들어진다. 다른 파일들은 이걸 가져다 쓴다
client = OpenAI(base_url=BASE_URL, api_key=API_KEY)
- BASE_URL
- 어디로 보낼지
- 회사 주소
- API_KEY
- 누가 보냈는지
- 내 키
- MODEL
- 누가 답할지
- gpt-4o-mini
5. 첫 호출
from config import client, MODEL # 접속 설정은 저기 한 군데에만 있다
res = client.chat.completions.create(
model=MODEL,
messages=[
{"role": "user", "content": "화장품 추천 서비스를 한 문장으로 소개해줘"},
],
)
print(res.choices[0].message.content)
# usage 에는 이번 호출에 쓴 토큰 수가 들어 있다. 이게 곧 청구서다
print("입력", res.usage.prompt_tokens, "출력", res.usage.completion_tokens)
- 고객의 피부 고민과 구매 이력을 분석해 맞춤 화장품을 추천해주는 서비스입니다.
- 입력 22 출력 27
답이 거의 바로 온다. 계산을 남의 컴퓨터가 하기 때문이다. 대신 인터넷이 끊기면 아무것도 안 된다.
6. 사고가 나는 세 가지 모양
| 무엇 | 어떻게 나나 | 막는 법 |
|---|---|---|
| 반복문 사고 | range(10000) 을 실수로 돌린다 | 개수를 먼저 찍어보고 돌린다 |
| 긴 근거 | 문서 전체를 프롬프트에 붙인다 | 필요한 조각만 넣는다 |
| 키 유출 | 깃허브에 그대로 올라간다 | .env + .gitignore |
# 반복문을 돌리기 전에 이 세 줄을 습관으로 붙인다.
# input 은 사람이 뭔가 칠 때까지 기다렸다가 친 글자를 돌려준다.
# raise SystemExit 는 프로그램을 그 자리에서 끝낸다
print("이번에 부를 횟수:", len(targets))
if input("계속할까? (y) ") != "y":
raise SystemExit
res = client.chat.completions.create(model=MODEL, messages=[{"role": "user", "content": "안녕"}])
# 한 줄에 두 개를 한꺼번에 담을 수 있다. 왼쪽 순서대로 짝지어 들어간다
n_in, n_out = res.usage.prompt_tokens, res.usage.completion_tokens
# 백만 토큰당 달러. 결제 화면에서 오늘 값을 확인해 넣는다
in_price, out_price = 0.15, 0.60
# 1_000_000 은 1000000 과 같다. 밑줄은 자릿수를 읽기 쉬우라고 넣는 것이고
# 파이썬이 무시한다. 백만 토큰당 값이니 백만으로 나눠 곱한다
per_call = n_in / 1_000_000 * in_price + n_out / 1_000_000 * out_price
print(f"한 번에 {per_call:.6f} 달러")
print(f"300번 돌리면 {per_call * 300:.3f} 달러")
7. 여러 명이 나눠 쓸 때 — 창구를 하나 세운다
혼자 쓸 것이면 여기는 건너뛴다. 여럿이 같이 배울 때 이야기다.
사람마다 결제하고 키를 발급받게 하면 가입 부담도 크고 비용도 흩어진다. 그래서 키를 든 서버를 하나 두고 모두 그리로 보낸다.
- 각자의 코드주소·번호 두 줄만 바뀐다
- 창구진짜 키는 여기에만
- 상용 API
BASE_URL = "http://192.168.0.15:8000/v1" # 안내받은 주소. 끝에 /v1 을 꼭 붙인다
API_KEY = "dcl-01" # 내게 배정된 번호. 진짜 키가 아니다
MODEL = "gpt-4o-mini"
| 장치 | 왜 필요한가 |
|---|---|
| 출력 길이 상한 | 긴 답을 요구해도 잘라낸다. 출력이 곧 돈이다 |
| 분당 호출 제한 | 한 명이 반복문을 돌려도 남의 실습이 안 멈춘다 |
| 모델 제한 | 비싼 모델을 실수로 부르는 것을 막는다 |
| 번호별 기록 | 누가 얼마나 썼는지 남는다 |
창구를 쓰면 만나는 번호들
| 번호 | 언제 나오나 | 처방 |
|---|---|---|
401 | 번호가 틀렸다 | dcl-1 이 아니라 dcl-01 이다. 두 자리로 적는다 |
400 | 허용 안 된 모델을 불렀다 | gpt-4o-mini — 하이픈이 두 개다 |
429 | 1분에 정해진 횟수를 넘었다 | 1분 기다린다. 반복문을 돌렸다면 왜 돌렸는지 본다 |
404 | 주소 끝에 /v1 이 빠졌다 | 제일 많이 나는 실수다 |
창구를 직접 띄우려면
- proxy
- main.py
- requirements.txt
- .env.example
python -m uvicorn main:app --host 0.0.0.0 --port 8000
--host 0.0.0.0 이 중요하다. 이게 없으면 그 컴퓨터 안에서만 접속된다. 남이 붙으려면 열어줘야 한다.
ipconfig 로 IPv4 주소 를 찾는다. 192.168. 이나 10. 으로 시작하는 값이고, 나눠줄 주소는 거기에 포트와 /v1 을 붙인 것이다.
- {
- "ok": true,
- "모델": ["gpt-4o-mini"],
- "출력상한": 800,
- "분당제한": 40
- }
이번 편에 나온 것
| 한 것 · 안 것 | 내용 |
|---|---|
| 토큰 | 글자가 아니라 조각 수. 출력이 더 비싸다 |
| 제일 큰 비용 | 긴 답이 아니라 긴 근거 |
| 자동 충전 | 반드시 끈다. 사둔 금액이 상한이 된다 |
| 처음 살 금액 | $10 정도 |
| 키 | 코드에 안 적는다. .env + .gitignore |
| 키가 샜다면 | 발급 화면에서 지운다. 그 순간부터 못 쓴다 |
config.py | 접속 설정을 한 군데로. 바꿀 곳이 한 파일이 된다 |
res.usage | 이번 호출에 쓴 토큰. 이게 청구서다 |
| 사고 셋 | 반복문 · 긴 근거 · 키 유출 |
| 여럿이 쓸 때 | 창구를 세운다. 401 번호 · 400 모델 · 429 분당 · 404 슬래시 v1 |
| 얻은 것 | 빠르고 품질이 좋다 |
| 잃은 것 | 부를 때마다 돈. 인터넷이 끊기면 멈춘다 |
미션
- 01
[필수] 한 번 부르고 토큰을 적는다
mission_api_1.py.01_hello.py를 돌려 입력·출력 토큰을 확인한다..gitignore에.env가 들었는지 먼저 확인한다. - 02
[응용] 얼마 쓸지 미리 계산한다
mission_api_2.py. 추천 한 건에 입력 400 · 출력 200 토큰이라 치고, 고객 30명을 하루 다섯 번 채점하면 나흘에 얼마인가. - 03
[도전] 상한을 코드로 건다
mission_api_3.py. 호출할 때마다 토큰을 파일에 쌓고 누적이 정한 값을 넘으면 멈추는 함수를 만든다. 창구가 하는 일을 작게 흉내내보는 것이다.
[도전] 스스로 멈추는 장치충분히 고민해본 뒤 꼭 필요한 경우에만 열어보세요
한 번 부를 때마다 기록하고, 부르기 전에 누적을 본다. 이게 창구가 하는 일의 뼈대다.
파일에 쌓는 이유는 프로그램을 껐다 켜도 남아야 하기 때문이다. 변수에만 담으면 다시 실행하는 순간 0이 된다 — 그리고 배우는 동안에는 다시 실행하는 일이 하루에 수십 번이다.
실무에서는 더 정교하게 한다. 사용자별로 나누고, 하루 단위로 초기화하고, 넘기 전에 경고를 보낸다. 그런데 뼈대는 지금 만든 것과 같다.
import json
import os
LOG_FILE = "usage.json"
LIMIT = 1.0 # 달러
def spent():
if not os.path.exists(LOG_FILE):
return 0.0
return json.load(open(LOG_FILE))["달러"]
def add(n_in, n_out):
value = spent() + n_in / 1_000_000 * 0.15 + n_out / 1_000_000 * 0.60
json.dump({"달러": value}, open(LOG_FILE, "w"))
return value
def safe_call(messages):
if spent() >= LIMIT:
raise SystemExit(f"한도 {LIMIT} 달러를 넘었다. 지금까지 {spent():.4f}")
res = client.chat.completions.create(model=MODEL, messages=messages)
total = add(res.usage.prompt_tokens, res.usage.completion_tokens)
print(f"누적 {total:.4f} 달러")
return res부르기 전에 확인하는 것이 핵심이다. 부른 뒤에 세면 이미 나간 돈이다.
그런데 이 장치에는 구멍이 있다 — 한 번 호출이 한도를 훌쩍 넘길 수도 있다. 긴 근거를 넣으면 그렇게 된다. 그래서 진짜 서비스는 여기에 출력 상한을 같이 건다.
뒤에서 「계산한 값과 실제로 나간 값이 왜 다른가」를 본다. 이 파일이 그때 재료가 된다.
정리하면
결제하고, 키를 받고, 첫 호출까지 왔다. 여기까지 왔으면 이후 편들은 그대로 따라온다.
제일 중요한 것은 자동 충전을 껐다는 것이다. 사둔 금액이 그대로 상한이면 사고가 나도 잃을 것이 정해져 있다. 그리고 키는 코드에 안 적었다. 공개 저장소에 올라간 키는 사람이 아니라 프로그램이 찾아간다 — .env 와 .gitignore 두 줄이면 막힌다.
config.py 를 따로 만든 것도 오늘의 소득이다. 주소·키·모델 세 값이 한 파일에만 있으니, 나중에 상대를 바꿔도 고칠 데가 한 군데다.
마지막으로 비용을 아끼는 순서를 봤다. 답을 짧게 시키는 것보다 안 보내도 되는 것을 빼는 쪽이 훨씬 크게 듣는다. 이건 8·9편의 개인정보 이야기와 정확히 같은 결론이 된다.
다음 편에서 개인정보를 다룬다. 추천을 만들려면 후기를 모델에게 줘야 하는데, 1,500건 중 152건에 전화번호가 들어 있다.
이 편으로 갈아탄 사람에게는 그게 남 이야기가 아니다. 앞 편의 내 노트북 방식은 글자가 컴퓨터 밖으로 안 나가지만, 여기서는 방금 그 글자들이 바깥 회사 서버까지 간다. 무엇을 빼고 보내야 하는지가 다음 편이다.