본문으로 건너뛰기

© 2026 Molayo

Qiita헤드라인2026. 06. 04. 21:22

Google ADK 2.0 업그레이드로 인해 Postgres 세션 스토어에서 겪은 3가지 문제

요약

Google ADK 2.0 업그레이드 시 Postgres 세션 스토어에서 발생하는 세 가지 기술적 이슈와 해결 방법을 다룹니다. 비동기 드라이버 URL 충돌, 스키마 변경에 따른 런타임 에러, 데이터 마이그레이션 문제를 상세히 설명합니다.

핵심 포인트

  • ADK 2.0은 asyncpg를 사용하므로 DB URL 스킴 변경이 필요함
  • 동기 SQLAlchemy 엔진과 비동기 URL 간의 충돌을 방지하기 위한 정규화 레이어 구현 필요
  • events 테이블에 input/output_transcription 컬럼 추가 필수
  • 스키마 미업데이트 시 컨테이너는 정상이나 채팅 시 500 에러 발생

Cloud Run + Cloud SQL (PostgreSQL)에서 구동 중인 에이전트의 google-adk를 1.x에서 >=2.0.0으로 올렸다. 세션 스토어(Session Store)에는 ADK의 DatabaseSessionService를 사용하고 있다. 의존성 한 줄만 올리면 될 줄 알았는데, 전혀 그렇지 않았다.

겪었던 문제는 3가지다. 사소한 순서대로 나열하면 다음과 같다.

  • ADK 2.0은 Postgres와 asyncpg로 통신하므로 연결 URL이 변경된다. 게다가 그 URL을 동기(Synchronous) 코드와 공유하고 있다.
  • events 테이블에 2개의 컬럼 추가가 필요하다. 이를 추가하지 않고 배포하면, 컨테이너는 살아있지만 채팅만 무응답으로 500 에러가 발생한다.
  • 레거시한 v0 (Pickle) 스키마는 여전히 작동한다. v1 (JSON)으로의 이행은 선택 사항이며, 게다가 인플레이스(in-place)로는 불가능하다.

차례대로 작성하겠다.

ADK 2.0의 세션 서비스는 비동기(async) 방식이며, 비동기 Postgres 드라이버를 기대한다. 구체적으로는 DATABASE_URL의 스킴(Scheme)이 변경된다.

postgresql://appuser:...@host/db # 1.x
postgresql+asyncpg://appuser:...@host/db # 2.0

여기까지는 간단하다. Secret을 수정하고 재배포하면 된다. 문제는, 이 동일한 URL을 비동기가 아닌 코드도 읽고 있다는 점이다. 우리 쪽은 독자적인 스토리지(토큰 저장, pending state 저장)를 순수 동기 SQLAlchemy로 구축해 두었는데, create_engine()+asyncpg를 이해하지 못한다. 2.0 URL을 전달하면, 동기 엔진이 비동기 드라이버를 import 하려고 시도하다가 기동 시점에 실패한다.

해결 방법은 작은 정규화 레이어(Normalization Layer)를 끼워 넣는 것뿐이다. URL은 비동기 형식으로 저장하고(주요 사용자가 ADK이므로), 동기 엔진을 만드는 곳에서 드라이버 지정을 제거한다.

from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
def _sync_db_url(db_url: str) -> str:
...

설계 의도로 강조하고 싶은 점은, Secret을 2개로 나누지 않고 URL은 1개로 유지하며 에지(Edge)에서 정규화한다는 판단이다. ADK에는 필요한 +asyncpg 형식을 전달하면서, 동기 측 사용자는 모두 create_db_engine()을 통해 드라이버 지정이 제거된 값을 받게 한다. replace(..., 1)을 사용하여 스킴 부분만 건드리기 때문에, 비밀번호에 우연히 같은 문자열이 섞여 있어도 깨지지 않는다. ADK 2.0과 함께 동기 DB 액세스가 남아 있다면, 이러한 종류의 심(Shim)은 필수적이다. 그렇지 않으면 비동기 URL이 create_engine()까지 흘러 들어가, 업그레이드와 무관해 보이는 기동 시점의 import 에러를 마주하게 된다.

