Post

microservice pattern.06

microservice patterns 책을 통해 학습한 내용을 정리한 글입니다.

microservice pattern.06

saga를 이용한 트랜잭션 관리

엔터프라이즈 애플리케이션에서 트랜잭션은 매우 중요합니다. 트랜잭션 없이는 데이터의 일관성을 지키기 불가능합니다.

트랜잭션의 ACID 성질 덕분에 각 트랜잭션이 데이터에 독점적인 접근 권한을 가진 것처럼 보이게함으로써 개발는 비교적 간단하게 트랜잭션을 이용할 수 있었습니다. 마이크로서비스 구조에서 단일 서비스 내에서 쓰이는 트랜잭션은 ACID 트랜잭션을 사용합니다. 하지만 문제는 여러 서비스가 소유한 데이터를 업데이트하는 오퍼레이션의 트랜잭션을 구현할 때 발생합니다.

예를 들어, 주문 생성 오퍼레이션은 다수의 서비스를 통해 처리됩니다. 이런 오퍼레이션은 서비스간 동작하는 트랜잭션 관리 메커니즘이 필요합니다.

마이크로서비스 구조에서의 트랜잭션 관리

엔터프라이즈 애플리케이션에서 처리되는 거의 모든 요청은 데이터베이스 트랜잭션과 함께 처리됩니다. 엔터프라이즈 애플리케이션 개발자들은 트랜잭션 관리를 간단하게 해주는 프레임워크와 라이브러리들을 사용합니다. 몇몇 프레임워크와 라이브러리들은 명시적으로 트랜잭션을 시작하고, 커밋하고, 롤백하는 API를 제공합니다. Spring 같은 프레임워크는 선언형 메커니즘을 제공합니다. 스프링은 트랜잭션 내부에서 메소드 호출이 이뤄질 수 있도록하는 @Transactional 어노테이션을 제공합니다. 암시적으로 사용할 수 있기에, 트랜잭션 비즈니스 로직을 간단하게 작성할 수 있습니다.

좀 더 정확하게 말하자면, 단일 데이터베이스에 접근하는 모놀리식 애플리케이션에서의 트랜잭션 관리가 수월합니다. 다수의 데이터베이스와 메세지 브로커를 사용하는 복잡한 모놀리식 애플리케이션에서 트랜잭션 관리는 더 어렵습니다. 그리고 마이크로서비스 구조에서는 트랜잭션은 각자 데이터베이스를 소유한 다수의 서비스에 걸쳐서 적용되야합니다. 이런 상황에서, 애플리케이션은 트랜잭션 관리하기 위해 좀 더 정교한 메커니즘을 사용해야 합니다.

주문 생성 시스템 오퍼레이션을 생각해봅시다. 주문 생성 오퍼레이션은 주문을 생성하려는 고객을 검증해야하고, 주문 정보를 검증하고, 고객의 신용 카드를 승인하고, 데이터베이스에 주문 데이터를 생성해야합니다. 이런 오퍼레이션을 모놀리식 애플리케이션에서 구현하는 것은 비교적 간단합니다. 주문을 검증하는 데 필요한 모든 데이터에 쉽게 접근할 수 있고, 데이터 일관성을 보장하기 위해 ACID 트랜잭션을 사용할 수 있습니다.

이런 오퍼레이션을 마이크로서비스 구조에서 구현하는 것은 더 복잡합니다. 데이터는 여러 서비스에 퍼져있고, 여러 서비스로부터 데이터를 읽어와야합니다. 서비스마다 데이터베이스를 가지고 있기에, 이 데이터베이스들 간의 일관성을 유지하기 위한 메커니즘이 필요합니다.

Screenshot 2024-10-13 at 19 11 26

분산 트랜잭션의 문제

