공부하고 기록하는, 경제학과 출신 개발자의 노트

강연

WoowaCon 2022 - 회원 시스템 이벤트 아키텍처로 구축하기

inspirit941 2022. 10. 27. 11:12
반응형

https://www.youtube.com/watch?v=b65zIH7sDug 

회원시스템 이벤트 기반 아키텍처 구축하기

  • 2015년 모놀리틱 구조로 배민 서비스 구현.
  • J커브 형태로 서비스가 급격히 성장하며 수없이 장애가 터짐
  • 따라서 2019년에 배민의 모든 서비스를 Microservice로 분리하였음.

서비스 자체를 microservice들의 집합으로 구성하는 사례는 이제 많아졌으니, 하나의 microservice를 event-driven으로 구성한 사례를 설명하고자 함.

 

스크린샷 2022-10-25 오후 2 07 01

 

스크린샷 2022-10-25 오후 2 08 31

 

Microservice와 event-driven이 같이 언급되는 경우가 많다.

  • Microservice는 서비스와 서비스 간 '느슨한 결합' (loosly coupled) 을 지향함.
  • Event-Driven이 '느슨한 결합' 형태를 지원하는 데 좋은 방식이기 때문.

microService 형태에서, 어떤 방식이 '느슨한 결합'일까?

스크린샷 2022-10-25 오후 4 39 24

 

대상 로직: 사용자가 본인인증을 해제할 경우, 가족계정 연동이 중단된다.

물리적 분리

서비스 자체는 물리적으로 분리되었지만, 코드 레벨의 호출이 http 호출로만 분리되었을 뿐.

  • initCertification 로직에는 반드시 family의 leave함수가 호출되어야 한다. (회원 서비스의 로직 후속행위로 가족계정의 로직이 반드시 필요함)
  • 두 개의 도메인은 여전히 '강한 결합' 상태.

 

비동기 요청 적용 - 쓰레드 의존성 제거와 느슨한 결합은 다르다

스크린샷 2022-10-25 오후 4 42 03

 

비동기 http 방식은 쓰레드 레벨에서의 의존을 제거할 뿐.

  • 회원 서비스의 로직 후속행위로 가족계정의 로직이 반드시 필요함.

메시징 시스템 도입? - 물리적 분리와 논리적 분리는 다르다

스크린샷 2022-10-25 오후 4 43 51

 

회원 서비스에서 queue로 메시지를 보내고, 가족계정 서비스에서 queue를 구독해 메시지를 받아 처리하는 작업.

  • 메시징을 도입한다고 해서 반드시 '느슨한 결합' 이 보장되는 것은 아님.
  • 메시지를 발송하는 것으로 물리적 의존은 제거했지만, 가족계정 서비스가 '탈퇴' 로직을 수행하길 바라는 논리적 결합은 그대로임.

발행한 메시지가 대상 도메인에게 기대하는 목적이 있다면, 비동기 요청일 뿐 우리가 기대하는 'event'가 아니다.

 

스크린샷 2022-10-25 오후 4 48 46

 

의도를 담아서 queue에 메시지를 보내는 것이 아니라, 상태의 변화를 이벤트로 보내는 것.

  • 회원은 본인인증 해제 로직에서 '본인인증 해제' 이벤트를 발생시켰을 뿐, 가족계정 서비스의 로직에 관여하지 않음.
  • 가족계정 서비스는 본인인증 해제 이벤트를 구독해서 가족계정 해제 로직을 수행함.

즉 메시지가 담긴 의도에 따라 전혀 다른 결과가 만들어질 수 있다.

 

스크린샷 2022-10-25 오후 4 53 14

 

"밥을 먹었다" 라는 이벤트가 발생했을 때

  • "밥 먹었으니 뭐 해라" 라는 이벤트를 발행하는 것이 아니라
  • "밥을 먹었다" 라는 이벤트.

목적을 담아 이벤트를 발생시키는 것이 아니라, 도메인에서 발생하는 이벤트 그 자체를 발행하는 것이 Event-Driven의 핵심

애플리케이션 내부에서 발생하는 이벤트의 발행 / 구독

스크린샷 2022-10-25 오후 4 55 52스크린샷 2022-10-25 오후 4 56 07

 

