PgBouncer 트랜잭션 풀링: advisory lock의 수명부터 확인하기

PgBouncer의 트랜잭션 풀링에서는 세션 수준 advisory lock에 의존하지 말자. 짧은 데이터베이스 갱신을 보호하려면 잠금 획득과 실제 작업을 하나의 트랜잭션에 넣고, 트랜잭션 수준 잠금의 수명을 맞춰야 한다. autocommit으로 잠금 SELECT만 따로 실행하면 뒤의 작업은 보호되지 않는다.

2026년 10월 9일 기준 PgBouncer와 PostgreSQL 18 공식 문서를 확인한 설계 가이드다. 최신 릴리스 소식이 아니며, 아래 SQL은 문서로 검토한 구조 예시다. 실제 서버에서 실행하거나 장애 상황을 재현하지는 않았다.

1. 연결 객체를 유지해도 서버 세션은 달라질 수 있다

PgBouncer의 transaction pooling은 트랜잭션이 진행되는 동안 서버 연결을 클라이언트에 할당하고, 트랜잭션이 끝나면 풀로 돌려보낸다. 다음 트랜잭션에서도 같은 서버 연결을 받는다는 보장은 없다. 공식 기능 표에서도 session-level advisory lock은 이 모드에서 지원하지 않는 기능으로 분류한다. PgBouncer 풀링 모드별 기능 표

이를 코드 리뷰에 적용하면 질문이 달라진다. “같은 데이터베이스 클라이언트를 사용했는가?”만 물어서는 부족하다. “그 클라이언트가 실제로 하나의 트랜잭션을 유지하는가?”, “잠금을 잡은 뒤 커밋하고 다음 쿼리에서 작업을 이어가지는 않는가?”를 함께 확인해야 한다. 클라이언트 객체의 생존 기간으로 서버 세션의 생존 기간을 추정하면 풀링 계층을 놓치기 쉽다.

가상의 잘못된 흐름을 순서대로 따라가 보자. 첫 번째 요청이 서버 세션 A에서 세션 잠금을 얻은 다음 트랜잭션을 끝낸다. 이후 작업 쿼리나 해제 쿼리가 서버 세션 B로 전달될 수 있다. 이때 애플리케이션 입장에서는 같은 함수의 시작과 끝이어도 잠금을 관리하는 PostgreSQL 세션은 달라진다. 이 설명은 문서의 연결 할당 규칙에서 도출한 실패 시나리오이며, 특정 설정에서 관측한 실험 결과는 아니다.

따라서 transaction pooling으로 전환하는 변경은 연결 수 설정만의 문제가 아니다. 배포 전에 코드에서 세션에 기대는 동작을 찾아야 한다. 특히 잠금 획득과 해제를 서로 다른 헬퍼에 숨겨 두었다면 호출 관계부터 펼쳐 보는 편이 좋다. 정상 경로에서 두 함수가 모두 호출된다는 사실만으로 올바른 해제를 입증할 수는 없다.

2. 짧은 데이터베이스 작업은 트랜잭션과 잠금 수명을 맞춘다

PostgreSQL의 세션 수준 advisory lock은 명시적으로 해제하거나 세션이 끝날 때까지 유지된다. 트랜잭션을 롤백해도 해당 트랜잭션에서 획득한 세션 잠금은 남는다. 반면 트랜잭션 수준 잠금은 트랜잭션 종료에 맞춰 해제된다. 이 차이가 짧은 데이터베이스 작업을 설계할 때의 출발점이다. PostgreSQL advisory lock 수명

보호할 갱신을 하나의 짧은 트랜잭션에 담을 수 있다면 다음처럼 구조를 잡을 수 있다. 숫자 두 개는 설명을 위한 예시이며, 실제 서비스에서는 어떤 자원을 뜻하는지 별도로 정의해야 한다.

BEGIN;
SELECT pg_advisory_xact_lock(4201, 73);
-- 같은 트랜잭션에서 보호할 상태를 읽고 갱신한다.
COMMIT;