여러 서비스, 데이터베이스, 메세지 브로커 간 데이터 일관성을 유지하기 위한 전통적인 방법은 분산 트랜잭션을 사용하는 것입니다. 분산 트랜잭션 관리를 위한 표준은 X/Open 분산 트랜잭션 처리 모델입니다. XA는 2단계 커밋을 이용하여 트랜잭션에 참여하는 모든 구성원이 커밋하거나 롤백하는 것을 보장합니다. XA를 준수하는 기술 스택은 XA를 준수하는 데이터베이스, 메세지 브로커, 데이터베이스 드라이버, 메세징 API, 그리고 XA 글로벌 트랜잭션 ID를 전달하는 프로세스 간 통신 메커니즘으로 구성됩니다. 대부분의 SQL 데이터베이스는 XA를 준수하며, 일부 메세지 브로커도 XA를 준수합니다.

2단계 커밋을 이용하여 간단하게 트랜잭션을 적용할 수 있을 것 같지만, 분산 트랜잭션에는 다양한 문제가 존재합니다. 문제 중 하나는 현재 사용되는 Mongodb, Cassandra와 같은 NoSQL을 포함한 많은 수의 기술들은 분산 트랜잭션을 지원하지 않습니다. 그리고 RabbitMQ, Kafka와 같은 기술도 분산 트랜잭션을 지원하지 않습니다. 그렇기에, 분산 트랜잭션을 사용하려한다면, 많은 수의 현대 기술들을 포기해야 합니다.

분산 트랜잭션의 또 다른 문제는 분산 트랜잭션은 동기적 IPC를 사용합니다. 그렇기에 가용성이 감소됩니다. 모든 분산 트랜잭션이 커밋하기 위해서는 참여하는 모든 서비스들이 가용해야합니다. 앞서 언급한 것처럼, 이런 경우 애플리케이션 전체의 가용성은 감소되게됩니다.

표면적으로 분산 트랜잭션은 매력적입니다. 개발자의 관점에서 보면 로컬 트랜잭션과 같은 프로그래밍 모델을 가집니다. 하지만, 앞서 언급한 여러 문제들 때문에, 현대 애플리케이션에 적합하지는 않습니다. 앞서서 데이터베이스 트랜잭션의 일부로 메세지를 발송해서 분산 트랜잭션을 사용하지 않는 방법에 대해서 다뤘습니다. 마이크로서비스 구조에서 더 복잡한 데이터 일관성 문제를 다루기 위해서는, 애플리케이션은 saga를 사용해야 합니다.

saga 패턴

saga는 분산 트랜잭션을 사용하지 않고 마이크로서비스 구조에서 데이터 일관성을 유지하는 메커니즘입니다. 여러 서비스의 데이터를 업데이트하는 시스템 커맨드마다 saga를 정의합니다. saga는 로컬 트랜잭션의 시퀀스입니다. 각 로컬 트랜잭션은 ACID 트랜잭션 라이브러리와 프레임워크를 사용해서 단일 서비스 내의 데이터를 업데이트합니다.

시스템 오퍼레이션은 saga의 첫번째 단계를 시작합니다. 로컬 트랜잭션이 종료되면, 다음 로컬 트랜잭션을 호출합니다. saga의 각 단계는 비동시 메세징을 이용해 구현됩니다. 비동기 메세징을 사용하는 것의 이점은, saga에 참여하는 서비스의 일부가 일시적으로 다운되어도, 모든 단계의 실행을 보장한다는 것입니다.

saga는 ACID 트랜잭션과 다른 점이 다수 존재합니다. 이후에 다루겠지만, saga는 ACID 성질 중에서 isolation 성질을 보장하지 않습니다. 그리고 각 로컬 트랜잭션이 커밋되기에, 트랜잭션의 롤백을 보상 트랜잭션을 이용해서 처리해야한다는 점도 차이점입니다.

saga의 예시를 통해 동작 과정에 대해서 살펴보겠습니다.

Screenshot 2024-10-13 at 19 52 02

Create Order Saga의 예시입니다. 주문 서비스는 이 saga를 이용해서 주문 생성 오퍼레이션을 구현했습니다. saga의 첫번째 로컬 트랜잭션은 주문 생성 외부 요청으로 인해 시작됩니다. 다른 로컬 트랜잭션들은 이전 트랜잭션의 종료로 인해 호출됩니다.

