티스토리 뷰

728x90
반응형

개요

앞선 포스팅에서 마이크로서비스 분산DB 환경에서 고려되어야 할 사항에 대해 살펴보았다. 자세한 내용은 아래 포스팅을 참고하기 바란다.

이번 포스팅에서는 분산트랜잭션 환경에서 데이터 원자성을 유지하기 위한 Saga Pattern에 대해 알아보자. Saga는 서비스 독립적으로 실행할 수 있도록 모델링되어 2PC와 같이 장기간 자원에 Lock을 잡을 필요가 없다. 지금부터 이 Saga를 모델링하는 과정에 대해 알아보도록 하자.


Saga Pattern

Saga는 단일 데이터베이스에서 장시간 동작하는 트랜잭션에 대해 서포트하는 메커니즘으로 계획되었지만, 여러 서비스에 걸친 트랜잭션을 관리하는 데에도 적합하다.

아래와 같이 계좌이체 요청에 대해 비즈니스 흐름을 살펴보도록 하자.

  • 계좌이체 시작
  • 고객서비스를 통해 먼저 타행 이체 서비스를 위한 대상 고객 정보를 조회한다.
  • 계좌서비스를 통해 타행 이체를 위한 잔액을 조회한다.
  • 이체서비스를 통해 타행으로 계좌이체를 요청한다.
  • 타행서비스를 통해 타행에 금액을 이체하고 결과를 이체서비스에 전달한다.
  • 계좌서비스를 통해 이체가 성공했을 경우 잔액을 반영한다.
  • 계좌이체 종료

여기서 계좌이체 요청은 단일 사가(Saga)로 표현되며, 이 흐름의 각 단계는 서로 다른 서비스가 수행할 수 있는 작업을 나타낸다. 각 서비스 내에서 모든 상태 변경은 로컬 ACID 트랜잭션 내에서 처리될 수 있다. 예를 들어, 이체서비스를 사용하여 이체한 금액에 대해 내부적으로 기록하하고 실패 시 롤백 처리할 수 있다. (물론 이체서비스의 경우 타행이체를 위한 또 다른 서비스와 연관되어 있어 이를 처리하는 로직이 필요하지만, 이에 대해서는 아래 보상트랜잭션 설계에서 자세히 알아보도록 하자.) 또한, 계좌서비스의 잔액반영 시에도 잔액 반영에 실패할 경우 로컬 데이터베이스 내에서 트랜잭션을 관리할 수 있다. 결국 각 서비스에 대한 정상 처리가 수행될 경우 로컬 트랜잭션 별로 ACID를 관리하는 것은 전혀 문제가 되지 않는다.


Saga 실패 시 처리 방안

마이크로서비스는 개별 트랜잭션으로 분할되면서 트랜잭션 실패가 발생했을 때 복구하는 방법을 고려해야 한다. Saga에서 설명하는 복구 방법으로는 backward/forward recovery 두가지가 있다.

먼저 backward recovery는 트랜잭션 실패시 롤백을 처리하는 방식이다. 따라서 이전에 커밋된 트랜잭션을 취소할 수 있는 보상 트랜잭션을 정의해야 한다. forward recovery는 장애가 발생한 지점에서 중지하지 않고 계속 처리하는 방식이다. 따라서 트랜잭션을 재시도할 수 있도록 충분한 정보를 유지하고 있어야 한다.

비즈니스 프로세스의 특성에 따라 모델링되는 모든 실패 상황에는 backward/forward recovery 또는 이 둘의 혼합을 통해 적절한 처리 방식을 선택해야 한다.

Saga Backward Recovery

단일 데이터베이스 내 ACID 트랜잭션을 사용하면 실패 발생 시 커밋 전에 롤백이 발생한다. 그러나 마이크로서비스 환경에서는 여러 트랜잭션이 관련되어 있어 서비스간 독립적인 트랜잭션이 발생할 때 실패 발생 전 커밋된 데이터에 대한 복원 정책을 마련해야 한다. 지금부터는 이러한 상황에서 어떻게 이전 데이터로 롤백을 처리할 것인지에 대해 알아보도록 하자.

