이것저것

MSA에서 Outbox Pattern에 대해

재심 2022. 11. 20. 12:38

[개요]

MSA 환경에서 마이크로서비스들이 RBMS환경과 메시징 서비스를 결합하여 처리하는 경우가 많다.

우리의 경우 결제 서비스등이 해당될 수 있는데 결국 메시징 서비스 + RDBMS 형태로 결합되어 서비스 될 것이다.

 

예를 들어, 주문 서비스의 경우 크게 2가지의 이벤트로 구분할 수 있는데 "주문 상태 변경"과 "주문 상태 전파"  정도가 있을 것이다.

"주문상태변경": RDBMS내에서 데이터가 변경되는 것

"주문상태전파": Kafka와 같은 메시징 플랫폼에 이벤트 메시지를 발급하는 것

 

참조: https://debezium.io/blog/2019/02/19/reliable-microservices-data-exchange-with-the-outbox-pattern/

 

Reliable Microservices Data Exchange With the Outbox Pattern

Debezium is an open source distributed platform for change data capture. Start it up, point it at your databases, and your apps can start responding to all of the inserts, updates, and deletes that other apps commit to your databases. Debezium is durable a

debezium.io

https://www.popit.kr/msa%EC%97%90%EC%84%9C-%EB%A9%94%EC%8B%9C%EC%A7%95-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0

 

MSA에서 메시징 트랜잭션 처리하기 | Popit

비동기 메시지를 사용하여 상호 간에 통신하는 방식을 메시징 Messaging[1] 이라고 부른다. 마이크로서비스 환경에서 비동기 처리 시 보통 카프카 Kafka 나 래빗엠큐 RabbitMQ 같은 메시지 브로커 Message

www.popit.kr

 

[예시]

증빙발급: 결제나 환불이 모두 진행된 후 "결제완료" or "환불완료" 이벤트를 발행한 후 해당 이벤트를 구독하여 증빙발급을 진행하는 것. 

 

[문제점]

"주문상태변경"과 "주문상태전파"는 하나의 트랜잭션으로 묶여야 한다. 

즉, DB에서는 주문이 모두 처리되었으나 이벤트 생성이 실패하는 경우 후속처리가 제대로 이루어지지 않을 수 있다.

 

위의 증빙 예시를 예로 들면 결제는 모두 진행되었으나 이벤트 발급이 제대로 되지 않아 이어지는 후속처리인 증빙발급이 되지 않는 것을 말할 수 있겠다.

 

[이벤트를 동기적으로 알려준다면?]

위의 예시에서 "주문상태변경"이 완료되면 동기식으로 다음 이벤트를 전달할 수 있도록 호출 (ex: 주문이 완료된 후 REST API를 호출하여 다음 작업이 진행될 수 있도록 하는 것) 하게된다면 일관성 있게 데이터가 유지 될 수 있을 것이다.

하지만 이 방법의 경우 의도치않은 커플링이 생긴다. 즉, API가 사용할 수 없는 상태가 될 때를 대비해야한다.

또한 능동적?이지 못해서 API를 호출하는 서비스가 요청한대로만 처리해야 한다. 즉, 서비스가 중복데이터 등을 다시 시도해도 똑같이 그 동작을 반복하게되는 등의 이슈가 있을 수 있다.

 

[Transactional OutBox Pattern]

위의 문제를 해결하기 위해 가장 보편적으로 사용하는 방법 중 하나.

"RDBMS"내에 메시지큐 개념으로 Outbox 테이블을 사용하고, RDBMS내 "주문상태변경"이 끝난 후 outbox 테이블에 메시지를 저장해주게되고 이후 message relay가 outbox 테이블에서 비동기적으로 메시지를 읽어 Kafka Broker에 메시지를 발행해주게 된다.

Outbox 테이블로의 이벤트 발행은 하나의 트랜잭션으로 묶이기 때문에, 데이터의 일관성을 유지할 수 있게 되는 것이다.  

 

Outbox Pattern

[활용 예시]

결제완료, 환불완료된 건에 대해 증빙 발급이 필요한 경우에 대한 예시

  1. 결제 or 환불 처리가 완료되면 outbox 테이블에 해당 건들을 insert한다.
  2. Message Relay가 outbox테이블을 읽어 카프카 메시지로 발급한다.
  3. 증빙 생성 Consumer는 "결제완료","환불완료" 토픽으로부터 메시지를 읽어 증빙발급을 진행한다.

내 생각 : 데이터 유실 측면에서..

  • 결제, 환불 서비스에서 outbox 테이블에 insert될때까지 하나의 트랙잭션으로 묶어서 데이터 유실이 없음.
  • Message Relay (Producer)에서 At Least Once 정책을 적용할 경우 데이터 중복은 있어도 유실은 없음.
  • 증빙 생성 서비스 (Consumer)는 증빙 생성이 완료되면 commit을 하여 미발행된 증빙이 없음.

