[MSA] 6. MSA의 트랜잭션 이야기 2 - Two-Phase commit과 Saga

이전 글에 이어서 MSA 내에서 트랜잭션을 원활히 하는 방법 2가지를 소개해드리고자 합니다. 

 

관계형 데이터베이스와 더불어 모놀리틱 아키텍처를 도입한 서버 애플리케이션은 DB 서버에서 제공하는 Commit과 Rollback을 이용하여 데이터를 일관성 있게 제어할 수 있었습니다.

 

그러나 MSA에서는 각 서비스별로 DB 인스턴스와 애플리케이션이 분리됨에 따라 관계형으로 맺어진 Entity들은 서로가 다른 인스턴스로 운영되고, 그들 리소스를 받기 때문에 데이터의 일관성이 무너지게 됩니다.

 

이를 위해 우리는 분산 트랜잭션 기술을 이용하여 이들 일관성을 지킬 수 있는 방법이 있습니다.

 

 

 

1. Two-Phase Commit

Two-Phase Commit이란, 2단계에 거쳐서 영속하는 작업을 말합니다. 이렇게 말하면 아마 JPA에 있는 영속성 컨텍스트와 혼동이 되실 수도 있을 것 같은데요. Two-Phase Commit은 영속성 컨텍스트와는 조금 다릅니다.

 

분산 DB 환경에서는 위 그림과 같이 주 DB와 보조 DB로 나누는데요. 그러나 실제 모놀리틱에서 연결된 메인 DB는 Primary DB인데, 이들이 이중화 된 데이터베이스 형태를 가지려면 그들이 동기화 형태로 되어야 합니다. Two-Phase Commit은 이러한 주 DB와 보조 DB 사이에 트랜잭션을 조율하는 조정자 (Coordinator)가 존재하며 이의 역할은 트랜잭션 요청이 들어왔을 때, 아래의 두 단계를 거쳐 트랜잭션 담당을 진행합니다.

 

  • Prepare
  • Commit

여기서 Prepare라는 작업이 모놀리틱과의 차이점인데, 이 부분이 중요합니다. 모놀리틱을 가진 형태는 그들의 인스턴스를 같이 사용하기 떄문에 트랜잭션을 적용하려는 DB가 트랜잭션이 가능한 상태인지를 알아야 할 필요가 없습니다. 그러나 인스턴스가 분리된 MSA에서는 대상 DB가 트랜잭션이 가능한 상태인지 미리 확인되어야 합니다.

 

위 그림은 쇼핑몰에서 상품을 주문했을 때, 발생하는 MSA 내의 데이터 변화를 도식한 것입니다. 먼저 주문을 하게 되면 사용자의 정보를 가져와야 하고, 그에 따른 결제가 이뤄지면 배송 기록이 남아야 합니다. 따라서 주문이 발생하면 상품과, 배송 정보, 사용자 정보 등에서 DB의 트랜잭션이 발생해야 하는데, 여기서는 간단하게 사용자와 배송 정보 데이터베이스만을 그림으로 그렸습니다.

 

주문이 발생하면 Order API로부터 요청 받은 DB는 Commit 작업을 위한 준비를 진행합니다. 이후 이와 연결되어 있는 DB의 영속 여부가 확인되면 조정자에게 준비가 완료되었음을 알리고, 위와 같이 Commit을 진행하게 됩니다.

 

반대로 관련된 DB 중 하나의 인스턴스라도 트랜잭션의 준비가 안된 상태라면 바로 Rollback을 실행해야 합니다. 그러므로 연관된 DB와 같이 트랜잭션이 이루어지기 때문에 트랜잭션의 범위는 처리하려는 DB와 연관된 DB가 전체로 진행됩니다.

 

이 순서를 간략하게 요약하면 다음과 같습니다.

  1. 조정자가 연관된 DB로 전달한 메시지에 대해 응답을 기다린다.
  2. 모든 메시지가 수신이 성공적으로 완료될 경우 commit을 진행한다.
  3. commit 단계에서 조정자는 연관된 DB에 데이터를 저장하라는 명령 메시지를 보낸다.
  4. 관련 DB들은 데이터를 영속화 한다.

 

 

Two-Phase Commit의 단점

Two-Phase Commit의 설계를 보면 분산 트랜잭션 형태를 지니고 있습니다. 따라서 분산 트랜잭션을 사용할 수 있는 애플리케이션이라면 어떤 데이터베이스든지 가능합니다.

 

그러나 최근에는 NoSQL이 자주 사용되고 있는데, 공교롭게도 NoSQL에서는 분산 트랜잭션을 지원하지 않습니다. 따라서 함꼐 사용하는 DB가 동일한 미들웨어이어야 하므로 DBMS polyglot 구성은 어렵습니다.

 

또한 DB 이중화 구조와 비슷하기 때문에 하나의 API 서버에서 요청이 들어오고 내부적으로 DB가 분산되어 있을 때 사용하는 형태로 되어 있어, MSA와 같이 API가 분리되어 있고, 각기 다른 특징을 가진 DB를 분리한 MSA에서는 구현이 쉽지 않다는 것도 이에 포함될 수 있습니다.

 

 

 

 

2. Saga Pattern

처음 Saga 패턴이라는 걸 들었을 때는 Redux의 Saga를 떠올렸습니다. 그러나 여기서 Saga는 다른 의미로 사용되는데, Saga는  분산 컴퓨팅 아키텍처인 Eventual Consistency를 바탕으로 둔 로컬 트랜잭션을 연속적으로 업데이트 수행하는 패턴입니다.

 

