지금까지, 모놀리식 아키텍처를 MSA 아키텍처로 분리하고 연결하는 과정을 경험해봤습니다.
하지만, 서비스가 분리되면서 생긴 트랙잭션 문제가 있는데요, 이를 해결하기 위해 어떤 방법들이 있는지 알아보겠습니다.
1. 분산 트랜잭션 문제
이전 글에도 나왔던 상황인, '실시간 장소 방문 인증' 로직은 두개의 서비스가 분리되어 통신하면서 로직을 처리합니다.
flow
사용자 : 실시간 리뷰 작성 -> project-service : 실시간 리뷰 저장 -> region-service : 지역 방문 횟수 증가
하지만 지역 방문 횟수를 증가시키는 과정에서 장애(DB 연결 실패,저장 실패)가 발생한다면, 실시간 리뷰 저장은 롤백되지 않아, 실시간 리뷰 개수와 방문 횟수 사이에 원자성이 깨지게됩니다.
이를 해결하기 위해 2가지 분산 트랜잭션 패턴이 존재합니다.
- 2Phase Commit 패턴 (2PC 패턴)
- Saga 패턴
2. 2Phase Commit 패턴
2Phase Commit 패턴은 분산 트랜잭션에서 원자성을 보장하기 위한 프로토콜로, 두 단계를 거쳐 트랜잭션을 일관되게 처리합니다.
1) Prepare Phase : 준비 단계
- 코디네이터(분산 트랜잭션 중앙 조정 장치)가 트랜잭션에 참여하는 참여자들(노드)에게 트랜잭션을 커밋할 준비가 되었는지 prepare 메세지를 통해 묻습니다.
- 각 참여자들은 커밋할 준비가 되었다면 yes 응답을 전달, 준비되지 않았거나 실패한 경우 No를 전달합니다. yes를 응답한 참여자는 트랜잭션을 로컬에 기록하여 영구히 유지합니다.
2) Commit Phase : 커밋 단계
- 모든 참여자가 yes를 답한 경우, 코디네이터는 트랜잭션을 커밋하라는 commit 메세지를 전달하고, 모든 참여자는 커밋을 완료한 후에 코디네이터에게 통보합니다.
- 어떤 참여자라도 no를 답하거나 응답이 없는 경우, 코디네이터는 모든 참여자들에게 트랜잭션 롤백하라는 rollback 메세지를 전달합니다. 모든 참여자는 트랜잭션을 취소한 후에 코디네이터에게 통보합니다.
문제점
- 지연 시간이 증가할 수 있다.
- 네트워크 통신과, 모든 요청을 처리될 때까지 해당 Row에는 Lock이 설정된다는 점으로 인해 시간 지연 및 장애 발생이 우려됩니다.
- 서비스 간 강한 결합을 가질 수 있다.
- 독립적인 서비스 운영을 위해 MSA 아키텍처로 분리하였지만, 2PC 적용으로 코디네이터 기반 강결합이 생기기 때문에 다른 서비스의 상태나 동작에 의존될 수 있으며 독립적인 운영이 어려워집니다.
- 2PC를 지원하지 않는 DB가 있다.
- NoSQL은 2PC 패턴을 지원하지 않습니다.
3. Saga 패턴
2PC 패턴은 한번에 커밋되거나 롤백되는 방식인 대신, saga 패턴은 순차적으로 트랜잭션을 처리합니다.
서비스가 독립적으로 로컬 트랜잭션을 처리하고, 트랜잭션 실패 시 보상 트랜잭션을 실행합니다. 각 서비스는 독립적으로 실행되며 비동기 이벤트나 메세지를 통해 트랜잭션 상태를 전달합니다. 이때 이벤트 소싱을 이용하게 됩니다
이벤트 소싱
상태 변화를 이벤트로 기록하고, 현재 상태를 추적하는 대신 과거의 모든 상태 변경 이벤트를 저장합니다.
서비스 별로 독립적으로 트랜잭션을 처리하고, 그 결과를 바탕으로 다음 트랜잭션을 이어갑니다. 이때 이벤트 소싱을 사용하여 각 트랜잭션의 결과를 이벤트로 기록합니다.
Saga 종류
- Choreographed Saga : 이벤트 및 보상 트랜잭션 처리 주체가 각 마이크로 서비스
- Orchestrated Saga : 이벤트 및 보상 트랜잭션 처리의 주체로 Orchestrator 가 존재하여 중앙에서 처리
실시간 리뷰를 작성하고 지역 방문 횟수를 증가하는 과정을 예시로 설명해보겠습니다.
3.1. Choreographed Saga
각 마이크로 서비스의 메세지 브로커를 이용하여 서비스끼리 이벤트 및 보상 트랜잭션을 처리합니다.
실시간 리뷰를 작성하고 지역 방문 횟수를 증가하는 과정을 예시로 설명해보겠습니다.
flow
- 사용자 : 실시간 리뷰 작성 요청
- 사용자는 리뷰 작성 요청을 보냅니다.
- Project-service : 실시간 리뷰 저장
- 서비스는 리뷰를 성공적으로 DB에 저장합니다. (트랜잭션 완료)
- "리뷰 저장 완료" 이벤트를 발행하여 kafka를 통해 region-service로 전달합니다.
- Region-service : 지역 방문 횟수 증가 요청 수신
- "리뷰 저장 완료" 이벤트를 수신하고, 사용자의 지역 방문 횟수를 증가시키려고 시도합니다.
- Region-service : 실패 이벤트 발행
- 방문 횟수를 증가시키는 과정 중 오류로 인해 실패합니다.
- 이때 오류에는 DB 연결 실패, 저장 실패 등이 있습니다.
- "방문 횟수 증가 실패" 이벤트를 발행하여 kafka를 통해 project-service로 전달합니다.
- Project-service : 보상 트랜잭션 실행
- "방문 횟수 증가 실패" 이벤트를 수신합니다.
- 이전에 저장된 리뷰를 삭제하는 보상 트랜잭션을 실행하여 데이터 일관성을 유지합니다.
- 최종 상태
- 리뷰를 삭제하여 사용자가 작성한 리뷰가 시스템에 남지 않도록 합니다.
- 모든 서비스가 일관된 상태를 유지하도록 보장됩니다.
장점
- 마이크로 서비스가 적다면 비교적 쉽게 구현할 수 있다.
- 기존 환경에서 추가적인 인프라 리소스가 필요하지 않다.
단점
- 서비스가 많아진다면, 이벤트 구조를 파악하기 어렵다.
- 모니터링 시 현재 이벤트 및 트랜잭션의 상태를 추적하기 어렵다.
3.2. Orchestrated Saga
중앙 집중식 컨트롤러인 오케스트레이터가 각 서비스의 단계를 조율하고, 실패시 보상 트랜잭션을 실행합니다.
위의 예시를 이용하여 설명해보겠습니다.
flow
- 사용자 : 실시간 리뷰 작성 요청
- 사용자는 리뷰 작성 요청을 보냅니다.
- Project-service : 실시간 리뷰 저장
- 서비스는 리뷰를 성공적으로 DB에 저장합니다. (트랜잭션 완료)
- "리뷰 저장 완료" 이벤트를 발행하여 kafka를 통해 Orchestrator로 전달합니다.
- Orchestrator : 지역 방문 횟수 증가 요청 전달
- Orchestrator는 이벤트 처리 순서에 따라 "지역 방문 횟수 증가" 요청 이벤트를 region-service에 전달합니다.
- Region-service : 지역 방문 횟수 증가 요청 수신
- "지역 방문 횟수 증가" 이벤트를 수신하고, 사용자의 지역 방문 횟수를 증가시키려고 시도합니다.
- Region-service : 실패 이벤트 발행
- 방문 횟수를 증가시키는 과정 중 오류로 인해 실패합니다.
- 이때 오류에는 DB 연결 실패, 저장 실패 등이 있습니다.
- "방문 횟수 증가 실패" 이벤트를 발행하여 kafka를 통해 Orchaestrator로 전달합니다.
- Orchestrator : 보상 트랜잭션 이벤트 전달
- "방문 횟수 증가 실패" 이벤트를 수신하고, project-service에 실패했다는 이벤트를 전달합니다.
- Project-service : 보상 트랜잭션 실행
- 방문 횟수 증가 실패 이벤트를 수신합니다.
- 이전에 저장된 리뷰를 삭제하는 보상 트랜잭션을 실행하여 데이터 일관성을 유지합니다.
- 최종 상태
- 리뷰를 삭제하여 사용자가 작성한 리뷰가 시스템에 남지 않도록 합니다.
- 모든 서비스가 일관된 상태를 유지하도록 보장됩니다.
Choreographed Saga 와 유사하지만, 이벤트를 각 서비스로 전달하는 것이 아닌 Orchestrator로 전달하여 이벤트 단계를 Orchestrator가 조율한다는 점에서 차이점이 있습니다.
장점
- 서비스의 트랜잭션 이벤트 구조를 파악하기 쉽다.
- 서비스 간의 이벤트 의존성이 적다.
- 현재 이벤트 및 트랜잭션 상태를 Orchestrator에서 쉽게 추적할 수 있다.
단점
- Orchestrator 구현을 위한 추가적인 인프라 리소스가 필요하다.
- Orchestrator가 이벤트를 관리하기 때문에, 단일 장애 지점이 되어 장애 발생시, 모든 서비스에 장애가 전파될 수 있다.
- Orchestrator 구현 난이도가 높다.
4. 분산 트랜잭션 적용 : Choreographed Saga
3개의 분산 트랜잭션 방법 중, Choregoraphed Saga 패턴을 이용하기로 결정하였습니다.
그 이유는 다음과 같습니다.
- 2PC 패턴은 서비스간 강한 결합과 DB Lock으로 인한 지연 시간 및 다른 문제들이 우려되어 제외하였습니다.
- Saga 패턴 중 Orchestrator Saga 패턴은 현재 구현하기 어려운 난이도라고 판단되었습니다.
- Choregoraphe Saga 패턴은 이벤트 구조를 파악하기 어렵다는 단점이 있지만, 현재 서비스는 많지 않아 구조를 파악하는데 어려움이 없을 것이라 판단하였습니다.
구현
Project-service에서 실시간 리뷰를 저장한 후, 지역 방문 횟수를 증가 요청 이벤트를 발행합니다.
private void addCountVisitRegion(User user, Place place, Review review) {
if(review.getReviewtype().equals("REVIEW_TYP_02")){
VisitRegionRequest visitRegionRequest = new VisitRegionRequest(place.getCity(), place.getCityDetail(), user.getUserId(), review.getReviewId());
kafkaTemplate.send("add_visitRegion", visitRegionRequest);
}
}
Region-service에서 이벤트를 수신하여, 방문 횟수를 증가하는 로직을 실행합니다.
로직 수행 중, 예외가 발생했을 경우 실패 이벤트를 발행합니다.
@KafkaListener(topics = "add_visitRegion", groupId = "region_visit")
public void addRegionVisitListener(ConsumerRecord data) throws JsonProcessingException {
VisitRegionRequest visitRegionRequest = objectMapper.readValue((String)data.value(), VisitRegionRequest.class);
try {
regionService.addCountVisitRegion(visitRegionRequest.city(), visitRegionRequest.cityDetail(), visitRegionRequest.userId());
} catch (Exception e) {
VisitRegionResponse failResponse = new VisitRegionResponse(false, visitRegionRequest.reviewId());
kafkaTemplate.send("add_visitRegion_transaction_response", failResponse);
}
}
Project-service에서 트랜잭션 실패 이벤트를 수신하여, 롤백하는 로직을 실행합니다.
@KafkaListener(topics = "add_visitRegion_transaction_response", groupId = "region_visit_transaction_response")
public void visitRegionResponseListener(VisitRegionResponse response) {
if(!response.visitRegionStatus()) {
reviewService.deleteReviewForTransaction(response.reviewId());
}
}
5. 분산 트랜잭션 테스트
postman으로 실시간 리뷰를 요청합니다. 이때, 방문 횟수를 증가하는 로직에서 Runtime 예외를 던져 보상 트랜잭션이 실행되는지 확인해 보겠습니다.
Region-service에서는 증가 로직으로 인해 DB insert 구문이 실행되었으나, Runtime 예외로 롤백되고 kafka에 실패 이벤트 메세지를 전송하는 것을 확인할 수 있습니다.
DB - 증가된 내역이 없음
kafka - 메세지 전달
Project-service 에서 리뷰가 insert 되는 명령어가 실행되었으나, 보상 트랜잭션으로 삭제 명령어가 실행되는 것을 확인
테스트 성공!
'댕댕어디가 프로젝트 > MSA' 카테고리의 다른 글
[댕댕어디가] MSA 아키텍처 전환기(7) - 인증서버 연결 (0) | 2025.01.14 |
---|---|
[댕댕어디가] MSA 아키텍처 전환기(6) - 서비스간 동기 통신(Spring Cloud Open Feign) (0) | 2025.01.12 |
[댕댕어디가] MSA 아키텍처 전환기(5) - 서비스간 비동기 통신 (kafka 연결) (0) | 2025.01.11 |
[댕댕어디가] MSA 아키텍처 전환기(4) - 서비스간 비동기 통신하기 (0) | 2025.01.08 |
[댕댕어디가] MSA 아키텍처 전환기(3) - API Gateway 구현 (0) | 2025.01.07 |