saga는 다음 로컬 트랜잭션들로 구성됩니다.

  1. 주문 서비스 : 주문을 APPROVAL_PENDING 상태로 생성합니다.
  2. 고객 서비스 : 주문을 생성한 고객을 검증합니다.
  3. 주방 서비스 : 주문 정보를 검증하고, CREATE_PENDING 상태로 티켓을 생성합니다.
  4. 결제 서비스 : 고객의 신용 카드를 승인합니다.
  5. 주방 서비스 : 티켓의 상태를 AWAITING_ACCEPTANCE 상태로 변경합니다.
  6. 주문 서비스 : 주문의 상태를 APPROVED 상태로 변경합니다.

saga에서 서비스들은 비동기 메세징을 이용해 통신합니다. 서비스는 로컬 트랜잭션이 완료되면, 메세지를 발행합니다. 이 메세지는 saga의 다음 단계를 호출합니다. 메세징을 사용하는 것은 서비스들의 느슨한 결합을 보장하는 것 뿐만 아니라 saga가 complete 될 것을 보장합니다.

서비스가 일시적으로 가용하지 않아도, 메세지 브로커가 메세지를 buffer해놨다가 다시 가용한 순간, 처리할 수 있게됩니다.

saga는 표면적으로 간단해보이지만, saga 간의 isolation을 보장할 수 없는 것, 그리고 에러가 났을 때 롤백 처리 같은 해결할 점들이 남아있습니다.

보상 트랜잭션을 이용한 롤백

ACID 트랜잭션은 비즈니스 규칙에 위반이 감지됐을 때 쉽게 트랜잭션을 롤백할 수 있다는 아주 좋은 기능을 가지고 있습니다. ROLLBACK 구문을 통해 만들어둔 변경 사항들을 취소할 수 있습니다. 불행히도 saga는 이렇게 자동적으로 롤백할 수 없습니다. 왜냐하면 saga의 각 단계에서 데이터베이스 커밋이 진행되기 때문입니다. 그렇기에 위 예시에서 신용카드 승인이 실패하면, 애플리케이션은 명시적으로 앞서 진행했던 3 단계를 롤백해야합니다. 보상 트랜잭션을 작성해서 롤백을 수행해야 합니다.

n+1번째 트랜잭션이 실패하면, 앞서 진행된 n개의 트랜잭션은 롤백되야합니다. 실행된 각 트랜잭션들은 대응되는 보상 트랜잭션을 가지고 잇씁니다. 앞서 진행된 n개의 트랜잭션을 롤백하기 위해서는 실행된 트랜잭션의 역순으로 보상 트랜잭션을 실행해야 합니다.

보상 트랜잭션을 실행하는 메커니즘도 트랜잭션을 실행하는 메커니즘과 다르지 않습니다. i번째 보상 트랜잭션이 종료되고 i-1번째 보상 트랜잭션을 호출합니다. 앞서 살펴본 주문 생성 saga에서 saga는 다양한 이유로 실패할 수 있습니다.

  • 유효하지 않은 고객
  • 유효하지 않은 식당 정보
  • 카드 승인 실패

로컬 트랜잭션이 실패됐을 때, saga 메커니즘은 주문 혹은 티켓까지도 취소하는 보상 트랜잭션을 실행해야 합니다.

주문 생성 saga에서 각 트랜잭션의 보상 트랜잭션은 다음과 같습니다.

stepservicetransactioncompensating transaction
1order servicecreateOrder()rejectOrder()
2consumer serviceverifyConsumerDetail()-
3kitchen servicecreateTicket()rejectTicket()
4accounting serviceauthorizeCreditCard()-
5kitchen serviceapproveTicket()-
6order serviceapproveOrder()-

위 표를 확인하면, 모든 트랜잭션에 대해 보상 트랜잭션이 존재하는 것은 아니라는 점을 확인할 수 있습니다. 읽기 작업만 수행하는 트랜잭션은 보상 트랜잭션이 필요 없습니다. 혹은 authorizeCreditCard() 같이 이후에 처리되는 작업들이 항상 성공하게되는 경우도 보상 트랜잭션이 필요 없습니다.