회원서비스 자체는 하나의 system + queue 로 구성되어 있음.

  • 시스템 내부적으로는 세 가지 event 종류, 세 가지 구독 layer가 있음
  • '느슨한 결합'이 필요한 상황이 꼭 microservice간에서만 필요한 건 아니기 때문.

 

1. First Layer : 애플리케이션의 이벤트를 AWS SNS로.

스크린샷 2022-10-25 오후 7 50 59스크린샷 2022-10-25 오후 7 52 50

 

spring의 application event : 분산 비동기 처리를 지원하는 event bus 제공 / txn 제어. 단일 애플리케이션 내에서 사용하기 유용함

  • 도메인 로직과 무관한 메시지 큐로 이벤트를 발행. 이벤트 subscribe는 event publisher와 무관하게 scalable함.
  • 도메인 영향 없이 message 발행, 구독, 확장할 수 있음.
  • 애플리케이션 내부에서 해결해야 하는 비관심사를 처리함

cf. 비관심사: 도메인 행위가 수행될 때 반드시 함께 실행되어야 하는 정책을 의미함. 서비스의 도메인 내부 응집력을 약화시키는 요인.

 

스크린샷 2022-10-25 오후 7 57 17

 

애플리케이션에서 발생하는 이벤트를 messaging system으로 전달하는 Queue로 AWS SNS를 사용함.

2. Second Layer - AWS SNS를 활용해 messaging system으로 전달

스크린샷 2022-10-25 오후 8 01 24

 

AWS SNS는 SQS와 같이 활용하면 1:N 형태의 Subscribe가 가능하며, 메시지 유실이 발생할 가능성을 크게 낮춰 줌.

  • AWS SQS로 전달된 이벤트를 Subscribe해 처리하는 Event worker가 존재함.
  • 애플리케이션 내부에서 처리해야 하는 로직을 제외한, 외부의 모든 비관심사 로직을 처리함.

비관심사 로직의 분리

스크린샷 2022-10-25 오후 8 47 05스크린샷 2022-10-25 오후 8 49 16

 

'로그인' 이라는 프로세스에서 발생하는 비관심사 로직의 종류

  • 디바이스 로깅
  • 동일 계정 로그인수 제한 규칙에 따라, 다른 디바이스에 로그인된 계정 로그아웃 처리
  • 동일 디바이스의 다른 계정을 로그아웃 처리

도메인 행위의 응집을 높이고, 도메인의 핵심로직과 무관한 비관심사 로직과의 결합도를 낮춰야 함.

 

스크린샷 2022-10-25 오후 8 50 24

 

  • 로그인 프로세스가 실행되면, Login Event가 발생.
    스크린샷 2022-10-25 오후 8 50 42
  • 로그인 이벤트는 AWS SQS를 통해 1:N 형태의 이벤트 Subscribe로 변환됨.
  • AWS SQS에서 이벤트를 구독한 뒤, Event Worker에서 비관심사 로직을 처리함.
    • Event worker의 로직도 상호 독립적. 다른 로직에 영향을 받지 않음.

스크린샷 2022-10-26 오전 9 35 59


도메인 서비스의 핵심 로직을 위처럼 간소화할 수 있다.


애플리케이션 이벤트 vs 내부 이벤트?

스크린샷 2022-10-26 오전 9 37 28

비관심사 로직도 애플리케이션 이벤트도 처리해도 되지 않나? 싶을 수 있음. 굳이 AWS SNS / SQS로 이벤트를 분리해가며 만든 이유?

 

스크린샷 2022-10-26 오전 9 38 59

 

애플리케이션 내부 이벤트로 처리할 경우: 애플리케이션과 txn 일치

  • 주요 로직과 강한 정합성을 보장해야 하는 작업. (데이터 값이 반드시 일치해야 하는 경우) -> Application Txn을 사용함

내부 이벤트로 처리할 경우: 애플리케이션과 txn 분리

  • 주요 로직과 강한 정합성을 보장할 필요 없는 작업.

비관심사를 애플리케이션 내부에서 처리하는 비용을 줄이고, 핵심 로직에 비관심사 로직이 영향을 미치지 않도록 하는 방향을 선택한 trade-off.

외부 이벤트 발행 - 다른 microservice에게 전달할 이벤트

