
동시성 제어를 다루다 보면 가장 흔히 접하게 되는 개념이 비관적 락(Pessimistic Lock)과 낙관적 락(Optimistic Lock)입니다.
비관적 락(Pessimistic Lock)
낙관적 락(Optimistic Lock)
Advisory Lock은 PostgreSQL에서 제공하는 경량 락으로 숫자(ID)를 지정해 락을 제어할 수 있습니다. 일반적인 테이블 락(Row Lock, Table Lock)과 달리 데이터베이스 객체(테이블, 행)를 직접적으로 잠그지 않고 사용자가 정의한 ID로 관리합니다.
세션(Session) 기반
pg_try_advisory_lock 함수
pg_advisoryr_unlock 함수
Advisory Lock은 PostgreSQL에서 제공하는 ID 기반 잠금으로 개발자가 명시적으로 "이 자원을 사용하겠다"고 선언하여 다른 프로세스의 접근을 차단하는 방식입니다. 이러한 방식은 낙관적이라기보다는 실제로 락을 획득해 충돌을 막아버리는 형태이므로 비관적 락의 일종으로 볼 수 있습니다.
다만 전통적인 DB Row 또는 Table Lock과 달리 데이터베이스의 구체적인 테이블이나 레코드가 아닌 '숫자(ID)'를 기준으로 락을 걸기 때문에 특정 어플리케이션 로직 또는 자원을 보호하기 위한 용도로 사용할 수 있습니다.
클라우드 환경이나 kubernetes 같은 오케스트레이션 화노경에서 여러 개의 Pod(또는 애플리케이션 인스턴스)가 동시에 같은 작업을 수행하려 할 때가 종종 있습니다. 예를 들어, 대규모 데이터 처리나 정기 스케줄 작업 등을 여러 곳에서 동시에 실행하려고 하면 아래와 같은 문제가 발생할 수 있습니다.
이러한 문제를 해결하기 위해 동시성 제어(Concurrency Control)는 필수적이며 그중 하나의 기법이 바로 PostgreSQL의 Advisory Lock을 이용하는 방법입니다.
아래는 FastAPI와 SQLAlchemy(AsyncSession)를 사용하여 Advisory Lock을 획득하고 해제하는 코드 예시입니다.
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.sql import text
async def acquire_advisory_lock(session: AsyncSession, lock_id: int) -> bool:
"""
PostgreSQL Advisory Lock 획득
"""
try:
result = await session.execute(
text("SELECT pg_try_advisory_lock(:lock_id)"),
{"lock_id": lock_id}
)
lock_acquired = result.scalar() # True if Lock acquired, False otherwise
print("Advisory lock acquired." if lock_acquired else "Advisory lock not acquired.")
return lock_acquired
except SQLAlchemyError as e:
print(f"Error acquiring advisory lock: {e}")
return False
async def release_advisory_lock(session: AsyncSession, lock_id: int):
"""
PostgreSQL Advisory Lock 해제
"""
try:
await session.execute(
text("SELECT pg_advisory_unlock(:lock_id)"),
{"lock_id": lock_id}
)
print("Advisory lock released.")
except SQLAlchemyError as e:
print(f"Error releasing advisory lock: {e}")
async def som_scheduled_task(get_session_func, lock_id: int):
async with async_get_session(get_session_func) as session:
# 1) 락 획득
lock_acquired = await acquire_advisory_lock(session, lock_id)
if not lock_acquired:
print("Another Pod is already executing the task. Skipping...")
return
try:
# 2) 실제 작업 수행
print("Executing scheduled task...")
# 작업 로직(예: 데이터 처리, API 호출 등)
finally:
# 3) 락 해제
await release_advisory_lock(session, lock_id)
위 코드처럼 세션 기반의 락을 사용하면 대부분 정상적으로 락이 해제되지만 다음과 같은 상황에서는 DB 서버에 세션이 남아있고 락도 풀리지 않는 경우가 생길 수 있습니다.
이때는 DB에 남아 있는 락을 수동으로 확인하고 해제해야 합니다.
PostgreSQL에서는 pg_locks 테이블에서 현재 락 정보를 조회할 수 있습니다.
SELECT *
FROM pg_locks
WHERE locktype = 'advisory';
Session A(Query Tool A): true

Session B(Query Tool B): false

DB 락 상태 확인

lock_id: 1004
pid: 11876
model: ExclusiveLock
PostgreSQL 서버의 PID 11876인 프로세스가 lock_id 1004로 ExclusiveLock(베타적 락)을 갖고 있습니다. 현재 11876의 프로세스는 다른 작업을 하고 있지 않기에 status가 'idle'입니다.
Session A(Query Tool A): true

Session B(Query Tool B): false

DB 락 상태 확인

동시성 제어는 시스템 신뢰성과 데이터 무결성을 지키기 위해 반드시 고려되어야 합니다. PostgreSQL Advisory Lock은 간단한 ID 기반의 경량 락으로 동시 작업을 제어하기에 매력적인 선택지입니다.
하지만 세션 기반의 특성상 예상치 못한 장애나 예외 상황에서 락이 해제되지 않고 남을 수 있는 점을 주의해야 합니다. 주기적인 모니터링과 예외 처리를 통해 시스템을 운영해야 하며 필요하다면 직접 pg_locks를 조회해 수동으로 락을 해제해야 합니다.
🙇🏻 잘못된 정보는 댓글을 통해 알려주시면 감사하겠습니다.