Backend/Python

FastAPI + Oracle DB 연결 오류 — SQLAlchemy 1.4와 oracledb 3.x 버전 충돌 해결기

포도쿵야 2026. 4. 7. 14:23

FastAPI에서 Oracle DB를 연결하는 순간 이런 오류를 마주쳤다.

Can't load plugin: sqlalchemy.dialects:oracle.oracledb

설치도 다 되어 있고 연결 URL도 문제없어 보이는데 dialect를 왜 못 찾는다는 걸까. 오류 자체는 단순해 보이지만 원인을 이해하려면 SQLAlchemy, Oracle 드라이버, dialect 이 세 가지 개념을 먼저 알아야 한다.


📌 SQLAlchemy란

SQLAlchemy는 Python에서 가장 널리 쓰이는 데이터베이스 추상화 라이브러리다. 쉽게 말하면 Python 코드로 DB를 다룰 수 있게 중간에서 통역 역할을 하는 도구다.

SQLAlchemy 없이 DB를 쓰면 이렇게 된다.

conn = cx_Oracle.connect("user/pass@host:1521/service")
cursor = conn.cursor()
cursor.execute("SELECT * FROM users WHERE id = 1")
rows = cursor.fetchall()

SQLAlchemy를 쓰면 이렇게 된다.

user = session.query(User).filter(User.id == 1).first()

DB 종류에 상관없이 동일한 Python 코드로 MySQL, Oracle, PostgreSQL 등을 다룰 수 있다. DB를 바꿔도 코드를 대부분 그대로 유지할 수 있다는 게 핵심이다.

SQLAlchemy는 크게 두 가지 사용 방식이 있다.

방식 설명

Core SQL을 Python 코드로 표현. SQL을 직접 제어하고 싶을 때
ORM Python 클래스를 DB 테이블에 매핑. 객체를 다루듯 DB를 조작

FastAPI 프로젝트에서는 보통 둘을 혼용하거나, 복잡한 쿼리는 SQL 파일로 분리해서 Core 방식으로 실행한다.


📌 Python이 Oracle에 연결되는 구조

Python 코드가 Oracle DB에 실제로 접속하려면 드라이버(Driver) 가 필요하다. 드라이버는 Python과 DB 사이에서 통신을 담당하는 라이브러리다.

Python 코드
    ↓
SQLAlchemy      ← "어떤 DB인지, 어떻게 연결할지" 추상화
    ↓
드라이버        ← 실제 DB와 통신 (cx_Oracle 또는 oracledb)
    ↓
Oracle DB

SQLAlchemy가 실제로 DB에 접속할 때는 반드시 드라이버가 필요하다. SQLAlchemy는 어떤 드라이버를 쓸지 연결 URL 에서 판단한다.

oracle+cx_oracle://user:pass@host:1521/service   ← cx_Oracle 드라이버 사용
oracle+oracledb://user:pass@host:1521/service    ← oracledb 드라이버 사용

URL에서 oracle 뒤에 오는 부분(cx_oracle, oracledb)이 바로 dialect(방언) 다. SQLAlchemy가 이 이름을 보고 어떤 드라이버를 불러올지 결정한다.


📌 cx_Oracle과 oracledb — 뭐가 다른가

둘 다 Oracle이 공식 제공하는 Python용 Oracle DB 드라이버다.

cx_Oracle 은 오래된 이름이다. 2000년대부터 사용된 드라이버로, 오랫동안 Python-Oracle 연결의 표준이었다. SQLAlchemy를 포함한 대부분의 라이브러리가 cx_Oracle을 기준으로 지원을 구축했다.

oracledb 는 cx_Oracle의 후속 버전이다. 2022년에 Oracle이 cx_Oracle을 oracledb로 이름을 바꾸고 새롭게 배포했다. 기능적으로는 cx_Oracle의 상위 호환이고, 더 이상 cx_Oracle에 새로운 기능이 추가되지 않는다.

드라이버 상태 SQLAlchemy 호환

