2022.07.22
이벤트 기반 아키텍처(event-driven architecture)는 확장성이 뛰어난 고성능 애플리케이션 개발에 널리 쓰이는 비동기 분산 아키텍처 스타일이다. 적응성이 아주 좋아서 소규모 애플리케이션부터 크고 복잡한 애플리케이션까지 두루 사용할 수 있다. 이벤트 기반 아키텍처는 이벤트를 비동기 수신/처리하는 별도의 이벤트 처리 컴포넌트들로 구성되며, 스탠드얼론 아키텍처 스타일로 사용하거나 다른 아키텍처 스타일(예: 이벤트 기반 마이크로 서비스 아키텍처)에 내장할 수도 있다.
애플리케이션은 대부분 요청 기반 모델(request-based model)을 따른다. 이 모델에서는 어떤 액션을 수행하도록 시스템에 요청하면 요청 오케스트레이터가 접수한다. 요청 오케스트레이터는 보통 유저 인터페이스이지만 API 레이어나 엔터프라이즈 서비스로도 구현할 수 있다. 이 컴포넌트의 임무는 다양한 요청 프로세서에 확정적으로, 동기적으로 요청을 전달하는 일이다. 요청 프로세서는 요청을 받아 데이터베이스에서 정보를 조회/수정하는 등의 작업을 수행하는 식으로 요청을 처리한다.
이벤트 기반 아키텍처의 주요 토폴로지는 중재자 토폴로지(mediator topology)와 브로커 토폴로지(broker topology)이다. 주로 중재자 토폴로지는 이벤트 처리 워크플로를 제어해야 할 경우에, 브로커 토폴로지는 신속한 응답과 동적인 이벤트 처리 제어가 필요할 때 각각 사용된다.
브로커 토폴로지는 중앙에 이벤트 중재자가 없다는 점에서 중재자 토폴로지와 다르다. 메시지는 경량 메시지 브로커를 통해 브로드캐스팅되는 식으로 이벤트 프로세서 컴포넌트에 분산되어 흘러간다. 이 토폴로지는 비교적 이벤트 처리 흐름이 단순하고 굳이 중앙에서 이벤트를 조정할 필요가 없을 때 유용하다.
브로커 토폴로지는 네 가지 기본 아키텍처 컴포넌트, 즉 시작 이벤트, 이벤트 브로커, 이벤트 프로세서, 처리 이벤트로 구성된다. 시작 이벤트는 단순한 이벤트든, 복잡한 이벤트든 전체 이벤트 흐름을 개시하는 이벤트를 말한다. 시작 이벤트는 이벤트 브로커의 이벤트 채널로 전송되어 처리된다. 이벤트를 관리/제어하는 중재자가 브로커 토폴로지에 없으므로 단일 이벤트 프로세서는 이벤트 브로커에서 시작 이벤트를 받자마자 관련된 처리 작업을 마친 뒤 처리 이벤트를 생성하고 시스템의 나머지 부분에 자신이 한 일을 비동기로 알린다. 이 처리 이벤트는 필요시 부가적인 처리를 위해 이벤트 브로커에 비동기 전송된다. 다른 이벤트 프로세서는 처리 이벤트를 리스닝하고 있다가 이벤트가 들어오면 그에 맞는 작업을 수행한 뒤 다시 새로운 처리 이벤트를 발행함으로써 자신이 한 일을 모두에게 알린다.
이벤트 브로커 컴포넌트는 보통 연합체(도메인 기반으로 클러스터링된 다수의 인스턴스)로 구성되며, 연합된 각 브로커에는 주어진 도메인의 이벤트 흐름에서 사용되는 모든 이벤트 채널이 들어 있다. 브로커 토폴로지는 속성상 파이어 앤드 포겟 방식으로 비동기 브로드캐스팅을 하므로 토픽은 일반적으로 발행-구독 메시징 모델을 사용 하는 브로커 토폴로지에서 사용된다.
브로커 토폴로지에서는 다른 이벤트 프로세서의 관심 여부와 무관하게 각 이벤트 프로세서가 자신이 한 일을 모두에게 알리는 게 항상 바람직하다. 그래야 나중에 이벤트를 처리하는 과정에서 기능 추가가 필요하게 되더라도 아키텍처를 쉽게 확장할 수 있다.
이 예제를 보면 모든 이벤트 프로세서가 고도로 분리되어 있고 서로 독립적으로 움직인다. 브로코 토폴로지는 릴레이 경주 같다고 생각하면 이해가 빠르다. 릴레이 경주는 주자가 바통을 들고 정해진 거리를 달리고 다음 주자에게 바통을 넘겨주는 식으로 마지막 주자가 결승선을 통과할 때까지 정해진 순번에 따라 진행된다. 이벤트 프로세서는 이벤트 전달 후 더 이상 그 이벤트 처리에는 관여하지 않고 다른 시작 이벤트 또는 처리 이벤트에 반응할 준비를 한다. 또한 각 이벤트 프로세서는 이벤트 처리 도중 가변적인 부하나 백업 조건을 처리하기 위해 서로 독립적으로 확장할 수 있다.
브로커 토폴로지는 성능, 응답성, 확장성 측면에서 장점이 많지만 그만큼 단점도 적지 않다. 무엇보다 시작 이벤트(예제는 PlaceOrder 이벤트)와 연관된 전체 워크플로를 제어할 수가 없다. 따라서 다양한 조건에 따라 상황이 매우 유동적이고 어느 시스템 파트도 실제 주문 트랜잭션이 언제 끝났는지 모른다. 에러 처리 역시 어렵다. 비지니스 트랜잭션을 관찰/통제하는 중재자가 없으므로 처리가 실패해도 다른 파트는 그 사실을 모른다.
비지니스 트랜잭션을 재시작하는 기능도 브로커 토폴로지에서는 지원되지 않는다. 처음 시작 이벤트를 처리할 때부터 이미 다른 작업이 비동기로 수행된 터라 시작 이벤트를 다시 넣는 것은 불가능하다.
장점 | 단점 |
---|---|
이벤트 프로세서가 디커플링됨 | 워크플로 제어 |
확장성 높음 | 에러 처리 |
응답성 우수함 | 복구성 |
성능 우수함 | 재시작 능력 |
내고장성 뛰어남 | 데이터 비일관성 |
중재자 토폴로지(mediator topology)는 좀 전에 살펴본 브로커 토폴로지의 단점들을 일부 보완한다. 여러 이벤트 프로세서 간의 조정이 필요한 시작 이벤트에 대하여 워크플로를 관리/제어하는 이벤트 중재자(event mediator)가 핵심이다. 중재자 토폴로지는 시작 이벤트, 이벤트 큐, 이벤트 중재자, 이벤트 채널, 이벤트 프로세서, 이렇게 5개 아키텍처 컴포넌트로 구성된다.
시작 이벤트가 전체 이벤트 프로세스를 개시하는 이베트인 점은 브로커 토폴로지와 동일하지만, 중재자 토폴로지에서는 시작 이벤트 큐를 거쳐 이벤트 중재자로 전달되는 차이점이 있다. 이벤트 중재자는 이벤트 처리에 관한 단계 정보만 갖고 있으므로 점대점 메시징으로 각각의 이벤트 채널(대부분 큐)로 전달되는 처리 이벤트를 생성한다. 그러면 각 이벤트 프로세서는 자신의 이벤트 채널에서 이벤트를 받아 처리한 다음 중재자에게 작업을 완료했다고 응답한다. 이벤트 프로세서가 다른 프로세서에게 자신이 한 일을 알리지 않는다는 것도 브로커 토폴로지와 다른점이다.
중재자 토폴로지 구현체에는 대부분 특정 도메인이나 이벤트 그룹과 연관된 중재자가 여럿 존재하므로 토폴로지의 단일 장애점(single point of failure, SPF)을 줄이고 전체 처리량과 성능을 높일 수 있다. 예를 들어, 전체 고객에 관한 이벤트는 고객 중재자가 처리하게 하고, 주문 관련 이벤트는 주문 중재자가 처리하게 하는 식이다.
중재자 컴포넌트는 브로커 토폴로지와는 달리 워크플로에 대해 잘 알고 있고 통제가 가능하다. 중재자는 워크플로를 제어하므로 이벤트 상태를 유지하면서 필요시 에러 처리, 복구, 재시작을 할 수 있다.
중재자 토폴로지는 브로커 토폴로지에서 불가능한 문제를 해결할 수 있지만 그만큼 부정적인 요소도 있다. 첫째, 복잡한 이벤트 흐름 내에서 발생하는 동적인 처리를 선언적으로 모델링하기가 매우 어렵다. 그래서 보통은 중재자의 내부 워크플로는 일반적인 처리만 하고 복잡한 이벤트 처리의 변화무쌍한 부분은 중재자 + 브로커 형태의 하이브리드 모델로 처리한다. 둘째, 이벤트 프로세서는 브로커 토폴로지와 동일한 방식으로 쉽게 확장할 수 있지만, 그러자면 중재자도 함께 확장해야 하므로 전체 이벤트 처리 흘므에 병목 지점이 생기기 쉽다. 셋쨰, 중재자 토폴로지는 이벤트 처리를 중재자가 제어하므로 이벤트 프로세서가 상대적으로 더 많이 커플링되어 성능은 브로커 토폴로지보다 좋지 않다.
장점 | 단점 |
---|---|
워크플로 제어 | 이벤트 프로세서가 커플링됨 |
에러 처리 | 확장성 낮음 |
복구성 | 성능 낮음 |
재시작 능력 | 내고장성 좋지 않음 |
데이터 일관성 | 워크플로 모델링 복잡함 |
브로커 토폴로지냐, 중재자 토폴로지냐, 결국 워크플로 제어와 에러 처리 기능이 우선인가, 아니면 고성능과 확장성이 더 중아한가의 트레이드오프를 잘 따져 선택할 수 밖에 없다. 중재자 토폴로지의 성능과 확장성도 그리 나쁜 편은 아니지만 아무래도 브로커 토폴로지만큼은 못한 게 사실이다.
이벤트 기반 아키텍처 스타일은(이벤트 컨슈머의 응답을 받아야 하는) 요청/응답 처리뿐만 아니라 파이어 엔드 포겟 처리 모두 비동기 통신만 사용한다는 점에서 다른 아키텍처 스타일과 차별화된다. 비동기 통신은 시스템 응답성을 전반적으로 높이는 강력한 기법으로 활용할 수 있다.
예를 들어, 위와 같이 유저가 작성한 제품 후기를 댓글로 게시하는 웹사이트를 생각해보자. 댓글 서비스는 여러 파싱 엔진(비속어 검사기, 문법 검사기, 문맥 검사기)을 거치는데, 보통 댓글 하나를 게시하려면 3,000 밀리초가 걸린다고 한다. 상단 그림처럼 REST로 동기 호출을 하면, 서비스가 댓글을 수신하는 데 50밀리초, 댓글을 게시하는 데 3,000밀리초, 댓글이 등록됐음을 유저에게 응답하기까지 네트워크 지연 시간이 50밀리초 소요되어 유저가 댓글을 게시하는 데 총 3,100 밀리초의 응답 시간이 걸린다. 그러나 메시지를 비동기 전송하면, 최종 유저 입장에서 웹사이트에 댓글을 게시하는데 소요된 25밀리초 밖에 안 걸린다. 물론, 실제로 댓글을 게시하려면 여전히 3,025밀리초(메시지 수신에 25밀리초, 댓글 게시에 3,000밀리초)가 걸리지만 최종 유저 관점에서는 이미 댓글의 처리는 완료된 셈이다.
이것은 응답성과 성능의 차이점을 잘 보여주는 예이다. 유저가 굳이 어떤 정보를 돌려받을 필요가 없으면 기다리게 할 이유 또한 없다. 응답성은 어떤 액션이 접수되어 곧 처리될 거라는 사실을 유저에게 알리는 것이고, 성ㅇ능은 종단간 프로세스가 더 빨리 수행되게끔 만드는 것이다.
그런데 여기서 한 가지 생각해볼 문제가 있다. 비동기 호출은 언젠가 댓글이 게시될 예정이라는 미래의 약속과 함께 확인 응답을 받은 것뿐이다. 최종 유저 눈에 댓글은 이미 게시가 끝난 것처럼 보이지만, 만약 댓글에 비속어 등이 포함되어 있다면 어떨까? 당연히 댓글 게시는 거부되지만 이제 최종 유저에게 돌아갈 방법이 없다.
비동기 통신에서는 에러 처리가 가장 큰 문제이다. 응답성은 엄청나게 개선되지만 에러를 제대로 처리하기가 쉽지 않기 때문에 이벤트 기반 시스템의 복잡도가 가중된다.
리액티브 아키텍처의 워크플로 이벤트 패턴은 비동기 워크플로에서 에러 처리 문제를 해결하는 한 가지 방법이다. 탄력성과 응답 이라는 두 마리 토끼를 겨냥한 리액티브 아키텍처 패턴의 일종이다.
워크플로 이벤트 패턴은 워크플로 대리자(workflow delegate)를 통해 위임, 봉쇄, 수리 작업을 한다. 이벤트 프로듀서는 메시지 채널을 통해 데이터를 이벤트 컨슈머에게 비동기 전송하고, 이벤트 컨슈머가 데이터를 처리하는 도중 에러가 발생하면 즉시 해당 에러를 워크플로 프로세서에게 위임한 뒤 이벤트 큐에 있는 다음 메시지로 넘어간다. 이렇게 에러가 발생해도 바로 다음 메시지를 바로 처리하므로 전체 응답성은 영향을 받지 않는다. 만약 이벤트 컨슈머가 손수 에러를 해결하느라 시간을 소비한다면 그동안 큐에 있는 다음 메시지는 읽지 않기 때문에 처리 대기 중인 나머지 메시지의 응답성도 영향을 받는다.
워크플로 이벤트 패턴의 예를 하나 들어보겠다. 어떤 지역의 거래 자문가가 다른 지역에 있는 대형 트레이딩펌을 대신하여 거래 주문을 받는다고 하자. 이 자문가는 거래 주문을 묶어 일괄 처리 후 주식을 매수할 수 있도록 트레이딩펌의 브로커로 비동기 전송한다.
여기서 예외가 발생해도 비동기 요청이라 동기적으로 에러를 조치해서 응답할 유저가 없다. 따라서 에러 조건을 로깅하는 것 외에 딱히 거래 처리 서비스가 할 수 있는 일이 없다.
워크플로 이벤트 패턴을 적용하면 프로그래밍 방식으로 에러를 조치할 수 있다. 트레이딩 펌은 거래 자문가가 보낸 거래 주문 데이터를 어찌 할 수가 없기 때문에 스스로 대응하여 조치해야 한다. Trade Placement 서비스는 동일한 에러가 발생하면 에러 정보를 Trace Placement Error 서비스에게 비동기 메시지로 전달한다.
워크플로 이벤트 패턴에서 한 가지 주의할 점은, 에러가 발생한 메시지를 조치 후 다시 제출하면 처리 순서가 바뀌는 것이다. 특정 계정의 모든 주식 거래는 순서대로 처리되어야 하므로 좀 전의 예제 역시 메시지 순서가 중요하다. 기술적으로 불가능하지는 않지만, 주어진 콘텍스트에서 메시지 순서를 유지하는 것은 매우 복잡한 작업이다. 한 가지 해결 방법은, Trade Placement 서비스가 에러가 발생한 거래의 계좌 번호를 큐에 담아 보관하는 것이다. 계좌 번호가 동일한 거래는 나중에 처리할 수 있게 임시 큐에 저장하면 될 것이다. 원래 에러가 난 거래가 조치되면 Trade Placement 서비스는 동일한 계정의 잔존한 거래를 큐에서 꺼내 순서대로 처리하면 된다.
비동기 통신을 할 때 데이터 소실은 언제나 중요한 관심사인데, 불행하게도 이벤트 기반 아키텍처는 데이터가 소실될 만한 곳이 참 많다. 데이터 소실이란, 메시지가 도중에 삭제되거나 최종 목적지에 도달하지 못한 상태를 말한다. 예를 들어, 이벤트 프로세서 A가 큐에 메시지를 비동기 전송하고 이벤트 프로세서 B는 이 메시지를 받아 데이터베이스에 삽입한다고 하자. 이런 일반적인 시나리오에서 데이터 소실이 일어나는 경우는 다음 세 가지로 정리할 수 있다.
데이터 소실 문제는 기본적인 메시징으로 어느 정도 해결할 수 있다.
1번 이슈는 동기 전송과 퍼시스턴스 메시지 큐를 이용하면 쉽게 해결된다. 퍼시스턴스 메시지 큐는 이른바 전달 보장(guaranteed delivery)도 지원한다. 즉 메시지 브로커가 메시지를 수신하면 신속한 조회를 위해 메모리에 저장하는 동시에 물리적 데이터 저장소(파일 시스템 또는 데이터베이스)에도 메시지를 저장하는 것이다.
2번 이슈 역시 클라이언트 확인응답 모드(client acknowlegde mode)라는 기본적인 메시징 기술을 이용하면 해결 가능하다. 원래 메시지는 큐에서 빠져나가는 즉시 삭제되는데, 클라이언트 확인응답 모드는 메시지를 큐에 보관한 채 다른 컨슈머가 메시지를 읽을 수 없게 클라이언트 ID를 메시지에 부착한다. 따라서 이벤트 프로세서 B가 잘못돼도 메시지는 큐에 계속 남아 있으니 데이터 소실을 방지할 수 있다.
3번 이슈는 데이터베이스 본연의 ACID(원자성, 일관성, 격리성, 내구성) 트랜잭션의 커밋으로 해결가능하다. 데이터베이스에 커밋이 일어나면 데이터가 확실하게 저장된다. 최종 참여자 지원을 활용하면 메시지 처리가 끝나 데이터베이스에 저장됐음을 확인한 이후에 큐에서 메시지가 삭제된다. 따라서 이벤트 프로세서 A에서 데이터베이스로 가는 도중에 메시지가 소실될 일은 없다.
이벤트 기반 아키텍처는 메시지를 누가 받은(컨슈머가 있다면), 그 메시지로 무슨 일을 하든 상관없이 이벤트를 브로드캐스트(전파)할 수 있다.
메시지 프로듀서는 자신이 보낸 메시지를 어느 이벤트로 프로세서가 수신할지, 또 메시지를 받아 무슨 일을 할지 모른다. 그러므로 어쩌면 브로드캐스팅은 여러 이벤트 프로세서를 가장 높은 수준으로 디커플링하는 수단이며, 최종 일관성, 복잡한 이벤트 처리 등 다양한 쓰임새를 지닌 필수 기능이다.
서비스나 이벤트 프로세서 간에 동기 통신이 필요한 경우도 있다.
이벤트 기반 아키텍처는 동기 통신을 요청-응답 메시징(의사 동기 통신, psedosynchronous communication이라고도 함) 방식으로 수행한다. 요청-응답 메시징 내부의 각 이벤트 채널은 요청 큐, 응답 큐로 구성된다. 처음 정보를 요청하면 요청 큐에 비동기 전송된 후 메시지 프로듀서에게 제어권이 반환되며, 메시지 프로듀서는 응답 큐에 응답이 도착하길 기다리며 차단 대기 상태가 된다. 메시지 컨슈머가 메시지를 받아 처리한 후 응답 큐에 응답을 보내면 이벤트 프로듀서는 응답 데이터가 포함된 메시지를 수신한다.
요청-응답 메시징을 구현하는 주요한 기술은 두 가지이다.
첫째, 가장 일반적인 기술로, 메시지 헤더에 상관(correlation) ID를 사용하는 것이다. 상관 ID는 응답 메시지의 필드로, 대부분 원요청 메시지의 메시지 ID로 세팅된다. 둘째, 응답 큐에 임시 큐를 두고 요청-응답 메시징을 구현하는 방법이다. 임시 큐는 지정된 요청에만 사용되는데, 요청이 들어오면 생성되고 요청이 종료되면 삭제된다. 임시 큐는 각 요청별로 이벤트 프로듀서만 알고 있는 전용 큐이므로 상관 ID는 필요하지 않다.
기술적으로는 임시 큐가 훨씬 단순하지만 메시지 브로커는 매번 요청을 할 때 마다 임시 큐를 생성/폐기하는 일을 반복해야 한다. 따라서 대용량 메시지 처리 시 메시지 브로커의 속도가 크게 떨어지고 전체 성능과 응답성 역시 영향을 받을 수 있다. 그래서 우리는 대체로 상관 ID를 사용하는 방법을 권장한다.
요청 기반 모델과 이벤트 기반 모델 모두 소프트웨어 시스템을 설계하는 유효한 접근 방식이다.
워크플로의 확장성과 제어가 중요하면 체계적인 데이터 기반의 요청에 특화된 요청 기반 모델을, 복잡하고 동적인 유저 처리 등 주로 고도의 응답성과 확장성을 요하는, 유연한 액션 단위의 이벤트를 처리한다면 이벤트 기반 모델이 좋은 선택이다.
요청 기반보다 좋은 점 | 장단점 |
---|---|
동적인 유저 콘텐츠의 응답성이 좋음 | 최종 일관성만 지원됨 |
확장성, 탄력성이 우수함 | 처리 흐름을 제어하기 곤란함 |
민첩성과 변화 관리가 우수함 | 이벤트 흐름의 결과를 예측하기 어려움 |
적응성과 확장성이 뛰어남 | 테스팅, 디버깅이 어려움 |
응답성과 성능이 좋음 | |
실시간 의사 결정이 가능함 | |
상황 인지에 따른 반응성이 좋음 |
이벤트 기반 아키텍처와 다른 아키텍처 스타일을 함께 사용하는 하이브리드 아키텍처 기반의 애플리케이션도 있다. 이벤트 기반 아키텍처를 다른 아키텍처 스타일의 일부로 활용하는 아키텍처 스타일로는 마이크로서비스 아키텍처, 공간 기반 아키텍처가 대표적이다.
어떤 아키텍처 스타일이든지 이벤트 기반 아키텍처를 추가하면 병목 지점을 제거하고 이벤트 요청을 백업하는 배압 지점(back pressure point)을 확보하는 데 유용하며, 다른 아키텍처 스타일에서는 찾아볼 수 없는 유저 응답성이 보장된다. 마이크로서비스 아키텍처, 공간 기반 아키텍처는 데이터 펌프에 메시징을 활용하며, 다른 프로세서에 데이터를 비동기 전송하여 데이터베이스 데이터를 업데이트 한다. 또. 서비스 간에 메시지를 주고받으며 통신할 때 마이크로서비스 아키텍처의 서비스와 공간 기반 아키텍처의 처리 장치 모두 이벤트 기반 아키텍처를 활용함으로써 프로그래밍 방식의 확장성을 달성할 수 있다.
아키텍처 특성 | 별점 |
---|---|
분할 유형 | 기술 |
퀀텀 수 | 하나 또는 여러개 |
배포성 | ⭐⭐⭐ |
탄력성 | ⭐⭐⭐ |
진화성 | ⭐⭐⭐⭐⭐ |
내고장성 | ⭐⭐⭐⭐⭐ |
모듈성 | ⭐⭐⭐⭐ |
전체 비용 | ⭐⭐⭐ |
성능 | ⭐⭐⭐⭐⭐ |
신뢰성 | ⭐⭐⭐ |
확장성 | ⭐⭐⭐⭐⭐ |
단순성 | ⭐ |
시험성 | ⭐⭐ |
이벤트 기반 아텍키텍처는 특정 도메인이 여러 이벤트 프로세서에 분산되어 있고 중재자, 큐, 토픽을 통해 서로 묶여 있는, 기술 분할된 아키텍처이다. 한 도메인에 변경이 발생하면 많은 이벤트 프로세서, 중재자, 다른 메시징 아티팩트에도 영향을 미치므로 이벤트 기반 아키텍처는 도메인 분할 아키텍처는 아니다.