배경: 광고 플랫폼의 구조적 문제
스포티파이는 광고 사업을 운영하면서 구조적인 문제에 직면했습니다. 다이렉트(직접 구매), 셀프 서브, 프로그래매틱 등 여러 구매 채널이 하나의 통합된 백엔드 위에 올라가 있지만, 각 채널마다 고유한 워크플로우와 의사결정 로직이 존재했습니다. 결과적으로 같은 코어 로직(예산 할당, 인벤토리 선택, 효율성 vs 도달률 균형)이 채널별로 중복 구현되고 시간이 지나면서 서로 다른 동작을 하게 되는 '행동 단편화(behavior fragmentation)'가 발생했습니다.
단순히 새로운 백엔드 서비스를 만드는 전통적인 접근법으로는 이 문제를 해결할 수 없었습니다. 워크플로우는 조합적(combinatorial)이고, 동일한 의사결정이 여러 표면에서 일관되게 나타나야 하며, 시스템은 '목표'를 이해하고 그에 따라 행동할 수 있는 인텐트 레이어(intent layer)가 부족했습니다. 스포티파이 팀은 이 갭을 메우기 위해 에이전틱(agentic) 접근법을 선택했습니다. (참고: 배경 코딩 에이전트를 위한 컨텍스트 엔지니어링 가이드)
아키텍처: 멀티 에이전트 시스템의 설계
스포티파이는 미디어 플래닝(Media Planning)을 첫 번째 유스 케이스로 선정했습니다. 광고 판매, 광고주, 인벤토리, 페이싱, 광고 상품이 모두 충돌하는 지점이기 때문입니다. 시스템은 크게 세 가지 구성 요소로 나뉩니다:
- 라우터 에이전트(RouterAgent): 사용자 메시지를 분석하여 어떤 정보가 포함되어 있는지 빠르게 판단하고, 필요한 전문화 에이전트에게 작업을 위임합니다.
- 전문화 해결 에이전트(Resolution Agents): 목표(GoalResolverAgent), 타겟 오디언스(AudienceResolverAgent), 예산(BudgetAgent), 일정(ScheduleAgent) 등 각각의 도메인을 담당합니다.
- 미디어 플래너 에이전트(MediaPlannerAgent): 모든 해결된 정보를 바탕으로 과거 성과 데이터를 활용한 휴리스틱 기반 최적화 추천을 생성합니다.
이 아키텍처의 핵심 장점은 병렬 실행입니다. 독립적인 에이전트들이 동시에 실행되므로 전체 응답 시간이 3~5초로 단축되었습니다. 각 에이전트는 Google ADK(Agent Development Kit)의 FunctionTool을 통해 실제 데이터에 접근하며, @Schema 어노테이션을 통해 LLM에 구조화된 파라미터 정보를 제공합니다.

핵심 코드 예제: 에이전트 도구 정의
다음은 Google ADK에서 FunctionTool을 사용하여 에이전트가 실제 데이터에 접근할 수 있도록 하는 예제입니다.
from google.adk.tools import FunctionTool
from pydantic import BaseModel, Field
from typing import Optional
class BudgetInput(BaseModel):
"""예산 입력 스키마: 다양한 형식의 예산을 파싱합니다."""
budget_text: str = Field(description="사용자가 입력한 예산 텍스트 (예: '$5000', '5k', '€10,000')")
currency: Optional[str] = Field(default="USD", description="통화 코드 (USD, EUR, KRW 등)")
def parse_budget(input: BudgetInput) -> dict:
"""
예산 텍스트를 파싱하여 마이크로 유닛으로 변환합니다.
실제 구현에서는 정규식과 환율 변환 로직이 포함됩니다.
"""
# 예: "$5,000" -> 5000000000 (마이크로 단위)
# 실무에서는 정규식으로 숫자와 통화를 분리
import re
numeric_part = re.sub(r'[^0-9.]', '', input.budget_text)
amount = float(numeric_part)
micro_amount = int(amount * 1_000_000)
return {
"amount_micro": micro_amount,
"currency": input.currency,
"formatted": f"{input.currency} {amount:,.2f}"
}
# ADK FunctionTool로 등록
budget_tool = FunctionTool(
name="parse_budget",
description="사용자의 자연어 예산 입력을 파싱하여 마이크로 유닛으로 변환합니다.",
func=parse_budget,
input_schema=BudgetInput
)
# 에이전트에 도구 연결
from google.adk.agents import Agent
budget_agent = Agent(
name="BudgetAgent",
instruction="""
당신은 예산 해석 전문가입니다. 사용자가 다양한 형식으로 입력한 예산을 파싱합니다.
- '$5000', '5k', '€10,000', '1000만원' 등 모든 형식을 처리합니다.
- 결과는 항상 마이크로 유닛(1/1,000,000 단위)으로 반환합니다.
- 통화 정보가 없으면 기본값으로 USD를 사용합니다.
- 파싱 실패 시 명확한 에러 메시지를 반환합니다.
""",
tools=[budget_tool]
)
주요 최적화 규칙 (의사 코드)
def generate_recommendations(budget, goals, audience, schedule, historical_data):
"""
과거 성과 데이터를 기반으로 최적의 광고 세트 추천을 생성합니다.
"""
recommendations = []
# 예산 규모에 따른 추천 개수 결정
if budget < 1_000_000_000: # €1,000 미만 (마이크로 단위)
max_recommendations = 1
elif budget < 5_000_000_000: # €5,000 미만
max_recommendations = 2
elif budget < 15_000_000_000: # €15,000 미만
max_recommendations = 3
else:
max_recommendations = 5
# 과거 캠페인 스코어링
scored_campaigns = []
for campaign in historical_data:
score = 0
# 비용 최적화: CPM, CPC, CPI가 중간값보다 낮은 경우 높은 점수
if campaign.cpm < historical_median_cpm:
score += 10
# 전달률 최적화: 100%에 가까울수록 높은 점수
if 0.95 <= campaign.delivery_rate <= 1.05:
score += 20
# 예산 매칭: 비슷한 예산 규모의 캠페인
budget_ratio = campaign.budget / budget
if 0.8 <= budget_ratio <= 1.2:
score += 15
# 타겟 매칭: 인구통계 및 관심사 중첩도 기반
score += calculate_target_overlap(campaign, audience) * 5
scored_campaigns.append((score, campaign))
# 상위 N개 선택 (다양성 보장: 포맷/목표 조합 중복 방지)
selected = select_diverse(scored_campaigns, max_recommendations)
return selected

