PostgreSQL Advisory Lock으로 안전한 동시성 제어 구현하기 With FastAPI

Dior·2025년 1월 9일

[DB]

목록 보기
1/1
post-thumbnail

비관적 락과 낙관적 락의 개념

동시성 제어를 다루다 보면 가장 흔히 접하게 되는 개념이 비관적 락(Pessimistic Lock)낙관적 락(Optimistic Lock)입니다.

  1. 비관적 락(Pessimistic Lock)

    • 말 그대로 '비관적으로' 접근합니다.
    • 공유 자원(예: DB Row)에 락을 걸어 다른 트랜젝션이 동시에 수정할 수 없도록 막아 충돌을 원천 차단합니다.
    • 충돌이 발생할 여지가 적은 경우에는 불필요한 대기 시간이 생길 수 있어 성능 저하가 우려됩니다.
  2. 낙관적 락(Optimistic Lock)

    • '낙관적으로' 충돌이 일어나지 않을 것으로 가정하고 실제 충돌이 발생했을 때만 롤백이나 재시도 등으로 처리합니다.
    • 버전 번호나 특정 컬럼 값(수정 시점)을 비교해 동시 수정 여부를 감지하는 방식을 자주 사용합니다.
    • 충돌이 빈번하지 않은 환경에서 오버헤드를 줄일 수 있지만 실제 충돌이 자주 발생하는 상황에서는 재시도가 계속 발생해 문제가 될 수 있습니다.

Advisory Lock이란 무엇인가?

Advisory Lock은 PostgreSQL에서 제공하는 경량 락으로 숫자(ID)를 지정해 락을 제어할 수 있습니다. 일반적인 테이블 락(Row Lock, Table Lock)과 달리 데이터베이스 객체(테이블, 행)를 직접적으로 잠그지 않고 사용자가 정의한 ID로 관리합니다.

  1. 세션(Session) 기반

    • 한 세션이 락을 획득하면 세션이 종료될 때, 락도 자동으로 해제됩니다.
    • 세션이 유실되거나 예상치 못한 예외가 발생했을 때, 락이 해제되지 않은 상태로 남을 수 있음에 유의해야 합니다.
  2. pg_try_advisory_lock 함수

    • 락 획득을 시도하고 이미 다른 세션이 락을 가지고 있다면 False를 반환합니다.
    • 비차단(Non-blocking) 방식으로 동작하기 때문에 작업 스케줄러나 다중 프로세스 환경에서 유용하게 사용할 수 있습니다.
  3. pg_advisoryr_unlock 함수

    • 특정 ID로 획득된 락을 해제합니다.
    • 명시적으로 락을 해제해야 할 상황이 있다면 꼭 호출해야 합니다.

Advisory Lock은 어디에 해당할까?

Advisory Lock은 PostgreSQL에서 제공하는 ID 기반 잠금으로 개발자가 명시적으로 "이 자원을 사용하겠다"고 선언하여 다른 프로세스의 접근을 차단하는 방식입니다. 이러한 방식은 낙관적이라기보다는 실제로 락을 획득해 충돌을 막아버리는 형태이므로 비관적 락의 일종으로 볼 수 있습니다.

다만 전통적인 DB Row 또는 Table Lock과 달리 데이터베이스의 구체적인 테이블이나 레코드가 아닌 '숫자(ID)'를 기준으로 락을 걸기 때문에 특정 어플리케이션 로직 또는 자원을 보호하기 위한 용도로 사용할 수 있습니다.


왜 동시성 제어가 필요한가?

