Cloud SQL Postgres 백엔드에서 Google ADK를 2.0으로 업그레이드하기: 우리를 괴롭힌 세 가지 문제
요약
Google ADK 2.0 업그레이드 과정에서 발생한 비동기 드라이버 충돌, 스키마 변경, 데이터 마이그레이션 문제를 다룹니다. 특히 비동기 URL을 동기식 SQLAlchemy 엔진과 공유할 때 발생하는 오류와 해결 방법을 상세히 설명합니다.
핵심 포인트
- ADK 2.0은 asyncpg를 사용하여 연결 URL 스킴이 변경됨
- 비동기 URL을 동기식 엔진에서 사용할 경우 드라이버 접미사 제거 필요
- 새로운 이벤트 컬럼 누락 시 운영 환경에서 500 에러 발생 가능
- 레거시 Pickle 스키마에서 JSON 스키마로의 마이그레이션 고려 필요
우리는 Google의 Agent Development Kit (ADK)를 기반으로 구축된 에이전트를 운영하고 있으며, 이는 Cloud Run에 배포되어 ADK의 DatabaseSessionService를 통해 Cloud SQL (PostgreSQL) 세션 저장소를 사용합니다. google-adk를 1.x에서 >=2.0.0으로 올리는 작업은 단순한 한 줄의 의존성 변경처럼 보였습니다. 하지만 그렇지 않았습니다.
점점 더 미묘해지는 순서대로, 세 가지 문제가 우리를 괴롭혔습니다:
- ADK 2.0은 asyncpg를 통해 Postgres와 통신하며, 이로 인해 연결 URL (connection-URL) 변경이 강제됩니다. 그리고 이 URL은 동기(sync) 코드와 공유됩니다.
events테이블에는 ADK 2.0이 무조건적으로 읽어야 하는 **두 개의 새로운 컬럼 (columns)**이 필요합니다. 이 컬럼들 없이 배포하면 조용히 500 에러가 발생합니다.- 레거시 **v0 (Pickle) 스키마 (schema)**는 여전히 작동하지만, 지원 중단 경고 (deprecation warning)를 발생시킵니다. v1 (JSON)으로의 마이그레이션은 선택 사항이며, 제자리에서 (in place) 수행할 수 없습니다.
다음은 상세 보고서입니다.
1. 비동기 드라이버 전환 — 그리고 이제 동기 코드와 공유하게 된 URL
ADK 2.0의 세션 서비스는 비동기(async) 방식이며 비동기 Postgres 드라이버를 기대합니다. 실제로 이는 DATABASE_URL의 스킴 (scheme)이 변경됨을 의미합니다:
postgresql://appuser:...@host/db # 1.x
postgresql+asyncpg://appuser:...@host/db # 2.0
충분히 쉬운 일입니다. 시크릿 (secret)을 업데이트하고 다시 배포하면 됩니다. 문제는 동일한 URL을 비동기가 아닌 코드가 읽는다는 점입니다. 우리는 일반적인 동기 방식의 SQLAlchemy를 기반으로 구축된 커스텀 스토리지 (토큰 스토리지, 대기 상태 스토리지)를 가지고 있는데, create_engine()은 +asyncpg를 이해하지 못합니다. 여기에 2.0 URL을 전달하면, 동기 엔진에 비동기 드라이버를 임포트하려고 시도하다가 실패하게 됩니다.
해결책은 아주 작은 정규화 계층 (normalization layer)을 만드는 것입니다. (ADK가 주요 소비자이므로) 비동기 URL을 저장하되, 동기 엔진이 생성되는 시점에 드라이버 접미사 (suffix)를 제거합니다.
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
...
언급할 가치가 있는 설계 결정 사항은 두 개의 비밀값(secrets) 대신 에지(edge)에서 정규화된 하나의 URL을 사용하는 것입니다. ADK는 원하는 +asyncpg 형태를 가져가고, 모든 동기식 소비자(sync consumer)는 create_db_engine()을 거치며 드라이버 접미사(driver suffix)가 제거됩니다. replace(..., 1)은 스킴(scheme) 부분만 수정하므로, 해당 리터럴 하위 문자열을 포함하는 비밀번호도 안전합니다. 만약 ADK 2.0과 함께 동기식 DB 액세스를 사용하고 있다면, 이와 같은 심(shim)이 필요합니다. 그렇지 않으면 비동기 URL이 create_engine()으로 유출되어, 업그레이드와 무관해 보이는 임포트 에러(import error)가 시작 시점에 발생하게 됩니다.
2. 누락된 이벤트 컬럼 — 운영 환경에서의 조용한 500 에러
이 문제는 우리가 발견하기 전까지 개발 환경에서 실제로 서비스를 중단시켰던 문제입니다.
ADK 2.0은 events 테이블에 두 개의 컬럼을 추가했습니다:
input_transcription jsonb
output_transcription jsonb
ADK 2.0은 세션 GET 및 /run_sse 스트리밍 엔드포인트에서 이 컬럼들을 조건 없이 읽습니다. 만약 데이터베이스가 1.x 버전에서 생성되었다면 해당 컬럼들이 존재하지 않으며, Postgres는 UndefinedColumnError를 발생시킵니다. 증상은 명확한 시작 시점의 충돌이 아닙니다. 컨테이너는 정상적으로 부팅되고 /health는 200을 반환하지만, 모든 채팅 턴(chat turn)에서 500 에러가 발생하고 세션 읽기에 실패합니다. 우리는 개발 환경에서 이를 정확히 재현했습니다: 컨테이너는 정상(healthy)이지만, 채팅은 불능(dead)인 상태였습니다.
해결책은 2.0 이미지를 배포하기 전에 반드시 실행해야 하는 하위 호환 가능한 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를 사용하면 멱등성(idempotent)이 보장되며, Null 허용(nullable) 컬럼을 추가하는 것은 Postgres에서 블로킹(non-blocking) 작업입니다. 즉, 테이블 재작성(table rewrite) 없이 운영 중인 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'
...
롤백(rollback)에 관한 좋은 소식은 다음과 같습니다. 이 컬럼들은 ADK 1.x에서 무시되므로, 이를 추가하더라도 이전 버전이 깨지지 않습니다. 업그레이드를 확정하기 전에 미리 패치를 적용할 수 있습니다.
3. v0 → v1 스키마 마이그레이션은 선택 사항입니다 (그리고 아마도 미루고 싶을 것입니다)
시작 시, 만약 데이터베이스가 1.x 환경에서 생성되었다면 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) 경고일 뿐, 필수 요구 사항은 아닙니다. 저희는 v0 스키마에서 2.0을 실행하고 마이그레이션을 미루기로 결정했습니다. 업그레이드와 마이그레이션은 독립적인 결정이며, 이 둘을 분리함으로써 위험한 배포(deploy) 범위를 줄일 수 있습니다.
마이그레이션을 수행할 때 중요한 제약 사항은 제자리에서(in place) 수행할 수 없다는 점입니다. 스키마 구조가 근본적으로 다르기 때문입니다:
events 컬럼 | v0 | v1 |
|---|---|---|
actions | bytea (Pickle) | — |
| ... |
v0는 이벤트 액션(event actions)을 개별 컬럼과 피클링된 블롭(pickled blob)으로 저장하지만, v1은 모든 것을 하나의 event_data JSONB 컬럼으로 통합합니다. 컬럼 세트가 변경되기 때문에, ADK는 하나의 DB에서 읽어 새로 생성된 DB에 쓰는 마이그레이션 명령어를 제공합니다:
# CREATE DATABASE는 트랜잭션 내부에서 실행할 수 없습니다 — 별도의 문장으로 실행
psql ... -d postgres -c "CREATE DATABASE appdb_v1;"
...
adk migrate session은 ADK 자체의 4개 테이블인 app_states, user_states, sessions, events를 처리합니다. 사용자가 직접 추가한 항목(OAuth 토큰, 앱 전용 상태 등)은 건드리지 않으며 별도로 복사해야 합니다. 하지만 이는 ADK의 범위를 벗어나며 본 포스트의 주제도 아닙니다.
마이그레이션 후 대상 데이터베이스를 확인하십시오:
# 1은 v1을 의미합니다
psql ... -d appdb_v1 -c \
"SELECT value FROM adk_internal_metadata WHERE key='schema_version';"
...
새로운 DB의 연결 비밀값(connection secret)을 다시 지정하고 재배포함으로써 컷오버(Cut over)를 수행합니다. 새로운 데이터베이스로 마이그레이션했기 때문에 기존 데이터베이스는 손상되지 않은 상태로 유지됩니다. 따라서 롤백(rollback)은 단순히 비밀값을 다시 이전으로 지정하는 것만으로 가능합니다. 데이터 손실이 없으며, 확신이 들 때까지 파괴적인 단계(destructive step)를 거치지 않아도 됩니다.
실제로 작동하는 배포 순서
종합하자면, 전체 시퀀스는 다음과 같습니다:
- DB 패치 (
ALTER TABLE events ...) — 500 에러 발생 구간을 방지하기 위해 다른 무엇보다 먼저 수행합니다. - URL 전환을
postgresql+asyncpg://로 변경합니다 (이때 동기식 컨슈머(sync consumers)가 이를 다시 정규화하도록 확인해야 합니다). - 2.0 이미지를 배포합니다.
- 스모크 테스트 (Smoke test):
/health→ 200 응답, 기존 세션 GET → 500 에러 미발생, 새로운/run_sse채팅 → 응답 스트리밍 확인. - (선택 사항, 추후) v0에서 v1으로 새로운 DB에 마이그레이션하고 컷오버를 수행합니다.
주의해야 할 사항 (Gotchas)
pg_dump버전 불일치. 로컬 클라이언트가 Cloud SQL 서버보다 버전이 낮을 경우(예: 클라이언트 16 vs 서버 17), 데이터를 복사하기 위해pg_dump를 사용하지 마세요. 실행이 거부됩니다. 버전을 맞추거나 스크립트를 통해 복사해야 합니다.- 트랜잭션 외부에서의
CREATE DATABASE. 이 명령은 트랜잭션 내부에서 실행될 수 없으므로, 권한 부여(grants)와 함께BEGIN ... COMMIT블록에 묶지 말고 별도의 문장으로 실행해야 합니다. - 버전 간 세션 호환성. 2.0에서 작성된 세션은 1.x(특히 구버전 1.x)에서 읽을 수 없을 수 있습니다. 컷오버 이후 생성된 모든 세션에 대해 버전 다운그레이드를 데이터 손실이 발생하는 것으로 간주하고, 이전 이미지는 단기적인 탈출구(escape hatch)로만 유지하세요.
/health는 거짓말을 합니다. 헬스 체크(health check)의 200 응답은 스키마(schema)가 일치하는지에 대해 아무것도 말해주지 않습니다. 실제 세션 읽기와 실제 채팅 턴(chat turn)을 통해 스모크 테스트를 수행하세요.
요약
google-adk 2.0 업데이트는 서류상으로는 작아 보이지만 실제로는 매우 날카로운 변화를 가져옵니다. 비동기 드라이버 (async driver)로의 전환은 해당 URL을 공유하는 모든 동기식 DB (sync DB) 코드에 파급 효과를 미칩니다. 새로운 events 컬럼은 패치를 적용하기 전에 배포할 경우, 겉보기에 멀쩡해 보이는 컨테이너를 채팅 장애 상태로 만들어 버립니다. 그리고 v0 버전의 지원 종료 (deprecation) 경고는 요란하지만 시스템을 지탱하는 핵심 요소는 아닙니다. v0 버전에 머물면서 별도의 일정에 따라 새로운 DB로 마이그레이션할 수 있습니다. 먼저 패치를 적용하고, 에지 (edge)에서 URL을 정규화하며, 실제 경로에 대해 스모크 테스트 (smoke-test)를 수행하십시오. 그리고 스키마 마이그레이션 (schema migration)은 별도의 프로젝트로 취급하십시오.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기