cx_Oracle 구버전, 지원 종료 예정 1.4 공식 지원
oracledb 신버전 (cx_Oracle 후속) 2.0 공식 지원

문제는 oracledb가 버전이 올라가면서 SQLAlchemy와 연결되는 방식 자체가 바뀌었다는 점이다.


📌 dialect란 무엇인가

SQLAlchemy는 여러 DB를 지원하기 때문에, 각 DB마다 SQL 문법과 드라이버 연결 방식이 다를 수 있다. 이 차이를 처리하는 모듈을 dialect(방언) 라고 부른다.

예를 들어 MySQL용 dialect, PostgreSQL용 dialect, Oracle용 dialect가 각각 따로 있고, Oracle 안에서도 어떤 드라이버를 쓰느냐에 따라 oracle+cx_oracle dialect, oracle+oracledb dialect가 나뉜다.

SQLAlchemy가 dialect를 불러오는 방식은 버전에 따라 다르다.

SQLAlchemy 1.4 — entry point 방식

  • 외부 패키지가 설치될 때 dialect를 entry point로 등록
  • SQLAlchemy가 그 entry point를 탐색해서 dialect를 로드
  • 드라이버 패키지가 entry point를 제공하지 않으면 찾지 못함

SQLAlchemy 2.0 — built-in 방식

  • 주요 dialect가 SQLAlchemy 패키지 안에 내장
  • 외부 패키지의 entry point 등록 없이도 동작

이 방식의 차이가 이번 오류의 직접 원인이다.


🔴 오류 발생 원인

문제가 된 환경의 패키지 버전 조합은 다음과 같았다.

패키지 버전 전제하는 방식

SQLAlchemy 1.4.52 entry point 방식으로 dialect 탐색
oracledb 3.2.0 SQLAlchemy 2.0 built-in 방식만 지원

이 두 버전은 서로 다른 dialect 로딩 방식을 전제하고 있다.

SQLAlchemy 1.4  →  "oracle+oracledb dialect를 entry point로 찾는다"
oracledb 3.x    →  "entry point? 2.x에서 없앴는데. SQLAlchemy 2.0 built-in 써"

결과: Can't load plugin: sqlalchemy.dialects:oracle.oracledb

정상 동작하는 버전 조합

SQLAlchemy 1.4  +  oracledb 1.x / 2.x   → entry point 방식 호환 ✅
SQLAlchemy 2.0  +  oracledb 3.x          → built-in dialect 방식 ✅
SQLAlchemy 1.4  +  oracledb 3.x          → 충돌 ❌  ← 현재 환경

✅ 해결 — cx_Oracle 호환 모드 활용

버전을 올리거나 내리지 않고 코드만 수정해서 해결할 수 있다.

oracledb에는 cx_Oracle 호환 모드가 내장되어 있다. 이 모드를 활용해서 oracledb를 cx_Oracle인 척 등록하면, SQLAlchemy 1.4가 이미 내장 지원하는 oracle+cx_oracle dialect를 그대로 사용할 수 있다.

Python의 sys.modules는 "모듈 이름 → 실제 모듈 객체" 매핑 테이블이다. 여기에 직접 등록하면 import cx_Oracle 구문이 실행될 때 실제로는 oracledb가 반환된다.

동작 흐름

sys.modules["cx_Oracle"] = oracledb 등록
        ↓
SQLAlchemy가 oracle+cx_oracle dialect 탐색
        ↓
내부적으로 import cx_Oracle 실행
        ↓
sys.modules에서 oracledb 반환
        ↓
연결 성공 ✅ (실제로는 oracledb가 처리)

변경 파일: utils/db_connection.py

import sys
import oracledb

# ① oracledb를 cx_Oracle 이름으로 sys.modules에 등록
# SQLAlchemy 1.4의 버전 체크(cx_Oracle 5.2 이상 요구)를 통과시키기 위해
# version을 8.3.0으로 지정 (실제 동작과 무관)
oracledb.version = "8.3.0"
sys.modules["cx_Oracle"] = oracledb