주문 생성 saga 트랜잭션들은 3가지로 분류할 수 있습니다. 첫 3 단계는 이후 실패할 수 있는 단계가 존재하기에 보상 가능 트랜잭션(compensatable transactions)이라 부릅니다. 4번째 단계는 이후의 단계들이 절대 실패하지 않기에 피벗 트랜잭션(pivot transaction)이라 부릅니다. 마지막 두 단계는 항상 성공하는 트랜잭션들로 재시도 가능한 트랜잭션(retriable transactions)이라 부릅니다.

saga 조정하기

saga는 saga의 각 단계를 조정하는 로직들로 구성되어 구현됩니다. 시스템 커맨드에 의해 saga가 시작되면, 조정 로직은 saga의 첫번째 참여자를 선택해야되고, 첫번째 참여자에게 로컬 트랜잭션을 실행하게 해야 합니다. 트랜잭션이 종료되면, saga의 순차 조정 록이 다음 참여자를 선택하고 호출하여 다음 트랜잭션이 실행됩니다. 이런 과정은 saga의 모든 단계가 종료될 때까지 진행됩니다. 만약 어떤 로컬 트랜잭션이 실패하면, saga는 역순으로 보상 트랜잭션들을 실행해야합니다. saga의 조정 로직을 구성하는데에는 몇가지 방법들이 존재합니다.

  • choreography : 의사결정과 순서를 결정하는 것을 참여자들에게 분산 시킵니다. 참여자들은 이벤트를 교환하여 소통합니다.
  • orchestration : 사가의 조정 로직을 사가 오케스트레이터 클래스에 중앙 집중화합니다. 사가 오케스트레이터는 사가 참여자들에게 어떤 작업을 수행할지 명령 메세지를 보내 지시합니다.

choreography-based saga

saga를 구현하는 한 가지 방법은 choreography를 사용하는 것입니다. choreography를 사용할 때, 사가 참여자들에게 지시하는 중앙 조정자가 존재하지 않습니다. 대신에 사가 참여자들은 각자의 이벤트를 구독하고, 그에 맞게 응답합니다. choreography saga의 동작 방식은 예시를 통해 보겠습니다.

Screenshot 2024-10-13 at 21 07 03

참여자들은 이벤트를 교환하며 통신합니다. 주문 서비스로 시작해서 각 참여자들은 각자의 데이터베이스를 업데이트하고, 다음 참여자를 호출하는 이벤트를 발행합니다.

  1. 주문 서비스는 주문을 APPROVAL_PENDING 상태로 생성하고, 주문 생성 이벤트를 발행합니다.
  2. 고객 서비스는 주문 생성 이벤트를 소비합니다. 고객을 검증하고 고객 검증됨 이벤트를 발행합니다.
  3. 주방 서비스는 주문 생성 이벤트를 소비합니다. 주문을 검증하고, 티켓을 CREATE_PENDING 상태로 생성하고, 티켓 생성됨 이벤트를 발행합니다.
  4. 결제 서비스는 주문 생성 이벤트를 소비합니다. CreditCardAuthorizationPENDING 상태로 생성합니다.
  5. 결제 서비스는 티켓 생성됨 이벤트와 고객 검증됨 이벤트를 소비합니다. 고객의 신용카드를 이용해 결제하고 카드 승인됨 이벤르를 발행합니다.
  6. 주방 서비스는 카드 승인됨 이벤트를 소비하고 티켓의 상태를 AWAITING_ACCEPTANCE로 변경합니다.
  7. 주문 서비스는 카드 승인됨 이벤트를 소비하고, 주문의 상태를 APPROVED로 변경, 주문 승인됨 이벤르를 발행합니다.

이때 5 번째 단계가 실패하면, 결제 서비스는 카드 승인 실패 이벤트를 발행하고 주방 서비스, 주문 서비스는 이를 소비하고 그에 맞는 처리를 합니다.

Screenshot 2024-10-13 at 21 22 06

