23. Session/Memory/Artifact [12]- ADK ArtifactService(2) : 에이전트가 생성한 파일(Artifact)을 Google Cloud Storage(GCS)에 영구적으로 저장하는 구
이번 실습은 지금까지 진행해 온 ADK의 세션(Session)과 메모리(Memory) 실습에서 한 단계 더 나아가는 중요한 과정입니다.
이번에는 에이전트가 생성한 파일(Artifact)을 Google Cloud Storage(GCS)에 영구적으로 저장하는 구조를 이해하는 것이 핵심입니다.
특히 이번 실습을 통해 다음과 같은 개념을 확실히 구분할 수 있어야 합니다.
-
Agent: 사용자의 요청을 이해하고 작업을 수행하는 주체
-
Tool: 실제 날씨 보고서를 생성하는 함수
-
Artifact: 에이전트가 생성한 파일이나 결과물
-
ArtifactService: Artifact를 저장하고 조회하는 서비스
-
GcsArtifactService: Artifact를 Google Cloud Storage에 저장하는 서비스
-
ToolContext: 현재 실행 중인 에이전트의 사용자, 세션 등의 정보를 전달하고 Artifact 저장 기능을 제공하는 실행 컨텍스트
이번 실습의 최종 목표는 다음과 같습니다.
전체 실행 구조

여기서 가장 중요한 점은 generate_weather_report() 함수가 직접 GCS에 파일을 업로드하지 않는다는 것입니다.
이 함수는 tool_context.save_artifact()를 호출하고, ADK에 연결된 ArtifactService가 실제 저장 작업을 담당합니다.
이 구조를 이해하면 나중에 PDF, HWP, DOCX, Excel, AI 분석 결과 등 다양한 생성물을 동일한 방식으로 관리할 수 있습니다.
1. 코드 전체 구조부터 이해하기
코드는 크게 세 부분으로 나뉩니다.
| 1 | generate_weather_report() | 날씨 보고서 생성 및 저장 |
| 2 | weather_agent = Agent() | Gemini 기반 에이전트 정의 |
| 3 | root_agent = weather_agent | ADK가 에이전트를 찾는 진입점 |
실제 실행 순서는 다음과 같습니다.
-
사용자가 ADK Web 화면에서 서울 날씨 보고서를 요청합니다.
-
Gemini 모델이 요청을 분석하고 generate_weather_report를 호출합니다.
-
Python 함수가 임의의 기온, 날씨, 습도를 생성합니다.
-
보고서 문자열을 작성합니다.
-
문자열을 UTF-8 바이트로 변환합니다.
-
바이트 데이터를 types.Part라는 Artifact 객체로 만듭니다.
-
tool_context.save_artifact()를 호출합니다.
-
ADK가 연결된 GCS ArtifactService를 통해 버킷에 저장합니다.
-
저장 성공 여부와 버전 번호를 Tool이 반환합니다.
-
Gemini가 결과를 사용자에게 한글로 안내합니다.
이제 코드 한 줄씩 살펴보겠습니다.
2. 라이브러리 및 환경 설정
import datetime
import os
import random
import re
from zoneinfo import ZoneInfo
from google.genai import types
from google.adk import Agent
from google.adk.tools import ToolContext
from google.adk.artifacts import GcsArtifactService
GCS_BUCKET_NAME = os.getenv("GCS_BUCKET_NAME", "")
각 라이브러리의 역할입니다.
| 라이브러리 | 역할 | 실제 사용 부분 |
| datetime | 날짜와 시간 처리 | 보고서 생성 시간 |
| os | 운영체제 환경변수 접근 | GCS_BUCKET_NAME, GOOGLE_MODEL |
| random | 임의 데이터 생성 | 기온, 습도, 날씨 |
| re | 정규표현식 처리 | 파일명에 사용할 도시 이름 정리 |
| ZoneInfo | 시간대 설정 | 한국 시간 적용 |
| types | Gemini 데이터 객체 생성 | Artifact 객체 |
| Agent | ADK 에이전트 생성 | WeatherBot |
| ToolContext | Tool 실행 컨텍스트 | Artifact 저장 |
| GcsArtifactService | GCS 저장 서비스 | 서비스 클래스 참조 |
여기서 조금 주의해서 볼 부분이 있습니다.
① GcsArtifactService를 import했는데 직접 생성하지 않는 이유
from google.adk.artifacts import GcsArtifactService
일반적으로 GCS에 저장하려면 다음과 같이 서비스를 직접 생성할 수 있습니다.
artifact_service = GcsArtifactService(
bucket_name="agent-project_512212-staging"
)
그리고 직접 Runner를 구성할 때 다음과 같이 전달합니다.
runner = Runner(
agent=weather_agent,
app_name="weather_app",
session_service=session_service,
artifact_service=artifact_service
)
하지만 이번에는 이 작업을 직접 하지 않습니다.
왜냐하면 adk web 명령어에서 다음 옵션을 전달하기 때문입니다.
--artifact_service_uri="gs://agent-project_512212-staging"
ADK CLI가 이 URI를 해석하여 GCS ArtifactService를 구성하고 실행 환경에 연결합니다.
따라서 이번 코드에서는 GcsArtifactService를 직접 생성할 필요가 없습니다.
참고로 현재 코드에서 GcsArtifactService import는 실제로 사용되지 않는 import입니다. 삭제해도 실행에는 지장이 없습니다.
② GCS_BUCKET_NAME 환경변수의 의미
GCS_BUCKET_NAME = os.getenv("GCS_BUCKET_NAME", "")
이 코드는 운영체제에 설정된 환경변수 GCS_BUCKET_NAME을 읽습니다.
예를 들어 PowerShell에서 다음과 같이 설정할 수 있습니다.
$env:GCS_BUCKET_NAME = "agent-project_512212-staging"
그러면 Python 코드에서 다음 값을 얻게 됩니다.
GCS_BUCKET_NAME
결과:
agent-project_512212-staging
그런데 중요한 점이 있습니다.
이 환경변수는 실제 저장 위치를 지정하는 역할을 하지 않습니다.
현재 코드에서는 단순히 로그를 출력하기 위해 사용합니다.
실제 저장 위치는 다음 ADK 실행 옵션이 결정합니다.
--artifact_service_uri="gs://agent-project_512212-staging"
즉, 두 설정은 역할이 다릅니다.
-
GCS_BUCKET_NAME: 사람이 로그를 확인하기 위한 변수
-
--artifact_service_uri: ADK가 실제 저장 서비스를 결정하는 설정
이 차이를 반드시 기억하세요.