# ② 연결 URL의 dialect를 cx_oracle로 변경
# Before: oracle+oracledb://  (SQLAlchemy 1.4에서 entry point 없음)
# After:  oracle+cx_oracle://  (SQLAlchemy 1.4 내장 지원)
connection_url = f"oracle+cx_oracle://{encoded_user}:{encoded_password}@..."

oracledb.version = "8.3.0" 이 필요한 이유 SQLAlchemy 1.4는 cx_Oracle을 import한 뒤 버전이 5.2 이상인지 체크한다. oracledb의 실제 버전(3.2.0)은 이 조건을 통과하지 못하기 때문에, version 속성만 8.3.0으로 강제 지정해서 버전 검사를 통과시키는 것이다. 실제 동작과는 무관하다.

변경 전후 비교

항목 변경 전 변경 후

연결 URL oracle+oracledb:// oracle+cx_oracle://
dialect 방식 entry point (oracledb 3.x 미지원) SQLAlchemy 1.4 내장
실제 드라이버 oracledb 3.2.0 oracledb 3.2.0 (동일)
추가 패키지 설치 - 없음
DB2 / MySQL 영향 - 없음

⚠️ 이 방법은 임시 우회다

sys.modules에 직접 등록하고 버전을 위장하는 방식은 내부 구현에 의존하는 패치다. 라이브러리 업데이트에 따라 언제든 깨질 수 있다. 두 가지 근본 해결 방향이 있다.

✅ [권장] SQLAlchemy 2.0으로 업그레이드

  • oracledb 3.x와 공식 호환. oracle+oracledb dialect를 built-in으로 지원
  • 단, 1.4 → 2.0 사이에 API 변경사항이 있어서 기존 코드 검토 필요
  • session.execute() 반환 방식 등이 달라짐
  • 장기적으로 가장 안전한 선택

[대안] oracledb 1.x / 2.x로 다운그레이드

  • SQLAlchemy 1.4와 공식 호환. 코드 변경 최소화
  • oracledb 신기능 사용 불가
  • 공유 서버 환경에서는 다른 패키지에 영향을 줄 수 있음

공유 서버 환경 주의사항 여러 서비스가 동일한 Python 환경을 공유하는 경우, 버전 변경은 다른 서비스에도 영향을 미칠 수 있다. 가능하다면 venv(가상 환경)나 컨테이너로 서비스별 의존성을 격리하는 것이 근본적인 해결책이다.


🚀 실제 해결 — CI/CD 재배포로 자연스럽게 정리됨

cx_Oracle 우회 코드를 작성한 뒤, Bamboo CI/CD를 통해 재배포를 진행했다. 그런데 재배포 이후 우회 코드 없이도 정상 동작하는 것을 확인했다. 이유는 requirements.txt에 있었다.

# requirements.txt
SQLAlchemy==2.0.36   ← 이미 2.0으로 명시되어 있었음
oracledb==3.2.0

사건의 순서를 재구성하면 이렇다.

① 최초 오류 발생
   서버에 SQLAlchemy 1.4.52 설치된 상태
   → oracledb 3.x와 충돌 → Can't load plugin 오류

② cx_Oracle 우회 코드 작성 (임시 해결)

③ Bamboo CI/CD로 재배포
   pip install -r requirements.txt 실행
   → requirements.txt에 SQLAlchemy==2.0.36 명시되어 있었으므로
   → 서버에 2.0.36 설치됨

④ SQLAlchemy 2.0 + oracledb 3.x = 공식 호환 조합
   → oracle+oracledb:// dialect 그대로 사용 가능
   → cx_Oracle 우회 코드 불필요

서버에 실제로 어떤 버전이 설치되어 있는지 확인하는 명령어는 다음과 같다.

pip show sqlalchemy | grep Version
# Version: 2.0.36

결국 requirements.txt는 맞게 관리되고 있었다. 문제는 배포가 누락된 채 서버에 구버전이 남아있었던 것이다.


⚠️ 배포 누락이 만들어내는 유령 오류

