이벤트 드리븐 아키텍처

hyuckhoon.ko·2022년 1월 8일
0

이벤트 드리븐 아키텍처

잘못된 접근법 1️⃣ - 시스템을 명사로 바라보기

"이 시스템의 '물건'마다 연관된 서비스가 있고 서비스는 HTTP API를 노출한다."(p.231)

유저, 매니저, 매치, 주문, 대관 등과 같이 명사로 표현해왔다.

이는 코드 복잡도가 증가할 수 있는 구조다.

happy path

유저 매치 지원 -> 매니저 통보 -> 매치 확정 및 진행

unhappy path

유저 무단 불참 -> 매니저가 운영팀 통보 -> 유저 환불 진행, 현재 참여 가능 유저 긴급 모집

구장 조명 out -> 매니저가 운영팀 통보 -> 유저 환불 진행, 구장주 연락

매니저 무단 불참 -> 유저가 운영팀 통보 -> 유저 환불 진행, 인근 거주 매니저 지원 요청

우천, 태풍 등 재난재해 -> 운영팀 판단 -> 유저 환불 진행, 구장주 연락

'자동화할 수 있는 부분이 무엇일까'에 대한 고민은 차치하더라도
절차지향적인 프로세스가 위험해 보인다.
또한, unhappy한 이벤트가 발생할 때마다 새로운 로직이 일관성 없이 추가되어 점점 복잡해진다.


잘못된 접근법 2️⃣ - 결합된 시스템

'모든 것은 망가진다'는 소프트웨어 엔지니어링에서 일반적인 규칙이다.

유저가 주문을 했다.
매치 시스템에 문제가 생겨 매치에 유저가 할당되지 않았다고 하자.

두 가지 처리 방법이 있다.
1) 주문 시스템은 문제없으니 유저에게 돈은 차감되나, 매치 신청은 안되게 한다.
2) 결과적으로 매치가 신청할 수 없는 상황이니 둘 다 실패 처리한다.

두 시스템을 모두 바꿔야 하는 경우를 결합됐다(coupled)라고 한다.


어떻게 해결할 것인가

비동기 메시징 기반의 시간적 결합

도메인 모델은 어떤 물건에 대한 정적인 데이터 모델이 아닌 동사에 대한 모델이다.

매치 시스템과 할당 시스템을
매치 신청 행위 관련 시스템과 할당 행위(allocating)관련 시스템으로 생각하자.

외부 세계로부터 커맨드를 받고 내부에서는 그 결과를 저장하기 위해 이벤트를 발생시킨다. 그 이벤트는 임의의 이벤트 핸들러의 트리거가 된다.

더 복잡해진 것 같은데, 좋아진거 맞나요?

1) 각 시스템이 독립적으로 실패해도 괜찮다.
주문을 취소하려고 했더니, 매치의 상태가 주문을 취소할 수 있는 조건이 아니라서 안되는 구조다. 그런데, 독립적으로 실패해도 독립적으로 복구시키면 된다는 것은 매력적이다.

2) 시스템 결합도가 낮아진다.
주문 시스템 서버가 불안정하다고? 나 매치 시스템은 멀쩡해~
나는 내 일만 잘하면 됨!

외부 세계에 대한 이야기

-메시지 브로커

이제 도메인 밖의 세계로 눈을 돌리자.
비동기 이벤트를 시스템 외부로 보내서 다른 시스템에 전달하는 매개체가 필요하다.
이런 인프라를 메시지 브로커라고 한다.

메시지 브로커의 역할은 발행자의 메시지를 받아서 구독자에게 배달하는 것이다.

E2E 테스트를 사용해 모든 기능 시범운영하기

tests/e2e/test_external_events.py

def test_change_batch_quantity_leading_to_reallocation():
    # 초기 주문 할당
    orderid, sku = random_orderid(), random_sku()
    earlier_batch, later_batch = random_batchref('old'), 
                                 random_batchref('newer')
    api_client.post_to_add_batch(earlier_batch, sku, qty=10, eta='2011-01-02')
    api_client.post_to_add_batch(later_batch, sku, qty=10, eta='2011-01-02')
    response = api_client.post_to_allocate(orderid, sku, 10)
    assert response.json()['batchref'] == earlier_batch
    
    # 메시지 버스로부터 주문 할당됨 메시지 수신
    subscription = redis_client.subscribe_to('line_allocated')
    # 주문 재할당
    redis_client.publish_message('change_batch_quantity', {    
    'batchref': earlier_batch, 'qty': 5
    })
    
    messages = []
    for attempt in Retrying(stop=stop_after_delay(3), reraise(True):
        with attempt:
            message= subscription.get_message(tiemeout=1)
            if message:
            	message.append(message)
                print(message)
            data = json.loads(messages[-1]['data']
            assert data['orderid'] == orderid
            assert data['batchref'] == later_batch
    

마치며

장점단점
- 복잡도를 줄일 수 있다.-전체 흐름을 파악하기 어렵다.
- 서비스가 결합되지 않아
개별 서비스의 변경과 추가가 쉽다.
-메시지의 신뢰성이 중요해진다.



⚽️ 시스템으로 들어오거나 나가는 메시지를 어떻게 구현할까

  • 어떻게 내 것으로 만들까

병렬적으로 진행되는 프로세스
유저 매치를 신청한다 ->
► SQS에 MatchApplied 이벤트 발행 -> Allocate 커맨드 트리거 -> Allocated 이벤트 발생 및 수신
► SQS에 PurchaseRequested 이벤트 발행 -> Purchase 커맨드 트리거 -> Purchased 이벤트 발생 및 수신

Q. Post/리디렉션/Get 패턴으로 매치 신청과 함께 결제 내역이 바로 나타나야 하는 페이지 구조의 경우 문제는 없을까
Q. 로그 관리가 중요해지지 않을까?
1) 매치에 유저 할당은 됐으나, 유저가 입금이 되지않았을 때
2) 매치에 유저 할당이 안되고, 입금은 됐을때
해당 로그를 바로 개발팀과 운영팀에 알리는 또 하나의 경보 시스템이 추가가 필요할 것 같다.
Q. 멱등성의 개념이 중요해진다.
인프라 관점에서 메시지를 처리하는 방식이 중요해진다.
소스 관점에서는 시스템 별 검증단에 if 모델 exists()문과 refresh_from_db 문이 있을 것 같다.

  • 이벤트 트리븐 아키텍처 참고 자료

참고 자료: https://dev.classmethod.jp/articles/summit-online-korea-event-driven/

0개의 댓글