이 예제에는 실제 테이블이나 업무 규칙이 없다. 그러므로 그대로 실행해 기능을 완성하는 코드가 아니라, 트랜잭션 경계를 설명하는 골격으로 읽어야 한다. 실제 구현에서는 잠금 획득, 상태 확인, 갱신, 커밋을 동일한 트랜잭션 컨텍스트에서 실행하고 오류 경로에서는 롤백해야 한다. ORM을 쓴다면 트랜잭션 콜백 안에서 전역 데이터베이스 객체로 빠져나가는 호출이 없는지도 살핀다.

가장 놓치기 쉬운 지점은 autocommit이다. 명시적인 트랜잭션 블록 없이 각 문장을 별도로 실행하면 PostgreSQL은 개별 문장을 트랜잭션으로 처리한다. 따라서 잠금 획득 SELECT가 완료된 뒤 별도 문장으로 실행하는 갱신까지 그 잠금이 이어지지 않는다. 드라이버가 자동으로 트랜잭션을 시작하는지도 확인해야 한다. PostgreSQL 트랜잭션 설명

설계 검토에서는 잠금 함수가 호출되는 줄에 표시하는 것으로 끝내지 말자. 보호가 시작되는 위치와 끝나는 위치를 업무 단계에 나란히 적어 보면 좋다. 상태를 읽은 뒤에 잠금을 획득한다면 이미 읽은 값으로 판단해도 되는지 검토해야 하고, 갱신 전에 커밋한다면 의도한 보호 구간을 다시 나눠야 한다.

3. try-lock 결과와 키 규칙도 업무 로직에 포함한다

pg_advisory_xact_lock은 필요한 경우 기다린다. 기다리지 않고 획득 가능 여부를 알고 싶다면 pg_try_advisory_xact_lock을 선택할 수 있다. 이 함수는 즉시 획득하면 true, 즉시 획득할 수 없으면 false를 반환한다. false를 무시하고 원래 작업을 실행하면 설계한 상호 배제가 성립하지 않는다. PostgreSQL advisory lock 함수 목록

여기서 결정할 것은 함수 선택뿐이 아니다. 다른 실행이 진행 중일 때 이번 요청을 건너뛸지, 사용자에게 처리 중이라고 응답할지, 나중에 다시 수행할지 정해야 한다. 건너뛴 요청을 완료로 기록하면 운영자는 처리되지 않은 일을 성공한 것으로 읽을 수 있다. 실행 성공, 잠금 획득 실패, 데이터베이스 오류는 애플리케이션의 결과 모델에서 구분하는 편이 명확하다.

키도 팀의 계약이다. 공식 함수는 하나의 64비트 정수 또는 두 개의 32비트 정수로 자원을 식별하며, 두 형식의 키 공간은 서로 겹치지 않는다. 같은 업무 자원에 대해 일부 코드가 단일 정수 형식을 쓰고 다른 코드가 정수 쌍을 쓰면 서로 협조하는 잠금이라고 생각해서는 안 된다. 잠금 키 형식 정의

예를 들어 정수 쌍의 첫 값을 업무 종류, 둘째 값을 자원 ID로 정했다면 그 규칙을 모든 진입점에서 공유해야 한다. 임의 해시로 문자열을 정수로 바꾸는 경우에는 충돌 가능성과 숫자 범위도 설계에 포함한다. 이 방식 자체가 정답이라는 뜻은 아니다. 운영 중 키를 보고 어떤 작업인지 설명할 수 있고, 서로 다른 구현이 동일한 대상을 같은 키로 표현하는지가 중요하다.

advisory lock은 애플리케이션이 협조해서 사용하는 장치다. PostgreSQL이 모든 관련 갱신에 이 잠금을 자동으로 강제하지 않는다. 따라서 관리용 스크립트, 수동 실행 경로, 별도 서비스가 같은 자원을 수정한다면 해당 경로까지 검토해야 한다. 데이터 자체의 불변 조건은 적절한 제약 조건과 함께 설계하는 편이 안전하다. Advisory lock의 협조적 사용

4. 장시간 작업과 외부 API는 별도 경계를 정한다

