오도근 AWS 솔루션즈 아키텍트

[아이티데일리]

오도근 AWS 솔루션즈 아키텍트
오도근 AWS 솔루션즈 아키텍트

오도근 솔루션즈 아키텍트는 YES24, 보광그룹, 이지케어텍, 쿠팡 등에서 20여 년 동안 애플리케이션을 개발하고 운영했다. 아마존웹서비스(AWS)에서는 금융회사들을 대상으로 클라우드 도입을 위한 기술을 지원하고 있으며, 이와 더불어 기업들이 애플리케이션 현대화를 위해 알아야 할 것들이 무엇인지 조언해 주는 ‘현대화 프리퀄(Modernization Prequel)’ 이니셔티브를 운영하고 있다.

서론

아마존 버너 보겔스(Werner Vogels) 최고기술책임자(CTO)는 지난해 열린 ‘AWS 리인벤트(re:Invent) 2022’에서 “이 세상에 동기적인 것은 전혀 없다. 좋은 컴퓨터 시스템을 구축하고 싶다면 현실 세계를 살펴봐야 하고, 현실 세계는 비동기적이다”라고 이야기했다. 또한 컴퓨팅 시스템에 대해서도 “이벤트 기반 아키텍처(Event Driven Architecture, EDA)에서 개별 단위의 병렬처리와 다른 리소스를 차단(block) 하지 않는 것”을 강조했다.

이번 기고문에서 다루고자 하는 내용도 이벤트를 이용해 애플리케이션의 의존성(Dependency)과 동시성(Synchrony)을 제거하는 방법이다. EDA는 주로 마이크로서비스 아키텍처(Microservice Architecture, MSA)와 함께 다뤄지기도 하지만, 꼭 MSA가 아니더라도 충분히 적용할 수 있다. 본 기고문에서는 EDA의 주요 개념과 몇 가지의 대표 패턴을 소개한다.


동기적 호출의 문제점

회원가입 프로세스에는 신규회원의 정보가 데이터베이스에 저장되는 것 외에도 가입 축하 메일 발송, 앱 푸시 알림, 가입 적립금 및 각종 쿠폰 지급 등의 부가적인 처리 과정들이 존재한다. 시스템 내부적으로도 여러 모듈(Module)이나 서비스(Service)가 복합적으로 사용되는데, 이러한 구조에서는 크게 두 가지 문제가 발생할 수 있다.

첫 번째는 지연시간이 길어진다는 것이다. 동기식은 호출과 응답이 순차적으로 일어나고, 응답이 오기 전까지는 후속 작업을 진행할 수 없다. 메일 발송, 앱 푸시 발송, 적립금 지급, 쿠폰 지급의 순서로 호출될 경우, 앞선 호출이 완료되기 전까지 후속 호출은 대기해야 하고 각 모듈의 처리 시간에 따라 지연시간이 길어질 수 있으며, 심할 경우 타임아웃이 발생할 수도 있다.

두 번째는 처리 과정 중에서 하나라도 오류가 발생할 경우 설계에 따라 시스템의 품질과 고객 경험에 큰 차이가 발생한다는 사실이다. 오래되긴 했으나 메일 발송이나 적립금 지급이 실패하는 등의 문제가 발생하면, 전체를 무효화하는 이른바 ‘롤백(Roll Back)’ 처리를 최선으로 여기던 시절도 있었다. 이는 데이터의 정합성이 중요하다는 논리에서 비롯된 처리 방식이었다.

하지만 고객 입장에서 생각해 보면 ‘회원으로 등록되는 것’이 주목적이고 축하 메일이나 적립금을 받는 것은 부수적인 목적에 불과하다. 축하 메일은 안 받아도 그만이고 누락된 적립금은 고객센터를 통해서 지급받아도 된다. 이처럼 부수적인 처리 과정으로 인해 주목적인 ‘회원가입’이 취소(Roll Back)되는 것은 현대 애플리케이션에서는 최선이라 할 수 없다.


결합이 강한 시스템의 문제점