트랜잭션의 관리 주체가 DB 서버 자신들이 아닌 애플리케이션에 있으며 애플리케이션이 분산되었을 때 각 애플리케이션 하위에 존재하는 DB는 자신의 트랜잭션만 처리하는 구조입니다.

 

Two-Phase Commit과 다르게 본인들의 트랜잭션만을 처리하며 애플리케이션 개발자가 트랜잭션 로직을 구현해야 하는 형태입니다.

 

Saga 패턴에는 아래의 2가지 종류가 있습니다.

  1. Choreography-Based Saga
  2. Orchestration-Based Saga

Saga 인스턴스를 별도로 사용하여 처리하느냐 그렇지 않느냐의 차이인데, 먼저 Choreography-Based Saga부터 알아보도록 하겠습니다.

 

 

1) Choreograpgy-Based Saga

 

Choreography-Based Saga 패턴은 자신이 보유한 서비스 내 DB만의 트랜잭션을 관리하며 트랜잭션이 종료되면 완료 이벤트를 발행합니다. 이어 수행해야 할 트랜잭션이 있다면 해당 애플리케이션으로 완료 이벤트를 발행하고, 해당 이벤트를 받은 애플리케이션에서 계속 트랜잭션을 이어 수행합니다. 마지막에 도달하면 메인 애플리케이션에 그 결과를 전달하여 최종적으로 DB에 영속하는 방법입니다.

 

이벤트 발행과 구독을 위해 RabbitMQ, Kafka와 같은 메시지 큐 미들웨어를 이용하여 비동기 방식 혹은 분산 처리 형태로 전달할 수 있습니다.

 

Rollback의 경우 각 애플리케이션에서 트랜잭션을 관리하는 로직을 구현하여 중간에 트랜잭션이 실패하면 해당 트랜잭션 취소 처리를 실패한 애플리케이션에서 보상 Event를 발행하여 Rollback 처리를 실행합니다.

 

위 구성이 모놀리틱에서 넘어왔을 떄는 꽤 번거로운 작업 같이 보이지만 실제로  MSA에서 트랜잭션을 처리하기 가장 쉬운 방법입니다. RabbitMQ의 RPC나 Publish-Subscribed 패턴만 잘 이용해본다면 쉽게 구현해볼 수 있습니다.

 

 

 

2) Orchestration-Based Saga

Orchestration-Based Saga는 트랜잭션 처리를 위한 인스턴스가 별도로 존재하며 이를 우리는 Manager라 부릅니다. 중계적인 역할을 하지만 클라이언트에서의 요청은 한 API에서 한정적이기 때문에 이 인스턴스는 클라이언트의 요청을 받을 애플리케이션과 서비스 인스턴스로도 움직일 수 있습니다. 위의 그림이 바로 그런 형태입니다.

 

트랜잭션을 수행하는 모든 애플리케이션은 Saga 인스턴스 매니저에 의하여 점진적으로 트랜잭션을 수행하여 결과를 Manager에게 전달하는 형태입니다. 비즈니스 로직상 마지막 트랜잭션이 끝나면 Saga 인스턴스는 전체 트랜잭션이 종료한 뒤, 인스턴스는 소멸됩니다. 

 

만약 중간에 실패하게 되면, Manager가 보상 트랜잭션을 실행하여 일관성을 유지하도록 해줍니다. 모든 관리를 중앙의 매니저가 모두 알아서 해주기 때문에  MSA에서도 트랜잭션을 중앙에서 해주는 구조가 만들어집니다. 가장 안정적이고 관리가 편하며 모놀리틱한 형태를 그대로 구현해줄 수 있다는 장점이 있습니다.

 

그러나 중간에 매니저는 인스턴스 형태이기 때문에 인프라 엔지니어링 입장에서는 번거로움이 많습니다. 마치 서비스 구조를 하나 만드는데, 있어 관리해야 할 미들웨어 내지 소프트웨어가 추가되는 것이기 때문이죠.

 

Spring Framework에서 대표적으로 Orchestration-Based Saga를 사용할 수 있는 방법으로 Axon Framework가 있습니다. 이 부분은 별도의 카테고리에서 다뤄보도록 하겠습니다.

 

 

 

마치며...

여기까지 MSA의 트랜잭션에 대해서 이야기 해봤습니다. MSA에 한 번 쯤 도전해보신 분들이라면 트랜잭션에 대해 고민하였을 것이라 생각합니다. 일반적으로 한 미들웨어에서 모든 데이터를 다루는 것이 아닌 별도의 인스턴스를 가지고 그들의 데이터를 호출하고 변경 내용을 반영하는 구조인 만큼 데이터의 일관성을 유지하기 위해 고민하였을 것이라 생각됩니다.

 

저 또한 최근에 직장을 다니면서 Choreography-Based Saga 패턴을 사용해 MSA 서비스를 개발하고 있습니다. 마음 같아서는 Saga 인스턴스 매니저를 별도로 두고 하는 것을 원했지만 아직까지 Python 진영에서 이를 해소할 만한 적절한 솔루션을 찾지 못했거나, 솔루션이 없더라도 이를 구현할 수 있다면 좋을텐데, 그럴 능력까지 못된다는 것이 아쉬울 뿐이네요. ㅠㅠ

 

아직까지는 메시지 큐에 의존하여 각 애플리케이션에서 간단한 트랜잭션 로직을 구현하는 것으로 시작하여 차츰 도전해볼 수 있다면 좋겠다는 생각을 하며 이 글을 마치도록 하겠습니다.

 

 

 

comments powered by Disqus

Tistory Comments 0