서비스간 통신을 해보자! - 구현과 의문

혁콩·2024년 8월 9일

모두의 음악

목록 보기
13/17
post-thumbnail
이전 포스트에서 이어지는 게시글입니다.

구현 방법?

1. 분산 트랜잭션

가장 먼저 고려해본건 분산 트랜잭션, 그 중 Saga Pattern 이었습니다.

Saga Pattern, 그 중 Choreography 방식은 로컬 트랜잭션의 실패 여부를 이벤트로 발행, 이미 진행된 로컬 트랜잭션을 되돌리면 보상 트랜잭션을 실행함으로써 데이터 정합성을 맞출 수 있어요.


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

결제 실패 시 실패했다는 이벤트를 발행하고, 이 토픽을 group-service 에서 구독하여 롤백을 진행하게 됩니다.

잠깐!

분산 트랜잭션이 왜 필요할까?

라는 원초적인 고민을 했었습니다. 서로 다른 마이크로서비스에서 일어나는 동작이 있기에, 로컬 트랜잭션으로 처리할 수 없는 동작이기에 필요하다는 결론에 다다랐는데요.

그렇다면 로컬 트랜잭션으로 해결하면 되지 않을까?

2. 이벤트 기반 통신


위 의문을 해결하기 위해 고안한 방식입니다. 기존과는 달리 결제 성공 시 그룹 인원을 증가시키고 있어요. 결제가 완료되기 전 미리 값을 변경시키지 않기에 보상 트랜잭션이 필요없어졌고, 하나의 트랜잭션으로 처리할 수 있게 되었습니다.

분산 트랜잭션이 단순한 이벤트 기반 통신으로 바뀜으로써 복잡도가 상당히 감소했다고 생각해요.

분산 트랜잭션 구현 전략 : 분산 트랜잭션을 피하라

Simple AWS에 기고된 내용 중 일부입니다. 관심 있으신 분들은 한 번 읽어봐도 좋을 것 같아요.

구현

1. BDUF, 그리고 TDD

구현에 앞서 겪었던 어려움에 대해 잠깐 이야기해보려 해요.

결제를 담당하는 마이크로서비스 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를 적용하지 않았습니다! 그래도 덕분에 과잉 설계를 피해 개발할 수 있었던 것 같아요.

2. 카프카 이벤트 흐름

  1. 먼저 group-service 에서 그룹 인원 증가 요청 토픽에 이벤트를 발행합니다.

  2. payment-service 는 요청 토픽을 구독하여 그룹 인원 증가 에 대한 결제를 수행합니다.

  3. 결제 성공 시 payment-service그룹 인원 증가 응답 토픽에 이벤트를 발행합니다.

  4. group-service 는 응답 토픽을 구독하여 그룹 인원 증가 동작을 수행합니다.

구현 코드는 이곳에서 확인하실 수 있습니다.

의문

1. 결합도를 낮춘다?

메세지 큐를 찾아보면 나오는 장점들 중 Decoupling이라는 것이 있습니다. Message Queue라는 미들웨어를 통해 통신함으로써 결합도를 낮춰 확장성, 유연성 등을 얻을 수 있다는 거죠.

위 그림을 다시 볼까요?

group-serviceKafka에 이벤트를 발행하기만 합니다. payment-service 발행된 이벤트가 무엇이던지 구독하여 처리합니다. 이로써 각 서비스는 서로를 몰라도 되고, 결국 약한 결합을 실현할 수 있다는 거죠.

하지만 이렇게만 하면 정말 결합도를 낮춘걸까요?

2. 너무 구체화된 이벤트

메세지 큐가 결합도를 어떻게 낮추는지에 대해 생각해봤습니다. Kafka의 경우, 이벤트를 통해 서로의 존재를 몰라도 각 서비스의 로직을 진행할 수 있도록 설계함으로써 결합도를 낮출 수 있다고 생각했어요.


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


다만, 현재 제 프로젝트는 위 그림과 같은 구조로 되어있다고 느꼈습니다.
Kafka를 통해 각 서비스간 통신을 진행했지만, group-service 가 발행한 이벤트는 결국 payment-service 를 목적으로 발행한 이벤트였기에 여전히 강하게 결합되어 있다고 느꼈으며, 이는 높은 확장성이라는 메세지 큐의 장점을 전혀 활용하지 못한다고 생각했어요.

마치면서

이번 개발을 진행하며 많은 키워드와 의문을 얻을 수 있었어요.
분산 트랜잭션Kafka, 이벤트 기반 아키텍처, 그리고 TDD 까지 많은 의문을 남게 하는 주제였네요.

특히나 이벤트 기반 아키텍처, EDA는 결제 기능 구현 시작부터 포스팅을 적고 있는 지금까지, 약 1달동안 계속 찾아보는 중인데도 갈피가 잘 안잡히네요.

로깅 시스템 을 구현할 때 이벤트에 대해 다시 한번 깊게 고민해봐야겠습니다.!

참고 자료

Saga패턴을 이용한 분산 트랜잭션 제어(결제 프로세스 실습)
Distributed Transactions in Event-Driven Architectures
회원시스템 이벤트기반 아키텍처 구축하기 #우아콘2022

profile
아는 척 하기 좋아하는 콩

0개의 댓글