[Outbox Table 보편적 구조]

  • id: 이벤트별 unique key. 중복 탐지 등의 용도로 활용할 수 있다.
  • aggregate type: 이벤트의 큰 구분. ex) Order, Member
  • aggregate id: 이벤트 내 집계 단위. 나중에 Kafka에서 키 값으로 활용될 수 있다. ex) 주문취소의 경우 주문성공보다 먼저 데이터가 들어가면 안되는데, 이 때 주문취소 이벤트에 주문성공을 찾을 수 있는 값을 넣어놓으면 나중에 kafka에서 하나의 파티션에 들어가게하여 주문성공이 주문취소보다 먼저처리되는 것을 보장할 수 있게 되는 것.
  • type: 실제 이벤트 (OrderCreated, OrderCanceled)
  • payload: 실제 데이터 내용이 포함된 JSON 타입의 구조.

Outbox로의 이벤트 발급 예시

하나의 주문이 일어날 때 outbox 테이블로 데이터가 생성되는 과정까지 트랜잭션으로 묶여있게된다. 

@Transactional
public PurchaseOrder addOrder(PurchaseOrder order) {
    order = entityManager.merge(order);
 
    event.fire(OrderCreatedEvent.of(order));
    event.fire(InvoiceCreatedEvent.of(order));
 
    return order;
}

 

[활용방안..고민]

증빙 설계를 좀 더 생각해보면 "현금영수증", "카드매출전표"와 같은 증빙 종류들이 있을 수 있다.

그럼 그림이 아래처럼 될 수 있다.

증빙 설계 중. "증빙 발급 판독기"에서 RDB에 연결이 안되어 있는 상황을 가정해보자.

"증빙 발급 판독기"의 동작을 요약해보면 아래와 같다.

 

  1. "결제완료" or "환불완료" 토픽을 poll
  2. RDBMS에 접근하여 해당 주문이 어떤 증빙의 발급이 필요한지 알아낸다
  3. 해당되는 증빙이벤트를 발급한다. 

 

이 때 2번 과정이 제대로 이루어지지 못하고 Exception이 발생하는 경우 어떻게 될 것인가.

 

Exception이 발생했을 때 별다른 처리가 없다면 AutoCommit이므로 해당 메시지는 그냥 commit이 될 것이다. 결과적으로 증빙발급이 필요하지만 증빙발급이 안될 것이다! 

→ Outbox 패턴을 활용해서 이 문제를 해결할 순 없을까..  → 위의 3단계가 하나의 트랜잭션처럼 묶여야한다. 

 

Load from first to second

이미 "결제완료, 환불완료" 토픽 자체가 이벤트로 발급되어 있다. 그래서 이 이벤트를 읽어서 증빙 발급 토픽이 발행되기 까지 하나의 트랜잭션으로 묶여야 한다. 결국 이렇게되려면.. "결제완료, 환불완료"은 증빙토픽이 발행된 후 커밋되어야 함. 

 

유사한 사례가 없나..? 

TCC (Try-Confirm/Cancel) 이라는 내용이 있길래 읽어보았다. 

https://www.popit.kr/rest-%EA%B8%B0%EB%B0%98%EC%9D%98-%EA%B0%84%EB%8B%A8%ED%95%9C-%EB%B6%84%EC%82%B0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-%EA%B5%AC%ED%98%84-1%ED%8E%B8/

 

REST 기반의 간단한 분산 트랜잭션 구현 - 1편 | Popit

REST 기반의 간단한 분산 트랜잭션 구현 -1편 TCC 개관 REST 기반의 간단한 분산 트랜잭션 구현 - 2편 TCC Cancel, Timeout REST 기반의 간단한 분산 트랜잭션 구현 - 3편 TCC Confirm(Eventual Consistency) REST 기반의

www.popit.kr

읽어보니 "결과적 일관성" 이라는 내용으로 "언젠가는 처리되겠지" 마인드로 Kafka에 메시지를 발행해놓은 후 실제로 처리가 되는 경우만 commit하는 수동처리 정책을 적용하고 있었다. 

→ 결국 여기서는 수동처리해야하나..? 

 

아예 DB연결이 안된다? 

→ 자동커밋인 경우 원안대로 증빙용 공통토픽에 발행

→ 수동커밋인 경우 커밋하지 않고, 계속해서 재시도하도록 둔다. 

 

Load from second to third

outbox pattern 적용을 통해 "증빙 발급 이벤트" 과정이 트랜잭션으로 묶이게 된다면 적어도 "증빙 이벤트 발급" 자체가 실패하게 되는일은 없다고 봐야 한다. 

 하지만 2->3번으로 이어지는 경우는 사실 producer가 "at least once" 정책을 사용하고 있으므로 큰 이변이 있지 않은 이상 토픽발급이 실패하는 일은 잘 없을 것이다.

→ 굳이 적용해야하나 싶음.. 

 

[참고: Debezium Connector를 활용한 이벤트 발행]

위의 그림에서 OrderDB에 outbox 테이블이 생성되어 데이터가 생성되어 있고, 이를 Debezium Connector를 통해 Kafka로 발행할 수 있다. 

 

'이것저것' 카테고리의 다른 글

nGrinder  (0) 2023.04.08
JMeter  (0) 2023.04.01
Intellij 단축키 모음  (0) 2023.03.09
Materialized View란?  (0) 2022.11.06
오브젝트 스토리지 (Object Storage) ?  (0) 2022.11.03