3. 날씨 보고서 생성 Tool
이제 이번 실습의 핵심 함수입니다.
async def generate_weather_report(city: str, tool_context: ToolContext):
이 함수는 크게 두 가지 일을 합니다.
-
날씨 보고서를 생성합니다.
-
생성한 보고서를 ArtifactService에 저장합니다.
3-1. 함수 선언과 매개변수
async def generate_weather_report(
city: str,
tool_context: ToolContext
):
먼저 async가 붙어 있습니다.
이는 비동기 함수라는 의미입니다.
왜 비동기를 사용했을까요?
보고서 생성 자체는 빠르지만, GCS에 파일을 저장하는 작업은 네트워크를 통해 외부 클라우드 서비스와 통신해야 합니다.
따라서 다음과 같은 비동기 작업을 수행할 수 있도록 설계한 것입니다.
version_id = await tool_context.save_artifact(...)
await는 비동기 작업이 완료될 때까지 기다린다는 의미입니다.
여기서 두 매개변수를 살펴보겠습니다.
city
city: str
사용자가 요청한 도시 이름입니다.
예를 들어 다음과 같습니다.
서울
대전
부산
tool_context
tool_context: ToolContext
이 부분이 중요합니다.
ToolContext는 ADK가 Tool을 실행할 때 전달하는 실행 컨텍스트입니다.
단순히 도시 이름만 전달하는 것이 아니라, 현재 어떤 사용자가 어떤 앱과 세션에서 작업하고 있는지도 알 수 있습니다.
대표적으로 다음과 같은 정보가 있습니다.(속성과 의미)
| app_name | 에이전트 애플리케이션 이름 |
| user_id | 사용자 식별자 |
| session_id | 현재 대화 세션 식별자 |
그리고 이번 실습에서 가장 중요한 기능이 있습니다.
tool_context.save_artifact()
이 메서드를 통해 파일을 저장할 수 있습니다.
즉, ToolContext는 단순한 정보 전달 객체가 아니라 ADK의 실행 환경과 ArtifactService를 연결해 주는 중요한 통로입니다.
ADK 공식 문서에서도 ToolContext를 이용하여 Artifact를 저장하고 불러오는 방식을 기본 인터페이스로 설명합니다.
3-2. ToolContext 확인
if tool_context is None:
raise ValueError("tool_context가 필요합니다.")
tool_context가 전달되지 않았다면 예외를 발생시킵니다.
왜 필요할까요?
이 함수는 GCS에 직접 접근하는 것이 아니라 다음 메서드를 사용하기 때문입니다.
tool_context.save_artifact()
만약 tool_context가 없다면 저장 기능을 사용할 수 없습니다.
참고로 정상적인 ADK Tool 실행에서는 ADK가 컨텍스트를 주입하므로 일반적으로 이 조건은 발생하지 않습니다.
다음 코드는 실행 로그를 출력합니다.
print(f"\n[*] Tool 실행: {city} 날씨 보고서 생성")
예상 출력:
[*] Tool 실행: 서울 날씨 보고서 생성
3-3. Mock 날씨 데이터 생성
temp = random.randint(10, 30)
conditions = [
"맑음",
"구름 조금",
"비",
"바람 강함"
]
condition = random.choice(conditions)
humidity = random.randint(40, 90)
여기서는 실제 기상청 API나 외부 날씨 서비스를 호출하지 않습니다.
실습을 위해 임의의 데이터를 생성합니다.
기온
temp = random.randint(10, 30)
10부터 30까지의 정수 중 하나를 무작위로 선택합니다.
날씨 상태
condition = random.choice(conditions)
네 가지 날씨 상태 중 하나를 무작위로 선택합니다.
습도
humidity = random.randint(40, 90)
40~90 사이의 정수를 생성합니다.
예를 들어 다음과 같은 결과가 만들어질 수 있습니다.
기온 : 23°C
날씨 : 맑음
습도 : 65%
주의할 점은 같은 도시를 두 번 요청해도 다른 날씨 데이터가 만들어질 수 있다는 것입니다.
실습용 데이터이므로 실제 기상정보와는 관계가 없습니다.
3-4. 한국 시간 생성
now = datetime.datetime.now(
ZoneInfo("Asia/Seoul")
)
현재 시간을 한국 표준시로 가져옵니다.
여기서 Asia/Seoul은 시간대 이름입니다.
컴퓨터의 시스템 시간대가 미국이나 다른 지역으로 설정되어 있어도 한국 시간을 기준으로 생성할 수 있습니다.
다음 두 변수는 출력 형식을 다르게 합니다.
today = now.strftime("%Y-%m-%d %H:%M")
date_string = now.strftime("%Y-%m-%d")
예를 들어 현재 시간이 2026년 10월 31일 오후 9시 10분이라면 다음과 같습니다.
| 변수 | 결과 |
| today | 2026-10-31 21:10 |
| date_string | 2026-10-31 |
왜 두 가지로 나누었을까요?
-
today: 보고서에 생성 시각을 기록하기 위해
-
date_string: 파일명을 만들기 위해
3-5. 파일명 생성
safe_city = re.sub(
r"[^a-zA-Z0-9가-힣_-]",
"_",
city
)
filename = f"report_{safe_city}_{date_string}.txt"
이 부분은 실제 파일 저장 시 상당히 중요합니다.
먼저 다음 코드를 보겠습니다.
re.sub(
r"[^a-zA-Z0-9가-힣_-]",
"_",
city
)
정규표현식을 이용하여 파일명에 사용할 도시 이름을 정리합니다.
정규표현식의 의미는 다음과 같습니다.(표현과 의미)
| a-z | 영문 소문자 |
| A-Z | 영문 대문자 |
| 0-9 | 숫자 |
| 가-힣 | 한글 |
| _ | 밑줄 |
| - | 하이픈 |
| ^ | 지정한 문자 집합에 포함되지 않는 문자 |
지정한 문자 이외의 문자는 _로 바꿉니다.
예를 들어 다음과 같습니다.
입력 : 서울 특별시!
결과 : 서울_특별시_
이후 파일명을 생성합니다.
filename = f"report_{safe_city}_{date_string}.txt"
서울을 요청했다면 다음과 같은 이름이 만들어집니다.
report_서울_2026-10-31.txt
여기서 중요한 특징이 있습니다.
파일명에 시간은 포함되지 않고 날짜만 포함됩니다.
따라서 같은 날 서울 보고서를 여러 번 생성하면 동일한 파일명을 사용하게 됩니다.
그런데 ADK ArtifactService는 같은 파일명을 다시 저장하더라도 기존 파일을 단순히 덮어쓰는 대신 새로운 버전을 생성합니다.
예를 들어 다음과 같습니다.
| 첫 번째 | report_서울_2026-10-31.txt | 0 |
| 두 번째 | report_서울_2026-10-31.txt | 1 |
| 세 번째 | report_서울_2026-10-31.txt | 2 |
이것이 Artifact의 버전 관리 기능입니다.
3-6. 보고서 내용 생성
report_content = f"""
[WEATHER INTELLIGENCE REPORT]
--------------------------------
대상 도시 : {city}
생성 시간 : {today}
--------------------------------
1. 현재 날씨 정보
- 기온 : {temp}°C
- 날씨 : {condition}
- 습도 : {humidity}%
2. AI 분석 의견
현재 {city}의 날씨 상태는 {condition}입니다.
본 보고서는 실습용 Mock 데이터를 기반으로 생성되었습니다.
실제 기상 관측 정보가 아닙니다.
--------------------------------
Generated by ADK Weather Agent
"""
Python의 f-string을 이용하여 문자열을 만듭니다.
f""" ... """는 여러 줄로 된 문자열에 변수 값을 삽입할 수 있도록 합니다.
예를 들어 다음 값이 생성되었다고 가정하겠습니다.
city = "서울"
today = "2026-10-31 21:10"
temp = 23
condition = "맑음"
humidity = 65
최종적으로 만들어지는 보고서는 다음과 같습니다.
[WEATHER INTELLIGENCE REPORT]
--------------------------------
대상 도시 : 서울
생성 시간 : 2026-10-31 21:10
--------------------------------
1. 현재 날씨 정보
- 기온 : 23°C
- 날씨 : 맑음
- 습도 : 65%
2. AI 분석 의견
현재 서울의 날씨 상태는 맑음입니다.
본 보고서는 실습용 Mock 데이터를 기반으로 생성되었습니다.
실제 기상 관측 정보가 아닙니다.
--------------------------------
Generated by ADK Weather Agent
이 시점까지는 단순히 Python 메모리 안에 문자열이 만들어진 상태입니다.
아직 GCS에는 저장되지 않았습니다.
3-7. 문자열을 Bytes로 변환
report_bytes = report_content.encode("utf-8")
이번 실습에서 반드시 이해해야 할 부분입니다.
왜 문자열을 바이트로 변환할까요?
Artifact는 단순한 문자열이 아니라 이미지, PDF, 음성, 문서 등 다양한 종류의 파일을 다루는 구조이기 때문입니다.
따라서 파일의 실제 데이터를 바이트 형태로 전달합니다.
예를 들어 다음과 같습니다.
text = "서울 날씨"
data = text.encode("utf-8")
그러면 data는 Python의 bytes 객체가 됩니다.
문자열과 바이트의 차이를 이해해 봅시다.
| 구분 | Python 자료형 | 의미 |
| 문자열 | str | 사람이 읽는 문자 |
| 바이트 | bytes | 파일이나 네트워크에서 사용하는 실제 데이터 |
왜 UTF-8을 사용했을까요?
한글을 포함한 문자를 안정적으로 인코딩하기 위해서입니다.
나중에 GCS에서 내려받은 바이트 데이터를 다시 문자열로 변환할 때는 다음과 같이 사용합니다.
restored_text = report_bytes.decode("utf-8")
즉, encode()와 decode()는 서로 반대 방향의 작업입니다.
데이터 변환 과정

