ecsimsw

Transactional outbox 패턴으로 메시징과 DB 트랜잭션 원자성 보장 본문

Transactional outbox 패턴으로 메시징과 DB 트랜잭션 원자성 보장

JinHwan Kim 2023. 12. 31. 17:50

기존의 문제점

PicUp 프로젝트에선 서비스 간 통신 방법으로 메시징을 이용한다. 서비스 간 처리가 필요할 때 이벤트를 발행, 메시지 큐에 담아두고 처리를 맡는 다른 서비스에서 이를 리스닝하여 처리한다. 

 

픽업 프로젝트에선 앨범 삭제 기능이 좋은 예시이다. 사용자가 앨범을 삭제할 때 관련 사진 파일들을 전부 삭제하는 처리 시간까지 사용자 요청 주기에 포함해선 안되므로 요청 주기 내에선 DB 데이터만 삭제하고 이 이벤트를 메시지 큐에 담았다가 비동기로 파일을 삭제하게 된다.

 

 

 

 

문제는 메시징과 DB 트랜잭션이 다른 원자성을 갖는다는 것이다. 원자성은 한 작업의 단위 내에서 세부 작업들은 모두 성공하거나 모두 실패해야 함을 말한다.

 

메시징과 DB 트랜잭션은 원자성이 보장되지 않기 때문에 DB 처리 실패와 메시징의 실패는 독립적이다. 

 

예를 들면 아래는 원자성이 지켜지고 있다. MQ 가 다운되어 전부 발행 실패됨을 확인하자 Album service 는 이미 삭제한 DB 데이터를 롤백 처리하고 있다. 이는 처리 과정이 실패했기에 모든 세부 작업들이 함께 실패 처리되어 원자성이 지켜지는 경우이다.

 

 

 

 

실제 상황은 위처럼 깔끔하지 않다. 메시징과 DB 처리가 여러개이거나 원하는 요구사항이나 상황이 보다 까다로워 데이터 롤백과 보상 메시징이 어려울 수 있다. PicUp 에선 아래의 두 상황을 처리하고 싶었다.

 

1. 좌측 상황 : N 개의 메시징 도중에 일부 몇개가 메시지 발행에 실패하는 경우 롤백 후 삭제 실패 처리할 수 없다. 사용자 입장에선 앨범을 삭제 처리했는데 요청은 실패하고 다시 조회하니 몇몇 삭제 실패 처리된 데이터만 남아있게 된다.

 

2. 우측 상황 : 메시징 이후 후처리에서 예외가 발생하는 경우 사진 파일도 동시에 제거되었기에 트랜잭션으로 묶인 DB 작업만 롤백되어선 안된다.

 

 

 

이렇게 DB 와 메시징이 한 작업 단위인 상황에서 원자성이 지켜지지 않아 생길 수 있는 문제를 해결하고 싶었다.

 

Transactional outbox pattern 

이런 여러 시스템 간의 트랜잭션을 분산 트랜잭션이라고 한다. 결국 메시징과 DB 처리가 한 DB 를 사용하지 않아 전통적인 DB의 트랜잭션으로 묶이기 못 해 생기는 문제이다.

 

Transactional outbox pattern 은 메시징 처리를 DB 처리와 같은 DB에 두어 메시징과 DB 처리를 동일한 트랜잭션으로 묶는 방식이다.

 

아래 그림처럼 아예 메시지 내용을 DB에 저장하고 실행기가 이 DB 를 읽어 메시지 브로커에 offer 할 수 도 있고, 또는 아예 DB 자체를 메시지 저장소로 사용하고 실행기로 이를 읽어 메시지 브로커를 대신할 수 도 있다.

 

PicUp 에선 아래 그림의 방식처럼 단일 DB에 메시지 내용을 저장하는 것으로 '앨범 제거'라는 작업의 메시징 작업과 그 전후의 DB 작업을 한 트랜잭션으로 묶었다. 메시지 내용을 담은 이 DB 를 Outbox 라고 한다. exe 로 표시된 실행기는 이 outbox 에 처리되지 않은 메시지가 있는지 수시로 확인을 반복하고 처리되지 않은 메시지가 존재한다면 이를 Message broker 에 전달하게 된다.

 

이를 통해 DB 작업과 메시징 작업의 원자성이 생겨 성공하면 전부 성공, 이 중 하나라도 실패한다면 모든 작업이 롤백되는 것이 보장된다.

이렇게 Outbox pattern 을 이용하여 MQ 의 상태나 메시지 발행의 예외 사항에 더 안전한 구조를 만들 수 있었다.

 

 

 

 

주의해야 할 문제

Transactional outbox pattern 을 추가하면서 고려했던 문제들을 소개한다. 

 

1. 메시지 처리 순서

 

메시징이 기존과 달리 매 작업마다 바로 Message queue 에 넘어가는 게 아니라 Outbox 에 먼저 저장되고 실행기(스케줄러)가 한 번에 메시지를 발행하기 때문에 Outbox 에 순서가 중요한 메시지들이 존재하는 경우 이 둘의 처리 순서에 따라 의도하지 않은 결과가 만들어질 수 있다. 

 

Outbox 에 들어온 순서대로 메시지 발행될 수 있도록 보장해야 한다. 메시지의 생성 시간이나 순서를 기록하고 실행기는 이를 정렬하여 순차적으로 발행하는 방식으로 처리하였다.

 

2. Segment 단위로 나누기

 

메시지 자체에 문제가 있어 발행 과정에서 문제 되는 메시지가 있다고 가정해 보자. 만약 발행 과정에서 실행기가 모든 Outbox 의 메시지를 발행하려고 하면 예외 메시지 하나 때문에 다른 모든 메시지들이 발행되지 못하고 계속 쌓이게 될 것이다.

 

Outbox 에서 꺼내 메시지를 발행할 때의 작업 단위 별 처리할 메시지 개수를 지정하여 예외 시 발행 실패 범위를 최소화하고, 메시지 자체에 예외가 있어 다른 메시지들이 한 Transaction 으로 묶여 처리가 안 되는 상황을 막는다.

 

3. 발행 시도 횟수 기록하기

 

앞선 메시지 발행에서 예외가 발행하는 Segment 는 항상 발행에 실패하고 매번 자원만 소모할 뿐일 것이다. Outbox에 발행 시도 횟수를 기록하고 재시도 최댓값을 지정하여 그 값을 넘어가는 경우 Outbox 에서 제거하고 에러로 로깅하는 등 개발자가 직접 처리할 수 있도록 한다.

 

4. 멱등성 

 

혹 이번엔 Outbox 를 publish 하는 트랜잭션에서 문제가 생기는 경우도 있을 것이다. 결국 어디선간 DB 처리와 메시징이 한 트랜잭션에서 일어나야할테니 말이다. 그런 상황을 대비하여 메시지의 처리는 여러 번 처리해도 같은 동작을 반환하도록 하였다. 

Comments