choreography 기반 saga를 구성할 때, 서비스간 커뮤니케이션에 연관된 몇 가지 이슈들을 고려해봐야합니다. 첫번째 이슈는 saga 참여자가 데이터베이스 업데이트와 이벤트 발행을 하나의 트랜잭션 내에서 진행하는 것을 보장해야한다는 점입니다. choreography 기반 saga는 데이터베이스를 업데이트하고 이벤트를 발행합니다. 그렇기에 데이터베이스 업데이트와 이벤트 발행은 원자적으로 실행되야합니다. 결과적으로 신뢰도 있는 통신을 위해서는 saga 참여자들은 앞서 다뤘던 transactional 메세징을 사용해야 합니다.

두번째 이슈는 saga 참여자들은 이벤트와 데이터간의 매핑을 할 수 있어야합니다. 주문 서비스가 카드 승인됨 이벤트를 받았을 때, 이 이벤트에 대응되는 주문이 무엇인지 알아야합니다. 한 가지 해결 방법은 saga 참여자들이 이런 매핑을 할 수 있게하는 correlation id를 포함해 이벤트를 발행하는 것입니다. 주문 서비스 같은 경우, 주문 아이디를 correlation id러 사용해서 이벤트와 주문을 연결할 수 있습니다.

choreography 기반 saga의 장점은 다음과 같습니다.

  • 간단함 : 서비스는 비즈니스 오브젝트를 생성, 수정, 삭제할 때 이벤트를 발행하면됩니다.
  • 느슨한 결합 : 이벤트를 발행하고 구독함으로 통신하기에 서비스들의 위치를 몰라도됩니다.

단점은 다음과 같습니다.

  • 이해하기 어려운 구조 : orchestration과 다르게, saga의 구현이 서비스 전반에 퍼져있습니다.
  • 서비스간 순환 참조 : saga 참여자들은 각자의 이벤트를 구독합니다. 종종 순환 참조가 발생하게 됩니다. (실질적으로 문제가 되지는 않습니다)
  • 단단하게 결합될 위험성 : saga 참여자들은 자신에게 영향을 미치는 모든 이벤트를 구독합니다. 그렇기에 서비스는 주문 서비스에 의해 구현된 주문 생명 주기에 맞춰 업데이트가 필요할 위험성이 있습니다.

choreography 기반 saga는 간단한 경우에는 잘 동작하지만, 복잡한 saga에서는 orchestration saga를 이용하는 것이 좀 더 적합합니다.

orchestration saga

orchestraion은 saga를 구현하는 또 다른 방법입니다. orchstration을 사용할 때, saga 참여자들에게 무엇을 할지 조정하는 책임을 가지는 오케스트레이터 클래스를 정의해야합니다. 오케스트레이터는 command / async reply 스타일 상호작용을 사용해서 참여자들과 통신합니다. saga 단계를 실행하기 위해선, 참여자에게 어떤 오퍼레이션을 수행해야하는지에 대한 커맨드 메세지를 전송합니다. saga 참여자가 오퍼레이션을 수행한 이후에, 오케스트레이터에게 메세지를 발행합니다. 오케스트레이터는 메세지를 처리하고, 다음 실행할 saga 단계를 결정합니다.

Screenshot 2024-10-13 at 21 56 23

주문 생성 saga의 orchestration 방식의 동작은 위와 같습니다. saga는 CreateOrderSaga에 의해 조정되며, 비동기 request/response 를 이용해 saga 참여자들을 호출합니다. 이 클래스는 프로세스의 진행도를 따라가며 saga 참여자들에게 커맨드 메세지를 발행합니다. CreateOrderSaga는 각 saga 참여자들이 보낸 응답 메세지를 읽고, 다음 단계를 결정합니다.

주문 서비스는 주문과 CreateOrderSaga 오케스트레이터를 생성합니다. 그리고 다음 과정을 통해 saga가 진행됩니다.

  1. 오케스트레이터가 고객 검증 커맨드를 고객 서비스에게 전송합니다.
  2. 고객 서비스는 고객 검증됨 메세지로 응답합니다.
  3. 오케스트레이터는 주방 서비스에 티켓 생성 커맨드를 전송합니다.
  4. 주방 서비스는 티켓 생성됨 메세지로 응답합니다.
  5. 오케스트레이터는 카드 승인 메세지를 결제 서비스에 전달합니다.
  6. 결제 서비스는 카드 승인됨 메세지로 응답합니다.
  7. 오케스트레이터는 티켓 승인 커맨드를 주방 서비스로 전달합니다.
  8. 오케스트레이터는 주문 승인 커맨드를 주문 서비스로 전달합니다.