3-8. Artifact 객체 생성
weather_artifact = types.Part.from_bytes(
data=report_bytes,
mime_type="text/plain"
)
이제 가장 중요한 Artifact 객체를 만듭니다.
types.Part는 Google GenAI SDK에서 제공하는 데이터 구조입니다.
이 객체는 다양한 종류의 데이터를 담을 수 있습니다.
예를 들어 다음과 같습니다.
| 파일 종류 | MIME Type |
| TXT | text/plain |
| application/pdf | |
| PNG | image/png |
| JPEG | image/jpeg |
| JSON | application/json |
| CSV | text/csv |
| Excel | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
이번에는 텍스트 보고서이므로 다음과 같이 지정했습니다.
mime_type="text/plain"
여기서 MIME Type은 파일의 데이터 형식이 무엇인지 알려주는 정보입니다.
중요한 점은 .txt라는 확장자와 MIME Type은 서로 다른 개념이라는 것입니다.
-
.txt: 사람이 파일명을 보고 이해하기 쉬운 확장자
-
text/plain: 프로그램이 파일의 데이터 형식을 이해하기 위한 정보
이렇게 만들어진 weather_artifact는 단순한 문자열이나 바이트가 아니라 ADK ArtifactService가 처리할 수 있는 표준 데이터 객체입니다.
3-9. 사용자와 세션 정보 확인
user_id = getattr(
tool_context,
'user_id',
'unknown_user'
)
app_name = getattr(
tool_context,
'app_name',
'unknown_app'
)
session_id = getattr(
tool_context,
'session_id',
'default_session'
)
여기서 getattr()은 Python의 내장 함수입니다.
객체에 특정 속성이 존재하면 그 값을 가져오고, 존재하지 않으면 기본값을 반환합니다.
예를 들어 다음과 같습니다.
getattr(tool_context, 'user_id', 'unknown_user')
이는 다음과 비슷한 의미입니다.
if hasattr(tool_context, 'user_id'):
user_id = tool_context.user_id
else:
user_id = 'unknown_user'
각각의 의미를 정리하면 다음과 같습니다.
app_name
현재 어떤 ADK 애플리케이션에서 실행되고 있는가?
user_id
어떤 사용자가 실행했는가?
session_id
어떤 대화 세션에서 실행했는가?
왜 이 정보가 중요할까요?
ADK는 Artifact를 단순히 파일명만으로 관리하지 않습니다.
기본적으로 다음과 같은 구분을 이용합니다.
app_name
└── user_id
└── session_id
└── filename
└── version
예를 들어 서로 다른 사용자가 모두 report.txt라는 파일을 저장하더라도 각자의 사용자 및 세션 범위에 따라 구분할 수 있습니다.
이러한 구조 덕분에 여러 사용자가 동시에 에이전트를 사용하는 환경에서도 Artifact를 구분하여 관리할 수 있습니다.
이후 GCS에는 ADK가 관리하는 계층적 객체 이름으로 저장됩니다.
3-10. GCS에 실제 저장하는 부분
드디어 이번 실습의 핵심입니다.
version_id = await tool_context.save_artifact(
filename=filename,
artifact=weather_artifact
)
이 한 줄을 정확하게 이해해야 합니다.
먼저 각 인수를 살펴보겠습니다.
| 인수 | 전달값 | 의미 |
| filename | report_서울_2026-10-31.txt | 저장할 파일명 |
| artifact | weather_artifact | 실제 파일 데이터와 MIME Type을 가진 객체 |
그리고 가장 중요한 메서드는 다음입니다.
tool_context.save_artifact()
이 메서드가 실행되면 다음과 같은 과정이 진행됩니다.
실제 저장 과정