이것이, 알아차리기 전에 dev 환경을 실제로 다운시킨 범인이다.

ADK 2.0은 events 테이블에 2개의 컬럼을 추가했다.

input_transcription jsonb
output_transcription jsonb

ADK 2.0은 세션 GET과 /run_sse 스트리밍 엔드포인트에서 이 컬럼을 무조건 읽는다. 1.x에서 만든 DB에는 컬럼이 없으므로, Postgres가 UndefinedColumnError를 던진다. 까다로운 점은 증상인데, 명확한 기동 크래시(Crash)로 이어지지 않는다는 것이다. 컨테이너는 정상적으로 기동되고 /health는 200을 반환한다. 그런데 채팅 턴(Turn)이 매번 500이 되고, 세션 읽기도 실패한다. dev 환경에서 정확히 이 현상을 재현했다. 컨테이너는 건강하지만, 채팅은 사망한 상태다.

해결 방법은 전방 호환(Forward Compatible)되는 ALTER TABLE배포보다 먼저 실행하는 것이다.

ALTER TABLE events ADD COLUMN IF NOT EXISTS input_transcription jsonb;
ALTER TABLE events ADD COLUMN IF NOT EXISTS output_transcription jsonb;

IF NOT EXISTS

IF NOT EXISTS를 사용하면 멱등성 (Idempotency)을 보장할 수 있고, Nullable 컬럼의 추가는 Postgres에서 테이블 재작성을 동반하지 않는 비차단 (Non-blocking) 작업이므로 가동 중인 DB에 적용해도 안전합니다. 순서가 중요하며, DB를 먼저 패치한 후 배포해야 합니다. 반대로 하면 새 이미지가 이전 스키마를 대상으로 동작하는 시간대가 발생하여 채팅 서비스가 중단될 수 있습니다.

Cloud SQL Auth Proxy를 경유할 경우 전체 패치는 다음과 같습니다.

cloud_sql_proxy -instances=PROJECT:asia-northeast1:INSTANCE=tcp:127.0.0.1:15433 &
PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -p 15433 -U appuser -d appdb <<'SQL'
ALTER TABLE events ADD COLUMN IF NOT EXISTS input_transcription jsonb;
...

롤백 (Rollback) 관점에서는 반가운 소식이 있습니다. 이 컬럼은 ADK 1.x 버전에서는 무시되므로, 추가하더라도 이전 버전이 망가지지 않습니다. 업그레이드를 최종 결정하기 전에 패치만 먼저 적용해 둘 수 있습니다.

1.x에서 생성한 DB의 경우, ADK 2.0은 기동 시 다음과 같은 메시지를 출력합니다.

The database is using the legacy v0 schema, which uses Pickle to serialize
event actions. The v0 schema will not be supported going forward and will be
deprecated in a few rollouts. Please migrate to the v1 schema which uses JSON
...

중요한 발견은 ADK 2.0은 v0 스키마를 읽고 쓸 수 있다는 점입니다. 이는 Deprecation (지원 중단 예정) 경고일 뿐, 강제적인 필수 요구사항은 아닙니다. 저희는 2.0을 v0 스키마 상태 그대로 운영하며 이전을 뒤로 미뤘습니다. 업그레이드와 이전은 독립적인 의사결정이며, 이를 분리할수록 리스크가 큰 배포 단위를 작게 만들 수 있습니다.

실제로 이전할 때의 핵심은 In-place (제자리) 방식으로는 불가능하다는 점입니다. 스키마 구조 자체가 다릅니다.

events 컬럼 |
|---|---|---|
| actions | bytea (Pickle) | 없음 |
| event_data | 없음 | jsonb (모든 이벤트 데이터) |
| 메타데이터 테이블 | 없음 | adk_internal_metadata |

v0는 이벤트의 actions를 개별 컬럼과 Pickle로 직렬화된 Blob으로 보유하지만, v1은 모든 것을 event_data jsonb 컬럼 하나로 통합합니다. 컬럼 구성이 바뀌기 때문에, ADK는 "한쪽 DB에서 읽어서 새로 만든 DB에 쓰는" 이전 명령어를 제공합니다.

