이 패턴이 필요한 상황:

카프카와 같은 메시지 큐에 메시지 전달과 데이터베이스 연산을 신뢰성 있게 보장하고 싶은 경우에 사용한다.

 

관계형 데이터베이스 트랜잭션은 모두 성공하거나, 모두 실패하거나 Atomic 하게 처리될 수 있다.

 

그런데 데이터베이스 밖의 영역인 메시지 큐로의 메시지 전달과 데이터베이스 내부 트랜잭션을 같이 Atomic 하게 보장하는 것은 어렵다.

 

트랜잭션은 실패하더라도, 메시지 발송은 될 수 있는 것이고 반대로 메시지 전달은 실패하는데 트랜잭션은 성공할 수 있으니까.

 

이런 경우에 Transactional Outbox 패턴을 사용하면 된다.

 

누군가는 이런 질문을 하기도 한다

  • (1) 데이터베이스 트랜잭션 스코프 내에서 메시지 발송도 하면 되는거 아니야? 그럼 메시지 발송이 실패하면 트랜잭션도 같이 실패하잖아. 
  • (2) 2PC 를 사용하면 되는거 아니야?

각각 문제점이 있다.

 

(1) 같은 경우는 트랜잭션이 롤백 되더라도 메시지는 전달될 수 있는 경우가 생긴다. 그리고 메시지를 소비하는 측에서 메시지를 읽어왔는데 아직 데이터베이스 커밋이 안되었을 수 있다. 그래서 이 데이터에 컨슈머가 의존하고 있다면 문제가 생길 수 있다.

 

(2) 같은 경우는 2PC 를 지원 하지 않는 데이터베이스와 메시지 큐도 있다. 그리고 2PC 는 네트워크에 매우 민감하기 떄문에 이 점을 고려해서 사용해야한다.

 

Transactional Outbox Pattern 구현

https://pradeepl.com/blog/transactional-outbox-pattern/

  • (1) 메시지 큐에 보낼 메시지를 outbox 테이블에 따로 넣는다. 그리고 이걸 데이터베이스 연산과 같은 트랜잭션으로 묶는 것이다.
  • (2) 주기적으로 outbox 테이블을 읽어서 메시지를 보낸다. 메시지를 발송할 땐 중복된 메시지를 보낼 수 있음을 유의해야한다. 그래서 필요하담녀 멱등성 키와 같은 것들을 같이 메시지에 담아서 보내면 됨.

 

Transactional Outbox Pattern 에서 Scalable 을 주고 싶다면?

생각한 아이디어는 여러가지가 있는데 각자 트레이드 오프를 고려해서 선택하면 되지 않을까 싶다.

  • 주기적으로 Outbox 테이블을 Polling 하는 어플리케이션의 총 수를 알고 있다면 Consistent Hashing 처럼 사용하는 것. 
  • debezium 과 같은 CDC (Change Data Capture) 를 이용하면 됨. 여기서 병렬 커넥터를 사용하면 된다. 
  • 관계형 데이터베이스를 사용하고 있다면 발송할 메시지를 outbox 테이블에서 가져올 때 SELECT FOR UPDATE SKIP LOCKED 를 이용하는 것. 이로 인해 어플리케이션에서 메시지를 가져올 때 Race Condition 을 완화할 수 있다.

+ Recent posts