LISTEN & NOTIFY 명령으로 구현하는 비동기식 작업

http://postgresql.kr/blog/pg_listen_notify.html

PostgreSQL LISTEN & NOTIFY SQL 명령어

데이터베이스의 비동기식 처리

이 글은 지극히 응용 프로그램 개발에 관계된 이야기입니다.

PostgreSQL을 이용한 이벤트 기반 프로그램을 어떻게 할 것인가에 대한 힌트입니다.

데이터베이스에 새 자료가 등록된 것을 확인하고 싶다면, 전통적으로 주기적으로 해당 테이블의 마지막 자료를 확인하고, 지금까지의 마지막 자료와 방금 조회한 마지막 자료가 다르다면, 새 자료가 등록되었다고 판단합니다.

또한 이 보다 좀 더 세련된 방법으로 해당 테이블에 트리거를 등록해서 새 자료가 입력되면, 알림 테이블에 그 내용을 기록하고, 그 알림 테이블을 주기적으로 살펴 보아 윗 방식과 같이 처리합니다.

하지만 이런 방법은 데이터베이스 자원을 필요 이상으로 사용하는 것이기도 하고, 실 자료 테이블에 대한 과도한 접근까지 일어나기에 해당 테이블에 대한 특정 이벤트를 검사하는 주기를 짧게 잡기 힘듭니다. 그 만큼 실시간성이 떨어지겠죠.

그래서, 이런 작업의 처리는 응용 프로그램의 도움을 받습니다. 물론 서버가 이런 비동기식 작업들에 대한 다양한 기능을 제공한다면, 보다 쉽게 구현할 수 있습니다. 그 가운데 대표적인 SQL 명령어가 listen과 notify 입니다.


PostgreSQL의 비동기 작업

PostgreSQL에서는 비동기 작업을 위해 다양한 API를 제공하고 있습니다. 대부분 libpq라는 C로 만들어진 데이터베이스 조작용 기본 클라이언트 라이브러리에서 제공합니다. libpq 라이브러리를 기반으로 하는 다른 언어의 PostgreSQL 조작 모듈들도 제 각각의 비동기식 작업에 대한 처리 방법을 제공합니다. 예: PostgreSQL 고가용성 글에서 소개한 Python 코드


LISTEN & NOTIFY SQL 명령어

이 명령어는 한 쪽에서는 어떤 채널에 어떤 내용을 공지하고(NOTIFY), 다른 여러 쪽에서는 그 채널이라는 것을 듣고(LISTEN) 있다가 필요한 작업을 할 수 있습니다.

사용법은 아주 간단합니다.

http://postgresql.kr/docs/current/sql-listen.html

http://postgresql.kr/docs/current/sql-notify.html

페이지가 SQL 설명서입니다.

일반적으로 이 명령어는 클라이언트-서버 환경에서 어떤 사용자가 어떤 자료 변경 작업 시 다른 사용자가 그 자료 변경 자료에 대한 동시성 제어를 위해서 설계되었지만, 요즘처럼 웹 기반 폴링처리 비용을 줄이기 위해서 많이(?) 사용합니다.


구현 예제

  • 모니터링 프로그램에서 자료 수집 처리기에서 해당 자료를 데이터베이스에 입력할 때 NOTIFY - 대쉬보드에서 그 입력된 자료만 데이터베이스에서 가져와서 처리 할 때LISTEN
  • 게시판에서 사용자가 새 글을 등록할 때 NOTIFY - SNS 발행 대행 데몬이 그것을 LISTEN하고 있다가 필요한 발행 처리
  • 작업량이 많은 배치성 통계 작업 시작-종료 시 NOTIFY - 통계 작업 이후 타 시스템으로 작업 된 내용만 전달하는 데몬이 LISTEN 하고 있다가 필요한 처리
  • 자신의 질문에 댓글이 달리면 자신이 사용하고 있는 메신져로 알려주는 기능

이 글에서는 맨 마지막 예제를 구현하는 방법을 설명하겠습니다.


NOTIFY

응용 프로그램에서 NOTIFY는 LISTEN 보다는 구현하는 부분에서는 간단합니다. 그냥

NOTIFY 채널이름, '구체적인 내용'

이런 형태의 쿼리만 서버로 보내면 됩니다.

문제는 채널 이름의 이름 규칙과, '구체적인 내용'의 내용 규칙 설계를 LISTEN을 사용하는 응용 프로그램과 잘 맞춰야 하는 것이 어렵습니다.