여기서 await가 중요합니다.
version_id = await ...
GCS에 저장하는 비동기 작업이 완료될 때까지 기다린 후, 저장 결과인 버전 번호를 version_id에 넣습니다.
예를 들어 첫 번째 저장이라면 다음과 같습니다.
version_id = 0
두 번째 저장이라면 다음과 같습니다.
version_id = 1
이 번호는 ADK의 Artifact 버전입니다.
주의: ADK Artifact 버전과 GCS 자체의 객체 버전은 별개입니다.
ADK는 파일을 저장할 때 버전별로 별도의 GCS 객체를 생성하는 방식으로 관리합니다. GCS 버킷의 Object Versioning 기능을 켜야만 ADK 버전 관리가 작동하는 것은 아닙니다.
3-11. 저장 성공 및 실패 처리
try:
version_id = await tool_context.save_artifact(
filename=filename,
artifact=weather_artifact
)
print(
f"[*] 저장 완료: {filename}, "
f"Version: {version_id}"
)
except Exception as e:
print(f"[!] Artifact 저장 실패: {e}")
raise
여기서는 try-except 예외 처리를 사용합니다.
정상적으로 저장되면 다음과 같은 로그가 출력됩니다.
[*] 저장 완료: report_서울_2026-10-31.txt, Version: 0
저장에 실패하면 다음과 같은 로그가 출력될 수 있습니다.
[!] Artifact 저장 실패: ...
대표적인 오류 원인은 다음과 같습니다.
| 오류 | 가능한 원인 |
| 403 Forbidden | GCS 버킷 접근 권한 부족 |
| 404 Not Found | 버킷 이름 오류 또는 버킷이 존재하지 않음 |
| 인증 오류 | ADC 인증 설정 문제 |
| 네트워크 오류 | 인터넷 또는 GCS 연결 문제 |
| ArtifactService 오류 | ADK에 ArtifactService가 연결되지 않음 |
마지막에 있는 다음 코드도 중요합니다.
raise
이것은 발생한 예외를 다시 상위 호출자에게 전달합니다.
만약 raise가 없다면 오류를 출력하고 함수가 정상 종료된 것처럼 진행될 여지가 있습니다.
하지만 현재 코드는 오류를 다시 발생시키므로 ADK도 Tool 실행 실패를 인식할 수 있습니다.
따라서 이 부분은 잘 작성되어 있습니다.
3-12. Tool 결과 반환
return {
"status": "success",
"message": "날씨 보고서가 생성 및 저장되었습니다.",
"file_info": {
"filename": filename,
"version": version_id,
"mime_type": "text/plain"
}
}
Tool이 작업을 마친 후 ADK에 반환하는 결과입니다.
예를 들어 다음과 같은 구조가 반환됩니다.
{
"status": "success",
"message": "날씨 보고서가 생성 및 저장되었습니다.",
"file_info": {
"filename": "report_서울_2026-10-31.txt",
"version": 0,
"mime_type": "text/plain"
}
}
여기서 중요한 점은 이 반환값 자체가 보고서 파일은 아니라는 것입니다.
실제 보고서 데이터는 이미 GCS에 저장되었고, Tool은 저장된 파일의 정보를 반환합니다.
이것은 좋은 설계입니다.
실제 보고서 전체를 매번 LLM에 전달하는 대신 파일명, 버전 등의 메타정보만 전달할 수 있기 때문입니다.
다만 status: success는 GCS에 저장된 후에만 반환되므로, 현재 코드에서는 저장 성공을 기준으로 결과를 구분하고 있습니다.
4. Agent 정의 부분
이제 Tool을 호출하는 Agent를 살펴보겠습니다.
weather_agent = Agent(
name="WeatherBot",
model=os.getenv(
"GOOGLE_MODEL",
"gemini-3.5-flash"
),
tools=[generate_weather_report],
instruction="""
당신은 유능한 날씨 정보 비서입니다.
사용자가 특정 지역의 날씨를 요청하면
반드시 generate_weather_report 도구를 사용하세요.
도구 실행 결과를 확인한 후 사용자에게 보고서 생성 결과를 알려주세요.
보고서가 성공적으로 저장되지 않았다면
저장되었다고 답변하지 마세요.
이 에이전트는 실습용 Mock 데이터를 사용하므로
실제 기상 관측 정보라고 표현하지 마세요.
답변은 모두 한글로 작성하세요.
"""
)
각 항목을 살펴보겠습니다.
① name
name="WeatherBot"
에이전트 이름입니다.
ADK Web에서 에이전트를 구분하는 데 사용됩니다.
② model
model=os.getenv(
"GOOGLE_MODEL",
"gemini-3.5-flash"
)
사용할 LLM 모델을 설정합니다.
환경변수 GOOGLE_MODEL이 설정되어 있다면 그 값을 사용하고, 그렇지 않으면 기본값을 사용합니다.
예를 들어 PowerShell에서 다음과 같이 지정할 수 있습니다.
$env:GOOGLE_MODEL = "사용할_모델_ID"
모델 ID는 현재 사용 가능한 Gemini 모델인지 확인해야 합니다.
③ tools
tools=[generate_weather_report]
이 부분이 Agent와 Tool을 연결합니다.
즉, WeatherBot은 generate_weather_report라는 함수를 도구로 사용할 수 있습니다.
사용자가 다음과 같이 요청하면,
서울 날씨 보고서를 만들어 줘.
Gemini는 Tool의 이름과 매개변수 정보를 바탕으로 함수를 호출할 수 있습니다.
여기서 tool_context는 ADK가 실행 시점에 주입하므로 사용자가 직접 입력하지 않습니다.
④ instruction
Agent의 행동 지침입니다.
특히 다음 문장이 중요합니다.
사용자가 특정 지역의 날씨를 요청하면
반드시 generate_weather_report 도구를 사용하세요.
이 지침으로 단순히 LLM이 날씨를 상상해서 답하는 것이 아니라 지정된 Tool을 호출하도록 유도합니다.
또 다음 지침도 중요합니다.
보고서가 성공적으로 저장되지 않았다면
저장되었다고 답변하지 마세요.
이는 파일 저장 작업에서 매우 중요한 원칙입니다.
실제 저장이 실패했는데도 LLM이 성공했다고 사용자에게 안내하는 문제를 방지하기 위한 것입니다.
5. ADK 실행 진입점
root_agent = weather_agent
이 코드는 간단하지만 반드시 필요합니다.
ADK CLI는 에이전트 프로젝트에서 root_agent라는 이름의 객체를 찾아 실행할 수 있습니다.
따라서 지금 정의한 weather_agent를 다음과 같이 연결합니다.
root_agent = weather_agent
이렇게 하면 ADK는 WeatherBot을 최상위 에이전트로 인식합니다.
6. 이제 실제 실행해 보자
사용자가 제시한 실행 명령은 올바른 형태입니다.
(venv) PS C:\Dev\book_agentic_ai\adk_>
adk web --port 8080 --artifact_service_uri="gs://agent-project_512212-staging"
현재 ADK 공식 CLI에서도 --artifact_service_uri 옵션과 gs://버킷명 형식을 지원합니다.
다만 실제 실행 전에 GCP 인증과 권한을 확인해야 합니다.
6-1. GCP 인증 확인
우선 PowerShell에서 다음 명령어를 실행합니다.
gcloud auth list
현재 Google Cloud CLI에 로그인된 계정을 확인합니다.
예상 출력:
Credentialed Accounts
ACTIVE ACCOUNT
* your-account@gmail.com
여기서 중요한 것은 Google Cloud CLI 인증과 Python 애플리케이션의 인증이 반드시 동일한 방식으로 설정되는 것은 아니라는 점입니다.
Python에서 Google Cloud SDK가 사용하는 기본 인증 방식은 Application Default Credentials(ADC) 입니다.
따라서 다음 명령어를 실행합니다.
gcloud auth application-default login
브라우저가 열리면 GCS 버킷에 접근 권한이 있는 Google 계정으로 인증합니다.
이후 다음 명령어로 인증 정보가 설정되었는지 확인할 수 있습니다.
gcloud auth application-default print-access-token
정상적으로 인증되면 액세스 토큰이 출력됩니다.
6-2. GCS 버킷 접근 권한 확인
GCS 버킷에 저장하려면 실행 계정에 적절한 IAM 권한이 있어야 합니다.
실습에서는 해당 버킷에 다음 역할을 부여하는 방법을 권장합니다.
Storage Object User
roles/storage.objectUser
이 역할은 객체 생성, 조회, 수정, 삭제 등 객체 작업에 필요한 권한을 제공합니다.
실습용 계정에 버킷 단위로 권한을 부여하는 것이 좋습니다.
Google Cloud Console에서 다음 경로로 이동합니다.
-
Google Cloud Console 접속
-
Cloud Storage 선택
-
Buckets 선택
-
agent-project_512212-staging 선택
-
Permissions 탭 선택
-
Grant access 선택
-
인증에 사용할 계정 입력
-
Storage Object User 역할 부여
여기서 주의할 점은 두 계정이 다를 수 있다는 것입니다.
-
Python 애플리케이션이 GCS에 접근하는 계정
-
웹 콘솔에서 GCS 객체를 확인하는 계정
따라서 두 계정 모두 적절한 접근 권한이 있어야 합니다.
6-3. 버킷이 존재하는지 확인
PowerShell에서 다음 명령을 실행합니다.
gcloud storage buckets describe gs://agent-project_512212-staging
정상적으로 버킷이 존재하고 조회 권한이 있다면 버킷의 정보가 출력됩니다.
또는 다음 명령으로 객체 목록을 확인할 수 있습니다.
gcloud storage ls gs://agent-project_512212-staging
아직 저장된 파일이 없다면 비어 있을 수 있습니다.
이것은 정상입니다.
7. ADK Web 실행 및 보고서 생성
이제 준비가 끝났습니다.
PowerShell에서 다음과 같이 실행합니다.
(venv) PS C:\Dev\book_agentic_ai\adk_>
adk web --port 8080 --artifact_service_uri="gs://agent-project_512212-staging"
실행 후 브라우저에서 다음 주소로 접속합니다.
WeatherBot을 선택한 후 다음과 같이 입력합니다.
서울 날씨 보고서를 만들어 줘.
정상적으로 작동하면 다음과 같은 순서로 로그가 출력될 것입니다.
[*] Tool 실행: 서울 날씨 보고서 생성
[*] 보고서 텍스트 생성 완료
[*] Artifact 객체 생성 완료
[*] 저장 시도 - App: ..., User: ..., Session: ...,
Bucket: agent-project_512212-staging
[*] 저장 완료: report_서울_2026-10-31.txt, Version: 0
이 중에서 가장 중요한 로그는 다음입니다.
[*] 저장 완료: report_서울_2026-10-31.txt, Version: 0
다만 이 로그는 ADK 저장 메서드가 성공적으로 반환되었다는 뜻이지, 사용자가 Google Cloud Console에서 직접 객체를 확인했다는 뜻은 아닙니다.
이제 실제 저장 여부를 확인해 보겠습니다.
8. Google Cloud Console에서 실제 저장 여부 확인하기
이제 이번 실습에서 가장 중요한 검증 과정입니다.
ADK가 GCS에 저장했다고 로그를 출력했더라도, 실제 Cloud Storage에 객체가 존재하는지 직접 확인하는 것이 좋습니다.
8-1. Google Cloud Console 접속
다음 사이트에 접속합니다.