앞서 제시한 사례에서 언급했듯, 하나의 회원가입 절차 안에서 이뤄지는 메일, 앱 푸시, 적립금 지급 등의 다양한 처리 과정들은 각각 다른 모듈이거나 또는 다른 서비스들로 만들어진다. 강한 결합은 회원가입 프로세스에서 다른 모듈을 직접 호출하면서 발생한다. 회원가입 모듈은 메일 모듈을 직접 참조해야 하고, 메일을 보내기 위해 정해진 스펙에 맞춰 파라미터도 설정해야 한다. 앱 푸시, 적립금 등 다른 모듈도 마찬가지다.

결국 이러한 직접 참조가 의존성을 유발해 강한 결합의 시스템이 되는 원리로 작용한다. 만약 메일 시스템에 장애가 발생할 경우 메일 모듈을 직접 참조하는 회원가입 프로세스도 장애를 겪게 된다. 메일 시스템의 버전이 업그레이드될 경우 회원가입 모듈에서도 새 버전으로 참조를 변경하고 스펙에 맞게 코드도 바꿔야 한다. 엔터프라이즈급의 거대한 시스템을 고려할 때, 메일과 같은 공통 모듈의 변경은 상당한 임팩트를 수반한다.


이벤트의 정의와 특징

앞서 설명한 두 가지 문제는 이벤트 기반 아키텍처로 대부분 해결할 수 있다. 이벤트란 도메인이라 불리는 특정 비즈니스 영역에서 주요 정보가 발생하거나 변경되는 상태 변화를 의미한다. 예를 들어 전자상거래 사이트에서 회원이 장바구니에 상품을 추가하면 ‘상품 추가됨’ 이벤트가 발생하는 것이고, 주문을 위해 결제하면 ‘결제가 승인됨’ 이벤트가 발생하는 것이다. 현실에서 과거에 발생한 사건을 되돌릴 수 없듯, 시스템 내의 이벤트도 한 번 발생하면 변경되지 않도록 불변(Immutable) 정보로 다뤄져야 하며 그 자체가 히스토리가 된다.

따라서 이벤트를 프로그래밍 코드로 표현할 때도 ‘ItemAdded’나 ‘OrderCreated’처럼 과거형으로 쓰는 것이 일반적이다. 이벤트의 결과로 발행되는 정보(이하 이벤트 메시지)에는 어떤 이벤트인지 나타내는 정보와 이벤트로 영향을 받은 도메인 객체의 정보가 담기게 된다.


이벤트 기반 아키텍처(EDA)의 구성요소

EDA에는 크게 세 가지 요소가 있다. 생산자는 시스템에서 다루는 주요 정보의 상태 변화가 발생하면 이벤트 메시지를 발행해 라우터로 전달한다. 라우터는 발행된 이벤트 메시지를 필터링하고 소비자에게 전달한다. 소비자는 라우터로부터 이벤트 메시지를 수신해 목적에 따라 처리한다.

 전자상거래에서의 EDA 예시 (출처: aws.amazon.com)
전자상거래에서의 EDA 예시 (출처: aws.amazon.com)


이벤트를 이용한 비동기 시스템의 장점

생산자의 역할은 자신의 비즈니스 처리를 끝낸 후 그 변화를 메시지에 담아 발행하면 끝난다. 이벤트 라우터를 통해 메시지를 수신한 소비자 역시 정해진 비즈니스 처리를 수행하면 되고, 메시지가 오지 않은 경우 그냥 대기하면 된다. 이처럼 양단의 처리가 비동기적으로 진행되기 때문에 결합은 느슨해지고, 업데이트와 배포 같은 유지·보수를 독립적으로 수행할 수 있다.

생산자에서 장애가 발생할 경우, 이벤트 메시지는 발행되지 않기 때문에 소비자 측에선 아무런 조치를 할 필요가 없다. 생산자의 장애가 해결되면 이벤트 메시지는 재발행 될 것이고, 소비자는 원래대로 동작할 테니 역시나 조치할 것이 없다. 소비자 측에서 장애가 발생해도 라우터만 정상 상태를 유지한다면 생산자는 아무런 영향도 받지 않는다. 이처럼 이벤트를 이용한 비동기 처리는 장애를 다른 곳으로 전파하지 않는 장점도 있다.