주요 교훈과 주의사항
교훈 1: 프롬프트 엔지니어링은 소프트웨어 엔지니어링이다
프롬프트를 코드처럼 취급해야 합니다. 버전 관리, 테스트, 반복 개선이 필수입니다. 작은 프롬프트 변경이 출력 일관성에 큰 영향을 미칠 수 있습니다. 출력 형식을 명확히 지정하고, 구체적인 예제를 제공하며, 프롬프트와 파싱 레이어 모두에 가드레일을 구축해야 합니다.
교훈 2: 에이전트 경계 설정이 중요하다
에이전트가 너무 많으면 지연 시간과 조정 오버헤드가 증가합니다. 너무 적으면 프롬프트가 거대해지고 유지보수가 어려워집니다. 경험상 에이전트당 하나의 고유한 스킬 또는 데이터 소스를 할당하는 것이 좋습니다.
교훈 3: 도구를 통한 근거 확보(Grounding)
LLM은 강력하지만 환각(hallucination)을 일으킬 수 있습니다. 실제 데이터(지역 타겟, 광고 카테고리, 과거 성과)에 접근할 수 있는 도구를 에이전트에 제공함으로써 출력을 현실에 근거시킬 수 있습니다. LLM은 '무엇을 할지' 추론하고, 도구는 정확한 데이터를 제공하는 역할 분담이 중요합니다.
한계 및 주의사항
- 동기식 vs 스트리밍: 현재는 동기식 응답을 사용하지만, 장기 실행 작업에는 스트리밍(SSE)이 더 나은 UX를 제공합니다.
- 메모리 vs DB 캐시: 히스토리컬 데이터를 인메모리 캐시에 저장하여 지연 시간을 최소화했지만, 메모리 사용량과 데이터 신선도 사이의 트레이드오프가 있습니다.
- 에이전트 조정 오버헤드: 멀티 에이전트 시스템은 단일 에이전트보다 복잡성이 높습니다. 에이전트 간 컨텍스트 공유와 평가 로직 일관성을 유지하는 것이 중요합니다.
한국 개발 생태계에서의 적용 맥락
국내 광고 플랫폼(예: 네이버, 카카오, 쿠팡)도 유사한 구조적 문제를 안고 있습니다. 여러 구매 채널(DSP, 네트워크, 직접 구매)이 존재하고, 각 채널마다 최적화 로직이 중복 구현되는 경우가 많습니다. 특히 SI 환경에서는 레거시 시스템과의 통합이 더욱 까다롭습니다. 스포티파이의 접근법은 다음과 같은 점에서 국내에 적용 가능합니다:
- 점진적 도입: 기존 서비스를 모두 재작성하지 않고, 가장 복잡한 도메인(예: 미디어 플래닝)부터 시작하여 점진적으로 확장
- 기존 API 활용: 새로운 기능을 처음부터 구현하지 않고, 기존 광고 API를 에이전트의 '도구'로 활용
- 병렬 실행으로 성능 확보: 국내 환경에서도 에이전트 병렬 실행은 3~5초 이내 응답 시간을 유지하는 데 핵심
다음 단계 학습 방향
- Google ADK 공식 문서: 에이전트 오케스트레이션 심화 학습
- Agentic Design Patterns: ReAct, Plan-and-Execute, Multi-agent Debate 등 다양한 패턴 연구
- Vertex AI Agent Builder: 프로덕션 레벨 에이전트 배포 및 모니터링 방법 학습
- RAG(Retrieval-Augmented Generation): 에이전트가 더 많은 컨텍스트를 활용할 수 있도록 RAG 패턴 적용

결론: 에이전틱 플랫폼이 광고의 미래다
스포티파이의 'Ads AI' 사례는 복잡한 멀티 스텝 워크플로우가 멀티 에이전트 아키텍처에 얼마나 잘 맞는지 보여줍니다. 미디어 플래닝 문제를 전문화된 에이전트로 분해함으로써, 각 에이전트가 집중된 프롬프트와 적절한 도구, 명확한 책임을 가지게 되어 유지보수성과 성능을 동시에 확보했습니다.
특히 인상적인 점은 '단순히 AI 기능을 추가하는 것'이 아니라 '구조적 문제를 해결하기 위한 플랫폼적 접근'을 했다는 것입니다. 하드코딩된 워크플로우 대신, 의사결정을 중앙화하고 모든 구매 채널과 표면에 일관되게 투영할 수 있는 에이전틱 플랫폼을 구축했습니다.
핵심 메시지: 복잡한 도메인 문제를 해결할 때는 '더 똑똑한 단일 시스템'을 만드는 것보다 '잘 정의된 여러 에이전트의 협업 시스템'을 구축하는 것이 더 효과적일 수 있습니다. (참고: Cloudflare Turnstile 챌린지 페이지 리디자인 UX 인사이트)