왼쪽 메뉴에서 다음 경로로 이동합니다.
Google Cloud Console
└── Cloud Storage
└── Buckets
└── agent-project_512212-staging
해당 버킷을 클릭합니다.
8-2. 저장된 객체 확인
버킷 내부의 Objects 화면을 확인합니다.
여기서 주의할 점이 있습니다.
ADK는 단순히 다음과 같이 파일을 저장하는 것이 아닙니다.
gs://agent-project_512212-staging/
report_서울_2026-10-31.txt
ADK ArtifactService는 애플리케이션, 사용자, 세션, 파일명 및 버전 정보를 고려하여 계층적인 객체 이름을 구성합니다.
따라서 버킷 내부에서 바로 파일명이 보이지 않고 여러 단계의 폴더처럼 보이는 구조로 나타날 수 있습니다.
예를 들어 개념적으로는 다음과 같습니다.
agent-project_512212-staging
│
└── WeatherBot 관련 경로
│
└── 사용자 관련 경로
│
└── 세션 관련 경로
│
└── 보고서 파일 및 버전
실제 객체 이름과 경로는 ADK의 구현 및 버전에 따라 확인해야 합니다.

report_서울_2026-10-31.txt
찾았다면 해당 객체를 클릭합니다.
다음 항목을 확인하면 좋습니다.
| 확인 항목 | 확인할 내용 |
| Name | 실제 저장된 객체 이름 |
| Size | 파일 크기 |
| Content-Type | text/plain 여부 |
| Created | 생성 시각 |
| Updated | 수정 시각 |
| Generation | GCS 객체 세대 번호 |
여기서 Generation은 GCS 자체의 객체 세대 번호입니다. 앞서 설명한 ADK의 version_id와는 다른 값입니다.
8-3. 파일 내용 직접 확인
파일이 저장된 것을 확인했다면 다음 단계는 실제 내용을 확인하는 것입니다.
Cloud Storage Console에서 해당 객체를 선택한 후 다운로드합니다.
다운로드한 TXT 파일을 메모장이나 VS Code로 열어 봅니다.
다음과 같은 내용이 확인되면 정상입니다.
[WEATHER INTELLIGENCE REPORT]
--------------------------------
대상 도시 : 서울
생성 시간 : 2026-10-31 21:10
--------------------------------
1. 현재 날씨 정보
- 기온 : 23°C
- 날씨 : 맑음
- 습도 : 65%
2. AI 분석 의견
현재 서울의 날씨 상태는 맑음입니다.
본 보고서는 실습용 Mock 데이터를 기반으로 생성되었습니다.
실제 기상 관측 정보가 아닙니다.
--------------------------------
Generated by ADK Weather Agent
이렇게 확인하면 다음 세 가지를 검증한 것입니다.
-
Python이 보고서를 정상적으로 생성했다.
-
ADK ArtifactService가 GCS에 파일을 저장했다.
-
GCS에 저장된 실제 파일의 내용을 읽을 수 있다.
8-4. PowerShell에서 GCS 객체 확인하기
웹 콘솔 외에도 명령어로 확인할 수 있습니다.
먼저 전체 객체 목록을 확인합니다.
gcloud storage ls --recursive gs://agent-project_512212-staging
이 명령은 버킷 내부 객체들을 재귀적으로 조회합니다.
ADK가 만든 파일 경로가 길더라도 전체 경로를 확인할 수 있습니다.
특정 파일명을 찾고 싶다면 다음과 같이 사용할 수 있습니다.
gcloud storage ls --recursive gs://agent-project_512212-staging |
Select-String "report_서울"
파일 경로를 확인한 후에는 다음과 같이 다운로드할 수 있습니다.
gcloud storage cp "gs://agent-project_512212-staging/실제_객체_경로" `
"C:\Dev\book_agentic_ai\adk_\downloaded_report.txt"
여기서 실제_객체_경로는 앞 단계에서 조회한 경로를 그대로 넣어야 합니다.
다운로드 후 다음과 같이 확인합니다.
Get-Content "C:\Dev\book_agentic_ai\adk_\downloaded_report.txt" `
-Encoding UTF8
이 방법은 실무에서도 유용합니다.
9. 정말 중요한 실습: 재시작 후에도 파일이 남아 있을까?
이것은 GCS를 Artifact 저장소로 사용하는 이유를 직접 확인하는 실험입니다.
우선 두 가지 저장 방식의 차이를 이해해야 합니다.
| 저장 장소 | Python 프로세스 메모리 | Google Cloud Storage |
| 프로그램 종료 | 저장 데이터 소멸 | 데이터 유지 |
| 프로그램 재시작 | 이전 파일 접근 불가 | 이전 파일 접근 가능 |
| 여러 인스턴스 간 공유 | 기본적으로 불가 | 동일 버킷에 접근 가능 |
| 장기 보관 | 적합하지 않음 | 적합 |
이것이 GCS를 사용하는 핵심적인 이유입니다.
다음 실험을 해보세요.
GCS 영구 저장 검증 실험
-
첫 번째 실행
ADK Web을 실행합니다.
adk web --port 8080 --artifact_service_uri="gs://agent-project_512212-staging" -
첫 번째 보고서 생성
서울 날씨 보고서를 만들어 줘.GCS 버킷에 파일이 생성되었는지 확인합니다.
-
ADK Web 종료
PowerShell에서 Ctrl + C를 누릅니다.
-
ADK Web 재실행
동일한 명령으로 다시 실행합니다.
adk web --port 8080 --artifact_service_uri="gs://agent-project_512212-staging" -
GCS 버킷 확인
첫 번째 실행에서 생성한 파일이 그대로 남아 있는지 확인합니다.
-
동일한 파일명으로 다시 저장
다시 서울 날씨 보고서를 생성합니다.
같은 날짜라면 같은 파일명으로 새 버전이 생성되는지 확인합니다.
여기서 한 가지 주의할 점이 있습니다.
ADK Web을 재시작하면 새로운 세션이 생성될 수 있습니다.
현재 파일명은 일반 파일명이므로 세션 범위에 연결됩니다. 따라서 재시작 후 새로운 세션에서 같은 도시와 날짜로 보고서를 만들더라도, 이전 세션의 Artifact와는 별개로 관리될 수 있습니다.
즉, GCS 객체가 남아 있다는 것과 ADK의 새 세션에서 이전 Artifact를 바로 조회할 수 있다는 것은 서로 다른 문제입니다.
이 부분은 뒤에서 다시 설명하겠습니다.
10. ArtifactService의 중요한 개념 4가지
이번 실습을 통해 반드시 알아두면 좋은 내용입니다.
10-1. Artifact는 단순한 파일 저장 기능이 아니다
Artifact는 ADK가 에이전트의 실행 과정에서 생성하거나 사용하는 파일을 체계적으로 관리하는 기능입니다.
예를 들어 다음과 같은 작업을 생각해 보겠습니다.
| 날씨 보고서 생성 | TXT, PDF |
| 기업 분석 | PDF, Excel |
| 문서 작성 에이전트 | HWP, DOCX |
| 데이터 분석 | CSV, XLSX |
| 이미지 생성 | PNG, JPEG |
| 음성 에이전트 | WAV, MP3 |
| AI 보고서 작성 | JSON, PDF |
모두 동일한 ArtifactService 인터페이스를 통해 저장할 수 있습니다.
따라서 나중에 OfficeXAI와 같은 문서 생성 서비스에 ADK를 연결할 때도 상당히 유용합니다.
10-2. save_artifact 외에도 여러 기능이 있다
ArtifactService는 저장만 하는 것이 아닙니다.
주요 기능은 다음과 같습니다.
| save_artifact() | 파일 저장 |
| load_artifact() | 파일 불러오기 |
| list_artifacts() | 파일 목록 조회 |
| list_versions() | 특정 파일의 버전 목록 조회 |
| delete_artifact() | 파일 삭제 |
예를 들어 저장한 보고서를 다시 읽으려면 다음과 같이 할 수 있습니다.
artifact = await tool_context.load_artifact(
filename="report_서울_2026-10-31.txt"
)
그러면 해당 세션의 최신 버전이 반환됩니다.
특정 버전을 불러오려면 다음과 같이 지정합니다.
artifact = await tool_context.load_artifact(
filename="report_서울_2026-10-31.txt",
version=0
)
불러온 객체의 실제 바이트 데이터는 다음과 같이 접근할 수 있습니다.
if artifact is not None:
report_bytes = artifact.inline_data.data
report_text = report_bytes.decode("utf-8")
print(report_text)
이렇게 하면 GCS에 저장한 파일을 다시 Python에서 읽어올 수 있습니다.
다만 load_artifact()는 현재 실행 컨텍스트의 범위에 맞는 파일을 조회한다는 점을 기억하세요.
10-3. Session Scope와 User Scope의 차이
이 부분은 실제 서비스 개발에서 매우 중요합니다.
현재 코드에서는 다음과 같이 파일명을 지정합니다.
filename = f"report_{safe_city}_{date_string}.txt"
이 파일은 기본적으로 Session Scope에 저장됩니다.
즉, 특정 사용자와 특정 세션에 귀속됩니다.
반면 다음과 같이 파일명을 지정할 수도 있습니다.
filename = f"user:report_{safe_city}_{date_string}.txt"
이렇게 user: 접두어를 붙이면 User Scope로 저장됩니다.
두 방식의 차이는 다음과 같습니다.
Session Scope
app_name
└── user_id
├── session_A
│ └── report.txt
└── session_B
└── report.txt
특정 대화에서 생성한 파일을 그 대화의 작업 결과로 관리할 때 적합합니다.
User Scope
app_name
└── user_id
└── user:report.txt
사용자가 다른 세션에서도 동일한 파일을 다시 사용해야 할 때 적합합니다.
이 구분은 ADK 공식 문서에 명시되어 있습니다. Agent Development Kit (ADK)
예를 들어 다음과 같은 서비스라면 User Scope가 더 적합할 수 있습니다.
-
사용자가 과거에 작성한 보고서를 다시 조회하는 서비스
-
기업별 문서 템플릿을 저장하는 서비스
-
사용자별 업무 자료를 지속적으로 관리하는 서비스
반대로 대화마다 생성하는 일회성 분석 결과라면 Session Scope가 더 자연스럽습니다.
10-4. GCS에 저장된 파일은 자동으로 삭제되지 않는다
InMemoryArtifactService와 달리 GCS는 영구 저장을 목적으로 합니다.
따라서 저장한 파일은 명시적으로 삭제하거나 버킷의 수명주기 정책 등을 설정하지 않는 한 계속 보관될 수 있습니다.
이것은 장점이자 관리해야 할 사항입니다.
실무에서는 다음과 같은 정책을 검토해야 합니다.
-
파일 보관 기간
-
오래된 파일 자동 삭제
-
사용자별 파일 접근 권한
-
파일 버전 관리
-
저장 비용 관리
GCS의 Lifecycle Management 기능을 활용하면 일정 기간이 지난 객체를 자동으로 삭제하거나 다른 저장 클래스로 이동시킬 수 있습니다.
11. GCS에 안전하게 저장하기 위한 보안 설정
사용자가 질문한 '안전하게 저장되었는가?'라는 관점에서 살펴보겠습니다.
파일이 GCS에 존재하는 것과 안전하게 관리되는 것은 별개의 문제입니다.
저장 여부뿐만 아니라 누가 접근할 수 있는지, 외부에 공개되어 있지 않은지 확인해야 합니다.
11-1. Public Access Prevention 확인
Google Cloud Console에서 버킷을 선택하고 다음 항목을 확인합니다.
Cloud Storage
└── Buckets
└── agent-project_512212-staging
└── Configuration
여기서 다음 설정을 확인하세요.
-
Public access prevention: Enforced
-
Access control: Uniform