마지막 단계에서 오케스트레이터는 주문 처리를 직접할 수 있지만, 주문 서비스를 통해 처리합니다. 오케스트레이터는 서비스를 통해 로직을 처리하는 것이 일관성을 지키는 방법이기에, 직접 처리하지 않고 서비스를 호출하는 것으로 처리합니다.

위 다이어그램처럼 하나의 깔끔한 시나리오가 존재할 수도 있지만, saga는 보통 수 많은 시나리오가 존재합니다. 그렇기에 state machine로 saga를 모델링하여 가능한 시나리오를 묘사하는 것이 유용합니다.

modeling saga orchestrators as state machines

state machine은 일련의 상태와 이벤트에 의해 촉발되는 상태 간의 전이 집합으로 구성됩니다. 각 전이는 다른 saga 참여자를 호출하는 액션을 가질 수 있습니다. 상태 간 전이는 saga 참여의 로컬 트랜잭션의 종료로 인해 호출됩니다. 현재 상태와 로컬 트랜잭션의 결과물은 상태 전이와 어떤 행동을 할지를 결정합니다. state machine을 위한 효과적인 테스트 전략들이 존재합니다. 그렇기에 state machine 모델은 saga를 디자인, 구현, 테스트하기 수월하게 해줍니다.

CreateOrderSaga의 state machine은 다음과 같습니다.

Screenshot 2024-10-13 at 22 14 18

state machine은 다음과 같은 상태들을 포함해서 구성됩니다.

  • Verifying Consumer : 초기 상태입니다. 이 상태에 있을 때, saga는 고객 서비스로 하여금 고객이 주문을 생성할 수 있는지 검증하는 것을 대기하고 있습니다.
  • CreatingTicket : saga는 티켓 생성 커맨드의 응답을 대기하고 있습니다.
  • AuthorizingCard : 결제 서비스가 고객의 카드를 승인하기를 대기하고 있는 상태입니다.
  • OrderApproved : 최종 상태로 saga가 정상적으로 종료됨을 의미합니다.
  • OrderRejected : 최종 상태로 참여자들 중 하나로 인해 주문이 취소됨을 의미합니다.

state machine은 상태 전이 또한 정의합니다.

state machine은 CreatingTicket 상태에서 AuthorizingCard 혹은 RejectedOrder 상태로 전환될 수 있음을 정의했습니다.

state machine의 초기 액션은 고객 서비스에게 고객 검증 커맨드를 보내는 것입니다. 고객 서비스의 응답은 다음 상태 전이를 호출합니다. state machin은 다양한 상태 전이를 통해 최종적으로 OrderApproved혹은 OrderRejected 상태가 되게합니다.

saga orchestration and transactional messaging

orchestration 기반 saga의 각 단계는 데이터베이스를 업데이트하는 것과 메세지를 발행하는 것을 포함합니다. 앞선 choreography 때와 마찬가지로 데이터베이스 업데이트와 메세지 발행이 원자적으로 이뤄져야합니다.

이후에 transactional messaging에 대해서 다룰 것입니다.

orchestration 기반 saga는 다음과 같은 장점을 가집니다.

  • 간단한 의존성 : 순환 참조가 존재하지 않습니다.
  • 느슨한 결합 : 오케스트레이터로 인해 saga가 처리되기에, 다른 saga 참여자를 몰라도 됩니다.
  • 비즈니스 로직에 집중 가능 : 오케스트레이터가 saga를 처리하기에, 도메인 오브젝트는 saga에 대한 고려를 하지 않아도 됩니다.

오케스트레이션에는 단점도 존재합니다. 오케스트레이터에 너무 많은 비즈니스 로직을 포함하게 될 수도 있습니다. 그렇기에 오케스트레이터는 비즈니스 로직에는 관여하지 않고, 시퀀싱에만 책임을 가지도록 디자인해야합니다.

This post is licensed under CC BY 4.0 by the author.