서론
단일 서버와 DB 로 구성되는 Monolithic 서버에서는 하나의 API 로직 내부에서 특정 부분에서 실패가 발생 한다면, DB 의 rollback 을 이용해서 commit 전에 작업하던 모든 데이터를 처리하기 전 상태로 돌려놓을수가 있다.
하지만 서비스의 규모가 커지다 보면 성능이나 관리적 이슈로 인해 MSA(Micro Service Achitecture)로 넘어가게 되는데, MSA 에서는 API 서버와 DB가 서비스별로 나누어져 있다. 서버와 DB 가 나누어져 있기 때문에 Monolithic 에서처럼 DB rollback 만으로는 실패한 트랜잭션의 원복이 어렵게 된다. 이렇게 여러 리소스가 나누어져 있을 때의 트랜잭션을 "분산 트랜잭션" 이라고 하고, 분산 트랜잭션에서 실패가 발생했을 때 복원시키기 위해 발생하는 또다른 트랜잭션을 "보상 트랜잭션" 이라고 한다.
보상 트랜잭션은 이름에서 알 수 있듯이 "트랜잭션" 이다. 분산 환경에서는 rollback 이 어렵기 때문에 개발자가 직접 롤백하기 위한 로직에 대한 트랜잭션을 구현하게 되기 때문에 "트랜잭션" 이라고 한다.
그렇다면 MSA 환경에서의 분산 트랜잭션과 보상 트랜잭션을 구현하기 위해서는 어떻게 해야 할까? 여러가지가 있겠지만 가장 널리 사용되는 것 중 하나가 이벤트 기반 아키텍처이다.
이벤트 기반 아키텍처
MSA 로 서비스들이 나눠지게 되면 하나의 동작을 위해 각각의 서비스에 동작 처리 요청을 하게 된다. 만약에 서비스 간에 API 요청으로만 모든것을 처리하며 동기로 통신하게 된다면 네트워크 지연 문제도 있을 것이고, 요청이 실패했을 때나 하나의 서버가 응답이 없는 경우에 무한정 기다리게 되는 등의 장애 전파 문제도 생길 수 있는 어려움이 있다.
그래서 나온 것이 각각의 서비스들을 '구독' 하면서 비동기로 서비스 로직 수행을 요청/응답하는 "이벤트 기반 아키텍처" 이다. 서비스간 데이터 요청을 비동기로 요청하고 처리하는 구조로써, 주로 MQ 와 같은 메시지 브로커를 통해서 수행하게 된다.
장점
- 서비스 간에 동기로 동작하는 것이 아니고, 이벤트로 요청이 처리되기 때문에 느슨한 결합이 형성된다.
- 비동기 동작으로 수행되기 때문에 장애 전파 가능성이 낮아진다
단점
- 단순 비동기 구현은 이벤트 호출의 순서를 보장하기가 어렵다
- 서비스들의 흐름을 파악하기가 어렵다
- 트랜잭션이 여러개로 나뉘기 때문에 실패에 대한 처리가 어렵다.
이벤트 기반 아키텍처의 단점인 트랜잭션 관리의 어려움을 어떻게 해결할수 있을까? 라는 질문에 해결책을 들고 나온 것이 아래의 2PC 패턴이다.
2 Phase Commit (2PC) Protocol
2 phase commit 은 말그대로 커밋을 2단계에 걸쳐 하는 커밋을 의미한다. 분산 트랜잭션을 구성하는 요소로는 "Coordinator" 와 "Participant" 가 있는데, Coordinator 는 분산 트랜잭션을 관리하는 조정자 역할을 하고 Participant 는 각각의 서비스 (DB) 를 의미한다.
2PC 에서는 아래와 같은 절차로 분산 트랜잭션의 안전한 처리를 구현한다.
1) Client 에서 Commit 요청이 발생하면, Coordinator 는 Participant 들에게 Commit 을 할수 있는지 확인하는 "Prepare" 요청을 날린다
2) Participant 는 commit 이 가능한 상태라면 가능하다로 응답하고, 불가능하면 불가능하다고 응답한다.
3) 만약 하나의 Participant 라도 prepare 에 대한 응답이 불가능으로 온다면, Coordinator 는 모든 Participant 들에게 rollback 요청을 날린다
4) 모든 Participant 들이 prepare 된 상태라면, commit 을 요청한다.
장점
- 구현이 비교적 간단하다 ( DBMS 에서 Participant 로써의 기능이 구현되어 있고, Java Spring JTA 에서 Coordinator 를 지원하기 때문에 개발자가 실질적으로 구현해야 될 부분은 없다 )
단점
- 각각의 prepare/commit 요청들은 모두 네트워크 요청이기 때문에 속도가 느리다
- NoSQL 등 지원하지 않는 경우도 있고 DB 플랫폼이 다르다면 사용이 불가할수도 있다. = 확장성이 낮다.
- prepare -> rollback 은 가능하나 commit 시점에서 누군가가 실패하면 개발자가 결국엔 직접 보상 트랜잭션은 구현해 줘야 한다. 기본적으로 이 프로토콜은 각 Participant 들이 prepare 에 OK 로 응답한다면, 반드시 Commit 을 해야한다는 책임이 있다는 전제 하에 짜여진 구조이기 때문이다.
SAGA 패턴
2PC 를 보면 대략적으로 알 수 있듯이 분산 트랜잭션의 제어에 대한 책임이 "DB" 에 있다. 그렇기 때문에 해당 책임을 구현하지 않는 DB 를 사용하게 된다면 2PC 는 사용할수 없게 되는데, SAGA 패턴은 이 책임을 "어플리케이션"으로 옮김으로써 이 문제를 해결한다.
SAGA 패턴은 각각의 서비스가 보상 트랜잭션을 발생시키는 책임을 가진 Choreography-Based 방식과, 보상 트랜잭션의 발생을 하는 중앙 노드가 있는 Orchestration-Based 방식이 있다.
Choreography-Based
1. 하나의 이벤트가 발생하면 서비스 하나가 자신이 수행 가능한 로직을 처리한 뒤 다른 서비스에게 로직을 요청하는 '이벤트'를 발생시킨다.
2. 이벤트를 수신한 다른 서비스들은 자신의 로직을 처리하고, 또다시 다른 서비스들에게 로직 처리를 요구하는 '이벤트'를 발생시킨다.
3. 각각의 서비스들에게 로직 수행을 요청하면서, 어떤 DB 데이터에 대해 이 로직을 처리해달라는 값을 전달해줄 필요가 있다.
장점
- 이런 식으로 각각의 서비스들이 어떤 서비스에게 어떤 요청을 해야하는지만 알고 자세한 내용은 모르는 '느슨한 결합' 을 가지고 동작하게 된다.
- 단순하게 이벤트만 발생시키면 되는 간단한 구조를 가지고 있다
단점
- 모든 로직이 각각의 서비스들에게 분산되어 있다 보니, 하나의 기능에 대한 순서를 파악하기가 어려운 면이 있다
- 모든 서비스들이 서로를 구독하는 형태로 연결되어 있다 보니, 서비스가 커질수록 시스템이 복잡해 질수도 있고 순환 의존성이 발생하여 시스템을 더 복잡하게 할 수도 있다
Orchestration-Based
1. 하나의 Orchestrator 를 담당하는 서비스가 모든 서비스들에게 처리를 요청하고 응답 받으면서 순차적으로 동작하는 구조이다.
2. 다른 서비스들은 요청온 것에 대한 응답만 하면 되기 때문에 서비스 간의 의존성이 없어지게 된다.
장점
- orchestrator 가 기능 하나에 대한 플로우와 각각의 서비스에 대한 요청을 담당하고 있기 때문에, 순환적 의존성이 발생하지 않고 flow 를 파악하기가 더 수월하다
단점
- 과도하게 많은 비즈니스 로직이 orchestrator 에게 들어갈 위험이 있다. 이를 예방하기 위해서는 Facade 와 같이 로직을 담당하기 보다는 다른 서비시들의 수행 순서에 대해서만 관심을 가지도록 할 필요가 있다.
'개발자의 길' 카테고리의 다른 글
[Database] MySQL 에서 Index 를 설계하고 추가하는 방법 (0) | 2025.02.12 |
---|---|
[Cache] 백엔드 서버 입장에서의 캐싱 전략과 활용 방안에 대해 (1) | 2025.02.04 |
[항해플러스] 미니 e-commerce 프로젝트에서 발생가능한 동시성 문제와 그 해결법 (0) | 2025.01.19 |
[Infra] Docker (1) - Docker Engine (0) | 2024.04.14 |
[Java] Boxing 과 Unboxing (0) | 2024.01.22 |