Data & AI Systems
Daily Compass: Azure 서버리스 데이터 파이프라인 설계
Azure Static Web Apps, Functions, Cosmos DB, Private Endpoint, Application Insights로 데일리 대시보드 데이터 흐름을 설계한 기록입니다.
Daily Compass는 하루 계획에 필요한 TODO, Calendar, 감정 기반 문구, 실시간 뉴스를 한 화면에 모으는 개인 대시보드 아이디어에서 출발했다. 기능만 보면 작은 웹 애플리케이션이지만, 데이터를 어디서 수집하고, 어떤 저장소에 넣고, 어떤 API로 제공하고, 어떤 경계로 보호할지 정리하다 보니 서버리스 데이터 파이프라인 설계 문제가 되었다.
처음부터 VM을 직접 관리하는 방식보다 Azure 관리형 서비스를 조합하는 방향을 잡았다. 서버 운영 부담을 줄이고, 작은 기능 단위로 API와 수집 작업을 나누고, Application Insights로 실행 상태를 확인하기 위해서다.
기능을 데이터 흐름으로 나누기
대시보드는 세 가지 기능으로 나뉜다.
Daily dashboard
- mood quote
- todo and calendar
- real-time news각 기능은 데이터 성격이 다르다. TODO와 Calendar는 사용자별 쓰기 데이터이고, 뉴스는 외부에서 수집해 갱신되는 읽기 데이터다. 감정 기반 명언은 사용자의 현재 선택을 입력으로 받아 추천 결과를 제공하지만, 원문 감정 텍스트를 장기 저장할 필요는 없다.
User-owned data
- todos
- calendar events
- preferences
System-collected data
- news articles
- source metadata
- fetched timestamp
Generated data
- selected quote
- optional speech output
- recommendation log이렇게 나누면 저장 정책도 달라진다. 사용자 TODO는 수정/삭제가 중요하고, 뉴스는 중복 제거와 갱신 시간이 중요하다. 감정 기반 문구는 추천 결과보다 입력을 얼마나 저장하지 않을지가 더 중요할 수 있다.
왜 Azure Static Web Apps와 Functions인가
프론트엔드는 Azure Static Web Apps에 두고, API는 Azure Functions로 구성하는 방식을 생각했다.
Browser
-> Static Web Apps
-> Functions API
-> Cosmos DBStatic Web Apps는 정적 프론트엔드 배포와 인증 연동 가능성이 있고, Functions는 작은 API와 이벤트 기반 작업을 나누기 좋다. TODO 생성, Calendar 조회, 뉴스 목록 조회, 감정 기반 명언 추천 같은 기능을 함수 단위로 나눌 수 있다.
예를 들어 API 경로는 다음처럼 설계할 수 있다.
GET /api/dashboard/today
GET /api/todos
POST /api/todos
PATCH /api/todos/{id}
DELETE /api/todos/{id}
GET /api/news
POST /api/mood/quote/api/dashboard/today는 첫 화면에 필요한 데이터를 한 번에 내려주는 aggregation endpoint로 둘 수 있다. 반면 TODO 수정이나 삭제는 리소스 단위 API로 나누는 편이 명확하다.
Cosmos DB MongoDB API를 선택한 이유
대시보드 데이터는 처음부터 강한 관계형 모델보다 문서 구조가 더 자연스럽게 느껴졌다. 사용자별 TODO, Calendar, 뉴스 문서, 명언 컬렉션은 각각 독립적인 document로 다룰 수 있다.
{
"userId": "user-001",
"date": "2026-07-04",
"todos": [
{
"id": "todo-001",
"title": "Review architecture diagram",
"done": false,
"priority": "high"
}
],
"calendar": [
{
"id": "event-001",
"title": "Cloud study session",
"startsAt": "2026-07-04T09:00:00+09:00"
}
]
}뉴스는 수집 시점과 출처를 함께 저장한다.
{
"articleId": "hash",
"title": "Cloud platform update",
"url": "https://example.com/news",
"source": "example",
"category": "cloud",
"fetchedAt": "2026-07-04T07:00:00+09:00",
"publishedAt": "2026-07-04T06:30:00+09:00"
}문서형 DB를 선택한다고 해서 모델링이 사라지는 것은 아니다. partition key, document size, 조회 패턴, TTL, RU 사용량을 함께 봐야 한다. 예를 들어 사용자 대시보드는 userId와 날짜를 기준으로 조회하고, 뉴스는 category와 fetchedAt 기준으로 조회할 수 있다.
Query pattern
- today's dashboard by userId + date
- latest news by category
- incomplete todos by userId
- calendar events by date range뉴스 수집은 API와 다른 실행 모델이 필요하다
실시간 뉴스 기능은 사용자가 화면을 열 때마다 외부 사이트를 긁는 방식보다, 주기적으로 수집해 저장해두는 방식이 안정적이다.
Timer trigger
-> fetch RSS / API
-> normalize article
-> deduplicate by URL/hash
-> save to Cosmos DB
-> record execution logAzure Functions의 Timer Trigger를 사용하면 일정 주기로 수집 작업을 실행할 수 있다. 수집 작업에서는 외부 API 실패, 응답 형식 변경, 중복 기사, 네트워크 timeout을 정상적인 실패 사례로 다뤄야 한다.
module.exports = async function (context, myTimer) {
context.log("News ingestion started");
try {
const articles = await fetchNewsSources();
const normalized = articles.map(normalizeArticle);
const unique = deduplicateArticles(normalized);
await saveArticles(unique);
context.log(`Saved ${unique.length} articles`);
} catch (error) {
context.log.error("News ingestion failed", error);
throw error;
}
};중복 제거 기준은 URL만으로 부족할 수 있다. 같은 기사가 추적 파라미터만 다르게 들어오거나, 여러 출처에서 제목이 조금씩 다르게 들어올 수 있기 때문이다. 처음에는 normalized URL과 title hash를 함께 쓰는 정도로 시작할 수 있다.
function articleKey(article) {
return hash(`${normalizeUrl(article.url)}|${article.title.trim().toLowerCase()}`);
}Private Endpoint와 네트워크 경계
작은 프로젝트에서도 데이터 저장소를 public endpoint로 그대로 열어두는 방식은 피하고 싶었다. Cosmos DB와 Storage에 Private Endpoint를 붙이는 구조를 고려했다.
frontend subnet
api subnet
data subnet
private endpoint subnet물론 Static Web Apps와 Functions, Private Endpoint를 어떻게 연결할지는 실제 Azure 플랜과 네트워크 통합 지원 범위를 확인해야 한다. 중요한 것은 “프론트엔드가 외부에 공개된다”와 “DB가 외부에 공개된다”를 같은 문제로 보지 않는 것이다.
Public boundary
- browser -> static app
- browser -> API endpoint
Private boundary
- API -> Cosmos DB
- API -> Storage
- ingestion function -> Cosmos DB보안 경계는 네트워크만으로 끝나지 않는다. Function App의 managed identity, Cosmos DB 권한, application settings의 secret 관리, GitHub Actions 배포 권한도 함께 봐야 한다.
GitHub Actions 배포 흐름
배포는 GitHub Actions로 구성할 수 있다. 프론트엔드와 Functions를 같은 저장소에서 관리할지, 별도 저장소로 분리할지는 팀 구조에 따라 달라진다. 작은 프로젝트라면 mono-repo가 단순하다.
repo/
apps/
web/
api/
infra/
docs/워크플로우는 대략 다음 순서다.
name: deploy-daily-compass
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run lint
- run: npm run test
- run: npm run buildAzure 배포 권한은 가능하면 장기 secret보다 OIDC 기반 federated credential을 쓰는 편이 낫다. 배포 파이프라인이 어떤 리소스를 수정할 수 있는지 최소 권한으로 제한해야 한다.
관측성과 비용
서버리스는 서버를 직접 관리하지 않아도 되지만, 실행 상태를 보지 않아도 된다는 뜻은 아니다. Daily Compass에서는 Application Insights를 중심으로 다음 신호를 보고 싶었다.
API
- request count
- response time
- error rate
- dependency failure
Ingestion
- scheduled run count
- fetched article count
- duplicate ratio
- source API latency
- failed source
Cosmos DB
- RU consumption
- throttled request
- query latency
- storage sizeCosmos DB는 사용량과 throughput 설정에 따라 비용이 달라진다. 교육용 아키텍처 기준으로는 월 $32-45 정도를 예상했고, 대부분은 Cosmos DB throughput에서 발생하는 구조였다. 실제 운영에서는 serverless capacity mode, autoscale, TTL, indexing policy를 조정해 비용을 낮출 수 있는지 확인해야 한다.
Cost levers
- Cosmos DB throughput mode
- indexing policy
- TTL for old news
- Function execution count
- Application Insights sampling
- Storage lifecycle policyAI/Speech 기능은 확장 지점으로 분리한다
감정 기반 명언을 음성으로 들려주는 기능은 Azure AI Speech와 연결할 수 있다. 다만 AI 기능을 핵심 요청 흐름에 강하게 묶으면 비용과 지연시간, 실패가 화면 전체에 영향을 줄 수 있다. 그래서 선택적 확장 기능으로 분리하는 편이 좋다.
POST /api/mood/quote
-> select quote
-> return text immediately
-> optionally request speech audio음성 변환 결과는 매번 생성하지 않고 캐시할 수 있다. 같은 문구와 같은 음성 옵션이면 Storage에 저장된 오디오 URL을 재사용한다.
{
"quoteId": "quote-001",
"text": "Small progress is still progress.",
"audioUrl": "https://storage.example.com/quote-001-ko-KR.mp3"
}AI 기능을 넣을 때는 입력 데이터와 로그도 조심해야 한다. 사용자의 감정 입력을 저장하지 않는 설계라면, Application Insights 로그에도 원문 입력이 남지 않게 해야 한다.
다시 설계한다면
지금 다시 이 구조를 다듬는다면 먼저 Terraform으로 인프라를 코드화할 것이다. Cosmos DB, Function App, Static Web Apps, Application Insights, Private Endpoint, Role Assignment를 코드로 남기면 환경 재현과 변경 리뷰가 쉬워진다.
또한 뉴스 수집과 대시보드 API를 더 명확히 분리할 것이다.
api service
- user dashboard
- todos
- calendar
- quote
worker service
- news ingestion
- deduplication
- cleanup old articles운영 대시보드도 처음부터 두는 편이 좋다. API 응답 시간, Function 실패율, 뉴스 수집 성공률, Cosmos DB RU 사용량을 보면 작은 프로젝트라도 병목과 비용 구조가 훨씬 빨리 보인다.
정리
Daily Compass는 개인 대시보드 아이디어였지만, 설계해보면 서버리스 데이터 파이프라인의 기본 질문을 많이 포함한다. 사용자 데이터와 외부 수집 데이터를 어떻게 나눌지, Functions를 API와 worker로 어떻게 분리할지, Cosmos DB의 문서 모델과 조회 패턴을 어떻게 잡을지, DB를 public으로 열지 않으려면 어떤 네트워크 경계가 필요한지 생각해야 한다.
서버리스는 인프라 운영 부담을 줄여주지만 아키텍처 고민을 없애주지는 않는다. 오히려 실행 단위가 작아지는 만큼 데이터 흐름, 실패 처리, 관측성, 비용을 더 명확히 설계해야 한다. 작은 대시보드라도 이 기준으로 보면 꽤 좋은 클라우드 네이티브 설계 연습이 된다.
공식 참고 문서
시리즈
AI와 데이터 자동화관련 글
Data & AI Systems
AI/LLM 워크플로우와 클라우드 플랫폼 연결
뉴스 브리핑, RAG, 서버리스 데이터 파이프라인을 수집-정제-검색-생성-검증의 운영 흐름으로 정리합니다.
Data & AI Systems
Python 기반 데이터 수집과 자동화 파이프라인 설계
Python 기본 문법, 가상환경, Pandas 데이터 처리, 웹 크롤링, Open API 호출 실습을 자동화 파이프라인 관점으로 연결해 정리합니다.
Cloud & Platform Engineering
S3, CloudFront, Terraform으로 정적 호스팅 구성하기
S3 정적 오리진과 CloudFront 배포를 Terraform으로 선언하고 GitHub Actions 배포 흐름까지 연결합니다.