스크린샷 2022-10-26 오전 9 46 41

 

  • 외부 시스템으로 이벤트를 전파하는 것도, 도메인 입장에서는 비관심사 로직.
  • 따라서 '외부 이벤트 발행'을 담당하는 로직을 AWS SNS -> Event worker Layer에 추가했다.

 

3. Third Layer - 애플리케이션 외부로 이벤트 전파, 다른 System이 구독

스크린샷 2022-10-26 오전 9 45 48

 

내부 이벤트 vs 외부 이벤트. 분리한 이유?

스크린샷 2022-10-26 오후 1 23 41

 

내부에는 열린, 외부에는 닫힌 이벤트를 제공할 수 있기 때문.

  • 예컨대 내부 이벤트에 name, age 필드가 있는데, age 필드가 더 이상 필요없어졌으므로 제거한다고 가정.
    • name, age 필드가 있는 이벤트를 사용하는 event worker에서 에러가 발생할 수 있음
    • 그러나 이 이벤트 자체가 system 내부 이벤트이므로, 외부 서비스에는 이벤트 변경이 영향을 미치지 않는 구조. 따라서 시스템 내부에서 통제 가능함.
    • 또는 event subscriber가 필요로 하는 필드를 payload에 추가 / 제거하는 등 관리가 쉬움.
    • 이런 상태를 '열려 있다' 라고 명명

 

스크린샷 2022-10-26 오후 1 29 44

 

  • 외부 이벤트는 다른 microservice 자체에 영향을 주게 됨.
  • payload 추가 / 제거에 따른 영향을 파악하는 데 비용이 많이 들고, 한 번 결졍된 사항을 변경하기가 쉽지 않음.
    • 이런 상태를 '닫혀 있다' 라고 명명

이벤트 일반화 - 닫힌 상태의 이벤트를 안전하고 flexible하게 제공하기 위한 방법

스크린샷 2022-10-26 오후 1 31 44

 

어떤 외부 이벤트라 해도, 이벤트를 인지하는 과정은 네 개의 필드로 쉽게 일반화할 수 있음.

  • 언제 - 시간
  • 누가 - 식별자
  • 무엇을 해서 - 행위
  • 어떤 변화가 일어났는가 - 속성

 

스크린샷 2022-10-26 오후 1 35 44

 

그 외에도, 로직을 수행하기 위해 이벤트에서 필요로 하는 필드가 있을 순 있음.

  • 하지만, 이벤트는 Subscriber가 어떤 동작을 수행할지 기대하지 않는 구조여야 '느슨한 결합'을 만족할 수 있다. 특정 Subscriber를 위한 payload 필드는 추가하지 않는 게 원칙
  • Zero-Payload 방식.
    • 보통 '이벤트의 순서 보장'을 해결하기 위해 쓰이지만, '느슨한 결합'을 만들어주는 효과도 있음

'외부 시스템과의 의존 없는 이벤트 구조'를 설계함.

'이벤트 저장소' 구축하기

스크린샷 2022-10-26 오후 1 40 53

 

  • AWS SNS - SQS 구간은 높은 신뢰성을 제공하고 있음
  • SQS - Event worker 구간은 높은 신뢰성 + deadlift queue 전략으로 내부 / 외부 이벤트 처리에 문제 없음.
  • 문제는 내부 이벤트 발행. http 통신을 쓰고 있으므로 문제가 발생할 소지 많음.

스크린샷 2022-10-26 오후 1 41 35스크린샷 2022-10-26 오후 1 43 34

 

  • spring event에서 도메인 로직 - Subscriber를 하나의 txn으로 묶어서, 이벤트 발행 실패 시 도메인 로직도 fail하도록 만들 수 있음
    • 이 방식은 '이벤트 발행 실패'를 시스템 장애로 전파하게 될 위험이 있음.
  • 도메인 로직과 이벤트 발행을 분리하면, 이벤트 발행 실패시 이벤트가 유실됨

따라서 이벤트 저장소(repository)가 필요함.

 

스크린샷 2022-10-26 오후 1 45 35

 

  • '도메인 로직 + 이벤트 저장소에 저장' 까지를 하나의 txn으로 지정.
  • 이벤트 발행이 실패하더라도, 이벤트 저장소에 정상적으로 이벤트가 저장되었다면 이벤트를 재발행할 수 있음.

어떤 이벤트 저장소가 필요할까