다음은 앞서 살펴본 계좌이체 서비스에 대해 처리 성공 실패를 표시한 내용이다.

  • 계좌이체 시작
  • 고객서비스를 통해 먼저 타행 이체 서비스를 위한 대상 고객 정보를 조회한다. (성공)
  • 계좌서비스를 통해 타행 이체를 위한 잔액을 조회한다. (성공)
  • 이체서비스를 통해 타행으로 계좌이체를 요청한다. (성공)
  • 타행서비스를 통해 타행에 금액을 이체하고 결과를 이체서비스에 전달한다. (성공)
  • 계좌서비스를 통해 이체가 성공했을 경우 잔액을 반영한다. (실패)

타행서비스 반영이 정상적으로 완료된 후 계좌서비스에서 잔액을 반영하고자 하였으나, 타행이체가 처리되는 동안 카드 값이 자동이체되어 잔액이 마이너스가 되는 상황을 가정해 보자.(물론, 금융권에서 이렇게 방어코드 없이 개발하지는 않겠지만, 시나리오 가정임을 이해해 주시기를..) 이때 앞서 반영한 타행서비스의 이체결과와 이체서비스의 계좌이체 성공이력을 롤백해야 하는 상황이 발생한다. 이미 타행서비스를 통해 계좌 이체가 처리 되어 +@ 만큼 통장 잔액이 증가했을 텐데 이에 대해 롤백 처리를 어떻게 구현해야 할지 알아 보도록 하자.

이 모든 단계가 단일 데이터베이스 트랜잭션에서 수행된 경우 간단한 롤백으로 정리할 수 있었을 것이다. 그러나 이체서비스, 타행서비스, 계좌서비스 서로 다른 세 서비스에서 상호간의 호출에 의해 처리 되었기 때문에 일괄 롤백으로 처리할 수는 없다. 대신 롤백을 구현하려면 보상 트랜잭션을 구현해야 한다. (보상 트랜잭션은 이전에 커밋 된 트랜잭션을 취소하는 작업)

위는 보상트랜잭션을 적용한 서비스 흐름을 재정리한 내용이다.

  • 계좌이체 시작
  • 고객서비스를 통해 먼저 타행 이체 서비스를 위한 대상 고객 정보를 조회한다. (성공)
  • 계좌서비스를 통해 타행 이체를 위한 잔액을 조회한다. (성공)
  • 이체서비스를 통해 타행으로 계좌이체를 요청한다. (성공)
  • 타행서비스를 통해 타행에 금액을 이체하고 결과를 이체서비스에 전달한다. (성공)
  • 계좌서비스를 통해 이체가 성공했을 경우 잔액을 반영한다. (실패)
  • 1) 타행이체취소 보상트랜잭션 요청
  • 2) 이체성공 > 이체실패처리 요청
  • 계좌이체 종료

이러한 보상 트랜잭션은 커밋 후에 롤백을 정의하므로 완전히 처음과 동일한 상태로 돌아기지 못하는 경우도 발생한다. 이와 같은 이유로 보상거래를 의미론적 롤백이라고도 한다.

또한, 롤백을 구현하며 기존 비즈니스를 개선하여 처리할 수도 있다. 계좌이체 트랜잭션의 마지막 단계에서 성공적으로 완료되었을 경우 Push로 정상 이체가 완료되었다는 알림을 보내는 형태가 존재한다고 가정하자. 만약 우리가 그것을 롤백하기로 결정한다면, 거래에 문제가 발생하여 이체가 취소되었음을 알리는 Push 알림을 고객에게 보낼 수도 있다.

Saga Forward Recovery