잠금을 오래 유지하려고 트랜잭션 전체를 길게 열어 두면, 그동안 PgBouncer의 서버 연결도 해당 트랜잭션에 할당된다. 따라서 외부 API 응답이나 긴 파일 처리를 트랜잭션 안에 넣는 설계는 연결 점유 시간을 함께 검토해야 한다. 간단한 잠금 예제를 장시간 배치의 전체 실행 구간으로 그대로 확장하지 않는 편이 좋다. 서버 연결의 할당 범위

업무가 여러 트랜잭션을 가로질러 반드시 같은 세션에 묶여야 한다면 session pooling이나 별도 직접 연결 경로가 필요한지 평가할 수 있다. 그 경우에도 애플리케이션 풀에서 연결을 전용으로 확보하고, 잠금 해제가 끝나기 전에 다른 작업에 반환하지 않는 소유권 규칙이 필요하다. 어떤 경로를 선택하든 연결 손실과 작업 중단을 어떻게 감지할지는 별도 검토 대상이다.

세션 초기화 설정을 만능 해결책으로 삼아서도 안 된다. PgBouncer의 기본 동작에서는 transaction pooling에 server_reset_query가 적용되지 않는다. server_reset_query_always로 기본 초기화 쿼리인 DISCARD ALL을 실행하도록 강제하더라도 트랜잭션이 끝날 때 세션 상태를 지우므로, 트랜잭션을 넘어 잠금을 유지하려던 설계와 맞지 않는다. PgBouncer 세션 초기화 설정

외부 효과에는 별도의 질문이 남는다. 외부 API는 성공했지만 로컬 데이터베이스 기록이 실패했다고 가정해 보자. 잠금의 존재만으로 외부 호출의 결과를 되돌리거나, 다음 실행에서 같은 요청이 중복되지 않도록 보장할 수는 없다. 따라서 advisory lock만으로 외부 작업이 정확히 한 번 실행된다고 설명해서는 안 된다. 외부 시스템의 멱등성 지원과 결과 확인 방법은 해당 시스템의 계약을 따로 확인해야 한다. 이는 잠금 문법의 기능 설명을 넘어선 설계상 주의점이다.

5. 배포 전에는 성공 경로보다 경계를 확인한다

관측할 때는 pg_locks에서 advisory 유형의 잠금, 소유하거나 기다리는 프로세스의 PID, 획득 여부를 나타내는 granted를 확인할 수 있다. 이 잠금은 데이터베이스별로 적용되므로 같은 숫자 키를 쓴다는 이유만으로 다른 데이터베이스의 실행까지 조정된다고 가정하지 않는다. pg_locks 관측 항목

실제 도입 전 검증에서는 두 독립 클라이언트가 같은 키를 요청하는 경우, 첫 트랜잭션을 종료한 뒤 다시 획득하는 경우, try-lock이 false인 경우를 구분해 확인하면 좋다. 애플리케이션의 연결 반환과 오류 처리까지 포함해야 하므로 SQL 콘솔에서 한 문장에 성공하는 것만으로 검증을 마쳤다고 보기는 어렵다. 이 글에서는 해당 검증을 수행하지 않았다.

  • 실제 접속 경로의 풀링 모드와 드라이버의 트랜잭션 동작을 확인한다.
  • 잠금 획득부터 보호할 갱신까지 같은 트랜잭션 컨텍스트가 유지되는지 확인한다.
  • 같은 자원을 처리하는 모든 경로가 동일한 키와 획득 규칙을 사용하는지 확인한다.
  • 잠금을 얻지 못한 결과와 실행 오류가 운영 기록에서 구분되는지 확인한다.
  • 외부 효과의 중복 방지 책임을 데이터베이스 잠금에만 맡기지 않았는지 확인한다.

PgBouncer와 advisory lock을 함께 검토할 때 유용한 출발점은 “누가 언제까지 서버 연결과 잠금을 소유하는가?”라는 질문이다. 이 답을 먼저 정하면 짧은 트랜잭션으로 충분한 작업과 별도의 실행 구조가 필요한 작업을 더 명확하게 나눌 수 있다.

댓글