이번 케이스에서 핵심적인 교훈은 라이브러리 버전 충돌 자체보다, "배포가 제대로 됐는지"를 먼저 확인해야 한다는 점이다.

requirements.txt를 아무리 잘 관리해도 서버에 실제로 반영되지 않으면 의미가 없다. 특히 다음 상황에서 이런 문제가 자주 생긴다.

  • 수동 배포 환경에서 pip install -r requirements.txt 실행을 빠뜨린 경우
  • CI/CD 파이프라인에서 특정 스텝이 실패했는데 눈치채지 못한 경우
  • 여러 서버가 있는 환경에서 일부 서버에만 반영된 경우
# 배포 후 반드시 서버에서 실제 설치 버전 확인
pip show sqlalchemy | grep Version
pip show oracledb | grep Version

# requirements.txt와 대조
cat requirements.txt | grep -i sqlalchemy
cat requirements.txt | grep -i oracledb

⚠️ 오류 메시지가 라이브러리 버전을 가리키고 있다면, 코드를 수정하기 전에 먼저 서버에 실제로 설치된 버전을 확인하자. requirements.txt에 올바른 버전이 명시되어 있어도 배포가 누락됐다면 구버전이 그대로 남아있을 수 있다.


💡 이 트러블슈팅에서 배운 것들

① SQLAlchemy의 dialect 로딩 방식이 버전마다 다르다 1.4는 entry point 방식, 2.0은 built-in 방식이다. 드라이버 라이브러리가 어떤 방식으로 dialect를 제공하는지 확인하지 않으면 이런 충돌이 생긴다.

② cx_Oracle과 oracledb는 같은 Oracle이 만든 드라이버다 이름이 달라서 완전히 다른 것처럼 보이지만 oracledb는 cx_Oracle의 후속 버전이다. 내부적으로 호환 모드도 제공하고 있어서 이번처럼 우회 처리가 가능하다.

③ sys.modules를 이용한 모듈 위장 패턴 Python의 import 시스템은 sys.modules를 먼저 조회한다. 여기에 직접 등록하면 import 구문 자체를 가로챌 수 있다. 호환 레이어 구현에 쓰이는 패턴이지만, 내부 동작에 의존하기 때문에 임시 방편으로만 사용해야 한다.

④ 배포 누락이 유령 오류를 만든다 코드와 requirements.txt가 올바르더라도 서버에 반영되지 않으면 의미 없다. 라이브러리 관련 오류가 발생하면 코드를 고치기 전에 서버에 실제 설치된 버전부터 확인하는 습관이 중요하다.

⑤ 의존성 버전 조합은 명시적으로 관리해야 한다 오류 메시지만 보면 "dialect를 못 찾는다"는 단순한 문제처럼 보이지만, 실제 원인은 두 라이브러리가 서로 다른 SQLAlchemy 버전을 전제하고 설계되어 있었기 때문이다. requirements.txt나 pyproject.toml에 버전 범위를 명시해두는 것이 이런 문제를 예방하는 방법이다.


📋 정리

항목 내용

오류 Can't load plugin: sqlalchemy.dialects:oracle.oracledb
원인 oracledb 3.x는 SQLAlchemy 2.0 built-in 방식만 지원. SQLAlchemy 1.4는 entry point 방식으로 탐색 → 충돌
임시 해결 sys.modules["cx_Oracle"] = oracledb 등록 + oracle+cx_oracle:// dialect 사용. 추가 패키지 설치 없음
version 위장 이유 SQLAlchemy 1.4의 cx_Oracle 버전 체크(5.2 이상) 통과 목적. 실제 동작과 무관
근본 해결 SQLAlchemy 2.0 업그레이드(권장) 또는 oracledb 1.x/2.x 다운그레이드
실제 해결 CI/CD 재배포로 requirements.txt의 SQLAlchemy==2.0.36이 서버에 반영됨. 우회 코드 불필요
핵심 교훈 라이브러리 오류 발생 시 코드보다 서버 실제 설치 버전을 먼저 확인. 배포 누락이 원인일 수 있다