Public Access Prevention
버킷의 객체가 인터넷에 공개되는 것을 방지합니다.
Uniform Bucket-Level Access
객체별 ACL보다는 버킷 수준의 IAM 정책을 통해 접근 권한을 일관되게 관리합니다.
실습용 버킷이라도 이 두 설정을 사용하는 것을 권장합니다.
11-2. IAM 권한 최소화
에이전트가 GCS에 접근한다고 해서 무조건 모든 권한을 부여할 필요는 없습니다.
이번 실습에서는 해당 버킷에 필요한 객체 작업 권한만 부여하는 것이 좋습니다.
Storage Object User
향후 운영 환경에서는 다음을 권장합니다.
-
개발 환경과 운영 환경의 서비스 계정 분리
-
사용자별 접근 권한 분리
-
서비스 계정 키 파일을 코드에 직접 저장하지 않기
-
가능한 경우 키 파일 대신 Google Cloud의 관리형 인증 방식 사용
특히 JSON 서비스 계정 키를 GitHub 등에 올리는 일은 절대 피해야 합니다.
11-3. 데이터 암호화
Google Cloud Storage는 기본적으로 저장 데이터를 암호화합니다.
따라서 별도의 암호화 코드를 작성하지 않더라도 기본적인 저장 데이터 암호화가 적용됩니다.
보다 엄격한 보안 요구사항이 있다면 Cloud KMS를 이용한 고객 관리형 암호화 키(CMEK)를 검토할 수 있습니다.
다만 이번 실습에서는 기본 Google 관리형 암호화로 충분히 시작할 수 있습니다.
11-4. GCS Object Versioning과 ADK Versioning 구분
여기서 다시 한번 중요한 개념을 구분하겠습니다.
| 구분 | ADK Artifact Version | GCS Object Versioning |
| 관리 주체 | ADK | GCS |
| 목적 | Artifact 파일의 버전 관리 | 객체의 이전 세대 보존 |
| 번호 | version_id | generation |
| 활성화 | ADK에서 자동 관리 | 버킷 설정으로 별도 활성화 |
| 저장 방식 | 버전마다 별도 객체 | 같은 객체 이름의 세대 보존 |
따라서 이번 실습에서는 GCS Object Versioning을 별도로 활성화하지 않아도 ADK의 Artifact 버전 관리는 작동합니다.
다만 운영 환경에서 객체 삭제나 덮어쓰기까지 복구할 수 있도록 하려면 GCS Object Versioning도 별도로 검토할 만합니다.
12. 이번 코드에서 개선하면 좋은 부분
현재 코드는 실습 목적에 잘 맞습니다. 다만 실제 서비스로 발전시킨다면 몇 가지 개선을 권장합니다.
① 저장 로그에 실제 GCS 버킷 정보 남기기
현재는 다음과 같습니다.
GCS_BUCKET_NAME = os.getenv("GCS_BUCKET_NAME", "")
환경변수를 지정하지 않으면 로그에 다음과 같이 표시됩니다.
Bucket: (runtime-configured)
실습 시에는 PowerShell에서 환경변수를 지정하면 확인하기 편합니다.
$env:GCS_BUCKET_NAME = "agent-project_512212-staging"
이렇게 하면 실제 저장 위치를 로그에서도 명확하게 확인할 수 있습니다.
② 파일명에 생성 시간을 포함할지 검토
현재 파일명은 날짜까지만 포함합니다.
filename = f"report_{safe_city}_{date_string}.txt"
만약 매번 새로운 파일을 생성하고 싶다면 다음처럼 변경할 수 있습니다.
filename = (
f"report_{safe_city}_"
f"{now.strftime('%Y%m%d_%H%M%S')}.txt"
)
그러면 다음과 같이 저장됩니다.
report_서울_20261031_211000.txt
report_서울_20261031_213000.txt
이렇게 하면 보고서별로 고유한 파일명을 갖게 됩니다.
다만 기존처럼 동일 파일명의 버전 관리를 학습하려는 이번 실습에서는 현재 코드가 더 적절합니다.
③ 저장 성공 여부를 더욱 명확하게 검증
현재는 save_artifact()의 반환값을 성공 여부의 기준으로 사용합니다.
실제 운영 환경에서는 추가로 load_artifact()를 호출하여 저장된 데이터를 다시 읽어 보는 검증도 가능합니다.
예를 들어 다음과 같습니다.
version_id = await tool_context.save_artifact(
filename=filename,
artifact=weather_artifact
)
# 저장한 버전을 다시 읽어 검증
saved_artifact = await tool_context.load_artifact(
filename=filename,
version=version_id
)
if saved_artifact is None:
raise RuntimeError("저장된 Artifact를 다시 읽을 수 없습니다.")
print("[*] GCS Artifact 저장 및 재조회 검증 완료")
이렇게 하면 단순히 저장 메서드가 반환되었는지뿐 아니라 실제로 해당 버전의 데이터를 다시 조회할 수 있는지도 확인할 수 있습니다.
단, 이 검증은 ADK 컨텍스트에서 읽을 수 있는지 확인하는 것이며, Google Cloud Console에서 객체를 직접 확인하는 검증과는 별개입니다.
13. 이번 실습을 마무리하기 위한 체크리스트
실제 실행할 때 다음 항목을 순서대로 확인해 보세요.
14. 이번 실습에서 반드시 기억해야 할 핵심
지금까지 학습한 ADK의 여러 저장 기능을 서로 비교해 보겠습니다.
| 구분 | 저장 대상 | 저장 서비스 | 주요 목적 |
| Session | 대화 이력 및 이벤트 | SessionService | 대화 상태 유지 |
| State | 사용자 및 세션 상태값 | SessionService 등 | 상태 정보 관리 |
| Memory | 장기 기억 및 검색 정보 | MemoryService | 대화 간 기억 |
| Artifact | TXT, PDF, 이미지 등 파일 | ArtifactService | 파일 저장 및 관리 |
이 네 가지는 서로 다른 역할을 합니다.
특히 지금까지 진행한 실습을 연결하면 다음과 같습니다.
지금까지의 ADK 학습 구조

지금까지 실습한 내용을 연결해 보면 상당히 의미가 있습니다.
-
PostgreSQL: ADK 세션 데이터를 지속적으로 저장
-
Vertex AI Memory Bank: 에이전트의 장기 기억 관리
-
Google Cloud Storage: 에이전트가 생성한 파일 저장
이제 에이전트가 대화만 하는 것이 아니라, 대화를 기억하고, 필요한 정보를 검색하고, 문서까지 생성하여 클라우드에 보관하는 구조를 갖추게 된 것입니다.
마지막으로 이번 실습에서 가장 중요한 세 가지를 강조하겠습니다.
-
ToolContext.save_artifact()는 실제 저장 서비스를 추상화하는 인터페이스입니다.
-
--artifact_service_uri="gs://버킷명"은 ADK CLI가 GCS ArtifactService를 연결하도록 지정하는 설정입니다.
-
GCS에 파일이 저장되었다는 사실과 새로운 세션에서도 해당 파일을 조회할 수 있다는 사실은 다릅니다. Session Scope와 User Scope를 반드시 구분해야 합니다.
공식 참고 자료: