먼저 기존의 전통적인 요청-응답 구조를 보도록 하자.
client <--> API Gateway <--> Subscription service
^
|
v
Payment service
^
|
v
user service
우리는 구독 서비스 기반의 프로그램을 만든다고 하자. client가 구독 버튼을 누르면 API Gateway
를 통해서 subscription service
로 요청이 전달된다. subscription service
는 payment service로 요청을 보내어 응답을 기다린다.
payment service는
user service를 통해
user`의 정보를 가져오거나, 결제가 완료되면 user의 정보를 업데이트한다.
이러한 문제를 해결하기 위해서 orchestration service
를 만들어 병렬적으로 각 service를 동작시킬 수 있도록 만들 수 있다. 참고로 orchestration service
는 API Gateway
내부의 일부 서비스로 만들 수도 있다.
Subscription service
/
/
client <--> API Gateway ------- Payment service
(Orchestation service)
\
\
user service
client의 요청을 받으면 API Gateway의 Orchestation service
가 각 service들에게 병렬적으로 요청을 보내는 것이다. 이는 가장 긴 transaction 처리 시간이 전체 시스템 응답 시간을 차지하겠지만, 여전히 복잡한 구조이고 API gateway와 service 간의 결합력이 더 강하게 유착된다는 문제가 남아있다.
이러한 요청 chain의 길이가 깊어질 수록 발생하는 가장 큰 문제는 각 service들끼리의 요청-응답을 기다려야하는 구조이다. 이는 사용자에게 너무 긴 시간동안 기다리게 만들어주는 문제를 발생시키고, 에러가 발생했을 때, 각 chain의 어떤 부분에서 에러가 발생했는 지 알기도 쉽지 않다.
event기반 아키텍처는 이러한 문제를 해결해주는 새로운 구조를 제사한다. 먼저 event기반 아키텍처에의 가장 핵심인 event는 다음의 정의를 가진다.
event: 어떠한 사실, action, 상태 변화로 항상 불변성을 가진다. 또한, 무기한으로 저장될 수 있으며, 다른 서비스들에 의해서 여러 번 소모될 수 있다.
event기반 아키텍처에서 event를 교환하는 참여자 3개 집단이 있다.
| -> Consumer1
---------- ---------------- | -> Consumer2
|Producer| --event---> |Message Broker|--| -> Consumer3
---------- ---------------- | -> Consumer4
| -> Consumer5
여기서 message broker는 event를 전달받으면 안전하게 관리해주며, 특정 라우팅 룰에 따라 consumer에게 어떤 event들을 라우팅해줄 지 결정해준다.
요청-응답 기반의 방식에 비해 event-driven방식이 가지는 특징은 다음과 같다.
1. Asynchronous
2. Inversion of control(제어의 역전)
3. Loose coupling
-------- --------
| | ----request-----> | |
|Client| |Server|
| | <---response----- | |
-------- --------
위와 같은 요청-응답의 구조는 synchronous하게 동작한다는 특징을 지닌다. 즉, clinet가 server에게 요청을 보낸다음 응답이 올 때까지 다른 일을 하지 못하고 기다려야한다는 문제가 있다는 것이다.
또한, client는 server의 API를 호출하기 위해서는 server의 API 특징과 매개변수와 응답에 대해서도 알아야한다. 이는 server와 client가 강하게 연결되었다는 것을 말한다.
반면에 event-driven 구조는 다음과 같다.
| -> Consumer1
---------- ---------------- | -> Consumer2
|Producer| --event---> |Message Broker|--| -> Consumer3
---------- ---------------- | -> Consumer4
producer는 consumer의 특징에 대해서 알 필요가 없다. 그저 event를 Message broker에게 전달하는 방법만 알면 된다. 이는 producer와 consumer 간의 관계가 약하게 결합되어있는 상태를 말한다. 또한, event를 전달하고 결과를 기다리지 않아도 되므로 asynchronous하게 동작한다는 것을 알 수 있다.
이는 client의 요청을 event로 만들어 요청을 처리하는 권한을 외부 엔티티로 전달하기 때문에 내부적으로 main process를 통해 처리하는 것이 아니다. 이는 IoC의 특징이며, 처리 권한을 외부의 엔티티로 전달하여 내부의 결합성을 제거하는 것이다. 덕분에 Event driven 구조에서는 component들 간에 서로를 인식할 필요가 없고, 분리되어있기 때문에 느슨한 결합 시스템을 구축하는데 적합한 선택이다.
그럼 언제 event driven architecture를 써야할까?? 다음의 대표적인 case들이 있다.
1. fire and forget: event가 발생시켰지만 거기에 대한 성공 여부가 사용자 경험에 영향을 주지 않는 경우이다. 가령 로그를 쓰는 것이 있다.
2. Reliable delivery: event가 반드시 실행되어야 하는 경우
3. infinite stream of events: iot data가 stream 형식으로 계속 들어온다면, 이를 event로 받아내어 계속 처리하도록 한다.
4. Anomaly detection/pattern recognition: 주기적으로 data를 받아내어 metric로 사용할 때
5. Broadcast: event를 받고자하는 모든 참여자에게 전송
6. Buffering: 대량의 traffic을 견딜 수 있도록 버퍼링을 제공해야할 때(Message broker의 특징)
반대로 요청-응답이 더 좋은 case들이 있다.
1. 빠른 요청-응답
2. 간단한 interaction
여러가지 event전달 pattern들이 있는데, 첫번째는 event streaming이다.
event streaming은 message broker가 event의 임시 또는 영구 저장소로 사용된다. 이는, event가 다른 소비자에게 소모되어도 다른 소비자들도 똑같은 event를 사용할 수 있다. 또한, 특정 소비자가 나중에 도착해도 event streaming을 replay하도록 하여 이전의 event를 재생시켜 새로 온 소비자도 event 처리를 실행시킬 수 있도록 할 수 있다.
따라서, 새로운 소비자를 만들어내어, 이전에 발생했던 event를 기반으로 이상 감지 패턴을 확인할 수 있도록 활용할 수 있다.
message broker 덕분에 event streaming 기능은 신뢰성있는 전달이 보장된다.
정리하자면, event streaming은 event를 연속적으로 캡쳐, 저장, 실시간 처리, 분석, 전달하는 데이터 처리 패턴으로 주로 모니터링, 실시간 데이터 처리, 로그 메트릭 수집 등에 사용된다.
두 번째 패턴은 pub-sub pattern이다. 소비자들은 subscriber가 되어 특정 topic을 message broker에 구독한다음 event가 발생했을 때, 해당 정보를 subscriber에게 broadcast해줄 수 있다. 또한, 새로운 소비자들이 기존의 topic을 구독하는 것도 가능하여 확장성이 매우 휼륭하다.
이는 message borker가 broadcast용 임시 저장소로 쓰인다고 볼 수 있다. 또한, fire and forget 방식처럼 event를 전달만하고, 그에 대한 실행 결과는 필요없을 때 많이 사용한다. 주로 알림이나 채팅 시스템 같은 경우가 있다.
3개의 집합인 producer, message broker, consumer 사이에서 network error나 지연이 발생하여 data 유실이 생기면 어떻게 해야할까?? 다시 event를 재전송 하면 된다고 생각하지만, 이 문제는 이전 event가 도착해버리면 중복 event가 발생할 수도 있다.
producer가 message Broker에 event를 발행할 때 ACK을 받아야한다. 만약 받지 못할 경우, 재전송을 하게 되는 경우가 있는데, 이 경우 이전에 전달했던 ACK이 늦게 온 경우라면 동일한 event가 message broker에 담겨있어 문제를 야기한다. 또한, consumer의 경우도 event를 받고 ACK을 전달해야하는데, ACK을 늦게보내면 event가 전달 도중에 누락된 줄 알고 다시 consumer에게 전달하는 경우가 있다.
이렇게 producer, message broker, consumer 사이의 event처리 정책들이 있는데, 이는 universal하게 쓰이는 기술적 용어로 생각하면 된다.
각 정책들은 producer와 consumer에 적용되며 동작은 약간 다르다.
At-Most-Once: 무슨 일이 있어도 최대 딱 한번만 전송한다. 이는 중복 전송을 피하고 싶어서 사용하는 것이다.
결과적으로 At-Most-Once는 데이터의 일부 손실을 허용한다.
At-Most-Once를 쓰는 경우는 위치 정보를 업데이트하는 경우가 그렇다. 적은 오버헤드와 지연시간을 가지므로, 수시로 데이터를 업데이트하면 된다. 일부 데이터가 유실되어도 큰 문제는 없다.
At-Least-Once: 반드시 데이터의 전송을 보장한다.
결과적으로 At-Least-Once는 이벤트를 절대로 일어버리지 않는다. 단, 이벤트 중복을 허용하는 문제가 있다.
알림을 보내는 경우를 생각해보자, 알림을 두 번 보냈다고 큰일이 생기지 않는다. 리뷰 역시도 마찬가지이다. 리뷰를 두 번 남기거나, 덮어쓰게해도 문제가 없다.
그러나, 이러한 방식은 지연 시간이 너무 긴 단점이 있다. 왜냐하면 producer가 event를 message broker에 전달하고 ack을 받을 때까지 blocking으로 동작해야하며, consumer 역시도 자신의 모든 로직을 마친 다음에 ack을 보내기 때문에 message broker는 message queue를 바로 비워내지 못한다. 또한, 손실이 발생하면 이를 처리하는 시간이 매우 오래걸린다.
Exactly-Once 재정이나 금융권에서 사용되는 것으로 가장 낮은 처리율과 높은 지연시간을 가진다. 즉, 오버헤드가 크지만 데이터의 안정성이 보장되고, 중복과 같은 에러가 발생하지 않는다.
consumer 측이 조금 복잡한데, 이는 message broker 이외의 외부 시스템인 DB에 같이 결합되어있어서 Exactly-once가 보장되지 않을 수 있기 때문이다. 그래서 DB에 event id를 적도록 하여 Exactly-once를 보장하는 것이다.
실제로 redis의 경우 pub-sub은 At-Most-Once이지만, stream data의 경우는 At-least-Once이다. kafka의 경우는 At-least-Once가 default이고 설정을 통해 바꿀 수 있다.