두 요청이 직렬화 되야하고 애플리케이션 레벨에서 동시성을 제어해야하는 경우
서버 개발을 하면서 많은 동시성 문제들을 만났던 기억을 되짚어보자. 내가 개발했던 대부분의 상황에서는 API 동시 요청으로 인해 충돌이 일어나더라도 크게 문제가 되지 않는 경우가 많았다.
하지만 일부 동시성 문제 발생시 심각한 장애나 서비스 사용에 문제가 생기는 경우 충돌이 발생하지 않도록 하거나 충돌 발생을 감지하고 해결해야한다. 오늘 다룰 Advisory Lock 은 충돌을 회피하는, 일종의 비관적 잠금 전략을 아주 유연하게 애뮬레이션 할 수 있게 하는 좋은 도구다.
Postgres 에서 제공하는 애플리케이션 레벨 제어 잠금으로 트랜잭션 혹은 세션 단위로 획득하며 배타적 잠금을 애뮬레이션한다
Postgres 에서는 다른 DBMS 와 마찬가지로 다양한 종류의 명시적 잠금을 제공한다.
DML, DDL 등에 의해 개발자가 명시적으로 잠금을 획득하지 않더라도 DBMS 가 데이터 일관성을 보장하기 위해 자동으로 획득하는 잠금이 있는 반면, Advisory Lock 은 애플리케이션에게 요구되는 동시성 문제를 해결하기 위해 제공되는 명시적 잠금의 일종이다.
요청을 받으면 외부 API에 의존해 로직을 처리하는 프로세스가 있다고 가정하자.
해당 외부 API 는 동시에 요청할 경우 문제가 발생하므로 동시에 요청할 수 없도록 외부 API 가 요청 중인 경우 동일한 요청은 대기하거나 실패시키는걸 목표로 한다. (외부 결제 모듈을 사용해 결제 요청을 보내는 상황을 떠올려보자)
나는 외부 API 에만 사용했지만 파일 시스템 접근 등 기존에는 DB 에 접근하지 않는 작업들도 트랜잭션을 여는데 부담이 없는 처리 시간이라면, AD Lock 을 활용해 효과적으로 동시성 문제를 해결 할 수 있다고 생각한다.
Advisory Lock 은 배타락을 애뮬레이션하므로 해당 프로세스는 외부 API 를 호출하기전 Advisory Lock 을 획득하도록 한다. 외부 API 의 응답을 받고 사후처리가 모두 완료되면 잠금을 보유한 프로세스는 잠금을 반환하면 된다.
단순히 요청한 특정 유저 단위가 아닌 유저가 속한 그룹 단위로 동시성을 보장해야하는 프로세스가 있다고 가정하자. 유저 A, B, C 는 그룹 1 일때 동일한 그룹이라면 충돌시켜야하는 상황이라고 할 수 있겠다.
특정 그룹의 유저가 글을 작성 중이라면, 동일 그룹내 유저들은 글을 작성 할 수 없어야한다고 가정하자.
이런 상황에서 해당 요청을 직렬화하기 위해 추가적인 데이터 구조를 정의하거나, 상태 관리를 더하는 것보다 AD Lock 을 사용하면 더욱 간단하고 유연하게 처리 할 수 있다.
POST v1/post
Header : user A Token -> Key 10 으로 잠금 획득 성공 -> 글 생성
POST v1/post
Header : user B Token -> Key 10 으로 잠금 획득 요청 -> 충돌
-- 충돌시 대기
begin;
select pg_advisory_xact_lock(10);
commit;
-- bool 값 반환
begin;
select pg_try_advisory_xact_lock(10);
commit;
select pg_advisory_lock(10);
-- 세션 레벨에서는 자동으로 AD Lock 이 반환되지 않음
select pg_advisory_unlock(10);
결국 AD Lock 을 활용하기 위해서는 직렬화할 요청을 식별할 키값을 획득해 AD Lock 를 걸어야한다.
Postgres AD Lock 획득 함수는 int4, int8 의 정수값만을 받기 때문에 식별 키값이 문자열이라면 곤란한 상황이 생긴다. 문자열을 서버 자체적으로 정수값으로 해시해 사용 할 수 있겠지만 해시 함수를 활용하면 간단하게 구현 할 수 있다.
SELECT pg_advisory_xact_lock(
('x' || md5('user_10'))::bit(64)::bigint
);
SELECT pg_advisory_xact_lock(
hashtext('user_10')
);
AD Lock 은 적재적소에 사용하면 동시성 문제를 해결하는데 있어서 서비스 복잡도를 크게 낮출 수 있는 좋은 도구라는 생각을 한다. 행 잠금이나 테이블 잠금과는 달리 잠금을 걸 행이나 테이블이 존재하지 않아도 애플리케이션 레벨에서 논리적으로 직렬화해야할 요청을 쉽게 직렬화하기 때문이다.
하지만 이런 특성 때문에 남용시에는 많은 사이드 이펙트가 발생할 수 있으므로 주의 해야겠다는 생각도 든다. 예를 들어 실제로 행 수준에서 배타락을 걸어 직렬화를 해야하는 상황인데 AD Lock 을 쓰기는 힘들다.
다른 모든 조회 로직 혹은 앞으로 추가될 로직에서 명시적으로 AD Lock 을 획득할 수는 없기 때문이다.