One to One (1:1) vs One to Many (1:N)

EDA에서 가장 기본 패턴은 1:1로 알려진 메시지 큐(Queue) 방식과 1:N으로 알려진 게시/구독(Pub/Sub) 방식이 있다. 두 방식 모두 먼저 발행한 메시지가 먼저 소비되는 FIFO(First-In-First-Out) 방식으로 작동하지만, 구체적인 작동 방식에는 차이가 있다.

큐 방식에서는 생산자가 발행한 이벤트 메시지가 하나의 소비자에게만 수신되고, 수신된 메시지는 큐에서 삭제돼 더 이상 사용할 수 없게 된다. 이는 이벤트 발행 순서를 지켜야 하는 경우에 많이 사용하는 방식이며, 수강 신청과 콘서트 티켓 예매, 식당 주문 등에 적용할 수 있다. 아파치 액티브(Apache Active) MQ, 래빗(Rabbit) MQ, 아마존 SQS, 아마존 MQ 등이 대표적인 큐 방식 툴이다.

 메시지 큐(Queue) 방식
메시지 큐(Queue) 방식

게시/구독 방식에서는 발행된 이벤트 메시지 수신을 원하는 소비자라면 누구나 수신할 수 있다. 메시지는 수신되더라도 일정 기간 라우터 내에 남게 돼 필요시 재수신할 수도 있다. 앞서 예를 들었던 회원가입의 경우, 회원 모듈에서 ‘회원이 등록됨’ 이벤트 메시지를 발행하면 각각의 모듈이나 서비스(메일, 앱 푸시, 적립금, 쿠폰, 이벤트 등)가 독립적으로 메시지를 수신해 정해진 처리를 진행한다. 아파치 카프카(Apache Kafka), 아마존 MSK, NATS, 레디스(Redis) Pub/Sub이 대표적인 게시/구독 방식의 툴이다.

 게시/구독(Pub/Sub) 방식
게시/구독(Pub/Sub) 방식

메시지 큐와 게시/구독 방식 모두 이벤트 메시지의 발행과 수신 사이 물리적 시차가 발생하지만, 그로 인한 지연시간은 한자리~두 자릿수 밀리세컨드(ms) 수준이라 준실시간(Near Real-Time) 수준으로 처리된다.


메시지 큐와 게시/구독의 결합

두 방식은 독자적으로도 사용되지만, 상호 결합해 워크플로우를 생성하기도 한다. 예를 들어 푸드코트에서 음식을 주문한다고 가정할 경우 한 사람은 한식, 한 사람은 중식으로 주문할 때 키오스크에서는 하나의 주문으로 접수된다. 주문은 하나지만 각각 한식과 중식 주방에서 해당 주문 이벤트 메시지를 수신해 음식을 만들게 되므로, 1:N 방식인 게시/구독 방식을 사용하면 된다. 각 주방에서는 주문된 순서대로 음식을 만들어야 하고, 주방에 요리사가 여러 명이더라도 한 번만 만들어야 하기에 1:1 방식인 메시지 큐 방식을 적용해야 한다. 이를 그림으로 나타내면 다음과 같다.

 메시지 큐와 게시/구독의 결합
메시지 큐와 게시/구독의 결합


이벤트를 이용한 트리거

다른 모듈이나 서비스의 행위를 유발하는 트리거로서 이벤트를 사용할 수도 있다. 예를 들어 전자상거래에서 ‘주문이 완료됨’ 이벤트 메시지가 발행되면, 이를 수신해 후속 처리를 시작할 수 있다. 재고 서비스에서는 해당 주문대로 배송하기 위해 프로세스를 시작하게 되고, 앱 푸시나 메일 서비스에서는 주문 내역을 메일과 푸시 메시지로 발송할 수 있다. 주문 완료 이벤트가 다른 서비스의 후속 처리를 트리거한 것이다.

 타 서비스의 트리거로 동작하는 예
