PostgreSQL Advisory Lock 동시성 제어를 위한 강력한 도구
Table of Contents
1. 서론
Advisory Lock이란?
Advisory Lock은 PostgreSQL에서 제공하는 사용자 정의 잠금(User-defined lock) 메커니즘입니다. 테이블이나 행을 직접 잠그는 것이 아니라, 사용자가 정의한 키(key) 값을 기반으로 잠금을 설정할 수 있습니다.
이 잠금은 PostgreSQL 내부에서 관리되며, 논리적(Logical) 잠금으로 작동합니다. 즉, Advisory Lock은 강제성이 없고, 애플리케이션 레벨에서 적절히 구현해야 합니다.
PostgreSQL에서의 Lock 개요
PostgreSQL에는 여러 종류의 잠금이 존재합니다.
- Row-level Lock:
SELECT ... FOR UPDATE를 사용하여 특정 행을 잠금 - Table-level Lock:
LOCK TABLE을 사용하여 테이블 전체를 잠금 - Advisory Lock: 특정 숫자 값을 기반으로 사용자가 직접 관리하는 잠금
Advisory Lock은 트랜잭션이나 테이블과 무관하게 특정 리소스(예: 사용자 ID, 주문 ID 등)에 대한 동시성을 제어하는 데 유용합니다.
Advisory Lock이 필요한 이유
- 특정 리소스(예: 특정 작업 ID)에 대한 동시 실행 방지
- 트랜잭션 기반이 아닌 애플리케이션 레벨에서 동시성 관리 가능
- 기존 행(Row)에 대한 직접적인 락을 걸지 않으므로 데드락(Deadlock) 위험 감소
- 분산 시스템에서도 활용 가능 (Advisory Lock을 통해 동기화)
2. Advisory Lock의 개념
PostgreSQL의 Advisory Lock vs. 일반 Lock
| Lock 유형 | 대상 | 자동 해제 | 강제성 |
|---|---|---|---|
| Row Lock | 특정 행 | 트랜잭션 종료 시 | 강제 |
| Table Lock | 특정 테이블 | 트랜잭션 종료 시 | 강제 |
| Advisory Lock | 특정 숫자 값 | 설정 방식에 따라 다름 | 애플리케이션이 관리 |
Advisory Lock은 PostgreSQL이 강제하는 것이 아니라, 애플리케이션에서 잘 설계해야 한다는 점이 핵심입니다.
세션 기반(Session-level) vs. 트랜잭션 기반(Transaction-level) Advisory Lock
-
세션 기반 (Session-level)
- 명시적으로
pg_advisory_unlock()을 호출해야 해제됨 - 세션이 종료되기 전까지 유지됨
- 예:
pg_advisory_lock(12345)
- 명시적으로
-
트랜잭션 기반 (Transaction-level)
- 트랜잭션이 종료되면 자동으로 해제됨
- 명시적으로 해제할 필요 없음
- 예:
pg_advisory_xact_lock(12345)
Advisory Lock의 특징과 장점
- 숫자 기반 잠금이므로 가볍고 빠름
- 원하는 키(key)를 직접 정의할 수 있어 유연함
- 기존 데이터(테이블/행)에 영향을 주지 않음
- 트랜잭션 기반을 사용하면 자동 해제가 가능하여 관리가 쉬움
3. Advisory Lock 사용 방법
3.1 세션 기반 Advisory Lock
-- 세션 수준 잠금 설정 (블로킹)
SELECT pg_advisory_lock(12345);
-- 잠금 해제
SELECT pg_advisory_unlock(12345);
이 방식은 명시적으로 해제해야 하며, 같은 세션에서만 유효합니다.
3.2 트랜잭션 기반 Advisory Lock
-- 트랜잭션 내에서만 유지되는 잠금
SELECT pg_advisory_xact_lock(12345);
트랜잭션이 끝나면 자동으로 해제되므로 따로 pg_advisory_unlock()을 호출할 필요가 없습니다.
3.3 비차단 (Non-blocking) 방식
잠금이 걸려 있을 경우 즉시 반환하도록 하려면 pg_try_advisory_lock()을 사용합니다.
SELECT pg_try_advisory_lock(12345);
true반환 → 성공적으로 잠금 획득false반환 → 이미 다른 세션이 잠금을 사용 중
4. Advisory Lock 실습
다음은 두 개의 세션에서 Advisory Lock을 사용하여 동작하는 모습입니다.
| 시간 | 세션 A | 세션 B |
|---|---|---|
| 1초 | SELECT pg_advisory_lock(100); |
|
| 2초 | SELECT pg_advisory_lock(100); -- 블로킹 상태 |
|
| 3초 | -- 여전히 대기 중... |
|
| 4초 | SELECT pg_advisory_unlock(100); |
|
| 5초 | -- 세션 A가 해제됨, 이제 실행됨 |
5. Golang에서 Advisory Lock을 활용한 동시성 제어
아래는 Golang에서 PostgreSQL Advisory Lock을 이용하여 동시성을 제어하는 예제 코드입니다.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"time"
_ "github.com/lib/pq"
)
func acquireLock(db *sql.DB, lockID int) bool {
var success bool
err := db.QueryRow("SELECT pg_try_advisory_lock($1)", lockID).Scan(&success)
if err != nil {
log.Fatalf("Error acquiring lock: %v", err)
}
return success
}
func releaseLock(db *sql.DB, lockID int) {
_, err := db.Exec("SELECT pg_advisory_unlock($1)", lockID)
if err != nil {
log.Fatalf("Error releasing lock: %v", err)
}
}
func main() {
connStr := "postgres://user:password@localhost:5432/mydb?sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
lockID := 12345
if acquireLock(db, lockID) {
fmt.Println("Lock acquired, processing...")
time.Sleep(5 * time.Second)
releaseLock(db, lockID)
fmt.Println("Lock released")
} else {
fmt.Println("Could not acquire lock, another process is using it.")
}
}
이 코드는 pg_try_advisory_lock()을 사용하여 락을 획득하고, 성공하면 5초간 작업 후 락을 해제하는 간단한 예제입니다.