스크린샷 2022-10-26 오후 1 49 08

 

  • 이벤트 자체의 특성과 Repository에서 필요로 하는 기능을 고려했을 때: 작은 단위로 저장 / 고속 처리 정도.
  • 그러나 도메인 행위 / 이벤트 간 신뢰성 확보를 위해 txn을 쓰고 있는 상황. 따라서 domain 서비스에서 사용하는 txn과 호환되는지를 체크해야 함.

 

스크린샷 2022-10-26 오후 1 51 48스크린샷 2022-10-26 오후 1 53 58

 

즉, 다중 데이터베이스에서 분산 txn을 구현해야 함. -> 실제로 구현해보면 쉽지 않다

  • 최소한의 수준으로 다중 DB의 분산 txn을 지원하던 spring 프레임워크의 ChainedTransactionManager도 '명확한 기능 수행이 어렵고, 대안을 제공하기 어렵다'는 이유로 Deprecated 처리함.

 

스크린샷 2022-10-26 오후 1 57 12

 

따라서, 다중 DB를 쓰는 대신 도메인 DB를 공유하는 식으로 선회.

  • txn 문제로 이벤트 유실이 있어선 안 되었기 때문.
  • 이런 방식을 Transactional Outbox Pattern 이라고도 함.
    • local txn을 사용해서 DB 저장 / 이벤트 발행의 정합성을 보장하는 방식

스크린샷 2022-10-26 오후 4 14 31스크린샷 2022-10-26 오후 4 14 38스크린샷 2022-10-26 오후 4 16 15

 

이벤트 일반화를 통해 '필요한 데이터 필드'를 확인 -> DB 모델링.

  • 여기에 '발행 여부' 정보 추가. (published, published_at)

 

이벤트 발행 / 전파 flow

 

스크린샷 2022-10-26 오후 4 18 04

 

  1. 도메인 로직 수행 -> 애플리케이션 이벤트 발행. 이벤트 저장 로직 수행.
    • 이 시점에서 published (이벤트 발행 여부)는 false.

 

스크린샷 2022-10-26 오후 4 21 10스크린샷 2022-10-27 오전 10 27 40

 

  1. First Layer에서 내부 이벤트 발행 -> Second Layer의 Subscriber에게 전달
    • AWS SNS - SQS를 거쳐 내부 이벤트의 정상 발행 여부를 기록하는 Subscriber.
    • 이벤트를 받을 경우 published를 true로 변경, published_at 필드에 기록

 

스크린샷 2022-10-27 오전 10 29 39스크린샷 2022-10-27 오전 10 29 59스크린샷 2022-10-27 오전 10 38 29

 

만약 이벤트 발행에 실패할 경우

  • 이벤트 저장소의 published 필드는 false
  • 재발행 이벤트는 별도의 batch로 실행 -> published false인 이벤트를 AWS SNS로 재발행
  • AWS SQS -> 이벤트 저장소의 필드를 업데이트하는 event worker로 전달, published true로 변경됨

 

기록 테이블 통합

회원 시스템은 사용자의 행동을 기록 / 개인정보를 안전하게 관리해야 함.

  • 따라서 사용자의 로그를 다양한 형태로 기록하고 있었음
  • 이벤트 저장소를 사용하면 하나로 통합할 수 있음. 도메인에서 일어나는 모든 행위가 기록되고 있기 때문.

 

스크린샷 2022-10-27 오전 11 04 51

 

event가

  • 어떤 경로로
  • 어떤 이유로
  • 누구에 의해 발행되었는지

기록하도록 함.

Outro

스크린샷 2022-10-27 오전 11 06 54

회원 시스템은

  • 어느 시스템에나 존재하는 평범한 도메인
  • 겉으로 잘 드러나지 않으며
  • 다른 모든 서비스의 도메인이 의존하고 있는 동시에
  • 개인정보를 집중적으로 다루고 있는 치명적인 도메인.

 

스크린샷 2022-10-27 오전 11 09 32

 

지금까지 설명한 회원 시스템 아키텍처는

  • 외부 시스템에 의한 영향이 없도록,
  • 외부 시스템에 영향을 주지 않도록,
  • 개인정보를 안전하게 다룰 수 있도록 고민한 결과.

앞으로도 계속 더 나은 방법을 고민할 예정.

반응형