3줄 요약
- Gemini 이미지 모델로 약 230장을 생성했더니 25,200원이 청구됐다(장당 약 110원).
- 원인은 모델이나 사용량이 아니라 결제 계정이 연결된 키를 썼다는 것이었다.
- 우리 내부 문서에는 그 키가 "무료"라고 적혀 있었고, 그 기록 자체가 틀린 것이 사고의 핵심이었다.
| 질문 | 답 |
|---|---|
| 얼마가 청구됐나 | 이미지 약 230장에 25,200원(장당 약 110원) |
| 원인은 무엇이었나 | 모델 문제가 아니라 결제 계정이 연결된 키를 사용한 것 |
| 왜 미리 몰랐나 | 우리 문서에 그 키가 "무료"라고 잘못 기록돼 있었다 |
| 키 회전은 왜 못 막았나 | 회전 임계가 키당 500장이라 230장은 첫 키 하나로 끝났다. 회전이 일어날 일이 없었다 |
| 재발 방지책은 무엇인가 | 무료 등급 키만 쓰고, 하드코딩을 제거하고, 미설정 시 즉시 중단하도록 바꿨다 |
이 글대로 하면, 이미지 배치를 돌리기 전에 명령 두 줄로 "지금 어떤 키가 실행될지"를 확인하고, 등급을 확인할 수 없는 키에서는 파이프라인이 스스로 멈추게 만들 수 있다.
이미지 약 230장을 만들었더니 왜 25,200원이 청구됐는가?
결제 계정이 연결된 키로 요청이 나갔기 때문이다. 블로그용 이미지를 대량으로 생성하는 과정에서 Gemini 이미지 모델을 사용해 약 230장을 만들었고, 이후 청구서에 25,200원이 부과돼 있었다. 장당 환산하면 약 110원이다. 처음에는 "이미지 생성 자체가 원래 이 정도 비용이 드는가"를 의심했지만, 원인은 모델의 가격 정책이 아니었다. 여러 개의 키를 회전시켜 쓰고 있었는데, 그중 실제로 요청이 몰린 키가 결제 계정이 연결된 키였다.
키 회전을 쓰고 있었는데 왜 못 막았는가?
회전 임계에 도달하지 못했기 때문이다. 이 모듈의 회전 규칙은 "키당 500장을 생성하면 다음 키로 자동 전환"이고, 사용량 카운터는 scripts/image-gen/.key_usage.json에 기록된다. 이번 배치는 약 230장이었다. 500장에 못 미치므로 회전은 애초에 일어나지 않았고 전량이 리스트 첫 키로 나갔다. 그 첫 키가 결제 계정에 연결된 키였다.
여기서 규칙이 하나 나온다. 키 회전은 한도 분산 장치이지 비용 안전장치가 아니다. 첫 키가 과금 키이면 회전이 정상 작동하더라도 첫 500장은 그대로 과금된다. 회전 리스트를 만들 때 확인할 것은 "리스트에 무료 키가 들어 있는가"가 아니라 "첫 자리에 무엇이 있는가" 다.
"무료"라고 적어둔 문서는 왜 틀렸는가?
한 번 확인한 사실을 영구히 참인 것으로 취급했기 때문이다. 오진 사슬은 이렇게 이어졌다.
| 단계 | 내용 | 왜 틀렸나 |
|---|---|---|
| 1. 관찰 | 청구서에 25,200원 | 사실 |
| 2. 1차 가설 | "이미지 생성 모델이 원래 유료인가" | 모델 가격이 아니라 어떤 키로 호출했는지가 변수였다 |
| 3. 근거로 삼은 것 | 모듈 문서의 "무료" 표기 | 그 표기 자체가 검증되지 않은 기록이었다 |
| 4. 실제 확인 | 콘솔에서 키별 결제 등급 조회 | 여러 개 중 하나만 무료 등급이었다 |
3번이 사고의 뿌리이고, 물증도 코드에 남아 있다. 이미지 생성 모듈 상단 주석의 모델 안내는 이렇게 적혀 있었다.
"""
[모델 선택]
- Model.FLASH : 무료 (500장/일), 빠름, 품질 중상
- Model.IMAGEN4_FAST : $0.02/장, 고품질 실사
- Model.IMAGEN4_STANDARD: $0.04/장, 더 정교함
"""
이 주석을 그대로 신뢰하고 파이프라인을 돌렸다. 실제로는 같은 모델이라도 결제 계정이 연결된 프로젝트의 키로 호출하면 과금된다. 무료인지 아닌지를 결정하는 것은 모델 이름이 아니라 키가 속한 프로젝트의 등급이었다. 코드가 잘못된 게 아니라, 코드가 참조한 사실 관계가 틀려 있었던 것이다.
재발을 막기 위해 무엇을 바꿨는가?
네 가지를 바꿨다.
| 대책 | 내용 |
|---|---|
| 엔진 교체 | 기본 이미지 생성 엔진을 별도 과금이 없는 방식(장당 약 3.5분 소요)으로 교체하고, 기존 모듈은 폴백으로 내렸다 |
| 키 하드코딩 제거 | 코드에 키 값을 박아 두지 않고 환경변수로만 주입하도록 바꿨다 |
| 미설정 시 즉시 중단 | 환경변수가 없으면 파이프라인이 실행을 시작하지 않도록 바꿨다 |
| 신뢰 대신 확인 | "무료"라고 적힌 문서를 그대로 믿지 않고, 실행 전에 콘솔에서 결제 등급을 직접 확인하는 절차를 넣었다 |
키 구성 코드는 아래 형태로 바뀌었다. 값은 코드에 없고 환경변수에서만 온다.
FREE_KEYS = [k for k in [
os.environ.get("GEMINI_API_KEY", "").strip(),
*[s.strip() for s in os.environ.get("GEMINI_API_KEYS", "").split(",")],
] if k]
빈 문자열이 걸러지므로 환경변수가 없으면 키 목록 자체가 비고 파이프라인은 시작하지 못한다. 이 몇 줄이 하는 일은 두 가지다. 첫째, 어떤 키가 실행되는지가 코드가 아니라 실행 환경의 환경변수 하나로 결정된다. 둘째, 환경변수를 빠뜨렸을 때 다른 키로 조용히 돌아가는 대신 실행이 선다. 실패를 조용한 과금이 아니라 시끄러운 중단으로 바꾼 것이다. 같은 파일에는 키를 코드에 박지 말라는 주석과, 과금 등급 키를 삭제하고 무료 등급 하나만 남겼다는 기록이 함께 붙어 있다.
네 가지 중 가장 근본적인 것은 마지막 "신뢰 대신 확인"이다. 앞의 세 가지는 이번 사고의 직접적인 재발을 막지만, 새로운 키나 새로운 서비스를 붙일 때 똑같은 실수 — 문서의 기록을 검증 없이 믿는 것 — 를 반복하지 않게 막아 주는 것은 이 원칙뿐이다. 문서를 실행하기 전에 받는 쪽을 먼저 확인한다는 같은 원칙을 조판 자동화에 적용한 사례는 md를 HTML로 바꿀 때 LLM에게 시키지 않은 이유에 있다.
배치를 돌리기 전에 무엇을 확인하는가?
번호대로 하면 "지금 어떤 키가 실행되는지"가 실행 전에 확정된다. 명령은 키 값을 화면에 찍지 않도록 짜여 있다.
- 코드에 키가 박혀 있는지 본다. 값을 출력하지 않고 파일 이름만 나열한다.
grep -rlE "(api_key|API_KEY)[[:space:]]*=[[:space:]]*[\"'][A-Za-z0-9_-]{20,}" scripts/
-
한 줄이라도 나오면 거기서 멈추고 그 파일부터 고친다. 하드코딩된 키가 남아 있으면 환경변수를 아무리 잘 설정해도 실행되는 키는 그쪽일 수 있다.
-
환경변수가 주입됐는지 확인한다. 값이 아니라 설정 여부와 길이만 본다.
python -c "import os;k=os.environ.get('GEMINI_API_KEY','');print('set' if k else 'MISSING', len(k))"
-
MISSING인데도 파이프라인이 돌아간다면 코드 어딘가에 폴백 키가 남아 있다는 뜻이므로 1번으로 돌아간다. -
콘솔에서 그 키가 속한 프로젝트의 결제 등급을 직접 조회한다. 문서나 주석의 "무료" 표기는 근거로 세지 않는다.
-
회전 리스트를 쓴다면 첫 자리 키의 등급을 확인한다. 배치 규모가 회전 임계보다 작으면 첫 키만 쓰인다.
-
등급이 확인된 뒤에만 배치를 시작한다. 확인되지 않으면 별도 과금이 없는 기본 엔진으로 돌린다.
판정은 아래 표로 한다.
| 관찰 | 판정 |
|---|---|
| 1번 검사가 파일을 뱉는다 | 실행하지 않는다. 코드에서 키를 제거하는 것이 먼저다 |
환경변수가 MISSING인데 파이프라인이 돈다 |
폴백 키가 코드에 있다. 실행하지 않는다 |
| 회전 리스트 첫 키의 등급을 모른다 | 배치 전량이 그 키로 나간다. 실행하지 않는다 |
| 콘솔에서 무료 등급으로 확인된 키만 목록에 있다 | 실행한다 |
확인할 수 없는 키는 왜 실행하지 않는 것이 맞는가?
기록은 시점이 지나면 근거가 되지 못하기 때문이다. 이 사고에서 얻은 가장 실무적인 규칙은 하나다. 등급을 확인할 수 없는 키는 실행하지 않는다. 결제 등급은 시간이 지나면서, 또는 여러 개의 키를 관리하는 과정에서 바뀔 수 있는 값이다. 키가 여러 개 섞여 있고 그중 무엇이 무료인지 콘솔에서 즉시 확인되지 않는다면, 확인될 때까지 실행을 미루는 편이 25,200원 같은 사고를 막는 데 더 싸게 먹힌다.
대기 비용은 크지 않다. 장당 약 110원이 붙는 경로에서는 100장만 돌려도 1만 원대가 되고, 확인에 드는 시간은 콘솔 조회 한 번이다. 이번 사고 역시 발행글 전체를 재감사하는 과정에서 함께 드러난 문제였고, 그 감사 전체 결과는 AI 두 기에게 발행글 12편을 감사시킨 결과에 정리돼 있다. "로그는 성공인데 결과는 다르다"는 같은 성격의 사고는 연결이 멎은 원인이 탭 하나였던 사례에서도 다뤘다.
이 대책이 통하지 않는 경우는 무엇인가?
이 절차는 "키 단위로 등급을 조회할 수 있는" 환경을 전제한다. 그 전제가 깨지는 경우가 있다.
| 실패 조건 | 그때 무엇을 하는가 |
|---|---|
| 콘솔에서 등급이 조회되지 않는 키다 | 실행하지 않는다. 무료 등급 프로젝트에서 만든 키로 바꾼다 |
| 무료 등급이지만 일일 한도를 넘긴다 | 초과분은 실패하거나 과금될 수 있다. 배치를 한도 아래로 쪼갠다 |
| 대체 엔진이 느려 일정을 못 맞춘다 | 장당 약 3.5분이므로 배치를 나눠 미리 돌리거나 이미지 수를 줄인다 |
| 대체 엔진의 사전 조건이 안 갖춰졌다 | 별도 로그인·설정이 필요하다. 조건이 안 되면 그날 배치를 미룬다 |
| 스케줄러가 다른 환경변수를 넣는다 | 실행 주체마다 따로 확인한다. 로컬에서 확인한 값이 그쪽에도 있다고 가정하지 않는다 |
한계도 적어 둔다. 여기 적은 금액과 장당 단가는 우리가 받은 청구서에서 나온 값이고, 요금 체계·무료 등급 조건·콘솔 화면은 서비스 쪽 사정으로 바뀔 수 있으므로 그대로 인용하기보다 자기 계정에서 다시 확인하는 편이 낫다. 대체 엔진이 별도 과금 없이 돌아가는 것도 그 서비스의 구독을 이미 쓰고 있다는 전제 위에서만 참이다. 환경에 상관없이 남는 것은 금액이 아니라 절차다. 실행 전에 어떤 키가 나가는지 확인하고, 확인되지 않으면 멈춘다.
자주 묻는 질문
Q. 이미지 생성 자체가 원래 유료인가?
A. 서비스와 모델에 따라 다르다. 우리 사고의 원인은 이미지 생성이 본질적으로 유료여서가 아니라, 무료 등급이 아닌 결제 연결 키를 실제로 사용했기 때문이었다. 같은 모델도 어떤 키로 호출하느냐에 따라 과금 여부가 갈린다.
Q. 키 회전을 쓰고 있었는데 왜 과금을 못 막았나?
A. 회전 임계가 키당 500장인데 이번 배치는 약 230장이었다. 임계에 못 미쳐 회전이 일어나지 않았고 전량이 리스트 첫 키로 나갔다. 회전은 한도 분산 장치이지 비용 안전장치가 아니다.
Q. "무료"라고 적힌 문서를 왜 처음부터 의심하지 않았나?
A. 그 문서를 작성할 때는 사실이었을 수도 있고, 키 상태가 바뀌었을 수도 있다. 정확한 시점은 확인하지 못했지만, 결과적으로 실행 시점에는 틀린 정보였다.
Q. 실행 전에 무엇을 확인하면 되나?
A. 코드에 키가 박혀 있지 않은지 파일 목록으로 확인하고, 환경변수가 설정됐는지 값 대신 길이로 확인하고, 콘솔에서 그 키의 결제 등급을 조회한다. 회전 리스트를 쓴다면 첫 자리 키의 등급을 본다.
Q. 키 하드코딩을 제거하면 무엇이 달라지나?
A. 코드 안에 값이 없으므로 실행 환경마다 어떤 키가 쓰이는지 환경변수 설정만 보면 파악할 수 있고, 환경변수를 빠뜨렸을 때 다른 키로 조용히 실행되는 대신 목록이 비어 파이프라인이 멈춘다.
Comments
Comments (0)
Leave a Comment