클라우드 환경이나 kubernetes 같은 오케스트레이션 화노경에서 여러 개의 Pod(또는 애플리케이션 인스턴스)가 동시에 같은 작업을 수행하려 할 때가 종종 있습니다. 예를 들어, 대규모 데이터 처리나 정기 스케줄 작업 등을 여러 곳에서 동시에 실행하려고 하면 아래와 같은 문제가 발생할 수 있습니다.

  • 중복 데이터 처리: 같은 데이터를 여러 번 처리해서 DB나 파일 시스템의 무결성이 깨질 수 있습니다.
  • 리소스 경합: CPU, 메모리, 네트워크 자원을 동시에 소모해 성능 저하 또는 장애가 발생할 수 있습니다.
  • 데드락(Deadlock) 위험: 여러 프로세스가 서로의 락이 풀리기를 기다리며 시스템이 멈출 수 있습니다.

이러한 문제를 해결하기 위해 동시성 제어(Concurrency Control)는 필수적이며 그중 하나의 기법이 바로 PostgreSQL의 Advisory Lock을 이용하는 방법입니다.


FastAPI와 SQLAlchemy에서 Advisory Lock 구현

사전 준비

  • FastAPII: 비동기(Async) 환경에서 API 서버를 구성하기 쉬운 Python 웹 프레임 워크
  • SQLAlchemy: 파이썬 ORM(Object-Relational Mapping) 라이브러리
  • 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)
  1. pg_try_advisory_lock으로 락 획득 시도
  2. 락을 획득 못 하면 이미 다른 인스턴스가 작업 중이므로 스킵
  3. 정상적으로 작업을 끝내거나 예외가 발생해도 finally 절에서 락을 해제

락이 해제되지 않는 경우가 존재하는가?

위 코드처럼 세션 기반의 락을 사용하면 대부분 정상적으로 락이 해제되지만 다음과 같은 상황에서는 DB 서버에 세션이 남아있고 락도 풀리지 않는 경우가 생길 수 있습니다.

  1. 세션 종료 로직 실패
    • 개발자가 의도한 finally 블록이 실행되지 않았거나 에기치 못한 예외가 발생하여 세션 종료 로직 자체가 동작하지 않는 경우
  2. 네트워크 장애
    • 컨테이너 혹은 노드가 갑작스럽게 죽으면서 DB 커넥션이 정상적으로 해제되지 않은 경우
  3. 프로세스 Idle 상태
    • PostgreSQL 서버에 연결된 프로세스가 Idle 상태로 남아서 락을 계속 유지하고 있는 경우

이때는 DB에 남아 있는 락을 수동으로 확인하고 해제해야 합니다.


DB 락 상태 확인하기

PostgreSQL에서는 pg_locks 테이블에서 현재 락 정보를 조회할 수 있습니다.

SELECT *
FROM pg_locks
WHERE locktype = 'advisory';
  • locktype이 'advisory'인 항목을 조회하면 어떤 세션이 어떤 ID의 락을 보유하고 있는지 확인할 수 있습니다.
  • 필요하다면 관리자가 직접 SELECT pg_advisory_unlock(lock_id)를 호출해 해당 락을 해제할 수 있습니다.

pgAdmin으로 테스트하기

서로 다른 세션으로 pg_try_advisory_lock 시도

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'입니다.

서로 다른 세션으로 pg_advisory_unlock 시도

Session A(Query Tool A): true

Session B(Query Tool B): false

DB 락 상태 확인

마치며

동시성 제어는 시스템 신뢰성과 데이터 무결성을 지키기 위해 반드시 고려되어야 합니다. PostgreSQL Advisory Lock은 간단한 ID 기반의 경량 락으로 동시 작업을 제어하기에 매력적인 선택지입니다.

하지만 세션 기반의 특성상 예상치 못한 장애나 예외 상황에서 락이 해제되지 않고 남을 수 있는 점을 주의해야 합니다. 주기적인 모니터링과 예외 처리를 통해 시스템을 운영해야 하며 필요하다면 직접 pg_locks를 조회해 수동으로 락을 해제해야 합니다.

🙇🏻‍ 잘못된 정보는 댓글을 통해 알려주시면 감사하겠습니다.

profile
Focus on growth rather than material

0개의 댓글