여기서는 단순하게,

NOTIFY newanswer, '해당 게시물 URL'

형태로 합니다. 댓글이 달리면, 댓글 처리 하는 끝 부분에, 위의 쿼리를 추가로 실행합니다.

NOTIFY 작업은 여기까지입니다. 


LISTEN

LISTEN 작업은 데이터베이스 쪽으로는 그냥,

LISTEN 채널이름

형태의 쿼리를 서버로 보냅니다. 물론 여러 개의 채널이 있다면 데이터베이스 접속 시 각 LISTEN 명령을 모두 실행 하는 것이 일반적입니다.  물론 특정 작업으로 진행할 때 - 예를 들어 공연장 좌석 예약 시작 같은 것) LISTEN 작업을 하고 있다가 예약 도중 다른 사람이 먼저 해당 좌석 예약을 완료 하면 사용자에게 해당 좌석은 먼저 예약 되어버렸다고 알려주는 식의 기능을 구현하고자 할 때는 작업 중간에 LISTEN 작업을 실행하기도 합니다. 

LISTEN newanswer

윗 NOTIFY라면, 이런 식으로 실행합니다.

다음 이 알림을 받기 위한 감시 작업 (polling 작업)을 응용프로그램에서 해야합니다.  대부분의 PostgreSQL 클라이언트 라이브러리들은 이 기능을 위한 api를 제공하고 있습니다.

이 글에서는 python psycopg 모듈을 예로 설명하겠습니다.  사용설명서는

http://initd.org/psycopg/docs/advanced.html#async-notify

핵심 코드는

conn.poll()
while conn.notifies:
    notify = conn.notifies.pop(0)
    print "Got NOTIFY:", notify.pid, notify.channel, notify.payload

부분입니다. notify.payload 에는 앞 NOTIFY 명령에서 지정한 '해당 게시물 URL'이 담겨져 있습니다.  윗 예제라면, 이 값은 단순한 url 문자열입니다. 이 값을 다시 url query string 분석작업을 하고, 댓글의 상위 글을 찾고, 그 상위 글의 글쓴이에게 알림 메시지를 보내는 것은 전적으로 응용프로그램 몫입니다.  (python 경우라면, 이 palyload 값을 json.dumps() 함수의 반환값인 문자열로 지정하고, json.loads() 로 가져와서 python 내 딕셔너리 자료형으로 처리합니다.)

이 작업은 connection 기준으로 notify가 더 이상 없으면 해당 순환문을 벗어납니다. 즉, 계속 알림이 있는지 확인하려면, 윗 코드가 작업 스케줄러를 이용하든, 상위 while 문을 이용하든 어떻게든 계속 반복 되어야합니다.

이 부분은 이벤트 기반 프로그래밍에서 사용하는 방식 대로 한 클래스의 사용자 정의 핸들러 함수로 등록하는 것이 일반적입니다.


사용할 때 기억해야 할 것들

  • LISTEN & NOTIFY 작업은 connection.poll() 같은 각 언어 별 클라이언트 라이브러리에서 제공하는 API를 사용하는 것이 작업 비용을 효율화 할 수 있습니다.
    단순하게, 'select 1' 쿼리를 계속 서버로 보내면서, 서버 notice 메시지를 확인하는 수준 낮은 코딩은 하지 마세요.
  • LISTEN 등록된 세션이 하나도 없는 경우의 NOTIFY는 버려집니다.
    일반 메시지큐처럼 다른 세션이 가져가지 않은 오래된 NOTIFY까지 모두 보관하고 있지 않습니다.
  • LISTEN 작업은 세션 단위로 이루워집니다.
    다른 세션이 해당 알림을 큐에서 꺼냈다 하더라도  자기 세션에는 그대로 남아 있습니다. (정확하게 말하면 세션 프로세스 ID 단위입니다. 그래서 새 새션을 맺었다면, LISTEN 명령으로 채널을 지정하지 않으면 알림을 받을 수 없습니다.)
  • NOTIFY는 해당 채널을 LISTEN 하는 모든 세션을 대상으로 진행됩니다.
    즉, 특정 세션만을 대상으로 NOTIFY를 할 수 없습니다. (꼼수가 있다면 채널 이름 자체를 세션 단위로 하면 되기도 하겠죠.)