때때로 장애가 발생한 지점에서 중지하지 않고 계속 처리해야 하는 경우가 발생할 수 있다. 실패가 발생한 시점에 즉시 복구가 중요하지 않은 서비스이거나, 실패한 내용이 성공한 부분보다 굉장히 사소한 부분일 경우, 또는 트랜잭션을 다시 시작할 경우 부하가중이 심각할 경우, 또는 보상트랜잭션을 구성하지 않고도 실패에 대해 알림을 주고, 그 경우가 클라이언트 입장에서 허용될 수 있을 경우 등에 Forward Recovery를 적용할 수 있다.

예를 들어, 물건을 구매하는 비즈니스를 예를 들어보자. 고객으로부터 돈을 받아 물건을 포장한 후 주문 처리까지 완료된 이후 배송을 처리하다 실패가 발생하였다. 배송에서 어떤 이유로 실패가 발생하였다고 해서 앞서 성공한 구매, 포장 프로세스까지 롤백 처리하는 것은 비즈니스 상 적절하지 않은 방안이라 할 수 있다. 이를 해소하기 위해 배송에 실패한 원인에 대해 분석하고 이를 재시도 함으로써 실패를 처리할 수 있다. 이때 실패에 대한 분석 작업, 클라이언트에게 배송간 문제가 발생했다는 알림 전송 등을 사람이 직접 개입하여 처리해야 하는 경우도 발생할 수 있다.

비즈니스 프로세스 개선

비즈니스 순서를 변경하면 롤백 시나리오를 보다 간단하게 만들 수 있다. 아래와 같이 잔액반영과 계좌이체의 순서를 변경하여 타행 서비스가 수행되기 전 계좌서비스 내 처리를 한번에 수행되도록 묶고, 타행서비스와 같이 외부 인터페이스로의 호출을 가장 뒤로 배치하였다.

이렇게 하면 앞서 실패가 발생한 원인이었던 타행이체가 진행되는 도중 카드값이체로 인한 잔액 부족 현상은 발생하지 않을 것이다. 또한, 타행서비스와 묶여있는 서비스를 단일화하여(이체서비스) 직접 트랜잭션을 관리할 수 없는 서비스에 대한 영향도를 최소한으로 줄일 수 있다.

"때로는 프로세스가 수행되는 방법을 조정하는 것만으로 롤백 작업을 간소화할 수 있다. 실패할 가능성이 가장 높은 단계를 앞당기거나, 여러 서비스를 번갈아가면서 처리하기보다는 단일 트랜잭션 내에서 처리 가능한 프로세스를 하나로 묶어 주는 것이 보상트랜잭션을 줄이는 방법이라 할 수 있다. 이러한 변화들이 수용될 수 있다면, 몇몇 단계들에 대한 보상적인 거래들을 만들 필요가 없기 때문에 보다 쉽게 분산트랜잭션을 설계하고 개발할 수 있다. 이는 보상거래를 구현하기 어려운 경우에 특히 중요할 수 있다."


Saga 구현

지금부터는 saga를 구현하는 방법에 대해 알아보자. saga는 대체로 두 가지 스타일로 구현한다. 먼저 Orchestrated saga주로 중앙에서 관리하는 프레임워크 또는 미들웨어를 통해 통제되는 방식이다. 보상트랜잭션을 관리하는 중앙집중식 방식으로 개발 부담을 덜 수 있지만, 결합도가 올라간다. 두번째로 Choreographed saga는 느슨하게 결합된 모델을 선호하는 경우 적용할 수 있지만, 비즈니스를 이해하고 적용할 수 있도록 높은 개발 난이도를 요구하며, saga의 진행 과정을 더 복잡하게 만들 수 있다.

Orchestrated Saga

