python sqlite3으로 db를 접근하는 과정(connection, cursor)

최지웅·2025년 9월 9일

RECO

목록 보기
4/10

connection: 데이터베이스와의 연결 통로

sqlite3.connect("my_db.db")를 호출하면 파이썬은 SQLite C라이브러리의 sqlite3_open()을 수행한다.

내부적으로 OS에게 db에 대한 file handle을 얻는다. 즉, 파일 접근 권한을 얻는 것인데 db인 만큼 작업 전에 원자성을 유지하기 위해 lock을 설정하여 다른 프로세스의 동시 수정을 방지한다.

다음으로 db연결 관리를 위한 (메타)데이터를 위해 sqlite3 C언어 구조체를 사용하는데, 이를 위한 메모리 공간을 우선 할당받는다. 해당 구조체 내부에는 db 파일 정보, 트랜잭션 상태(현재 lock), 에러 코드 등 연결에 대한 모든 상태 정보를 저장한다.

sqlite3 C언어 구조체가 lock상태로 생성되면, 파이썬은 해당 구조체를 가리키는 포인터의 wrapper인 Connection객체를 반환한다.

즉, Connection객체에는 db연결 관리를 위한 구조체의 메모리 주소를 가리키는 포인터를 가지고 있게된다. 이를 통해 트랜잭션 관리, 연결 종료 등 명령을 내리는 역활이 가능해진다.

Q. 구조체를 받는다고 기능을 수행하진 못할 텐데(sqlite3구조체는 db 메타정보만 갖고있으니) 실질적인 method실행은 누가 하는가? 그리고 os에게 초기 lock을 설정하는 주체는 누구인가?
A. SQLite C 함수가 수행. 이 때 이 수행 함수들의 첫 인자로 sqlite3 구조체를 가리키는 포인터가 전달된다. sqlite3_open()함수가 수행되면 내부적으로 운영체제 시스템 콜fcntl() in Unix/Linux를 사용하여 DB에 대한 잠금을 요청하고 획득한다. 즉, 모든 로우 레벨의 파일시스템 제어는 이 sqlite C 라이브러리가 전담한다.

cursor: SQL 실행을 위한 관리자

conn.cursor()를 호출하면 Connection객체는 Cursor객체를 생성하는데, 이때 Cursor는 DB와 직접 상호작용하는 것이 아닌, Connection과 상호작용한다.

cursor.execute("SELECT ...")를 호출하면 sqlite3은 내부적으로 SQLite C라이브러리의 sqlite3_prepare_v2()함수를 호출하여, 실행 이전에 실행 계획을 수립한다.

실행 계획은 아래와 같다.
SQL 문법을 확인하는 구문분석
-> 인덱스 등 효율적인 데이터 가져오는 최적화
-> SQLite가 SQL을 bytecode로 컴파일. 이때 플레이스 홀더? 는 "빈칸"으로 해석된다.

그 후 매개변수 바인딩을 시작한다. execute()시 전달한 인자는 sqlite3bind*()계열의 C함수를 통해 채워진다.

이 때 파이썬의 str, int와 같은 타입은 SQLite문법의 TEXT, INTEGER로 변환된다.
또한 입력값과 SQL문을 분류하기 위해 해당 값은 literal value(순수값)으로 취급하게하여 SQL Injection을 방지한다.

바인딩이 완료되면, sqlite3_step()함수가 VDBE(Virtual Database Engine)를 이용해 컴파일 된 바이트코드를 한단계 씩 실행시킨다.

SELECT문의 경우 결과 테이블의 행을 DB에서 내부 버퍼로 가져온다.

cursor.fetchone()이나 cursor.fetchall() 메서드는 sqlite3_step()을 반복적으로 호출하여, 내부 버퍼에 있는 데이터를 하나씩 혹은 전부 가져와 파이썬 객체(튜플 등)으로 변환하여 반환한다.

매개변수 개수 오류

sqlite3_prepare_v2()에서 처음 실행 계획을 세울 때, 바인딩 해야하는 파라미터의 개수를 파악하고 sqlite3_stmt(prepared statement)객체를 생성하여 기록해둔다.

파이썬 sqlite3래퍼는 execute에 인자로 넘긴 튜플의 길이를 확인한다.

sqlite3bind*()함수에서 값을 채우는 과정에서 불일치가 발생하면, SQL C라이브러리는 에러코드 SQLITE_MISUSE를 반환하고, 파이썬 래퍼가 이 에러코드를 해석하여 sqlite3.ProgrammingError: Incorrect number of binding supplied.를 발생시킨다.

Q. sqlite3래퍼가 execute인자로 넘긴 튜플의 길이를 확인한 값이, 어떤 과정을 통홰 sqlie3bind*()에게 전달되는가?
A. sqlite3bind*()함수에게 튜플의 길이가 직접 전달되지는 않는다.
sqlite3_prepare_v2()함수가 SQL문을 분석할 때, 플레이스 홀더 개수를 sqlite3_stmt 객체 내부에 저장학고, 이 값은 sqlite_bind_parameter_count()라는 C함수를 통해 확인할 수 있다.

동시에 파이썬 래퍼는 파이썬 C API 함수인 PyTuple_Size()를 이용해 execute시 인자로 전달된 튜플의 길이를 알아낸다.

이제 래퍼는 sqlite C 라이브러리가 알려주는 필요파라미터개수와 파이썬튜플로 제공된 인자 개수를 알아내어 비교한다. 이때 일치하지 않으면 래퍼는 sqlite3bind*()함수(매개변수 바인딩 C라이브러리)를 호출하지도 않고 즉시 파이썬 레벨에서 ProgrammingError 예외를 발생시킨다. 이게 우리가 보는 오류의 실체이다.

만약 파라미터 개수가 일치한다면, 래퍼는 튜플의 길이만큼 반복문을 돌며 각 요소(전달받은 파라미터)에 맞는 sqlite3_bind_text(), sqlite3_bind_int()를 타입에 맞게 C함수를 호출하여 바인딩한다.

결론

튜플의 길이 정보는 C 함수에 '값'으로 전달되는 것이 아니라, 파이썬 래퍼가 사전에 검사를 수행하고 C 함수를 올바른 횟수만큼 호출하는 '로직'을 제어하는 데 사용된다. 미리 sql을 수행하기 전에 파이썬 래퍼가 확인하는 것이다.

profile
이제 4학년!!!

0개의 댓글