가장 먼저 고려해본건 분산 트랜잭션, 그 중 Saga Pattern 이었습니다.
Saga Pattern, 그 중 Choreography 방식은 로컬 트랜잭션의 실패 여부를 이벤트로 발행, 이미 진행된 로컬 트랜잭션을 되돌리면 보상 트랜잭션을 실행함으로써 데이터 정합성을 맞출 수 있어요.

위 그림처럼 이벤트 흐름이 진행될거예요. 실패했을 때도 생각해볼까요?

결제 실패 시 실패했다는 이벤트를 발행하고, 이 토픽을 group-service 에서 구독하여 롤백을 진행하게 됩니다.
분산 트랜잭션이 왜 필요할까?
라는 원초적인 고민을 했었습니다. 서로 다른 마이크로서비스에서 일어나는 동작이 있기에, 로컬 트랜잭션으로 처리할 수 없는 동작이기에 필요하다는 결론에 다다랐는데요.
그렇다면 로컬 트랜잭션으로 해결하면 되지 않을까?

위 의문을 해결하기 위해 고안한 방식입니다. 기존과는 달리 결제 성공 시 그룹 인원을 증가시키고 있어요. 결제가 완료되기 전 미리 값을 변경시키지 않기에 보상 트랜잭션이 필요없어졌고, 하나의 트랜잭션으로 처리할 수 있게 되었습니다.
분산 트랜잭션이 단순한 이벤트 기반 통신으로 바뀜으로써 복잡도가 상당히 감소했다고 생각해요.
분산 트랜잭션 구현 전략 : 분산 트랜잭션을 피하라
Simple AWS에 기고된 내용 중 일부입니다. 관심 있으신 분들은 한 번 읽어봐도 좋을 것 같아요.
구현에 앞서 겪었던 어려움에 대해 잠깐 이야기해보려 해요.
결제를 담당하는 마이크로서비스 payment-service 를 구현하기 위해 위와 같은 구조를 설계했습니다. 하지만 막상 구현하려니 너무 막막했어요.
실제 PG사를 연동하는 작업은 나중으로 미룰 생각이였음에도 너무나 많은 생각이 머리속을 떠나지 않았었죠.

얼마 전 소프트웨어 장인이라는 책을 읽었습니다. 해당 책에선 과한 설계를 피하기 위해, Agile하게 개발하기 위해 TDD를 적용하라는 작가의 추천이 있었어요.
에라 모르겠다! 아무 진전 없이 고민만 하고 있는 것보단 뭐라도 하는게 좋을 것 같아 TDD 방법론을 적용해보기로 했습니다.
@Test
@DisplayName("결제에 성공한다")
void success_pay_process() {
//given
Long userId = 1L;
BigDecimal amount = new BigDecimal("100.00");
//when
PayEvent payEvent = payService.processPayment(userId, amount);
//then
verify(payEventRepository, times(1)).save(payEvent);
}
잘 모르겠고 일단 테스트를 작성했습니다. 최소한의 테스트를 작성하고, 이를 통과하는 코드를 작성합니다. 동일한 과정을 반복하며 살을 점점 붙이는 방식이예요.
@Test
@DisplayName("결제에 성공한다")
void success_pay_process() {
//given
Long userId = 1L;
BigDecimal amount = new BigDecimal("100.00");
PayEvent payEvent = PayEvent.builder()
.amount(amount)
.userId(userId)
.build();
when(payEventRepository.save(any(PayEvent.class))).thenReturn(payEvent);
//when
PayEvent savedEvent = payService.processPayment(amount, userId);
//then
assertEquals(userId, savedEvent.getUserId());
assertEquals(amount, savedEvent.getAmount());
verify(payEventRepository, times(1)).save(any(PayEvent.class));
}
사실 너무 익숙하지 않아 어느정도 틀이 잡히고 나선 TDD를 적용하지 않았습니다! 그래도 덕분에 과잉 설계를 피해 개발할 수 있었던 것 같아요.

먼저 group-service 에서 그룹 인원 증가 요청 토픽에 이벤트를 발행합니다.
payment-service 는 요청 토픽을 구독하여 그룹 인원 증가 에 대한 결제를 수행합니다.
결제 성공 시 payment-service는 그룹 인원 증가 응답 토픽에 이벤트를 발행합니다.
group-service 는 응답 토픽을 구독하여 그룹 인원 증가 동작을 수행합니다.
메세지 큐를 찾아보면 나오는 장점들 중 Decoupling이라는 것이 있습니다. Message Queue라는 미들웨어를 통해 통신함으로써 결합도를 낮춰 확장성, 유연성 등을 얻을 수 있다는 거죠.
위 그림을 다시 볼까요?

group-service 는 Kafka에 이벤트를 발행하기만 합니다. payment-service 발행된 이벤트가 무엇이던지 구독하여 처리합니다. 이로써 각 서비스는 서로를 몰라도 되고, 결국 약한 결합을 실현할 수 있다는 거죠.
하지만 이렇게만 하면 정말 결합도를 낮춘걸까요?
메세지 큐가 결합도를 어떻게 낮추는지에 대해 생각해봤습니다. Kafka의 경우, 이벤트를 통해 서로의 존재를 몰라도 각 서비스의 로직을 진행할 수 있도록 설계함으로써 결합도를 낮출 수 있다고 생각했어요.

위 그림은 제가 생각한 이상적인 이벤트 기반 통신입니다. 이벤트가 발행되고, 여러 서비스는 해당 이벤트를 구독하여 각자의 로직을 처리하는 방식이예요.

다만, 현재 제 프로젝트는 위 그림과 같은 구조로 되어있다고 느꼈습니다.
Kafka를 통해 각 서비스간 통신을 진행했지만, group-service 가 발행한 이벤트는 결국 payment-service 를 목적으로 발행한 이벤트였기에 여전히 강하게 결합되어 있다고 느꼈으며, 이는 높은 확장성이라는 메세지 큐의 장점을 전혀 활용하지 못한다고 생각했어요.
이번 개발을 진행하며 많은 키워드와 의문을 얻을 수 있었어요.
분산 트랜잭션과 Kafka, 이벤트 기반 아키텍처, 그리고 TDD 까지 많은 의문을 남게 하는 주제였네요.
특히나 이벤트 기반 아키텍처, EDA는 결제 기능 구현 시작부터 포스팅을 적고 있는 지금까지, 약 1달동안 계속 찾아보는 중인데도 갈피가 잘 안잡히네요.
로깅 시스템 을 구현할 때 이벤트에 대해 다시 한번 깊게 고민해봐야겠습니다.!
Saga패턴을 이용한 분산 트랜잭션 제어(결제 프로세스 실습)
Distributed Transactions in Event-Driven Architectures
회원시스템 이벤트기반 아키텍처 구축하기 #우아콘2022