PostgreSQL을 이용하여 데이터베이스를 구축
1. Windows에서는 정상
2. Linux (Ubuntu) 에서는 아래 오류 발생하는 경우
DB insert error: (psycopg2.DatabaseError) error with status PGRES_TUPLES_OK and no message from the libpq
작업 환경과 사용 라이브러리는 다음과 같다
OS: Ubuntu 22.04
DB 연결: psycopg2-binary
ORM: sqlalchemy
위 에러가 발생하는 코드는 insert query후 commit을 하는 단계에서 발생했다.
문제는 Windows에서는 발생하지 않는데, Ubuntu에서만 발생하는 에러라는 점이다.
insert commit 단계에서 발생하는 에러임에도 insert는 정상적으로 동작한다.
위 에러는 DB 단에서 발생해서 insert가 실패한 것이 아니라, PGRES_TUPLES_OK 라는 상태 코드처럼,
posgres 단에서 성공적으로 query가 수행됐다는 의미를 갖는다.
그러나 psycopg2와 DB간에 동기화가 되지 않아서 발생하는 문제이다.
그런데, 주의할 점은 PGRES_TUPLES_OK 상태 코드는, insert에 대한 성공이 아니라, 데이터를 가져올 수 있는 query에 대한 상태 코드이다. (아래 정리 참고)
| 상태 코드 | 의미 | 주로 발생하는 쿼리 |
| PGRES_COMMAND_OK | 명령 성공 | INSERT, UPDATE, DELETE |
| PGRES_TUPLES_OK | 결과 데이터 준비됨 | SELECT, FETCH, SHOW |
| PGRES_FATAL_ERROR | 심각한 에러 | 구문 오류, 제약 조건 위반 등 |
| PGRES_COPY_OUT | 서버에서 데이터 복사 시작 | COPY TO |
| PGRES_COPY_IN | 클라이언트에서 데이터 복사 시작 | COPY FROM |
## PostgreSQL 주요 결과 상태 (ExecStatusType)
### 1. 성공적으로 완료되었을 때 ✅
- PGRES_COMMAND_OK
- 의미: "명령이 성공적으로 완료되었습니다."
- 발생 쿼리: 데이터를 반환하지 않는 모든 DML/DDL 명령.
- 예시: INSERT, UPDATE, DELETE, CREATE TABLE, ALTER TABLE 등.
- PGRES_TUPLES_OK
- 의미: "결과 데이터(튜플/행)가 준비되었으니 가져가세요."
- 발생 쿼리: 행 데이터를 반환하는 모든 쿼리.
- 예시: SELECT, SHOW, FETCH 등. (결과 행이 0개인 SELECT도 이 상태를 반환합니다.)
### 2. 에러가 발생했을 때 ❌
- PGRES_FATAL_ERROR
- 의미: "쿼리 처리가 완전히 실패했습니다."
- 발생 상황: 구문 오류(SyntaxError), 존재하지 않는 테이블 접근, 제약 조건 위반(예: UNIQUE 키 중복) 등 대부분의 데이터베이스 에러.
- PGRES_BAD_RESPONSE
- 의미: "서버로부터 이해할 수 없는 응답을 받았습니다."
- 발생 상황: 네트워크 문제나 서버/클라이언트 간 프로토콜 버전 불일치 등. fork로 인한 통신 오염도 이와 유사한 문제를 일으킬 수 있습니다.
### 3. 특별한 작업을 위한 상태 ⏳
- PGRES_COPY_OUT
- 의미: "지금부터 서버가 대용량 데이터를 복사해서 보낼 것입니다."
- 발생 쿼리: COPY ... TO STDOUT 명령 실행 시. 이 신호를 받은 클라이언트는 복사 데이터를 수신할 준비를 해야 합니다.
- PGRES_COPY_IN
- 의미: "이제 클라이언트가 대용량 데이터를 보내야 합니다."
- 발생 쿼리: COPY ... FROM STDIN 명령 실행 시. 이 신호를 받은 클라이언트는 서버로 데이터를 스트리밍할 준비를 해야 합니다.
- PGRES_EMPTY_QUERY
- 의미: "아무 쿼리도 받지 못했습니다."
- 발생 상황: 빈 문자열("")을 쿼리로 보냈을 때.
문제는 insert후, 왜 select 등에 해당하는 상태코드가 오는 문제가 발생하냐 이다.
문제의 상황은 다음과 같다.
해당 프로젝트는 하나의 repository안에서 여러가지 프로세스를 구동시킨다.
from multiprocessing import Process
main_process = Process(target=main)
main_process.start()
sub_process_1 = Process(target=sub_1, args=("sub", "sub1"))
sub_process_1.start()
sub_process_2 = Process(target=sub_2, args=("sub", "sub2"))
sub_process_2.start()
그리고 db 구성과 관련하여 create_engine을 repository 단에서 실행하고,
해당 db를 코드상으로 프로세스들이 공유하는 구조이다.
문제는 프로세스가 실행되는 방식에 있었다.
Windows에서는 오류가 나지 않는데,
Windows는 각 프로세스들이 실행될 때, 독립적으로 생성 및 실행이 된다.
이는 일반인의 입장에서 가장 직관적인 프로세스 생성 방식일 것이다.
그런데 Ubuntu는 위와 같이 실행이 되지 않고,
부모 프로세스의 메모리를 자식 프로세스에서 복사해서 사용한다.
이게 무슨 소린가 하면, 해당 repository가 프로세스로 실행이 되면서 가장 먼저 실행되기에 부모 프로세스가 되고,
내부에서 실행되는 프로세스들은 부모 프로세스의 메모리를 복사해서 생성이 되는 자식 프로세스가 된다.
Windows에서와 같이 프로세스가 생성되는 방식을 'spawn' 방식이라 하며,
Linux에서와 같은 방식을 'fork' 방식이라고 한다.
fork 방식을 통해 생성을 하는 경우, Database connection과 관련하여 문제가 생긴다.
부모 프로세스와 자식 프로세스가 같은 connection pool을 사용하게 되고,
부모 프로세스에서 insert query후 psycopg2에서는 PGRES_COMMAND_OK 에 해당하는 데이터 스트림을 받아야 하는데, 같은 채널(connection)을 사용하는 자식 프로세스에서 Insert 후, Select를 하는 쿼리가 발생하여, psycopg2 내부 상태코드가 PGRES_TUPLES_OK가 되게 된다.
그리고 부모 프로세스는 PGRES_COMMAND_OK에 해당하는 데이터 스트림이 아닌, PGRES_TUPLES_OK에 해당하는 데이터를 받게 되면서, 프로토콜에 맞지 않는 데이터가 들어왔음으로 판단하고 에러가 발생하게 된다.
위와 같이 여러 프로세스를 통한 DB 접근을 할 때, 프로세스가 생성되는 방식이 달라서 에러가 생기게 되었다.
해결책은 어떤게 있을까?
크게 세가지로 나눌수가 있다.
1. NullPool 사용
2. 프로세스 생성 방식을 Windows에서와 같이 'spawn' 방식을 사용
3. Queue - DB 트랜잭션을 단독으로 사용하는 프로세스를 별도 생성하여, 해당 프로세스를 통해서만 SQL을 사용하는 방식
NullPool의 경우, 매 쿼리마다 연결을 끊고 맺고를 반복하기 때문에, sql 성능 저하가 있을 수 있다.
단, connection pool과 관련된 오류를 잠정적으로 해결할 수 있으므로 안정성 측면에서 도움이 된다.
2. spawn 방식 사용. connection pool의 이점을 살릴 수 있다. 단, connection pool 관리를 제대로 하지 않으면 max_connection 설정값 이상의 connection이 생기면서 오류가 발생하는 등 side effect 를 고려해야 한다.
3. 가장 이상적인 방식이라 볼수 있다. QueuePool의 성능을 가져오면서, fork의 프로세스 방식을 해결할 수 있는 방식이다.
multiprocessing의 Queue를 사용할 수도 있고, reddis 등을 사용할 수 도 있다.
단, 해당 프로세스 queue에 담기 위한 작업이 선행되어야 하기 때문에 처음부터 고려되지 않았다면, 코드 전환의 비용이 있을 수 있다. (DB 트랜잭션을 다루기 위한 process 생성, 각 쿼리마다 해당 프로세스 queue에 task를 담는 작업, 데이터 직렬화 등)
** fork 방식이 아닌 vfork 방식을 사용하도록 코드를 구성할 수도 있다.
multiprocessing의 Process가 아닌 subprocess를 사용
구체적인 코드 변경은 다음과 같다 (비공개 글)
https://taltal-dev-note.tistory.com/530
(multiprocessing의 Process는 fork-exec 방식, subprocess 방식은 python 3.8 이후 버전부터 사용가능하며 vfork 방식)
참고:
https://gemini.google.com/app/e452c28178266a4d