타 서비스의 트리거로 동작하는 예


이벤트 기반 요청/응답

EDA에서는 다른 모듈이나 서비스로의 요청과 응답 시에도 이벤트 메시지를 이용한다. 요청과 응답이라고 표현은 했지만, 엄밀히 말하면 이벤트 메시지의 발행과 수신이 반복되는 것을 그렇게 표현할 뿐이다. 예를 들어 주문 서비스에서 주문 생성 이벤트 메시지가 발행되면, 배송 서비스에서는 이 이벤트 메시지를 수신해 새로운 배송 프로세스를 시작한다. 배송을 위한 첫 단계인 상품 준비가 시작되면 ‘상품 준비가 시작됨’ 이벤트가 발생하고, 이에 대한 이벤트 메시지도 발행된다. 주문 서비스에서는 이 메시지를 수신해 배송 상태를 ‘상품 준비 중’으로 업데이트할 수 있다. 상품이 발송되면 ‘배송 시작됨’ 이벤트가 발생하고, 주문 서비스가 이 이벤트 메시지를 수신해 배송 상태를 ‘배송 중’으로 업데이트할 수 있다. 이런 방식으로 메시지의 티키타카는 ‘배송 완료’까지 진행된다.

 메시지를 이용한 요청과 응답
메시지를 이용한 요청과 응답


동일한 데이터, 다른 목적으로 저장

전자상거래에서 상품 서비스의 데이터베이스에는 모든 상품정보가 저장돼 있다. 상품 중에는 품절, 판매 종료, 판매 개시 전 등의 사유로 판매가 불가한 상품들도 있기 마련이다. 외부 채널을 통해 상품을 광고할 때, 이런 판매 불가 상품들은 고객에게 부정적인 경험을 전달하기 때문에 제외돼야 한다.

광고할 상품정보들을 주기적으로 광고 채널에 보내야 하는데, 매번 모든 상품을 스캔해 판매할 수 있는 상품만 필터링해 전송하는 방식은 처리 시간도 오래 걸릴뿐더러 상품 서비스의 API에도 과부하를 줄 수 있다. 시간이 지날수록 등록 상품 수는 많아지기 때문에 처리 시간은 점점 길어진다.

전체 상품 중 판매 가능한 상품의 수보다는 판매 불가능한 상품의 비중이 더 크기 마련이다. 경험상 판매 가능한 상품은 전체의 30% 내외 수준이었다. 이런 경우에는 판매 가능 상품만 모아 별도의 데이터베이스로 저장해 이용하는 것을 고려할 만하다. 다루는 상품 수가 전체 대비 30% 수준이니 전체를 스캔하는 데 소요되는 시간도 훨씬 단축되고, 무엇보다 광고를 클릭하는 고객이 품절 상품을 보게 되는 부정적 경험을 제거할 수 있다. 상품의 상태가 변해 이벤트 메시지가 발행되면, 소비자는 ‘판매 가능한 상품’만 저장하면 된다. 만약 판매 불가능한 상태로 변경된 이벤트 메시지가 수신되면 해당 상품을 데이터베이스에서 삭제해 항상 ‘판매 가능한 상품’만 유지할 수 있다.

 동일 데이터를 다른 목적으로 저장
동일 데이터를 다른 목적으로 저장


여러 서비스의 데이터를 한 곳에 저장 (CQRS 패턴)

회원제 서비스의 마이페이지나 전자상거래의 주문 목록 서비스는 여러 정보를 종합적으로 보여주는 것이 일반적이다. API 호출을 통해 각 정보를 가져오는 방법도 있지만, 이 경우 필요한 정보의 수가 많을수록 API 호출에 따른 지연시간도 길어지게 돼 고객은 ‘느린’ 페이지를 경험하게 된다.

