분산시스템에서 데이터를 전달하는 방법

@Hiyen · August 05, 2024 · 13 min read

단일 DB 그리고 외부 API도 호출하지 않을 경우 데이터의 전달에 대해 크게 걱정할 필요가 없다. DBMS의 트랜잭션 기능을 사용하여 처리하면 되고 실패시 실패했다는 응답을 사용자에게 보내고 롤백하면 된다.

하지만 실제 웹서비스는 여러 개의 다른 DBMS를 사용하거나 다른 서비스와 네트워크로 연결되는 경우가 많다. 만약 결제 서비스에서 결제가 성공된 후 상품 서비스에서 재고가 감소되지 않는다면? 결제는 실패했으나 재고가 감소되었다면? 결제가 완전히 성공되었다고 보기 어려울 것이다.

2 Phase Commit

여러 DBMS를 하나의 트랜잭션으로 처리하기 위해 2PC을 적용할 수도 있다.

Pasted image 20240725133342

데이터베이스 1,2(3,4...)의 로컬 트랜잭션 기능을 사용하고 로컬 트랜잭션들을 관리하는 코디네이터(트랜잭션 관리자)를 둔다.

각 데이터베이스들은 쓰기 요청을 평소처럼 실행하고 코디네이터는 커밋을 할 준비가 되었는지 질의한다. 모든 데이터베이스들이 준비가 되었다면 코디네이터는 2단계에서 커밋 요청을 보내고 커밋이 실제로 일어난다. 1단계에서 어느 DB하나라도 no를 보낸다면 코디네이터는 모든 DB에 Abort를 보낸다.

이 방식은 여러 문제점을 가진다.

  1. NoSQL과 같이 트랜잭션에 참여하는 DBMS가 트랜잭션을 지원하지 않는다면 사용할 수 없다.
  2. 1단계에서 yes라 대답한 DB가 2단계에서는 커밋이 불가할 수도 있다.
  3. 코디네이터에 장애가 나면 DB사이의 일관성이 지켜지지 않을 수 있다.
  4. ACID를 위한 잠금이 길어져 성능면에서 불리하다. 3번과 연결되어 영원한 잠금이 일어날수도 있다.

이외에도 이 방식은 실제 분산 시스템에서 거의 사용되지 않는다. 일반적으로 MSA 환경은 각각의 DB, WAS를 따로 두고 다른 마이크로 서비스와 API 통신을 하거나 Message 시스템을 사용한다.

글로벌 트랜잭션을 관리하기 위해 코디네이터를 사용하는 것은 기껏 분리한 서비스를 다시 하나로 묶어버려 수많은 마이크로 서비스를 한 가지 시스템에 모두 의존하게 만든다. MSA를 하는 목적(각 서비스의 독립성, 유연성, 장애 격리)이 불분명해진다.

네트워크의 비신뢰성

MSA서비스들은 네트워크에 의존한다. 하지만 애플리케이션 로직과 달리 물리세계는 통제할 수 없다. 패킷 손실, 네트워크 지연, 네트워크 다운 등의 문제는 예측 불가능하고 개발자가 통제할 수 없다.

@Transactional
public void save(final Member member) {
	repository.save(member);
	//외부 API 요청
	other.send(member);
}

이러한 코드가 있다고 생각해보자. 문제가 없어보이지만 사실 네트워크 문제가 개입된다면 문제가 생길 수 있다.

RDB에 save 되었으나 send에서 네트워크 문제가 생긴 경우. RDB에서 rollback이 되었으나 send에서 요청은 보낸 경우와 같이 하나로 묶여야 하지만 원자성을 지킬 수 없는 경우가 많이 일어날 수 있다.

즉, 네트워크를 사용하는 분산 시스템에서 데이터는 즉각적인 일관성을 가질 수 없다.

SAGA 패턴

SAGA 패턴은 분산 환경에서 데이터 일관성을 보장하기 위한 설계 패턴이다. 연속된 개별 서비스의 로컬 트랜잭션이 이어져 전체 비즈니스 트랜잭션 하나를 구성한다. 서비스1의 로컬 트랜잭션 -> 서비스2의 로컬 트랜잭션으로 트랜잭션을 순차적으로 구성하고 개별 트랜잭션이 실패했을 때는 이를 보상하는 트랜잭션을 발생시켜 '최종적 일관성'을 목표로 한다.

  • 보상 트랜잭션?

보상 트랜잭션은 실패한 트랜잭션을 기준으로 이전의 작업들에 대해 트랜잭션이 시작되기전의 상태로 돌려서 트랜잭션의 원자성을 즉각적으로는 아니지만 최종적으로 보장하기 위한 방법을 말한다.

  • 보상 트랜잭션은 꼭 필요한가?

결제 성공이 재고 감소, 회원 주문 목록 갱신, 배송 등록, 리뷰 알림 목록 추가.. 등등 많은 서비스에 걸쳐 순차적 트랜잭션을 형성해야 한다고 가정한다.

마지막에 가서 딱 하나의 (엄청 중요하지는 않은) 서비스에서 예외가 발생했다면 앞의 모든 트랜잭션들을 롤백하는 것은 불필요한 리소스 낭비일 가능성이 크고 또한 모든 서비스에서 커밋된 데이터를 이전으로 돌려내는 구현은 굉장히 복잡할 가능성이 높다.

