Lambda로 이벤트 기반 아키텍처 설계하기
Lambda로 이벤트 기반 아키텍처 설계하기 (Creating event-driven architectures with Lambda)
이벤트(event)는 Lambda 함수를 실행하게 만드는 모든 것을 말해요. 이벤트는 크게 직접 호출(push) 과 이벤트 소스 매핑(pull) 두 가지 방식으로 Lambda 함수를 트리거해요. Lambda 함수에 전달되는 이벤트는 JSON 형식이며, 생성한 서비스와 이벤트 유형에 따라 구조가 달라져요. Lambda 함수는 최대 15분까지 실행될 수 있지만, 사실 1초 이하의 짧은 호출에 가장 잘 맞아요. 특히 이벤트 기반 아키텍처에서는 각 Lambda 함수를 좁은 영역의 특정 작업을 담당하는 하나의 마이크로서비스로 다뤄요.
본문
이벤트 기반 아키텍처는 시스템 간 통신을 네트워크로 처리하기 때문에 변동적인 지연(latency)이 생겨요. 고주파 트레이딩처럼 초저지연이 필요한 워크로드에는 부적합할 수 있지만, 확장성과 가용성이 뛰어난 워크로드나 트래픽 패턴을 예측하기 어려운 워크로드에는 효과적인 선택이에요.
이벤트 기반 아키텍처의 장점
Lambda는 이벤트 기반 아키텍처에서 두 가지 호출 방식을 지원해요.
- 직접 호출(push 방식): AWS 서비스가 Lambda 함수를 직접 트리거해요. 예를 들면 Amazon S3가 파일 업로드 시 함수를 트리거하고, API Gateway가 HTTP 요청을 받으면 함수를 트리거해요.
- 이벤트 소스 매핑(pull 방식): Lambda가 이벤트를 가져와서 함수를 호출해요. 예를 들면 Lambda가 Amazon SQS 큐에서 메시지를 가져와 함수를 호출하고, DynamoDB 스트림에서 레코드를 읽어 함수를 호출해요.
폴링·웹훅을 이벤트로 대체하기
전통적인 아키텍처는 컴포넌트 간 상태를 전달하는 데 폴링(polling)과 웹훅(webhook) 메커니즘을 주로 사용해요. 폴링은 새 데이터가 생기고 나서 다운스트림 서비스와 동기화되기까지 시차가 있어 비효율적일 수 있고, 웹훅은 통합하려는 서비스가 항상 지원하지 않거나 별도의 인증·인가 설정이 필요할 수 있어요. 둘 다 개발팀이 추가로 작업하지 않으면 온디맨드 확장이 어려워요.
이 두 메커니즘 모두 이벤트로 대체할 수 있는데, 이벤트는 필터링·라우팅·다운스트림 소비 마이크로서비스로의 push가 가능해요. 이 방식은 대역폭 소비, CPU 사용률, 가격을 줄일 수 있고, 기능 단위가 작아 코드가 적어지므로 복잡도도 낮아져요.
이벤트 기반 아키텍처는 배치 처리에서 벗어나 근실시간(near-real-time) 시스템을 설계하기도 쉬워요. 이벤트는 애플리케이션 상태가 바뀌는 시점에 생성되므로, 마이크로서비스의 코드는 단일 이벤트 처리를 담당하도록 설계하면 돼요. 확장은 Lambda 서비스가 처리하므로 커스텀 코드를 바꾸지 않고도 트래픽 급증을 소화할 수 있어요. 이벤트가 늘어나면 그것을 처리하는 컴퓨팅 계층도 함께 늘어나요.
복잡도 줄이기
마이크로서비스를 쓰면 복잡한 워크플로를 단순화할 수 있어요. 예를 들어 이커머스 모놀리스는 주문 접수, 결제, 재고, 출고, 회계 서비스로 쪼갤 수 있죠. 모놀리스에서는 관리·오케스트레이션이 복잡했던 일이, 이벤트로 비동기 통신하는 일련의 분리된 서비스가 되어요.
이 방식은 처리 속도가 다른 서비스들을 함께 조립할 수도 있게 해줘요. 주문 접수 마이크로서비스는 Amazon SQS 큐에 메시지를 버퍼링해 대량의 들어오는 주문을 저장할 수 있고, 결제 처리는 보통 더 느리기 때문에 SQS 큐에서 꾸준히 메시지를 가져오며, AWS Step Functions로 복잡한 재시도·오류 처리 로직을 오케스트레이션하고 수십만 건의 주문에 대한 결제 워크플로를 조율할 수 있어요.
대안적 접근: 표준 프로그래밍 언어로 오케스트레이션하려면 Lambda durable functions를 사용할 수 있어요. durable functions는 자동 체크포인트와 재시도를 갖춘 코드로 주문 접수, 결제 처리, 알림 로직을 작성하게 해줘요. 워크플로가 주로 Lambda 함수로 이뤄지고 오케스트레이션 로직을 코드로 유지하고 싶다면 이 접근이 잘 맞아요.
확장성·확장성 개선하기
마이크로서비스는 생성한 이벤트를 Amazon SNS·Amazon SQS 같은 메시징 서비스에 게시하는데, 이는 마이크로서비스 사이의 탄력적인 버퍼 역할을 하며 트래픽 증가 시 확장을 돕고, 규칙에 따라 Amazon EventBridge가 이벤트 내용을 필터링·라우팅해요. 그래서 이벤트 기반 애플리케이션은 모놀리식 애플리케이션보다 확장성이 뛰어나고 중복성도 높아요.
이 시스템은 확장성(extensibility)도 높아서, 다른 팀이 주문·결제 처리 마이크로서비스에 영향을 주지 않고 기능을 추가할 수 있어요. EventBridge로 이벤트를 게시하면 재고 마이크로서비스 같은 기존 시스템과 통합되고, 앞으로 생길 어떤 애플리케이션도 이벤트 소비자로 통합될 수 있어요. 이벤트 생산자는 소비자를 알 필요가 없어서 마이크로서비스 로직을 단순화해 줘요.
이벤트 기반 아키텍처의 트레이드오프
변동적 지연
이벤트 기반 애플리케이션은 네트워크를 통해 통신하므로 변동적인 지연이 생겨요. 지연을 최소화하도록 설계할 수는 있지만, 모놀리식 애플리케이션은 확장성과 가용성을 희생해서라도 더 낮은 지연으로 최적화할 수 있는 경우가 거의 항상 있어요. 은행의 고주파 트레이딩, 창고의 서브밀리초 로봇 자동화처럼 일관된 저지연이 필요한 워크로드는 이벤트 기반 아키텍처에 적합하지 않아요.
최종적 일관성
이벤트는 상태 변화를 나타내며, 한 시점에 아키텍처의 여러 서비스를 흐르는 많은 이벤트가 있으므로 이런 워크로드는 대개 최종적으로 일관적(eventually consistent) 이에요. 이는 트랜잭션 처리, 중복 처리, 시스템의 정확한 전체 상태 판단을 더 복잡하게 만들어요.
강한 일관성이 필요한 워크로드에는 이를 지원하는 아키텍처 패턴이 있어요.
- DynamoDB는 강한 일관성 읽기(strongly consistent reads) 를 제공할 수 있는데, 때로는 기본 모드보다 높은 지연과 더 많은 처리량을 소비해요. DynamoDB는 데이터 일관성 유지를 돕는 트랜잭션도 지원해요.
- ACID 속성이 필요한 기능에는 Amazon RDS를 쓸 수 있지만, 관계형 데이터베이스는 DynamoDB 같은 NoSQL 데이터베이스보다 확장성이 낮은 편이에요. Amazon RDS Proxy는 Lambda 함수 같은 임시 소비자들의 연결 풀링과 확장을 관리하는 데 도움을 줘요.
이벤트 기반 아키텍처는 큰 데이터 배치보다 개별 이벤트 중심으로 설계돼요. 일반적으로 여러 이벤트를 동시에 다루기보다 개별 이벤트나 실행 흐름의 단계를 관리하도록 워크플로를 설계해요. 서버리스에서는 배치 처리보다 실시간 이벤트 처리를 선호해요. 배치는 더 작고 많은 증분 업데이트로 대체해야 해요. 이는 워크로드를 더 가용·확장 가능하게 하지만, 이벤트가 다른 이벤트를 인식하기는 더 어렵게 만들어요.
호출자에게 값 반환하기
대부분의 경우 이벤트 기반 애플리케이션은 비동기적이라 호출자 서비스는 다른 서비스의 요청을 기다리지 않고 계속 진행해요. 이는 확장성과 유연성을 가능하게 하는 핵심 특성이지만, 반환 값이나 워크플로 결과를 전달하는 것은 동기 실행 흐름보다 복잡해요.
프로덕션 시스템의 대부분 Lambda 호출은 Amazon S3·Amazon SQS 같은 서비스의 이벤트에 응답하는 비동기 호출이에요. 이런 경우 이벤트 처리 성공·실패가 값 반환보다 더 중요할 때가 많아요. Lambda의 데드 레터 큐(DLQ) 같은 기능은 호출자에게 알리지 않고도 실패한 이벤트를 식별·재시도할 수 있게 해줘요.
서비스·함수 간 디버깅
이벤트 기반 시스템 디버깅은 모놀리식 애플리케이션과 달라요. 여러 시스템·서비스가 이벤트를 주고받으므로 오류 발생 시 여러 서비스의 정확한 상태를 기록·재현할 수 없어요. 각 서비스·함수 호출이 별도의 로그 파일을 가지므로, 오류를 일으킨 특정 이벤트에 무슨 일이 있었는지 파악하기가 더 복잡해져요.
이벤트 기반 시스템에서 성공적인 디버깅 접근법을 구축하려면 세 가지가 중요해요. 첫째는 견고한 로깅 시스템으로, AWS 서비스 전반과 Lambda 함수에 Amazon CloudWatch가 제공해요. 둘째는 모든 이벤트에 각 트랜잭션 단계마다 기록되는 트랜잭션 식별자를 갖추는 것이에요. 마지막으로 AWS X-Ray 같은 디버깅·모니터링 서비스로 로그 파싱·분석을 자동화하는 것을 적극 권장해요. X-Ray는 여러 Lambda 호출과 서비스에 걸친 로그를 소비해 문제의 근본 원인을 찾기 훨씬 쉽게 만들어 줘요.
Lambda 기반 이벤트 기반 애플리케이션의 안티패턴
Lambda로 이벤트 기반 아키텍처를 구축할 때 다음의 흔한 안티패턴은 피하세요. 동작은 하지만 비용과 복잡도를 높여요.
Lambda 모놀리스
전통 서버(Amazon EC2 인스턴스, Elastic Beanstalk 애플리케이션)에서 마이그레이션한 애플리케이션은 기존 코드를 그대로 옮기는 '리프트 앤 시프트'를 하는 경우가 많아요. 이렇게 하면 모든 이벤트에 트리거되는, 모든 애플리케이션 로직을 담은 단일 Lambda 함수가 생기기 쉬워요. 이 접근의 단점은 다음과 같아요.
- 패키지 크기: 모든 경로의 코드를 담아 함수가 훨씬 커져 Lambda 서비스가 실행하기에 느려져요.
- 최소 권한 적용 어려움: 모든 경로에 필요한 모든 리소스에 대한 권한을 실행 역할이 허용해야 해서 권한이 매우 광범위해져요. 보안 문제가 되죠. 모놀리스의 많은 경로는 부여된 권한을 다 쓸 필요가 없어요.
- 업그레이드 어려움: 프로덕션에서 단일 함수 업그레이드는 위험하고 전체 애플리케이션을 깨뜨릴 수 있어요. 함수의 한 경로를 업그레이드하는 것이 곧 전체 함수 업그레이드예요.
- 유지보수 어려움: 모놀리식 코드 저장소라 여러 개발자가 함께 작업하기 어렵고, 개발자의 인지 부담이 늘며 적절한 테스트 커버리지를 만들기 어려워요.
- 코드 재사용 어려움: 재사용 가능한 라이브러리를 모놀리스에서 분리하기 어려워 코드 재사용이 힘들어요. 프로젝트가 늘수록 코드 지원과 팀 속도 확장이 어려워져요.
- 테스트 어려움: 코드 줄 수가 늘면 코드베이스의 모든 입력 조합과 진입점을 단위 테스트하기 어려워져요. 코드가 적은 작은 서비스는 단위 테스트 구현이 보통 더 쉬워요.
선호하는 대안은 모놀리식 Lambda 함수를 개별 마이크로서비스로 쪼개고, 단일 Lambda 함수를 하나의 잘 정의된 작업에 매핑하는 것이에요. API 엔드포인트가 몇 개인 간단한 웹 애플리케이션에서는 결과적인 마이크로서비스 기반 아키텍처가 API Gateway 라우트를 기준으로 설계될 수 있어요.
폭주하는 Lambda 함수를 만드는 재귀 패턴
AWS 서비스가 이벤트를 생성해 Lambda 함수를 호출하고, Lambda 함수는 AWS 서비스에 메시지를 보낼 수 있어요. 일반적으로 Lambda 함수를 호출하는 서비스·리소스는 함수가 출력하는 서비스·리소스와 달라야 해요. 이걸 관리하지 않으면 무한 루프가 생길 수 있어요.
예를 들어 Lambda 함수가 Amazon S3 객체를 쓰고, put 이벤트로 같은 Lambda 함수가 다시 호출되는 경우예요. 호출로 버킷에 두 번째 객체가 쓰여지고, 그것이 다시 같은 Lambda 함수를 호출해요. 무한 루프 가능성은 대부분의 프로그래밍 언어에 존재하지만, 서버리스 애플리케이션에서는 리소스를 더 소비할 위험이 있어요. Lambda와 Amazon S3 모두 트래픽에 따라 자동으로 확장되므로, 루프가 Lambda를 확장시켜 모든 가용 동시성을 소비하고 Amazon S3는 계속 객체를 쓰며 Lambda용 이벤트를 생성하게 돼요. 이 예시는 S3지만 재귀 루프 위험은 Amazon SNS, Amazon SQS, DynamoDB 등 다른 서비스에도 존재해요. 재귀 루프 감지(recursive loop detection) 를 사용해 이 안티패턴을 찾고 피할 수 있어요.
Lambda 함수가 Lambda 함수 호출하기
함수는 캡슐화와 코드 재사용을 가능하게 해요. 대부분의 프로그래밍 언어는 코드베이스 안에서 함수를 동기 호출하는 개념을 지원해요. 이 경우 호출자는 함수가 응답을 반환할 때까지 기다려요.
참고
Lambda 함수가 다른 Lambda 함수를 직접 호출하는 것은 비용·복잡도 때문에 일반적으로 안티패턴이지만, 다단계 워크플로를 오케스트레이션하기 위해 다른 함수를 호출하도록 특별히 설계된 durable functions에는 해당되지 않아요.
전통 서버·가상 인스턴스에서는 OS 스케줄러가 다른 가용 작업으로 전환해요. CPU가 0%든 100%든 서버를 소유·운영하는 고정 비용을 지불하므로 전체 비용에 영향을 주지 않아요.
하지만 이 모델은 서버리스 개발에는 잘 맞지 않아요. 주문을 처리하는 세 개의 Lambda 함수로 이뤄진 간단한 이커머스 애플리케이션을 생각해 보면, Create order 함수가 Process payment 함수를 호출하고, 그것이 다시 Create invoice 함수를 호출하는 구조예요. 서버의 단일 애플리케이션에서는 동작할 수 있지만 분산 서버리스 아키텍처에서는 몇 가지 피할 수 있는 문제가 생겨요.
- 비용: Lambda는 호출 기간(duration)에 대해 비용을 지불해요. Create invoice 함수가 실행되는 동안 다른 두 함수도 대기 상태로 실행되고 있어요.
- 오류 처리: 중첩 호출에서는 오류 처리가 훨씬 복잡해져요. 예를 들어 Create invoice의 오류가 Process payment에서 결제를 취소하도록 하거나, Create invoice를 재시도하도록 할 수 있어요.
- 밀접 결합(tight coupling): 결제 처리는 보통 인보이스 생성보다 오래 걸려요. 이 모델에서는 전체 워크플로의 가용성이 가장 느린 함수에 의해 제한돼요.
- 확장: 세 함수의 동시성(concurrency)은 같아야 해요. 바쁜 시스템에서는 필요 이상의 동시성을 소비해요.
서버리스 애플리케이션에서 이 패턴을 피하는 두 가지 흔한 접근이 있어요. 첫째는 Lambda 함수 사이에 Amazon SQS 큐를 두는 것이에요. 다운스트림이 업스트림보다 느리면 큐가 메시지를 튼튼하게 유지하고 두 함수를 분리해요. 예시에서 Create order 함수는 Amazon SQS 큐에 메시지를 게시하고, Process payment 함수가 큐에서 메시지를 소비해요. 둘째는 AWS Step Functions를 사용하는 것이에요. 여러 유형의 실패·재시도 로직이 있는 복잡한 프로세스라면 Step Functions가 워크플로 오케스트레이션에 필요한 커스텀 코드 양을 줄여줘요. Step Functions가 작업을 오케스트레이션하고 오류·재시도를 견고하게 처리하며, Lambda 함수는 비즈니스 로직만 담아요.
단일 Lambda 함수 안에서의 동기적 대기
잠재적으로 동시에 수행될 수 있는 활동을 단일 Lambda 함수 안에서 동기적으로 예약하지 않도록 하세요. 예를 들어 Lambda 함수가 S3 버킷에 쓴 다음 DynamoDB 테이블에 쓰는 경우예요. 이 설계에서는 활동이 순차적이므로 대기 시간이 누적돼요. 두 번째 작업이 첫 번째 작업의 완료에 의존하는 경우, 두 개의 별도 Lambda 함수를 사용해 총 대기 시간과 실행 비용을 줄일 수 있어요. 첫 번째 Lambda 함수는 Amazon S3 버킷에 객체를 넣은 직후 응답하고, S3 서비스가 두 번째 Lambda 함수를 호출해 DynamoDB 테이블에 데이터를 씁니다. 이렇게 하면 Lambda 함수 실행의 총 대기 시간을 최소화할 수 있어요.