이를 극복할 수 있는 방법이 CQRS(Command Query Responsibility Segregation) 패턴이다. 상태 변화를 수반하는 이벤트는 각 서비스에서 발생하고(Command), 발행된 이벤트 메시지들을 수신해 주문 상세 문서로 만들어 보여주는(Query) 방식이다. 각 서비스의 이벤트 메시지들은 문서의 조각인 셈이고, 조각이 덜 모여서 보여줄 수 없을 때는 유효하지 않은 상태(Invalid)가 된다. 문서를 보여줄 수 있을 만큼 조각들이 모이면 문서는 유효한 상태(Valid)가 되어 주문 상세 페이지에 노출된다.

주문 상세 정보를 회원의 식별키와 시간 순서대로 저장하면 매우 빠르게 조회(Query)할 수 있다. 통상적으로 이러한 문서 형태의 정보는 관계형 데이터베이스보다는 아마존 도큐먼트DB(Amazon DocumentDB)나 몽고DB(MongoDB) 같은 NoSQL 타입의 데이터베이스에 저장하는 것이 효율적이다.

 CQRS 패턴
CQRS 패턴


이벤트를 이용한 워크플로우

회원제 사이트에는 회원이 탈퇴하거나 휴면상태가 되면 해당 회원의 정보를 별도의 저장 공간으로 옮겨야 하는 법적 규제가 적용된다. 아래 그림처럼 주문 데이터베이스에는 회원의 식별자를 포함하고 있지만 마케팅을 위한 주문 데이터베이스에는 회원 정보가 없다고 가정했을 때, 특정 회원이 휴면상태로 변경되면 회원 식별자를 포함한 이벤트 메시지가 발행되고, 주문 서비스에서는 이를 수신해 해당 회원 정보를 별도의 데이터베이스로 이전한다. 주문 데이터의 이전이 완료되면 주문 식별자를 포함한 이벤트 메시지가 발행된다. 마케팅 주문 서비스에서는 이 메시지를 수신해 관련 주문 정보를 모두 별도의 데이터베이스로 이전한다.

 휴면회원 처리 프로세스
휴면회원 처리 프로세스


롱텀 트랜잭션의 처리 (SAGA 패턴)

워크플로우는 순차적일 수도 있고 병렬적일 수도 있는데, 여기서 중요한 것은 바로 ‘최종적 일관성(Eventual Consistency)’이다. 전자상거래에서 주문이 완료되는 시점부터 배송이 완료되는 시점 사이에는 시간의 간격이 길고(Long-Term), 여러 서비스가 순차적 또는 병렬적으로 체인을 이룰 수 있다. 중간 과정에서 문제가 발생했을 경우를 위한 자동화된 대처도 필요하다. 서비스 개별 입장에서 보면 이벤트 메시지를 수신해 처리하고 자신의 상태 변화를 이벤트 메시지로 발행하는 것으로 끝나지만, 전체를 조망하면 아래 그림과 같이 마치 하나의 긴 플로우처럼 간주될 수 있다.

리워드 서비스의 ‘적립금 지급됨’ 이벤트 메시지를 주문 서비스에서 수신하면 비로소 하나의 플로우가 끝난다. 만약 이 과정에서 장애나 오류가 발생하면, 롤백이 아닌 그에 따른 보정 메시지(빨간색, Compensation)를 발행한다. 후행 서비스가 발행한 보정 이벤트 메시지를 선행 서비스가 수신하면 적절한 보정 처리를 하게 된다. 이 과정은 상황에 따라 맨 앞단까지 연쇄적으로 수행될 수 있다. 이를 코레오그래피 사가(Choreography SAGA) 패턴이라고 한다.

 코레오그래피 사가의 예
코레오그래피 사가의 예

코레오그래피 사가 패턴에서는 전체적인 플로우를 논리적으로 표현할 순 있지만, 이 플로우를 주관하는 실체가 없는 것이 특징이다. 이러한 부분이 불편하다면 아래와 같은 오케스트레이션 사가(Orchestration SAGA) 패턴도 고려해 볼 만하다.

