EDA + 서버리스로 구축하는 AI 파이프라인: AWS Lambda, EventBridge, RAG부터 실시간 이상 감지까지
솔직히 말하면, 처음 AI 파이프라인을 설계할 때 저도 "그냥 API 서버 하나 올리면 되지 않나?"라는 생각을 했습니다. 요청이 들어오면 전처리하고, 임베딩 만들고, 모델 추론하고, 결과 반환하는 흐름—단순해 보였거든요. 그런데 실제로 운영해보면 이야기가 달라집니다. 트래픽이 예측 불가능하게 몰리고, 임베딩 생성이 병목이 되고, 한 단계의 지연이 전체를 블로킹하는 상황이 생깁니다. AI 워크로드는 본질적으로 버스티(bursty) 하고, 단계별로 필요한 자원이 극단적으로 다릅니다.
EDA(이벤트 기반 아키텍처)와 서버리스를 조합한 AI 파이프라인은 이 문제를 정면으로 다룹니다. 각 처리 단계를 이벤트로 느슨하게 연결하고, AWS Lambda 같은 서버리스 함수가 이벤트를 트리거로 자동 확장되는 구조입니다. AWS가 EDA를 "서버리스 AI의 백본"으로 공식 명명할 만큼(AWS Prescriptive Guidance), 이 패턴은 이제 실험적 아이디어가 아닌 업계 표준에 가까워지고 있습니다.
이 글을 읽고 나면 기존 AI API 서버의 임베딩 단계를 이벤트로 분리하는 PR을 당일 안에 열 수 있습니다. EventBridge + Lambda로 RAG 파이프라인을 구성하는 방법, Kafka로 실시간 이상 감지를 구현하는 코드, Step Functions로 멀티 에이전트를 오케스트레이션하는 패턴을 직접 코드로 풀어봅니다.
핵심 개념
EDA: 컴포넌트를 이벤트로 연결한다는 것
전통적인 아키텍처에서는 서비스들이 서로 직접 호출합니다. A가 B를 알고, B가 C를 알아야 하는 구조이죠. 서비스가 N개면 연결 복잡도는 O(N²)으로 폭발합니다. 팀에서 누군가 새 서비스를 추가할 때마다 슬랙에서 "저 API 명세 어디 있어요?", "이 호출은 누구한테 해야 해요?" 물어봐야 하는 그 상황입니다. EDA는 이 문제를 이벤트 브로커를 중심에 두는 방식으로 해결합니다. 생산자는 이벤트를 브로커에 발행하고, 소비자는 관심 있는 이벤트만 구독합니다. 복잡도가 O(N)으로 줄어들고, 각 컴포넌트는 서로의 존재를 알 필요가 없어집니다.
느슨한 결합(loose coupling): 컴포넌트들이 서로 직접 의존하지 않고 이벤트라는 계약만 공유하는 설계입니다. 한 컴포넌트를 교체하거나 확장해도 다른 컴포넌트에 영향이 없습니다.
AI 파이프라인에 이 원칙을 적용하면 각 처리 단계—전처리, 임베딩, 추론, 후처리—가 독립적인 함수로 분리됩니다. 임베딩 생성 로직을 바꾸고 싶으면 해당 함수만 교체하면 됩니다. 추론 모델을 업그레이드해도 앞뒤 단계는 영향받지 않습니다.
서버리스: 인프라가 아닌 로직에 집중
서버리스의 핵심은 "이벤트가 왔을 때만 실행되고, 실행한 만큼만 비용이 발생한다"는 것입니다. AI 추론처럼 CPU/GPU를 집중적으로 사용하다가 한동안 조용한 워크로드에 특히 잘 맞습니다. 상시 떠 있는 서버에 비용을 내는 대신, 실제로 트랜잭션이 있을 때만 자원이 소비됩니다.
EDA의 핵심 강점 중 하나는 fan-out입니다. 하나의 이벤트가 여러 소비자에게 동시에 전달될 수 있어서, 문서 업로드 이벤트 하나가 임베딩 생성과 메타데이터 추출을 동시에 트리거하는 게 가능합니다. 처리 실패 시에는 DLQ(Dead Letter Queue)에 이벤트가 보관되어 나중에 재처리할 수 있다는 점도 운영 안정성에 크게 기여합니다.
외부 이벤트
└─► 이벤트 브로커 (Kafka / EventBridge)
├─► Lambda: 전처리
│ └─► 이벤트 발행 (fan-out)
│ ├─► Lambda: AI 추론 A
│ └─► Lambda: AI 추론 B
│ └─► 이벤트 발행
│ └─► Lambda: 후처리 / 액션
└─► DLQ: 실패 이벤트 보관 및 재처리EDA+Serverless가 AI에 자연스럽게 맞는 이유
제가 처음 이 조합을 써봤을 때 가장 인상적이었던 부분은, AI 워크로드의 특성이 이 패턴과 거의 완벽하게 맞아떨어진다는 점이었습니다. 임베딩 생성 단계는 CPU 집약적이고, 추론 단계는 GPU가 필요하고, 후처리는 또 가볍습니다. 이 세 단계를 하나의 서버에 묶어두면 가장 무거운 단계에 맞춰 리소스를 프로비저닝해야 하거든요.
| AI 워크로드 특성 | EDA+Serverless의 대응 |
|---|---|
| 버스티한 요청 패턴 | 이벤트 큐가 버퍼 역할, 서버리스가 자동 확장 |
| 단계별 극단적 자원 차이 | 각 단계를 독립 함수로 분리해 개별 스케일링 |
| 모델·로직의 잦은 교체 | 느슨한 결합으로 특정 단계만 교체 가능 |
| 비동기 처리 선호 | EDA의 비동기 통신과 자연스럽게 일치 |
실전 적용
예시 1: RAG 파이프라인 — 문서 업로드부터 LLM 응답까지
RAG(Retrieval-Augmented Generation)는 EDA+Serverless와 가장 잘 맞는 사례 중 하나입니다. 문서 파싱, 청킹, 임베딩 생성, 벡터 DB 저장, 검색, 추론이 각각 독립적으로 스케일링되어야 하기 때문이죠.
아래 코드에서 event['Records'][0]['s3']는 S3에 파일이 업로드될 때 Lambda가 자동으로 받는 트리거 페이로드입니다. S3 이벤트 알림이 Lambda를 직접 호출하고, 그 호출 데이터 안에 버킷 이름과 객체 키가 담겨 있습니다.
# Lambda: 문서 파싱 & 청킹 (전처리 단계)
import boto3
import json
def handler(event, context):
s3 = boto3.client('s3')
eventbridge = boto3.client('events')
record = event['Records'][0]['s3']
bucket = record['bucket']['name']
key = record['object']['key']
try:
obj = s3.get_object(Bucket=bucket, Key=key)
text = obj['Body'].read().decode('utf-8')
chunks = chunk_text(text, chunk_size=512, overlap=64)
eventbridge.put_events(Entries=[{
'Source': 'rag.preprocessing',
'DetailType': 'DocumentChunked',
'EventBusName': 'rag-pipeline-bus', # 프로덕션에서는 커스텀 버스 지정 필수
'Detail': json.dumps({
'document_id': key,
'chunks': chunks,
'total_chunks': len(chunks)
})
}])
return {'statusCode': 200, 'chunksCount': len(chunks)}
except Exception as e:
print(f"처리 실패: {e}")
raise # 예외를 다시 던져 SQS/EventBridge 재시도 트리거
def chunk_text(text: str, chunk_size: int, overlap: int) -> list[str]:
# 실제 운영에서는 tiktoken 같은 토크나이저 기반 청킹을 권장합니다
# 단어 기반 분할은 한국어 등 CJK 문자에서 토큰 수 예측이 어렵습니다
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = ' '.join(words[i:i + chunk_size])
chunks.append(chunk)
return chunks# Lambda: 임베딩 생성 & 벡터 DB 저장
import boto3
import json
bedrock = boto3.client('bedrock-runtime')
def handler(event, context):
detail = json.loads(event['detail'])
document_id = detail['document_id']
chunks = detail['chunks']
try:
embeddings = []
for chunk in chunks:
response = bedrock.invoke_model(
modelId='amazon.titan-embed-text-v2:0',
body=json.dumps({'inputText': chunk})
)
embedding = json.loads(response['body'].read())['embedding']
embeddings.append({
'chunk_text': chunk,
'embedding': embedding,
'document_id': document_id
})
# OpenSearch 배치 인덱싱 — opensearchpy bulk API로 구현
batch_index_vectors(embeddings)
return {'indexed': len(embeddings)}
except Exception as e:
print(f"임베딩 실패: {e}")
raise
def batch_index_vectors(embeddings: list) -> None:
# opensearchpy 클라이언트로 벡터 인덱싱 — 구현 생략
pass| 코드 포인트 | 설명 |
|---|---|
EventBusName 명시 |
생략 시 default 버스로 전송됩니다. 프로덕션에서는 커스텀 버스 분리를 권장합니다 |
raise 재전파 |
Lambda에서 예외를 삼키면 이벤트가 성공 처리된 것으로 간주됩니다. 재시도를 위해 반드시 재전파해야 합니다 |
| 청킹 함수 분리 | 임베딩 단계가 독립적으로 재시도·확장될 수 있습니다 |
bedrock.invoke_model() |
서버리스 LLM API 직접 호출로 GPU 인프라 관리가 불필요합니다 |
예시 2: 실시간 이상 감지 — 트랜잭션에서 경고까지 수백 밀리초
다음은 배치가 아닌 실시간 스트리밍 시나리오입니다. 실무에서 자주 맞닥뜨리는 상황인데, 하루치 트랜잭션을 배치로 분석하던 팀이 실시간 감지로 전환하는 케이스입니다. Kafka + Lambda + SageMaker 조합으로 기존 수 시간의 탐지 지연을 수백 밀리초 수준으로 줄일 수 있습니다.
# Lambda: Kafka 트리거 — 특징 추출 및 추론
import boto3
import json
import base64
sagemaker = boto3.client('sagemaker-runtime')
eventbridge = boto3.client('events')
def handler(event, context):
results = []
for record in event['records']['transaction-topic-0']:
try:
payload = json.loads(base64.b64decode(record['value']))
features = extract_features(payload)
# 레코드 수가 많다면 배치 예측 엔드포인트나 asyncio 병렬 처리 검토를 권장합니다
# 동기 루프는 레코드 수에 비례해 처리 시간이 선형 증가합니다
response = sagemaker.invoke_endpoint(
EndpointName='fraud-detection-serverless',
ContentType='application/json',
Body=json.dumps({'instances': [features]})
)
prediction = json.loads(response['Body'].read())
fraud_score = prediction['predictions'][0]['score']
if fraud_score > 0.85:
eventbridge.put_events(Entries=[{
'Source': 'fraud.detection',
'DetailType': 'HighRiskTransaction',
'EventBusName': 'fraud-detection-bus',
'Detail': json.dumps({
'transaction_id': payload['id'],
'fraud_score': fraud_score,
'amount': payload['amount']
})
}])
results.append({'transaction_id': payload['id'], 'score': fraud_score})
except Exception as e:
print(f"레코드 처리 실패: {e}")
continue # 개별 레코드 실패가 전체 배치를 막지 않도록
return results
def extract_features(transaction: dict) -> list[float]:
return [
transaction['amount'],
transaction['merchant_category_code'],
transaction['hour_of_day'],
transaction['is_international'],
transaction['velocity_1h'],
]멱등성(idempotency): 같은 이벤트가 두 번 처리되어도 결과가 동일한 성질입니다. Kafka는 at-least-once 전달을 보장하므로 같은 메시지가 두 번 처리될 수 있습니다.
transaction_id같은 고유 키로 중복 처리를 방지하는 로직이 반드시 필요합니다.
예시 3: AI 에이전트 오케스트레이션 — 병렬 에이전트 패턴
에이전트 간 조율이 복잡해지면 코드로 오케스트레이션하기보다 Step Functions에 상태를 맡기는 편이 낫습니다. 저도 처음엔 Lambda끼리 직접 호출로 에이전트를 연결했는데, 에러 재시도·타임아웃·중간 결과 저장을 각 함수에서 직접 관리하다 보니 코드가 금세 지저분해지더군요. Step Functions는 이 상태 관리를 선언적으로 분리해줘서 에이전트 Lambda는 완전히 무상태로 유지할 수 있습니다.
사용자 요청을 라우팅 에이전트가 분석하고, 여러 특화 에이전트를 병렬로 실행한 뒤 결과를 합성하는 구조입니다. Step Functions의 워크플로우 정의는 아래처럼 JSON 기반의 ASL(Amazon States Language)로 작성합니다.
{
"Comment": "병렬 에이전트 오케스트레이션 — Amazon States Language",
"StartAt": "RouteRequest",
"States": {
"RouteRequest": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:routing-agent",
"Next": "ParallelAgents"
},
"ParallelAgents": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "SearchAgent",
"States": {
"SearchAgent": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:search-agent",
"End": true
}
}
},
{
"StartAt": "CalculationAgent",
"States": {
"CalculationAgent": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:calc-agent",
"End": true
}
}
}
],
"Next": "SynthesizeResults"
},
"SynthesizeResults": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:synthesis-agent",
"End": true
}
}
}특화 에이전트를 추가하고 싶으면 ParallelAgents의 Branches 배열에 하나만 더 추가하면 됩니다. 기존 에이전트 코드는 전혀 건드릴 필요가 없죠.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 탄력적 확장 | 이벤트 볼륨 기반 자동 스케일링, AI 워크로드의 버스티한 특성에 최적 |
| 비용 효율 | Pay-per-use 모델로 유휴 자원 비용 제거, 소규모 트래픽에서 특히 유리 |
| 낮은 결합도 | 각 AI 단계가 독립적으로 배포·확장·교체 가능 |
| 실시간성 | 배치 주기 파이프라인 대비 레이턴시를 대폭 단축 |
| 장애 격리 | 특정 단계 실패가 전체 파이프라인에 영향 최소화 |
| 운영 부담 최소화 | 인프라 관리 없이 AI 로직에 집중 가능 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| 콜드 스타트 | 유휴 후 첫 호출 시 300ms~수 초 지연. Lambda 내부에서 S3의 대형 모델 파일을 로드할 경우 수십 초까지 늘어날 수 있음 | Provisioned Concurrency 적용, Bedrock/SageMaker Endpoint로 모델 서빙 분리 |
| 상태 관리 복잡성 | LLM KV 캐시(모델이 이전 토큰 계산을 재사용하는 내부 상태) 등 상태 의존 추론을 무상태 함수와 결합하기 어려움 | Step Functions으로 상태 외재화 |
| 대형 모델 제약 | 수 GB~TB 모델을 서버리스 환경에 직접 로드하는 것은 비실용적 | SageMaker 전용 엔드포인트 분리, Bedrock API 활용 |
| 비용 역전 | 대용량 AI 모델은 리소스 소비가 커서 Pay-per-use 이점이 사라질 수 있음 | 트래픽 볼륨 기반 비용 사전 검증 |
| 분산 추적 난이도 | 이벤트 체인 디버깅 및 전체 파이프라인 관찰가능성 확보가 복잡 | X-Ray, OpenTelemetry로 전 단계 계측 |
| 벤더 종속 | Lambda+EventBridge 등 플랫폼 의존성 발생 | Knative(CNCF 졸업 프로젝트)로 멀티클라우드 추상화 검토 가능 |
Provisioned Concurrency: AWS Lambda가 미리 인스턴스를 준비해두는 기능입니다. 콜드 스타트를 사실상 제거할 수 있지만, 준비된 인스턴스 수만큼 대기 비용이 발생합니다. 사용자 대면 실시간 시스템에서는 적극적으로 검토해볼 만한 옵션입니다.
실무에서 가장 흔한 실수
1. 이벤트 스키마를 계약 없이 느슨하게 운영하다가 호환성 문제 발생
처음에는 "이벤트 필드 하나 바꾸면 되지 뭐"라고 생각하기 쉽습니다. 그런데 팀이 커지면 어느 Lambda가 어떤 필드를 기대하는지 추적이 안 되기 시작합니다. "나중에 정리하자"는 생각은 팀이 커질수록 기술 부채로 쌓입니다. 처음부터 Confluent Schema Registry나 AWS Glue Schema Registry로 스키마를 강제하는 것을 권장합니다.
2. 콜드 스타트 비용을 개발 환경에서 측정하지 않고 운영 배포
저는 이 실수를 첫 운영 배포 이틀 뒤에 발견했습니다. 개발 환경에서는 함수가 항상 따뜻한 상태라 콜드 스타트를 체감하지 못하거든요. 오전 9시 첫 요청이 들어왔을 때 RAG 파이프라인 전체가 몇 초씩 멈추는 걸 보고 나서야, 스테이징에서 반드시 콜드 스타트 시나리오를 시뮬레이션해야 한다는 걸 몸으로 배웠습니다.
3. 멱등성 없이 Kafka 컨슈머 구현
Kafka는 at-least-once 전달을 보장하므로 같은 메시지가 두 번 처리될 수 있습니다. 이상 감지 예시에서 fraud_score > 0.85인 경우 이벤트를 발행한다면, 같은 트랜잭션이 중복으로 경고를 생성하지 않도록 Redis나 DynamoDB로 처리 여부를 체크하는 패턴을 권장합니다. transaction_id 같은 고유 키로 중복 처리를 방지하는 로직이 빠지면 운영 중 데이터 오염이 발생할 수 있습니다.
마치며
지금 운영 중인 AI 파이프라인이 있다면, 가장 무거운 단계 하나를 골라보는 것부터 시작해볼 수 있습니다. 임베딩 생성처럼 독립적이고 무상태인 단계를 이벤트로 분리하는 것만으로도 전체 아키텍처가 얼마나 유연해지는지 체감할 수 있습니다. EDA+Serverless AI 파이프라인은 완벽한 솔루션이 아닙니다. 콜드 스타트와 분산 추적의 복잡성은 여전히 현실적인 과제입니다. 하지만 AI 워크로드의 버스티함과 단계별 자원 이질성을 가장 자연스럽게 다루는 패턴이라는 건 직접 써보면 납득이 됩니다.
지금 바로 시작해볼 수 있는 3단계입니다.
-
SAM CLI로 첫 파이프라인 체험 — SAM CLI를 설치(약 5분)한 뒤, AWS Prescriptive Guidance의 "Building serverless architectures for agentic AI" 문서를 따라 EventBridge + Lambda 연결 파이프라인을 로컬에서
sam local invoke로 실행해볼 수 있습니다. CloudFormation 없이도 이벤트 흐름을 빠르게 체감할 수 있습니다. -
기존 AI API 서버의 가장 무거운 단계 하나를 이벤트로 분리 — 임베딩 생성처럼 독립적이고 무상태인 단계부터 Lambda로 추출하고, 기존 서버가 EventBridge에 이벤트를 발행하도록 변경하는 것만으로도 부분 적용을 시작할 수 있습니다. 전체 재설계 없이 점진적 전환이 가능합니다.
-
AWS X-Ray로 이벤트 체인 전체를 시각화 — Lambda 핸들러에
@tracer.capture_lambda_handler데코레이터(Python Powertools) 한 줄만 추가해도 X-Ray 서비스 맵에서 전체 파이프라인 레이턴시와 병목을 한눈에 파악할 수 있습니다. 관찰가능성을 초기부터 갖추는 것이 이 아키텍처를 오래 운영하는 핵심입니다.
참고 자료
- Event-driven architecture: The backbone of serverless AI | AWS Prescriptive Guidance
- Designing serverless AI architectures | AWS Prescriptive Guidance
- Building serverless architectures for agentic AI on AWS
- Event-Driven Architecture for AI Agents: Patterns and Benefits | Atlan
- Event-Driven Architecture: A Complete Guide (2026) | RisingWave
- Serverless EDA with AWS Lambda & Kafka | Aiven
- Build event-driven architectures with Amazon MSK and Amazon EventBridge | AWS Big Data Blog
- The Hidden Cost of Cold Starts in Serverless AI Workloads | DigitalOcean
- AI Goes Serverless: Are Systems Ready? | ACM SIGOPS
- Building Event-Driven Architectures: Top 5 Best Practices | Confluent
- Cloud Native Computing Foundation Announces Knative's Graduation | CNCF
- From Message to Job: A Serverless Event-Driven Data Pipeline on GCP