PostgreSQL 캐스트를 붙인 :today::text가 today가 아닌 toda로 인식돼, 서버 접속 없이 바인드 목록을 확인하고 CAST(:today AS text)로 수정했다. SQLAlchemy text()에 SELECT to_jsonb(:today::text)를 넣고 바인드 목록을 출력했다. today가 나올 줄 알았는데, 목록에는 toda가 들어 있었다.
전제 환경 — Python 3.12 / uv 임시 환경에 sqlalchemy>=2 설치 / 기록에서 확인된 문제 범위: SQLAlchemy 2.1
한 줄 답 — SQLAlchemy 2.1에서 :today::text는 바인드 이름이 toda로 잘리는 클라이언트 파싱 문제였으며, CAST(:today AS text)로 바꿔 today가 온전히 인식되도록 고쳤다.
먼저 결론
- DB 실행 전에 text()가 만든 _bindparams를 확인한다.
- :today::text를 CAST(:today AS text)로 바꾼다.
- 설치 버전과 수정된 표현의 바인드 이름을 함께 확인한다.
전체 그림
서버에 전달하기 전의 실패 지점
[텍스트 SQL: :today::text]
→ [SQLAlchemy text() 파싱]
→ [바인드 이름 toda: 실패 지점]
[텍스트 SQL: CAST(:today AS text)]
→ [SQLAlchemy text() 파싱]
→ [바인드 이름 today 유지]
→ [이후 DB 실행: 이 기록의 검증 범위 밖]
무엇이 문제였나
PostgreSQL의 축약 캐스트 문법처럼 보이더라도, 텍스트 SQL은 서버에 도달하기 전에 SQLAlchemy의 text() 파서를 거친다. 이 파서는 콜론을 바인드 변수 표식으로 처리한다. 처음에는 PostgreSQL에서 흔히 쓰는 표현이니 그대로 사용할 수 있다고 봤다. 하지만 text() 직후의 _bindparams를 출력해 보니, 서버로 보내기도 전에 이름이 잘려 있었다.
확인한 대상은 SELECT to_jsonb(:today::text)와 SELECT to_jsonb(CAST(:today AS text))다. 서버의 SQL 실행 오류가 아니라 파싱 직후의 바인드 목록을 비교했다. 기록에서 확인한 정확한 결론은 “SQLAlchemy 2.1에서 :today::text는 파라미터 이름이 toda로 잘못 잡힌다”였다. 다른 SQLAlchemy 버전이나 드라이버에서도 동일하다는 검증 결과는 없다.
처음엔 이렇게 봤다
답부터 보면 당연해 보이지만, 처음 몇 시간은 여기에 썼습니다.
| 처음 의심한 것 | 그렇게 본 이유 | 아니라고 판단한 근거 |
|---|---|---|
| :today::text를 그대로 사용할 수 있다고 판단했다. | PostgreSQL에서 흔히 보이는 캐스트 표현이므로 바인드 변수에도 그대로 적용할 수 있어 보였다. | text() 직후 _bindparams를 출력한 비교 결과는 다음과 같았다. 'SELECT to_jsonb(:today::text)' -> bind params: ['toda'] 'SELECT to_jsonb(CAST(:today AS text))' -> bind params: ['today'] 축약 캐스트에서는 today가 아니라 toda로 인식돼 서버 실행 전부터 이름이 잘려 있었으므로, 그대로 사용할 수 있다는 판단을 배제했다. |
무엇이 달라졌나
바인드 변수 바로 뒤의 축약 캐스트를 명시적 CAST로 교체
이전
-- 기록상 바인드 이름이 toda로 인식됨
SELECT to_jsonb(:today::text);
바꾼 뒤
-- today가 온전한 바인드 이름으로 인식됨
SELECT to_jsonb(CAST(:today AS text));
파라미터
| 이름 | 값 | 무엇을 정하는가 |
|---|---|---|
--python |
3.12 |
임시 검증 환경의 Python 버전 |
--with |
sqlalchemy>=2 |
임시 환경에 설치할 의존성 조건. SQLAlchemy 2.1 고정 아님 |
작업 순서
1. 두 표현의 바인드 목록을 비교한다
이걸 먼저 보는 이유는 데이터 값이나 PostgreSQL 함수를 살피기 전에, SQLAlchemy가 이름을 제대로 읽었는지 확인할 수 있기 때문이다. 실제로 DB 접속 없이 여기서 원인을 좁혔다. 축약 캐스트에서 ['toda'], CAST 표현에서 ['today']가 나오면 다음 단계에서 SQL을 교체한다.
진단 — uv를 사용할 수 있는 환경에서 실행: <작업디렉터리>는 실제 작업 경로로 교체
cd <작업디렉터리>
uv run --quiet --no-project --python 3.12 --with 'sqlalchemy>=2' python -c "
from sqlalchemy import text
for sql in [
'SELECT to_jsonb(:today::text)',
'SELECT to_jsonb(CAST(:today AS text))',
]:
statement = text(sql)
print(repr(sql), '-> bind params:', list(statement._bindparams))
"
출력 예
'SELECT to_jsonb(:today::text)' -> bind params: ['toda']
'SELECT to_jsonb(CAST(:today AS text))' -> bind params: ['today']
실패하면 — 출력이 다르면 다음 단계에서 출력하는 SQLAlchemy 버전을 확인한다. sqlalchemy>=2는 2.1을 고정하는 조건이 아니므로, 다른 버전의 결과를 이 기록과 같다고 단정하지 않는다.
2. 축약 캐스트를 CAST로 바꾸고 버전과 바인드 이름을 확인한다
SQL만 보면 둘 다 캐스트 표현이라, 고친 뒤에도 바인드 목록으로 확인했다. CAST 표현에서 ['today']가 나오고 설치 버전이 남으면 이 기록의 파싱 검증은 끝이다. 아래 비교 명령은 애플리케이션의 SQL 변경 여부나 DB 실행 성공까지 확인하는 명령은 아니다.
조치 — 애플리케이션에서 text()에 전달하는 SQL 교체
-- :today::text 대신 명시적 CAST 사용
SELECT to_jsonb(CAST(:today AS text));
검증 — 같은 작업 디렉터리에서 실행: <작업디렉터리>는 실제 작업 경로로 교체
cd <작업디렉터리>
uv run --quiet --no-project --python 3.12 --with 'sqlalchemy>=2' python -c "
import sqlalchemy
from sqlalchemy import text
print('sqlalchemy', sqlalchemy.__version__)
for sql in [
'SELECT to_jsonb(:today::text)',
'SELECT to_jsonb(CAST(:today AS text))',
]:
statement = text(sql)
print(repr(sql), '-> bind params:', list(statement._bindparams))
"
출력 예
# 첫 줄에 sqlalchemy와 설치 버전이 출력되는지 확인
# 아래는 SQLAlchemy 2.1에서 확인한 바인드 목록
'SELECT to_jsonb(:today::text)' -> bind params: ['toda']
'SELECT to_jsonb(CAST(:today AS text))' -> bind params: ['today']
실패하면 — 수정한 SQL에 :today::text가 남아 있는지 확인한다. 비교 결과가 기록과 다르면 설치 버전을 먼저 대조한다.
끝내고 확인할 것
- 애플리케이션의 해당 SQL이 CAST(:today AS text)를 사용한다.
- 수정된 표현의 bind params 출력이 ['today']다.
- 확인 명령 첫 줄에 설치된 SQLAlchemy 버전이 출력된다.
증상과 대처
| 현상 | 원인 | 대응 |
|---|---|---|
| :today::text의 바인드 파라미터 이름이 today가 아니라 toda로 인식됨 | SQLAlchemy 텍스트 SQL 파서와 바인드 바로 뒤의 PostgreSQL 축약 캐스트 문법 충돌 | CAST(:today AS text)로 바꾸고 _bindparams를 확인한다. |
롤백
되돌리기 명령은 기록에 없다. 원래의 :today::text로 복원하면 확인된 바인드 이름 문제가 다시 생기므로 복원을 해결책으로 사용하지 않는다.
요점
- DB 고유 문법과 바인드 표식을 함께 쓴 텍스트 SQL은 서버 오류를 추측하기 전에 SQLAlchemy가 인식한 바인드 목록부터 확인한다.
자주 묻는 것
SQLAlchemy에서 :today::text의 바인드 이름이 toda로 나오는 이유는?
SQLAlchemy 2.1의 text() 파서가 해당 표현에서 바인드 이름을 toda로 인식했기 때문이다. 이 기록에서는 DB에 접속하지 않고 _bindparams 출력으로 확인했다.
SQLAlchemy 바인드 변수에 PostgreSQL text 캐스트를 어떻게 붙이나요?
이 사례에서는 :today::text를 CAST(:today AS text)로 바꿨다. SELECT to_jsonb(CAST(:today AS text))의 바인드 목록은 ['today']로 확인됐다.
이 문제가 SQLAlchemy 2 버전 전체에서 발생하나요?
이 기록만으로는 SQLAlchemy 2 계열 전체에 일반화할 수 없다. 확인된 범위는 SQLAlchemy 2.1의 해당 표현이며, 다른 버전이나 드라이버의 동일 동작은 검증하지 않았다.
'시스템 > Devops' 카테고리의 다른 글
| Terraform Redis→Valkey 전환 plan에 Aurora 변경이 섞일 때, 모듈 버전부터 고정한다 (0) | 2026.10.07 |
|---|---|
| Capacitor Android 게임 부팅 실패를 막는 네이티브 초기화 오류 격리 (0) | 2026.10.06 |
| LLM 문항이 학년 범위를 벗어나면 입력 API부터 비평 요청까지 확인한다 (0) | 2026.10.02 |
| LLM 초안에 원본 기록 없는 숫자·인용이 생길 때 — 코드 측정과 원본 대조 (0) | 2026.09.29 |
| JSON Schema 런북 출력이 숫자만 남을 때 — expect 제한을 400자로 늘리기 (0) | 2026.09.28 |