오케스트레이터 컴포넌트는 워크플로우에 맞춰 이벤트 메시지를 관리하고, 각 서비스에서 발행된 이벤트 메시지는 회신 채널을 통해 오케스트레이터로 전달된다. 주문이 생성되면 오케스트레이터는 이벤트 메시지를 발행하고, 결제 서비스에서는 해당 메시지를 수신한다. 결제 처리가 완료되면 회신(Reply) 채널에 이벤트 메시지를 발행하고, 오케스트레이터는 다음 단계인 배송 서비스를 위한 이벤트 메시지를 발행한다. 오케스트레이터가 적립금 서비스의 이벤트 메시지를 수신하면 비로소 긴 트랜잭션이 완료된다.

 오케스트레이션 사가 패턴의 예
오케스트레이션 사가 패턴의 예


반드시 고려해야 할 부분들

EDA는 모듈이나 서비스 간에 강한 결합을 없애거나 느슨하게 해주는 좋은 아키텍처다. 하지만 메시지 라우터 유지·보수 및 높아지는 복잡도 등의 고려사항도 다수 존재한다.

① 생산자나 라우터의 문제로 동일한 이벤트 메시지가 여러 번 발행될 수 있음을 가정해야 한다. 따라서 동일한 이벤트 메시지에 대해 소비자는 한 번만 처리되도록 해야 한다. 이를 멱등성(Idempotence)이라고 하는데, 이벤트마다 식별자나 버전을 부여해 해결할 수 있다.
② 이벤트 메시지의 발행 속도가 소비 속도보다 더 빠른 경우도 대비해야 한다. 가장 쉬운 방법은 소비자의 인스턴스를 늘리거나 줄이는 스케일 인-아웃 전략을 적용하는 것이다. 만약 이 전략을 적용할 수 없는 경우에는 생산자의 이벤트 발생 속도를 조절할 수 있는 방안도 마련해 두는 것이 좋다.
③ 버그, 결함, 일시적 서버 장애 등 여러 원인으로 이벤트 메시지 처리에 실패하는 경우도 대비해야 한다. 실패한 이벤트 메시지(Dead Letter)를 재처리하거나 그에 맞는 로직을 마련하는 것도 좋은 방법이다.
④ 이벤트 메시지가 잘 소비되고 있는지도 상시로 모니터링돼야 한다. 만약 이벤트 메시지는 발행되고 있는데 소비가 없다면 이는 장애 상황일 수 있다.
⑤ 이벤트의 원본을 일정 기간 저장하는 것도 고려해야 한다. 아파치 카프카와 같은 툴은 자체적으로 메시지를 일정 기간 보관하는 기능이 있다. 따라서 소비자들은 메시지를 재처리하거나 특정 과거 시점부터 다시 수신할 수 있다. 이러한 툴이 아니더라도 파일이나 RDBMS 등에 이벤트 내용 원본을 저장하는 방식도 많이 사용되며, 이러한 저장소를 이벤트 스토어라 부르기도 한다.


결론

이벤트 기반 아키텍처는 마이크로서비스 아키텍처(Microservice Architecture, MSA)와 같은 최신 아키텍처와 함께 거론되곤 한다. 하지만 EDA를 꼭 MSA에서만 적용할 수 있는 것은 아니다. 모놀리스(Monolith) 아키텍처에서도 모듈 간의 의존성을 제거하고 느슨한 결합으로 만들기 위해 적용할 수 있다. 일단 느슨한 결합의 아키텍처로 재편되면, 해당 모듈들을 별도 서비스로 분리하기 쉬워지고, 독립적 유지·보수와 배포가 가능해지게 된다.

EDA는 처음부터 전체 시스템에 전면 적용하기보다는 비즈니스 임팩트가 적은 영역부터 점진적으로 적용하는 것을 권장한다. 점진적 적용을 반복하는 과정에서 다양한 시행착오를 겪게 될 것이고, 이는 곧 내부의 EDA 역량을 성장시킬 수 있는 밑거름이 될 것이다. EDA는 본문에 기술된 패턴들 외에도 이미 수많은 패턴이 존재하니, 비즈니스에 적합한 패턴을 찾아 적용하면 보다 수월하고 신속한 적용이 가능할 것이다.

저작권자 © 아이티데일리 무단전재 및 재배포 금지