Orchestrated Saga는 실행 순서를 정의하고 필요한 보상 작업을 트리거하기 위해 중앙 오케스트레이터를 사용한다. 중앙 오케스트레이터는 어떤 일이 언제 발생하는지 제어하며, 이를 통해 주어진 saga에서 어떤 일이 일어나고 있는지에 대한 가시성을 확보할 수 있다.

  • 계좌이체 시작
  • Transfer Orchestrator는 계좌서비스에 잔액반영을 요청한다.
  • Transfer Orchestrator는 이체서비스에 계좌이체를 요청한다.
  • Transfer Orchestrator는 타행서비스에 타행이체를 요청한다.
  • 계좌서비스는 잔액이 충분하여 잔액반영 후 서비스응답처리 채널을 통해 Transfer Orchestrator에 결과를 알린다. (성공)
  • 이체서비스는 계좌이체 대상의 계좌 정보 확인 후 서비스응답처리 채널을 통해 Transfer Orchestrator에 결과를 알린다. (성공)
  • 1) 타행서비스는 타행이체 시 타행의 점검 시간과 겹쳐 이체에  실패하였고 이를 서비스응답처리 채널을 통해 Transfer Orchestrator에 결과를 알린다. (실패)
  • 2) Transfer Orchestrator는 성공한 서비스인 계좌서비스와 이체서비스에 롤백을 수행하기 위해 보상 처리를 위한 정보를 제공하고, 보상 트랜잭션을 요청한다.
  • 3) 계좌서비스와 이체서비스를 보상 트랜잭션을 처리하기 위해 각각 잔액롤백처리와 계좌이체실패처리에 대한 이벤트를 수신하고, 롤백(의미론적 롤백)을 처리한다.
  • 계좌이체 종료

오케스트레이터의 역할을 하는 Transfer Orchestrator가 프로세스를 조정한다. 서비스 처리는 물론 보상을 처리하기 위해 어떤 서비스가 필요한지 알고 있으며, 언제 해당 서비스를 요청해야 할지 결정한다. 만약 요청이 실패한다면, 그에 따르는 보상처리를 위해 무엇을 해야 할지 결정할 수 있다. 이러한 오케스트레이션된 프로세서는 서비스 간 요청/응답 호출을 많이 사용하는 경향이 있다. Transfer Orchestrator는 서비스(예: 계좌, 이체, 타행서비스)에 요청을 보내고 요청이 성공했는지 여부를 알려주고 요청 결과를 제공하는 응답을 기다린다.

Transfer Orchestrator 내부에서 비즈니스 프로세스를 명시적으로 모델링하는 것은 매우 유용하다. 요청과 보상을 처리하는 지점을 단일화하여 동작 방식을 쉽게 이해할 수 있도록 도와주며, 새로운 서비스가 더해질때 보다 쉽게 서비스 프로세스를 정의할 수 있다. 반면에 Orchestrated Saga는 오케스트레이터를 기반으로 결합된 방식이다. Transfer Orchestrator는 모든 관련 서비스에 대해 알아야 하므로 도메인 커플링이 높아질 수 있다.

Choreographed Saga

Crchestrated Saga가 중앙 오케스트레이터에 의한 제어 방식이라면, Choreographed Saga는 운영에 대한 책임을 마이크로서비스에 분산시키는 방식이다.

  • 0. 이체 요청 및 이체금액, 이체대상 은행, 고객명 등에 대한 이벤트를 생성한다.
  • 1~2. 계좌서비스는 이체금액에 대한 이벤트를 수신하여 잔액이 충분할 경우 잔액을 반영 후 이벤트를 생성하고, 잔액이 부족할 경우 잔액부족을 알리고 트랜잭션을 종료한다.
  • 3~4. 이체서비스는 계좌이체 대상 은행, 고객명 등에 대한 이벤트를 수신하고, 계좌이체 대상이 확인되었으면, 이벤트를 생성, 계좌이체 대상이 확인되지 않을 경우 계좌서비스에 보상트랜잭션을 요청한다.
  • 5~6. 타행서비스는 타행이체 대상 은행, 고객명, 이체 금액 등에 대한 이벤트를 수신하고, 타행으로부터 이체 결과를 기다린다. 이체 결과가 성공일 경우 이체 요청자에게 Push 알림을 전송하고, 실패할 경우 이체서비스, 계좌서비스에 보상트랜잭션을 요청한다.
  • 계좌서비스는 잔액이 충분하여 잔액반영 후 서비스응답처리 채널을 통해 Transfer Orchestrator에 결과를 알린다. (성공)