따라서 모든 서비스의 모든 작업에 대해 보상 트랜잭션을 적용해야 한다!보다는 정말 중요하고 하나가 되어야 하는 로직에 (이체-잔고 감소 등) 사용해야 할 것 같다.

(내 의견으로는 굳이 이런 중요한 로직에 복잡성을 늘리는 것보다 서비스 분리가 정말 필요하지 않다면 DBMS의 트랜잭션을 사용하는 게 속 편하지 않을까? 라는 생각이 든다.)

At Least Once를 위하여

SAGA패턴은 설계 패턴일 뿐 분산 환경과 서비스의 성격에 따라 하나의 비즈니스 트랜잭션을 구성하는 방법은 모두 다를 수 있다. 다만 공통적으로 중요한 것은 로컬 트랜잭션이 성공하고 이 성공에 대한 데이터를 다른 서비스에 신뢰성 있게 전달할 수 있느냐 이다.

신뢰있게 데이터를 전달한다는 건 무엇일까? 데이터를 전달 받는 시스템의 상황, 네트워크에 상관없이 적어도 한 번은 메시지가 전달되는 것을 보장하는 것을 말한다.

At Most Once는 위의 예제에서 보았던 신뢰성 없는 데이터 전달을 말한다. 즉 API호출, Message 발행등을 딱 한번하고 이 후 상황은 신경쓰지 않는다.

At Least Once는 최소 한 번 전달, 즉 A -> B가 있을 때 A는 최소 한 번이상 발송해야 하고 B는 이를 최소 한 번이상 수신해야 한다. 여기서 중요한 것은 B가 한번 이상 수신해야 하므로 멱등성(여러 번 요청해도 결과가 같음)이 보장되어야 하는 것이다.

트랜잭션 Outbox 패턴

트랜잭션 Outbox패턴은 외부 API 호출을 이벤트로 보고 해당 이벤트를 RDBMS에 같이 저장하는 방법을 말한다.

따라서 이벤트+데이터가 DBMS의 하나의 트랜잭션으로 묶이게 되어 send와 save는 원자성을 가지게 된다.

이제 적재된 이벤트 데이터를 Polling으로 처리하는 쓰레드나 시스템을 두어 해당 이벤트를 발행하면 된다. 이벤트 레코드에 상태를 명시하는 컬럼을 두어 commit된 데이터는 처리되게 할 수 있다.

이 때 중요한 것은 이 발행도 당연히 네트워크를 사용해야 하니 발행을 처리하는 시스템은 멱등하게 구성되어야 한다.

메시지 브로커 사용

RabbitMQ, Kafka와 같은 메시지 브로커 시스템을 사용한다. Pub-Sub의 기본모델은 Pub이 이벤트 발행하면 Message Queue에 적재, Sub이 이 적재된 메시지를 처리하는 방식이다.

이 때 아무 장치 없이 구현한다면 Pub의 이벤트 발행, Queue의 적재, Sub의 처리를 모두 보장할 수 없다. Pub이 이벤트를 발행해도 네트워크 때문에 실패할 수 있고 Queue가 꽉차서 메시지는 유실될 수 있고 Sub이 받은 메세지를 제대로 처리하지 못할 수도 있다.

RabbitMQ와 Kafka는 pub-sub 모델에서 신뢰성 있는 메시지 전달을 도와주는 프레임워크들이다.

RabbitMQ는 Exchanger를 Message Queue내에 두고 Publisher에게는 메시지를 큐에 잘 적재했다는 Publisher Confirm을 쏘고 , Consuer로부터는 메시지를 잘 받았다는 Consumer Ack을 확인하고 큐에서 메시지를 뺀다. 또한 Dead letter큐를 따로 두어 정상적으로 처리하지 못한 메시지를 따로 관리한다.

Kafka도 발행자에게 ack와 같이 잘 메세지를 기록했다는 신호를 보내고 소비자가 메시지를 처리한 만큼의 오프셋을 커밋하는 방식으로 메시지의 전달을 보장한다.

실제로는 어떻게 ?

Pasted image 20240725163559 (초록색은 스프링 이벤트, 회색은 AWS SNS) 우아한 형제들의 회원시스템 이벤트기반 아키텍처 구축하기 에서 일부분이다.

SpringEvent 발행 이후 SNS에 메세지를 발행하는 과정에서 네트워크 통신이 있어 이벤트 발행을 보장하기 위해 트랜잭션 Outbox 패턴을 적용하여 회원에 대한 이벤트를 RDB에 같이 적재하여 다른 서비스로 발행하는 모습을 볼 수 있다.

즉, 분산환경에서 비즈니스 트랜잭션을 처리하는 방법에는 여러가지가 있을 수 있고 중요한 것은 A가 B에 데이터를 네트워크의 비확실성에도 불구하고 신뢰있게 전달할 수 있느냐 인 것을 알 수 있다.

https://monday9pm.com/%EB%B6%84%EC%82%B0-%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98-distributed-transaction-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0-d0a10ad5dd53

https://tech.kakaopay.com/post/msa-transaction/

https://www.youtube.com/watch?v=b65zIH7sDug&t=1079s

https://techblog.woowahan.com/7835/

@Hiyen
Always want to write sometimes