Gemini 2.5 Pro Code Execution, 백엔드에서 타입 안전하게 다루는 법
솔직히 처음 Code Execution 응답을 받았을 때 당황했습니다. response.text로 꺼내려고 했더니 None이 떠서 한참 헤맸거든요. 알고 보니 코드 실행 응답은 candidates[0].content.parts 안에 여러 타입의 Part 객체로 쪼개져서 들어옵니다. 이걸 모르고 일반 텍스트 응답처럼 다루면 파이프라인 전체가 조용히 깨집니다.
Gemini Code Execution은 모델이 Python 코드를 Google의 격리된 샌드박스에서 실행하고, 그 결과를 구조화된 Part 객체로 돌려주는 도구입니다. 일반 Function Calling처럼 개발자가 외부에서 함수를 실행하는 게 아니라, API 백엔드가 코드 실행을 전적으로 처리합니다. 서버 입장에서는 "코드 실행 결과가 보장된 응답"을 받을 수 있다는 게 핵심입니다.
이 글에서는 백엔드 서비스에 Code Execution을 연결할 때 응답 파트를 어떻게 타입으로 다루는지, outcome 분기 처리를 어떻게 잡는지, 그리고 프로덕션에서 챙겨야 할 것들을 짚어봅니다. (본문 코드는 2026년 9월 기준 google-genai SDK 최신 버전을 전제로 하며, match 구문을 사용하는 예시는 Python 3.10 이상이 필요합니다.)
Code Execution이 다른 이유 — 응답 구조부터 이해해야 합니다
일반 generate_content 호출은 response.text로 깔끔하게 끝나지만, Code Execution을 활성화하면 응답이 여러 타입의 Part로 분리됩니다.
| Part 타입 | 필드 | 내용 |
|---|---|---|
Part.text |
text (str) |
모델의 설명·해석 텍스트 |
Part.executable_code |
language, code |
모델이 생성해 실행한 Python 코드 |
Part.code_execution_result |
outcome, output |
실행 결과 상태 코드와 표준 출력 |
여기서 outcome은 문자열이 아니라 열거형입니다. google.genai.types.Outcome 타입으로 OUTCOME_UNSPECIFIED, OUTCOME_OK, OUTCOME_FAILED, OUTCOME_DEADLINE_EXCEEDED 값을 가집니다. 이 중 OUTCOME_UNSPECIFIED는 SDK/서버 간 프로토콜 미스매치나 응답이 완결되지 않은 상태에서 관찰되는 방어적 기본값이므로, 분기에서 명시적으로 알 수 없는 상태로 처리해두는 편이 안전합니다.
한 번의 generate_content 요청 안에서 모델은 코드 생성→실행→결과 확인→수정을 여러 차례 반복할 수 있습니다(공식 문서에는 정확한 상한 횟수가 명시되어 있지 않고, 각 실행의 시간 상한은 Gemini API 문서에 기재된 대로 제한이 존재합니다). 그래서 응답 파트가 하나가 아니라 [text, executable_code, code_execution_result, text, executable_code, code_execution_result, ...] 처럼 여러 개가 이어질 수 있습니다. 처음 파싱 코드를 짤 때 이걸 생각하지 않으면 첫 번째 결과만 잡고 끝나버립니다.
타임아웃이 발생한 뒤 모델이 접근 방식을 바꿔 재시도하는지 여부는 공식 문서에서 명확히 정의된 동작이 아니라 관측에 의존하는 부분이 있습니다. 서버 코드에서는 "타임아웃 이후에도 파트가 이어질 수 있다"고 열어두고, 마지막 code_execution_result의 outcome을 최종 상태로 삼는 방식이 무난합니다.
기본 호출 — ToolCodeExecution을 config에 넣는 것부터
from google import genai
from google.genai.types import Tool, ToolCodeExecution, GenerateContentConfig
client = genai.Client()
response = client.models.generate_content(
model="gemini-2.5-pro",
contents="판다스로 숫자 리스트의 평균과 표준편차를 계산하는 코드를 실행해줘.",
config=GenerateContentConfig(
tools=[Tool(code_execution=ToolCodeExecution())]
),
)google-genai 패키지(pip install google-genai)의 공식 타입들을 사용합니다. ToolCodeExecution()은 빈 생성자로 도구를 활성화하는 역할만 합니다.
응답 파트 순회 — outcome으로 분기하는 패턴
응답을 받은 뒤 parts를 순회하며 각 타입별로 처리하는 게 기본 패턴입니다. 여러 파싱 스타일이 가능하지만, 이 글에서는 모든 파트를 순서대로 수집하고 마지막 code_execution_result의 상태를 최종 결과로 삼는 한 가지 패턴으로 통일합니다. 중간 단계 정보(재시도 흐름, 실패 로그)도 함께 보관되어 관측성이 좋기 때문입니다.
from google.genai.types import Outcome
def parse_code_execution_response(response):
result = {
"texts": [],
"codes": [],
"outputs": [],
"final_outcome": None,
}
for part in response.candidates[0].content.parts:
if part.text is not None:
result["texts"].append(part.text)
elif part.executable_code is not None:
result["codes"].append({
"language": part.executable_code.language,
"code": part.executable_code.code,
})
elif part.code_execution_result is not None:
outcome = part.code_execution_result.outcome
output = part.code_execution_result.output
result["outputs"].append({
"outcome": outcome,
"output": output,
})
result["final_outcome"] = outcome
return result
parsed = parse_code_execution_response(response)
# match 구문은 Python 3.10 이상에서 사용 가능합니다.
match parsed["final_outcome"]:
case Outcome.OUTCOME_OK:
print("성공:", parsed["outputs"][-1]["output"])
case Outcome.OUTCOME_FAILED:
print("실행 실패. 마지막 출력:", parsed["outputs"][-1]["output"])
case Outcome.OUTCOME_DEADLINE_EXCEEDED:
print("실행 시간 초과. 연산량을 줄이거나 입력 데이터를 분할하는 것을 고려해보세요.")
case Outcome.OUTCOME_UNSPECIFIED | None:
print("상태가 완결되지 않았거나 알 수 없는 응답입니다.")
case _:
print("알 수 없는 상태:", parsed["final_outcome"])if part.text: 같은 truthiness 체크가 아니라 is not None을 쓰는 이유는, 빈 문자열이나 빈 코드 파트도 파이프라인 관측 관점에서 놓치지 않기 위함입니다. 타입 안전성을 지향하는 코드에서 truthiness에 의존하면 나중에 은근한 버그가 됩니다.
데이터 분석 API 붙이는 예시
실제로 유용한 케이스는 "사용자가 CSV 데이터를 붙여넣으면 자동으로 집계해주는 API"입니다. Code Execution이 이런 용도에 잘 맞는 이유는, 모델이 생성한 텍스트를 믿는 게 아니라 실제 실행된 결과만 클라이언트에 돌려주기 때문입니다.
앞 절과 동일한 파싱 패턴을 따르되, analyze_csv_data는 그 위에 얹은 얇은 래퍼입니다.
import json
from google import genai
from google.genai.types import (
Tool, ToolCodeExecution, GenerateContentConfig, Outcome,
)
client = genai.Client()
def _strip_markdown_fence(text: str) -> str:
# 모델이 JSON 앞뒤에 ```json ... ``` 를 붙이는 경우가 잦습니다.
# 이 로직은 단일 코드 펜스만 안전하게 처리합니다.
# 중첩 펜스나 본문 내 백틱 3연발이 있는 경우엔 오작동할 수 있습니다.
stripped = text.strip()
if stripped.startswith("```"):
stripped = stripped.split("\n", 1)[1] if "\n" in stripped else stripped
stripped = stripped.rsplit("```", 1)[0]
return stripped.strip()
def analyze_csv_data(csv_content: str, user_question: str) -> dict:
prompt = f"""다음 CSV 데이터를 분석해서 질문에 답해줘.
결과는 반드시 JSON 형태로 출력해.
CSV 데이터:
{csv_content}
질문: {user_question}"""
response = client.models.generate_content(
model="gemini-2.5-pro",
contents=prompt,
config=GenerateContentConfig(
tools=[Tool(code_execution=ToolCodeExecution())]
),
)
parsed = parse_code_execution_response(response)
if not parsed["outputs"]:
return {"status": "error", "outcome": "no_result", "output": ""}
last = parsed["outputs"][-1]
if last["outcome"] != Outcome.OUTCOME_OK:
return {
"status": "error",
"outcome": str(last["outcome"]),
"output": last["output"],
}
raw_output = _strip_markdown_fence(last["output"])
try:
data = json.loads(raw_output)
except json.JSONDecodeError as e:
# 모델이 유효한 JSON을 생성하지 않은 경우. 호출자가 재시도 또는 폴백을 결정합니다.
return {
"status": "error",
"outcome": "invalid_json",
"output": raw_output,
"error": str(e),
}
return {"status": "ok", "data": data}코드 펜스 스트리핑은 근본적으로 취약합니다. 모델 출력이 본문 안에 백틱 3연발을 포함하거나 중첩 펜스를 사용하면 위 로직은 잘못 자릅니다. 안정적인 대안은 프롬프트에서 "펜스 없이 순수 JSON만 출력"을 강하게 요구하거나, 응답 스키마(Structured Output)를 병행 설정하는 것입니다. Code Execution과 응답 스키마 조합의 호환성은 사용하는 SDK 버전에 따라 다르니 실환경에서 검증이 필요합니다.
프로덕션 파이프라인 — HTTP 429 대응과 재시도
트래픽 스파이크가 오면 429를 만날 수 있어서 지수 백오프 재시도 로직이 필요합니다. Gemini API 공식 문서의 rate limits 페이지에서 현재 계정·모델에 적용되는 정확한 한도를 확인하는 게 먼저입니다.
동기 백엔드용 예시입니다.
import time
import google.api_core.exceptions
def call_with_backoff(client, model, contents, config, max_retries=4):
delay = 1.0
for attempt in range(max_retries):
try:
return client.models.generate_content(
model=model,
contents=contents,
config=config,
)
except google.api_core.exceptions.ResourceExhausted:
if attempt == max_retries - 1:
raise
time.sleep(delay)
delay *= 2google.api_core.exceptions.ResourceExhausted가 HTTP 429에 매핑되는 예외 클래스입니다. FastAPI, aiohttp, asyncio 기반의 비동기 백엔드에서 위 함수를 그대로 쓰면 time.sleep이 이벤트 루프를 블로킹합니다. async 환경에서는 asyncio.sleep으로 대체하고 SDK의 async 클라이언트를 사용해야 합니다.
import asyncio
async def call_with_backoff_async(client, model, contents, config, max_retries=4):
delay = 1.0
for attempt in range(max_retries):
try:
return await client.aio.models.generate_content(
model=model,
contents=contents,
config=config,
)
except google.api_core.exceptions.ResourceExhausted:
if attempt == max_retries - 1:
raise
await asyncio.sleep(delay)
delay *= 2트레이드오프 — 도입 전에 팀에서 확인할 것들
| 항목 | 장점 | 단점·주의사항 |
|---|---|---|
| 실행 환경 | Google 관리 샌드박스, 인프라 영향 없음 | 상태 비보존 — 호출 간 변수·파일 유지 안 됨 |
| 언어 지원 | numpy, pandas, matplotlib 등 사전 설치 | Python 전용 |
| 자동 수정 | 실행 실패 시 모델이 코드를 수정해 재실행 | 반복 실행 시 이전 코드·출력이 컨텍스트에 누적되어 입력 토큰이 증가합니다. 재시도 횟수가 늘어날수록 요청당 청구 토큰이 커집니다 |
| 네트워크 | 격리 환경으로 보안상 안전 | 샌드박스 내 외부 API 호출·pip install 기본 차단 |
| 실행 시간 | 단일 실행 시간 상한 존재 | 대용량 데이터나 복잡한 연산에서 OUTCOME_DEADLINE_EXCEEDED 발생 가능 |
| 응답 타입 | outcome 열거형으로 분기 명확 |
폴백 처리, 타임아웃 핸들링은 개발자 몫 |
가장 흔한 실수는 두 가지입니다.
첫째, response.text로 응답을 꺼내려다 None을 만나는 것. Code Execution이 활성화된 응답에서는 parts를 직접 순회해야 합니다.
둘째, OUTCOME_OK가 아닌 경우를 무시하는 것. 특히 실행 시간 초과(OUTCOME_DEADLINE_EXCEEDED)는 데이터 크기가 커지면 갑자기 나타납니다. 이때 아무 처리 없이 output을 그대로 반환하면 클라이언트에 빈 결과나 혼란스러운 메시지가 전달됩니다.
이미지 파트 처리 — inline_data는 이미 bytes입니다
모델이 Matplotlib으로 차트를 그리면 PNG 바이너리가 Part.inline_data로 들어옵니다. 보고서 자동 생성 파이프라인이라면 순회 로직에 이 부분도 추가할 수 있습니다.
google-genai SDK에서 Part.inline_data.data는 이미 bytes로 반환되므로 base64 디코딩을 다시 수행하면 안 됩니다. 그대로 파일에 쓰면 됩니다.
def save_inline_images(response, prefix="chart"):
saved = []
for idx, part in enumerate(response.candidates[0].content.parts):
if part.inline_data is not None and part.inline_data.data:
mime = part.inline_data.mime_type or "application/octet-stream"
ext = "png" if mime.endswith("png") else "bin"
path = f"{prefix}_{idx}.{ext}"
with open(path, "wb") as f:
f.write(part.inline_data.data)
saved.append((path, mime))
return saved확장을 검토할 때의 옵션
호출 한 번짜리 파이프라인을 넘어서 멀티 스텝 에이전트를 만들 계획이라면 Google Agent Development Kit(ADK)의 code execution 도구를 살펴볼 만합니다. 에이전트 선언에 도구로 연결하는 방식이라 멀티 에이전트 파이프라인 통합이 단순해집니다(pip install google-adk).
상태 유지, VPC Service Controls 연동, 대용량 파일 처리 같은 엔터프라이즈 요건이 있다면 Vertex AI 쪽 코드 실행 옵션을 검토해볼 수 있습니다. 세부 상한(상태 유지 기간, 파일 크기 한도 등)은 문서와 조직의 계약 조건에 따라 달라지므로, 도입 전에 최신 공식 문서에서 정확한 수치를 확인하는 게 안전합니다.
언제 쓰고 언제 쓰지 말아야 하는가
Code Execution은 다음 조건에서 값을 냅니다.
- 결과의 정확도가 텍스트 생성의 매끄러움보다 중요할 때(수치 계산, CSV·JSON 집계, 통계량)
- 샌드박스 운영·격리·보안 관리를 직접 하고 싶지 않을 때
- 응답이 사전 설치된 라이브러리(numpy, pandas, matplotlib 등)로 처리 가능한 범위일 때
반면 다음 경우엔 다른 접근이 낫습니다.
- 호출 간 상태(변수, 파일)를 유지해야 할 때 → 자체 샌드박스 또는 Vertex AI 쪽 옵션
- 외부 API 호출이나 임의 패키지 설치가 필요할 때 → 자체 실행 환경 또는 Function Calling으로 외부 코드 위임
- Python 이외 언어 실행이 필요할 때
- 실행 시간 상한을 넘길 만한 대용량 연산 → 데이터를 분할하거나 배치 처리 경로 분리
서버 관점의 통합 체크리스트는 짧습니다. parts를 순회해 세 가지 타입을 분리하고, outcome 열거형으로 분기하고, 429·타임아웃·JSON 파싱 실패를 각각 별개의 실패 모드로 다루는 것. 이 세 가지만 고정해두면 나머지는 애플리케이션 특성에 맞춰 얹을 수 있습니다.
참고 자료