위와 같이 서비스가 보상트랜잭션을 결정하고 처리하는 방식이 바로 Choreographed 방식이라 할 수 있다. 이러한 서비스들은 수신되는 이벤트에 반응하고 있다. 이벤트는 요청자 또는 서비스로부터 송신되며, 이를 구독하는 또 다른 서비스가 수신하는 방식이다. 즉 이벤트를 서비스에 직접 보내는 것이 아니라 이벤트에 관심 있는 서비스가 이벤트를 수신하고 그에 따라 조치를 취하는 방식이다. 이 예시에서 계좌서비스는 첫 번째 이체요청-잔액확인 이벤트를 받을 때 통장 잔액을 확인하고 잔액이 충분할 경우 잔액을 반영하고 이벤트를 생성하지만, 잔액이 부족할 경우 해당 시점에서 이체요청이 중단될 수 있다.

이와 같은 방식에 적합한 적용 방식이 바로 Kafka와 같은 MQ 서비스를 적용하는 것이다. 여러 서비스가 동일한 이벤트에 반응할 수 있다. 특정 유형의 이벤트에 관심이 있는 서비스는 이러한 이벤트가 어디에서 왔는지 고민할 필요 없이 특정 주제를 구독하고 브로커는 Topic의 내구성 관리 및 해당 이벤트가 구독자에게 성공적으로 전달되도록 보장한다.

Orchestrated Saga와 다르게 Choreographed Saga의 장점은 Publishing 한 서비스가 누구인지 알 필요 없이 특정 이벤트가 수신되었을 때 무엇을 해야 하는지만 알면된다는 점이다. 본질적으로, 이것은 Loosly Coupled 아키텍처를 만든다. 반면에 단점으로는 서비스 간 결합도가 낮아짐에 따라 추적이 점점 더 어려워질 수 있다는 것이다. 오케스트레이터의 명시적인 모델링과 다르게 간단한 비즈니스 프로세스도 각 서비스의 동작을 개별적으로 살펴보고 비즈니스를 재구성해야 한다.

특히 Choreographed Saga의 경우 이벤트에 대한 처리 상태를 저장할 수 있는 별도의 공간이 부재하다는 문제가 있다. 오케스트레이터가 담당하던 역할 역시 Choreographed Saga에서도 보상 처리를 위해 필수적으로 필요하며, 이를 간단히 구현하는 방법에 대해 알아보자.

"이벤트를 소비할 때 Saga의 상태를 저장하는 방법을 사용한다. Saga에 대한 Unique ID를 생성하고 송/수신되는 모든 이벤트에 이를 포함한다. 이 ID를 기반으로 보상트랜잭션을 위한 Event 정보를 관리하거나 각 서비스의 상태 정보를 확인할 수 있다."


결론

지금까지 마이크로서비스의 분산 트랜잭션 처리 방식과 Saga Patten에 대해 알아 보았다. Saga Pattern은 분산 트랜잭션 환경에서 서비스 간 결합도를 낮추고 보상 처리를 설계하는 패턴 중 하나이다. Saga는 비즈니스에 적합한 적용 방식에 따라 서비스 to 서비스 간 비즈니스 흐름을 개선하여 보상 트랜잭션을 최소화 하는 것이 바람직하다.

물론 그럼에도 불구하고 보상 트랜잭션을 설계해야 한다면, 프로젝트의 규모에 따라 Choreographed Saga와 Orchestrated Saga를 선택하는 것이 좋다. 선택기준으로 명확히 제시된 것은 없지만, 일반적으로 프로젝트의 규모에 따라 선택기준을 분리해 볼 수 있다. 단순하게 Loosley Coupled 지향의 Choreographed Saga는 대규모 프로젝트에서 각 팀간의 결합도를 낮추고 개발에 영향을 최소화 하기 위해 선택하기 용이하다. 반면에 Orchestrated Saga는 단일 팀 내에서 개발 가능한 경우 중앙 관리 방식으로 개발하는 것이 효과적일 것이다.

728x90
반응형