# CREATE DATABASE는 트랜잭션 내에서 실행할 수 없으므로 단독으로 실행
psql ... -d postgres -c "CREATE DATABASE appdb_v1;"
SOURCE_URL="postgresql://appuser:${PW}@127.0.0.1:15433/appdb"
...

adk migrate session이 관리하는 대상은 ADK 자체의 4개 테이블인 app_states, user_states, sessions, events입니다. 직접 추가한 테이블(OAuth 토큰이나 앱 고유의 state 등)은 대상에서 제외되어 별도로 복사해야 하지만, 이는 ADK의 범위 밖이므로 이 글에서는 다루지 않습니다.

이전 후에는 대상 DB를 검증합니다.

# 1이 반환되면 v1
psql ... -d appdb_v1 -c \
"SELECT value FROM adk_internal_metadata WHERE key='schema_version';"
...

컷오버 (Cutover)는 연결 Secret을 새 DB로 향하도록 재배포하기만 하면 됩니다. 새로운 DB로 이전한 상태이므로 기존 DB는 무결하며, 롤백은 Secret을 되돌리기만 하면 됩니다. 확신이 들 때까지 데이터 손실이나 파괴적인 작업은 발생하지 않습니다.

요약하자면 다음과 같은 순서가 됩니다.

DB를 패치 (ALTER TABLE events ...

)— 500 에러의 늪에 빠지지 않기 위해, 무엇보다 먼저 수행합니다. -
URL을 전환하여 postgresql+asyncpg://로 변경합니다 (동기 측에서 제대로 되돌려 정규화하고 있는지 확인). -
2.0 이미지를 배포 (Deploy) 합니다. -
스모크 테스트 (Smoke Test): /health → 200, 기존 세션의 GET → 500이 발생하지 않음, 신규 /run_sse 채팅 → 응답이 스트리밍됨. -
(선택 사항·추후) v0 → v1을 새 DB로 마이그레이션하여 컷오버 (Cutover) 합니다.

.
로컬 클라이언트가 Cloud SQL 서버보다 버전이 낮으면 (예: client 16 vs server 17), pg_dump의 버전 차이로 인해 pg_dump를 통한 데이터 복사가 단순히 거부됩니다. 버전을 맞추거나 스크립트로 복사해야 합니다. -
.
트랜잭션 내에서 실행할 수 없으므로, CREATE DATABASE는 트랜잭션 외부에서 실행해야 합니다. BEGIN ... COMMIT 블록 안에 GRANT와 함께 묶지 말고, 단독 문장으로 실행하십시오. -
버전 간의 세션 호환성 (Session Compatibility). 2.0이 작성한 세션은 1.x (특히 오래된 1.x)에서 읽을 수 없을 가능성이 있습니다. 컷오버 후에 생성된 세션에 대해서는 다운그레이드 시 데이터 손실이 있다고 간주하고, 구형 이미지는 단기적인 긴급 탈출구로만 남겨둡니다. -
.
헬스 체크 (Health Check)의 200 응답은 스키마가 일치하는지는 아무것도 말해주지 않습니다. 실제 세션 읽기와 실제 채팅 턴을 통해 스모크 테스트를 수행하십시오. /health는 거짓말을 할 수 있습니다.

google-adk 2.0으로의 업데이트는 서류상으로는 작아 보이지만, 현장에서는 날카롭습니다. 비동기 (async) 드라이버로의 전환은 URL을 공유하는 동기 DB 코드에 파급 효과를 미치며, 새로운 events 컬럼은 패치 전에 배포할 경우 "정상적인 컨테이너 × 채팅 장애"라는 상황을 초래합니다. v0의 Deprecation 경고는 요란하지만 필수 사항은 아니므로, v0 상태로 운영하면서 자신의 일정에 맞춰 새 DB로 마이그레이션할 수 있습니다. 먼저 패치하고, URL은 에지 (Edge)에서 정규화하며, 실제 경로로 스모크 테스트를 수행하고, 스키마 마이그레이션은 별도의 프로젝트로 취급하십시오. 이것으로 충분합니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0