본문 바로가기
기타

Kafka 메세지를 안정적으로 다루는 방법 (Transaction Outbox Pattern)

by Limm_jk 2024. 10. 13.

Transaction Outbox 패턴에 대하여 간단히는 알고 있었는데요. 앞으로 이벤트를 다루며 잘 알아두면 좋을 내용이라고 생각해서 구체적으로 어떤 문제를 해결하기 위하여 사용하며, 어떤 장/단점이 있는지 찾아보고 남겨봅니다. 기본적으로 Chris Richardson 선생님의 Pattern: Transactional outbox 아티클을 참고합니다.

 

풀고 싶은 문제

어플리케이션 서버에서 비즈니스 로직을 처리하는 동안, 메세지의 발행까지 Transactional 함을 보장해야 하는 경우가 많습니다. 하지만, 기본적으로 이종 간의 Transaction은 완벽할 수 없습니다.

 

임시로 대응을 해보자면, Transaction Scope에서 Rollback이 가능한 작업을 모두 한 이후에 메세지 발행을 챙기는 방식을 통하여 원자성을 보장할 수 있습니다. 하지만, 비즈니스가 복잡해지면서 문제점이 생깁니다. 발행할 메세지가 하나가 아니라던지, 또 다른 Rollback이 어려운 Operation이 생기는 등의 시나리오가 떠오르네요.

 

그리고 복구가 어렵습니다. 장애 시점 이후로도 데이터는 계속 변화하는데, 해당 시점의 메세지를 동일하게 발행하는 것은 꽤나 복잡하고 귀찮은 일입니다.

 

이를 해결하기 위하여 이벤트 소싱과 같은 해결 방법도 있겠지만, 이번 글에서는 Transaction Outbox CDC 패턴에 대하여 이야기를 해보겠습니다.

 

 

해결 방안

위 문제를 해결하기 위하여 Transaction Outbox Pattern은 이름 그대로 Outbox, 발신함을 두고 발신함에서 순차적으로 꺼내가서 수행하는 방식을 말합니다.

 

예시로 구독형 서비스에서 정기 결제를 하고, 결제 정보에 대한 이메일을 메세지로 발행하여 처리하는 시나리오를 가정하고, 이에 대한 시퀀스 다이어그램을 그려보겠습니다.

sequenceDiagram
    participant App as 구독 서비스
    participant DB as Database
    participant Executor as Executor
    participant Kafka as Kafka
    participant Email as 이메일 발송 서비스
    participant User as User

		note over App: 정기 결제 로직 시작

    App->>DB: 결제 로직 시행 후, 결제 정보를 DB에 저장 (within transaction)
    App->>DB: 이메일 메세지를 아웃박스 테이블에 저장 (within transaction)
    DB-->>App: Commit transaction

    note over App: 정기 결제 로직 정상 종료

    Executor->>DB: 새로운 Outbox 테이블에 새로운 메세지가 있는지 확인
    Executor->>Kafka: 새로운 메세지가 있다면 발행
    Kafka-->>Email: 메세지 consume
    Email-->>User: 결제 이메일 발송

 

위와 같이 Transaction 내부에서는 Outbox Table에 메세지 정보를 저장만 하고, 발행의 책임을 Executor에 넘깁니다.

 

위에 언급된 Executor 팀의 상황에 맞게 세팅할 수 있습니다.

Cron을 통하여 비교적 간단하게 세팅하여 사용할 수도 있고, 학습이 좀 필요하지만 Debezium과 같은 CDC(Change Data Capture)를 사용할 수도 있습니다.

 

특징

Transaction 내에 Outbox에 넣는 것을 보장함으로 인하여 Transaction이 commit된 시점에만 메세지가 전송될 수 있도록 보장할 수 있습니다.

그리고 저장할 때, offset 같은 것을 주어서 순서를 안정적으로 보장할 수 있고, 장애가 발생했을 경우에도 publish가 되지 않은 시점부터 재생해서 비교적 복구가 쉬워집니다.

하지만, 관여하는 인프라가 늘어남에 있어서 생기는 문제점도 있습니다.

 

메세지를 두 번 이상 발행할 수 있음

Outbox에서 상태 관리를 하겠지만, 발송하는 사이에 Executor가 또 가져가는 등의 중복 발송되는 사례가 있을 수 있습니다. Consume 하는 로직이 멱등하다면 문제가 없겠지만, 그렇지 않다면 로직이 두 번 실행되는 문제가 발생할 수 있습니다.

적용 이전에 반드시 해결해야 하는 문제입니다.

 

Executor의 SPOF화(Single Point of Failure)

위에서 작성한 흐름에서 Executor가 아예 죽어버린다면 어떻게 될까요? Outbox에서 메세지를 꺼내가지 못하므로 메세지를 사용하는 모든 어플리케이션에 장애가 전파됩니다. 이를 위하여 Executor의 높은 민감도의 모니터링 시스템과 failover 로직이 필요합니다.

 

이런 상황을 막기 위하여 어플리케이션에서 이중 발행을 해볼 수 있습니다. publisher를 제공할 때, 내부적으로 outbox에 저장하는 것뿐 아닌 after transaction으로 message publish도 수행하는 것이죠. 위에 언급했던 메세지가 두 번 이상 발행되는 빈도가 늘어나겠지만, 멱등하게 로직을 작성했다면 문제가 되지는 않겠네요.

 

이를 적용한 시퀀스 다이어그램은 아래와 같습니다. 트랜잭션 환경이 아니라면 post transaction 대신 async로 발행한다고 볼 수 있겠습니다!

sequenceDiagram
    participant App as 구독 서비스
    participant DB as Database
    participant Executor as Executor
    participant Kafka as Kafka
    participant Email as 이메일 발송 서비스
    participant User as User

		note over App: 정기 결제 로직 시작

    App->>DB: 결제 로직 시행 후, 결제 정보를 DB에 저장 (within transaction)
    App->>DB: 이메일 메세지를 아웃박스 테이블에 저장 (within transaction)
    DB-->>App: Commit transaction

    note over App: 정기 결제 로직 정상 종료
    
    App->>Kafka: 메세지 발행 (post-transaction)
    Kafka-->>Email: 메세지 consume
    Email-->>User: 결제 이메일 발송

    Executor->>DB: 새로운 Outbox 테이블에 새로운 메세지가 있는지 확인
    Executor->>Kafka: 메세지 발행 (발행이 안된 메세지가 존재한다면)
    Kafka-->>Email: 메세지 consume
    Email-->>User: 결제 이메일 발송

댓글