0. 들어가기 전
기존의 모놀리식 구조에서는 각 도메인뿐만 아니라 도메인을 영속화하는 DB까지 하나의 애플리케이션에 존재했다.
따라서, 두 개 이상의 서비스를 거쳐야하는 프로세스에서도 DBMS가 기본적으로 제공하는 트랜잭션 기능을 통해 한 번에 트랜잭션으로 묶어 원자성과 일관성을 유지할 수 있었다.
하지만, MSA 환경에서는 각 서비스가 별도의 애플리케이션으로 동작하며, 각 서비스는 자체 DB를 가지는 경우가 많다.
이로 인해 A 서비스의 DB에서 발생한 변경 사항을 B 서비스의 DB에 반영하려고 할 때는 두 서비스 간 통신 방식과 트랜잭션 처리가 중요한 과제가 된다.
이전에 우리는 OpenFeign을 활용해 서비스 간 통신을 구현해 본 적이 있다.
하지만 OpenFeign은 HTTP 기반 통신을 단순화하는 동기식 클라이언트 라이브러리로, 트랜잭션 관리를 포함하지 않는다.
문제 없이 항상 완료만 되는 상황이라면 좋겠지만, 현실은 그렇지 못하고 Openfeign은 통신 실패 시 트랜잭션을 되돌리거나(롤백) 보상 트랜잭션을 제공하는 메커니즘이 내장되어 있지 않으므로 추가적인 구현이 필요하다.
즉, 네트워크 장애나 서비스 장애 등 외부 요인에 의해 트랜잭션의 원자성과 일관성이 깨질 가능성이 있다는 뜻이다.
따라서 두 개 이상의 분산된 DB를 하나의 트랜잭션으로 묶어 처리하기 위한 새로운 방법이 필요하다.
1. Two Phase Commit 패턴(2PC 패턴)
2PC 패턴은 분산 환경에서 트랜잭션의 원자성을 보장하기 위한 패턴이다.
2PC 패턴은 이름에서 알 수 있듯 Prepare Phase와 Commit Phase의 2단계를 거쳐 데이터를 영속화 하게 되는데
여러 DB가 분산된 환경에서 트랜잭션을 조율하기 위한 조정자(Coordinator)가 존재하고
이 조정자는 트랜잭션 요청이 들어왔을 때 앞서 말한 두 단계를 거쳐 트랜잭션의 진행을 담당한다.
1) Prepare Phase
첫 번째 단계인 Prepare Phase는 Coordinator가 연관된 DB에게 데이터를 저장할 수 있는 상태인지 질의하는 과정에 해당한다.
- 1. Coordinator가 게시글 작성과 관련한 DB에게 상태 질의
- A DB에 Row 생성가능 여부 질의 & 해당 Row에 Lock 설정
- B DB에 Row 수정 가능 여부 질의 & 해당 Row에 Lock 설정
- 2. 각 DB에서 Coordinator에게 응답
- 각 DB에서 요청에 대해 처리 가능한지 여부를 Coordinator에게 응답
여기서 중요한 점은 요청 처리 가능 여부를 Coordinator에게 응답한다고 바로 Commit 되지 않는다는 것이다.
Coordinator는 관련한 모든 서비스들의 응답을 듣고 그 후에 Commit 여부를 판단하기 때문에,
연관된 모든 서비스에 동시에 질의한 후 모든 서비스의 응답을 받을 때까지 응답이 온 서비스의 작업에 Lock을 설정한다.
2) Commit Phase
Coordinator는 모든 응답의 수신이 완료되면 Commit Phase를 진행한다.
이 때, 조정자는 모든 DB에서 요청을 처리할 수 있다는 응답을 받았을 때에만 DB에 Commit 명령을 내린다.
만약, 두 단계를 거치는 동안 연관된 DB 중 하나라도 요청 처리 불가 응답을 보낸다면 모든 DB에 Rollback을 요구한다.
그렇게 모든 DB에서 트랜잭션의 처리가 완료되면 전체 트랜잭션을 종료하는 동시에 모든 DB 데이터가 영속화 된다.
3) MSA 환경에서 2PC 패턴의 문제점
- 모든 요청을 처리할 때까지 관련한 모든 DB에 Lock이 설정된다
- 연관 DB가 많아질 수록 서비스 수에 비례해 지연시간이 길어지게 된다.
- 서비스 간 강결합을 초래한다.
- Coordinator를 기반으로 서비스 간의 강력한 결합이 생기게 된다.
- 서비스 간의 결합을 줄이고 독립적인 서비스를 구축하기 위한 MSA 구조의 도입 의도와 반대된다.
- 2PC 패턴은 DBMS간 분산 트랜잭션을 지원해야 적용가능하다.
- 하지만 NoSQL 제품군은 이를 지원하지 않고,
- 함께 사용되는 DBMS가 동일한 제품군이어야 하기 때문에 DBMS Polyglot 구성에서는 사용할 수 없다.
이러한 문제점들로 인해 MSA 환경에서 2PC 패턴은 분산 트랜잭션의 처리 방안으로 잘 사용되지 않는다.
2. Saga 패턴
SAGA 패턴은 MSA에서 데이터 일관성을 관리하는 방법으로 2PC 패턴의 문제점들을 보완, 해결할 수 있다.
앞서 2PC 패턴을 소개할 때 관련 모든 서비스들의 DB 트랜잭션을 동시에 처리하고 각 DB가 응답하는 대로 요청이 완료될 때까지 DB에 Lock을 설정하는 방식을 사용한다고 설명했다.
그러나 Saga 패턴에서 각 서비스는 애플리케이션 각각의 로컬 트랜잭션을 가지고 있으며, 순차적으로 해당 서비스의 DB가 업데이트될 때 메시지 또는 이벤트를 발행해서 다음 단계 트랜잭션을 호출하게 된다.
만약, 해당 프로세스가 실패하게 되면 데이터 정합성을 맞추기 위해 이전 트랜잭션에 대해 보상 트랙잭션을 실행한다.
이를 이해하기 쉽게 E-Commerce의 주문-결제 서비스를 구현한다고 해보자.
1. Order 서비스는 새로운 주문이 들어오면 DB에 주문을 등록하는 로컬 트랜잭션을 처리한다.
2. 메시지 브로커에 주문 등록 이벤트를 Publish한다.
3. 이 이벤트를 Subscribe 중인 Payment 서비스에서 DB를 업데이트하는 로컬 트랜잭션을 실행한다.
4. 이 결과를 메시지 브로커에게 Publish한다.
5. 이 때, Payment 서비스에 문제가 생겨 트랜잭션이 롤백 된다면 이전 이벤트를 발행했던 Order 서비스에 '보상 이벤트'를 발행하여 트랜잭션을 롤백하는 '보상 트랜잭션'을 실행한다.
보상 트랜잭션이란?
보상 트랜잭션은 일반적인 DBMS에서의 트랜잭션에서 진행하는 물리적 롤백과는 다르게 트랜잭션 자체를 롤백하는 것이 아니라 롤백하는 것처럼 보이게 만드는 비즈니스 로직을 구현하는 것을 의미한다.
위 예시의 주문 생성 -> 결제 완료의 Flow에 Saga 패턴을 적용한다고 해보자
이 때, 주문 생성 트랜잭션이 정상적으로 완료되고 결제 완료 트랜잭션에 문제가 있어 롤백이 되면 SAGA 패턴에서는 보상 트랜잭션을 실행하게 된다.
일반적으로 보상 트랜잭션의 구현은 'Status'를 추가하고 이 Status를 변경하는 것으로 구현한다.
예를 들면 OrderStatus를 생성하고 주문 시에 Success 상태로 설정해놓았다가 보상 트랜잭션 로직을 실행하면 OrderStatus를 Cancled 상태로 변경한다.
그리고 최종적으로 Success 상태의 주문만 정상주문 처리하게 하면 최종적으로는 롤백한 것처럼 구현할 수 있는 것이다.
SAGA 패턴은 이처럼 연속적인 업데이트의 연산으로 이루어져 있고 전체 DB의 데이터가 동시에 영속화 되는 것이 아니라 순차적인 단계로 각각의 애플리케이션 별로 트랜잭션이 이루어진다. 그렇기 때문에 DBMS를 다른 제품군으로 구성할 수 있다는 장점이 있다.
또한 2PC 패턴과는 다르게 SAGA 패턴은 데이터 격리성을 보장해 주지 않는다. 하지만 보상 트랜잭션을 통해 최종 일관성과 원자성을 달성할 수 있기 때문에 분산되어 있는 DB간에 정합성을 맞출 수 있다.
3. Saga 패턴의 종류
Saga 패턴은 크게 2종류로 구분할 수 있다.
앞서 설명한 Saga 패턴의 주요 특징은 둘 다 갖고 있지만, 이벤트 및 보상 트랜잭션 처리의 주체에 따라 구분된다.
- Orchestrated Saga : 'Orchestrator'라는 중앙 처리 주체가 이벤트 및 보상 트랜잭션을 처리
- Choreographed Saga : 각 마이크로 서비스가 이벤트 및 보상 트랜잭션을 처리
3-1.Orchestrated Saga
Orchestrated Saga 패턴은 앞에서 다음과 같이 소개했다.
'Orchestrator'라는 중앙 처리 주체가 이벤트 및 보상 트랜잭션을 처리
Ohchestrated Saga 패턴은 트랜잭션 처리를 위해 Saga 인스턴스(Orchestrator)가 별도로 존재한다.
트랜잭션에 관여하는 모든 애플리케이션은 이 Orchestrator에 의해 순차적으로 트랜잭션을 수행하며 결과를 Orchestrator에 전달하게 되고, 비즈니스 로직상 마지막 트랜잭션이 끝나면 Orchestrator를 종료해 전체 트랜잭션 처리를 종료한다.
만약 중간에 실패하게 되면 Orchestrator에서 보상 트랜잭션 실행 이벤트를 publish해서 일관성을 유지한다.
이는 모든 트랜잭션의 관리가 Orchestrator에 의해 이루어지고 호출되기 때문에 분산 트랜잭션의 중앙 집중화가 이루어진다.
장점
- 각 마이크로 서비스의 트랜잭션 이벤트들을 Orchestrator에서 처리하기 때문에
- 서비스가 추가되더라도 이벤트 구조를 파악하기 쉽다.
- 마이크로 서비스 간의 이벤트 순환 의존이 없다.
- 각 마이크로 서비스들은 다른 마이크로 서비스에 대해서 알 필요가 없어지기 때문에 결합이 적어진다.
- 트랜잭션 및 이벤트 처리가 Orchestrator에서 이루어지므로 현재 이벤트 및 트랜잭션 상태를 Orchestrator에서 쉽게 추적할 수 있다.
단점
- 중앙 관리 시스템인 Orchestrator 구현을 위해 추가적인 인프라 리소스가 필요하다.
- Orchestrator가 전체 Flow를 관리하기 때문에 단일 장애 지점(SPOF)가 되어 장애 발생 시 모든 서비스에 장애가 전파될 수 있다.
- Orchestrator 구현이 상대적으로 어렵다.
3-2. Choreoghraphed Saga
앞서 Choreoghraphed Saga 패턴을 소개할 때, 각 마이크로 서비스가 이벤트 및 보상 트랜잭션의 처리 주체라고 소개했다.
하지만 정확히는, '각 마이크로 서비스의 메시지 브로커'가 이벤트 및 보상 트랜잭션의 처리 주체이며 이벤트를 pub/sub를 하는 방식으로 통신한다.
즉, 각 마이크로 서비스들의 모든 이벤트를 Orchestrator가 pub/sub하는 Orchestrated Saga 패턴과는 다르게 각각의 마이크로 서비스들의 메시지 브로커가 서로 연관있는 서비스들끼리 이벤트를 Subscribe하고 publish한다는 것이다.
장점
- 마이크로 서비스가 적다면 쉽고 간단하게 구성이 가능하다.
- 기존 MSA 환경에서 추가적인 인프라 리소스가 필요하지 않다.
단점
- 마이크로 서비스가 많아진다면,
- 각 서비스 간의 이벤트 구조를 파악하기가 어렵고, 의존성 순환이 발생할 위험이 있다.
- 모니터링 시에 현재 이벤트 및 트랜잭션의 상태를 추적하기가 어렵다.
4. 프로젝트에 적용 - Choreographed Saga
프로젝트에 적용할 분산 트랜잭션 처리 방안으로는 Saga 패턴 중에서도 Choreographed Saga를 선택했다.
우선, 2PC 패턴은 여러가지 단점들과 실제 MSA 환경에서 사용하기에 적합하지 않은 패턴이라 생각되어 제외했고
Saga 패턴 중에서도 Orchestrator의 추가 구현이 필요하지 않고
규모가 작은 프로젝트에 적합한 Choreographed Saga 패턴을 적용하는 것이 좋겠다고 판단했다.
이제부터 본격적으로 Chreograped Saga 패턴을 프로젝트에 적용해 보기에 앞서
Choreograped Saga 패턴에서는 메시지 브로커가 이벤트를 pub/sub하는 방식으로 통신한다고 설명했었는데 이를 위한 메시지 브로커로 Kafka를 사용하기로 결정했다.
때문에 Kafka가 무엇인지도 알아야 할 필요가 있다고 생각되어 다음 게시글에서는 Kafka가 무엇인지 그리고 어떻게 적용할 수 있는지도 함께 알아보려고 한다.
'Develop > Java & Spring' 카테고리의 다른 글
[MSA] MSA 전환 프로젝트 - 9. 분산 트랜잭션 (2) with Kafka (0) | 2025.03.26 |
---|---|
[MSA] MSA 전환 프로젝트 - 7. 서비스 장애 처리(Circuit Breaker) with Resilience4J (0) | 2025.03.26 |
[MSA] MSA 전환 프로젝트 - 6. 마이크로 서비스 간 통신 with Spring Cloud OpenFeign (0) | 2025.03.26 |
[MSA] MSA 전환 프로젝트 - 5. Spring Cloud Bus (0) | 2025.03.26 |
[MSA] MSA 전환 프로젝트 - 4. Spring Cloud Config (0) | 2025.03.26 |