프로덕션급 AI 대시보드 구축: BMW AutoTrend 플랫폼의 아키텍처 통찰
요약
BMW AutoTrend 플랫폼의 아키텍처를 소개하며, Dockerized Python과 React를 기반으로 로컬 LLM(Ollama)과 SQLite FTS5를 활용한 AI 대시보드 구축 방법을 다룹니다. 클린 아키텍처와 결정론적 폴백 전략을 통해 외부 API 의존성을 최소화한 로컬 우선 시스템 설계를 보여줍니다.
핵심 포인트
- Clean Architecture를 적용하여 데이터 추출, 저장, 분석 계층을 엄격히 분리
- Ollama 기반 로컬 LLM과 Regex 폴백을 결합하여 시스템 안정성 확보
- SQLite FTS5를 활용한 고성능 로컬 텍스트 검색 및 데이터 관리
- 추상 인터페이스를 통한 새로운 스크래핑 어댑터의 확장성 제공
Dockerized Python + React 스택 내에서 Clean Architecture, Subprocess Scrapers, SQLite FTS5, 그리고 Local LLMs (Ollama)를 오케스트레이션하기.
1. 요약 및 설계 목표 (Executive Summary & Design Goals)
BMW AutoTrend Dashboard는 BMW 관련 뉴스의 시장 트렌드와 감성(Sentiment)을 스크래핑, 처리, 분류 및 시각화하는 로컬 우선(Local-first) 방식의 AI 기반 자동차 인텔리전스 플랫폼입니다. 이 시스템은 완전히 로컬에서 실행되도록 설계되었으며, 기사 요약 및 엔티티 추출(Entity extraction)을 위해 Ollama를 통한 로컬 LLM 인스턴스를 사용하고, 결정론적 규칙 기반의 정규 표현식(Regex) 폴백(Fallback) 엔진을 백업으로 지원합니다.
이 플랫폼은 다음과 같은 설계 목표를 가지고 구축되었습니다:
- Clean Architecture (클린 아키텍처): 데이터 추출 계층, 저장 메커니즘, 분석 연산 및 API 컨트롤러를 분리합니다.
- Deterministic Fallbacks (결정론적 폴백): 로컬 LLM 리소스(Ollama)가 오프라인 상태이거나 리소스가 제한적인 상황에서도 (규칙 기반 정규 표현식 프로세서를 사용하여) 애플리케이션이 완전히 기능하도록 보장합니다.
- Local-First & High Performance (로컬 우선 및 고성능): 즉각적인 전체 텍스트 검색(Full-text search)을 위해 SQLite FTS5를 사용하며, 모든 스크래핑된 데이터, 검색 인덱스 및 분석 파일을 외부 SaaS API 의존성 없이 로컬에 저장합니다.
- Plug-and-Play Extensibility (플러그 앤 플레이 확장성): 통합된 추상 인터페이스(Abstract interface)를 구현함으로써 새로운 스크래핑 어댑터(예: Autoblog, MotorTrend)를 쉽게 추가할 수 있도록 합니다.
2. 클린 시스템 아키텍처 (Clean System Architecture)
애플리케이션은 Clean Architecture 원칙을 준수하여, 엄격한 단방향 데이터 흐름을 유지하고 외부 의존성으로부터 비즈니스 로직을 격리합니다.
graph TD
subgraph Frontend [React SPA - Nginx를 통해 서빙됨]
UI[대화형 대시보드 페이지]
...
- Frontend Layer (프론트엔드 계층): Vite로 구축된 React + TypeScript SPA입니다. 표준화된 JSON REST 엔드포인트를 통해서만 백엔드와 상호작용합니다.
- API Controllers (API 컨트롤러): Pydantic을 사용하여 스키마를 검증하고, 저장소를 쿼리하며, 백그라운드 태스크를 트리거하는 FastAPI 핸들러입니다.
- Core Business Logic (핵심 비즈니스 로직):
- Ingestion Pipeline (수집 파이프라인): 스크래핑, 중복 체크 및 데이터 수집 (Ingestion)을 조율합니다.
- AI Processing Pipeline (AI 처리 파이프라인): 텍스트 분석, 요약 및 태그 추출을 수행합니다.
- Analytics Engine (분석 엔진): 메트릭을 집계하고 일일 스냅샷을 유지합니다.
- Data Providers (데이터 프로바이더): Node.js 기반 CLI 도구(
webcmd)의 실행을 캡슐화하는 서브프로세스 (Subprocess) 래퍼입니다. - Database Layer (데이터베이스 계층): SQLAlchemy ORM을 통해 관리되는 SQLite이며, 로우 SQL (raw SQL) 데이터베이스 트리거로 기능을 보강했습니다.
3. Ingestion Pipeline & Subprocess WebCMD Scraping (수집 파이프라인 및 서브프로세스 WebCMD 스크래핑)
데이터 수집은 [providers/base.py]에 정의된 추상 기본 클래스(Abstract Base Class)인 NewsProvider에서 시작됩니다. providers/bmwblog.py에 있는 BMWBlogProvider와 같은 구체적인 프로바이더(Concrete Provider)들이 발행사로부터 데이터를 크롤링하는 책임을 집니다.
유지보수가 어렵고 취약한 커스텀 웹 스크래퍼를 직접 작성하는 대신, 백엔드는 내부적으로 @agentrhq/webcmd CLI 도구를 활용합니다. BMWBlogProvider는 구조화된 기사 피드를 가져오기 위해 webcmd를 서브프로세스로 실행합니다.
The Subprocess Execution Mechanism (서브프로세스 실행 메커니즘)
# snippet from backend/providers/bmwblog.py
def _run_webcmd(self, args: List[str]) -> str:
cmd = ["webcmd"] + args
...
파이프라인은 두 단계로 실행됩니다:
- Metadata Fetching (메타데이터 가져오기):
webcmd bmwblog latest -f json을 실행하여 최신 기사 10개를 가져옵니다 (메타데이터만 포함: URL, 제목, 짧은 발췌문). - Deep Article Ingestion (심층 기사 수집): (SQLite 인덱스 확인을 통해 결정된) 각 새로운 URL에 대해,
webcmd bmwblog article <url> -f json을 쿼리하여 전체 본문 텍스트, 카테고리, 저자를 추출하고 OpenGraph 이미지(og:image)를 위해 로우 HTML을 스크래핑합니다.
4. Hybrid AI Classification Pipeline (Ollama & Regex Fallbacks) (하이브리드 AI 분류 파이프라인 (Ollama 및 정규표현식 폴백))
전체 기사 텍스트가 수집되면 ai/processor.py로 전달됩니다. 프로세서는 평가 엔진을 선택하기 전에 Ollama의 가용성을 동적으로 확인하는 하이브리드 시스템을 사용합니다:
graph TD
A[New Article Ingested] --> B{Is Ollama Server Online?}
B -->|Yes| C[Call Ollama Llama 3.2 API]
...
1. Ollama LLM 파이프라인 (Pipeline)
Ollama 서버가 온라인 상태이고 llama3.2가 실행 중인 경우, 백엔드는 정교하게 설계된 프롬프트(prompt)와 함께 /api/generate로 POST 요청을 보냅니다. 결정론적(deterministic)인 통합을 보장하기 위해, 요청은 구조화된 JSON 출력을 강제합니다:
payload = {
"model": settings.OLLAMA_MODEL,
"prompt": prompt,
...
모델은 감성(Sentiment) (긍정(Positive), 중립(Neutral), 부정(Negative))을 분류하고, 관련 차량 모델(예: Neue Klasse, BMW M3)을 추출하며, 기술 태그(예: 배터리 기술 (Battery Technology), 첨단 운전자 보조 시스템 (ADAS))를 매핑하고, 짧고 상세한 요약 및 TL;DR(Too Long; Didn't Read) 요약을 생성합니다.
2. 규칙 기반 정규표현식 (Regex) 폴백 파이프라인 (Fallback Pipeline)
Ollama가 오프라인 상태이거나 유효한 JSON을 반환하지 못하는 경우, _analyze_with_rules가 실행됩니다. 이는 사전 컴파일된 정규표현식(regular expressions)을 사용하여 키워드와 태그를 매칭합니다:
TECHNOLOGY_TAGS = {
"Electric Vehicles": r"\b(ev|evs|electric|zero-emission|zero emission|battery electric|bev)\b",
"Battery Technology": r"\b(battery|batteries|solid-state|cell|cells|rimac)\b",
...
이 하이브리드 모델은 데이터베이스 컬럼이 유효한 카테고리와 태그로 채워지도록 보장하며, 하드웨어 제한으로 인해 검색 기능이 저하되지 않도록 합니다.
5. 데이터베이스 트리거 (Triggers)를 활용한 고성능 SQLite FTS5 검색 인덱싱 (Indexing)
고속의 로컬 우선(local-first) 검색을 위해, 이 프로젝트는 느린 LIKE %query% SQL 연산을 우회하고 SQLite의 네이티브 FTS5 (Full-Text Search, 전문 검색) 확장을 활용합니다.
데이터베이스 스키마 초기화 및 트리거 (Triggers)
database/connection.py에서 애플리케이션은 SQLAlchemy를 사용하여 데이터베이스를 부팅하지만, 표준 연결 이벤트(connection events)에 후킹하여 외래 키 제약 조건(PRAGMA foreign_keys=ON)을 강제하고 FTS5 가상 테이블(virtual table) 및 동기화 트리거(synchronization triggers)를 수동으로 프로비저닝합니다.
-- FTS5 가상 테이블 설정 (FTS5 Virtual Table Configuration)
CREATE VIRTUAL TABLE articles_fts USING fts5(
title,
...
이러한 트리거는 검색 인덱싱(search indexing) 작업을 SQLite 엔진으로 직접 오프로드(offload)합니다. SQLAlchemy를 통해 새로운 기사가 커밋될 때마다, SQLite는 FTS5 섀도우 테이블(shadow tables) 내의 제목, 발췌문(excerpt), 본문(content)을 자동으로 인덱싱합니다.
관련성 기반 검색 엔드포인트 (Relevance-Based Search Endpoint)
사용자가 대시보드에서 검색할 때, 백엔드는 BM25 관련성 순위 지정(relevance-ranking) 쿼리를 수행합니다.
# backend/api/routes.py의 스니펫 (Snippet)
query_str = """
SELECT rowid FROM articles_fts
...
이 작업은 밀리초(sub-milliseconds) 미만 단위로 실행되어, 프론트엔드에 즉각적인 "입력 시 검색(Search-as-you-type)" 기능을 제공합니다.
6. 동적 분석 엔진 및 이동 트렌드 계산 (Dynamic Analytics Engine & Rolling Trend Computations)
analytics/engine.py의 **분석 엔진 (Analytics Engine)**은 어떤 자동차 트렌드가 탄력을 받고 있는지 결정하기 위해 이동 창(rolling window) 계산을 수행합니다.
단순한 횟수를 추적하는 대신, 이 엔진은 이동 7일 창(rolling 7-day window)과 이전 7일 창을 비교하여 차량 모델 및 기술에 대한 언급량을 바탕으로 **주간 성장률 (weekly growth rate)**을 계산합니다.
# 7일 경계 (7-day boundaries)
seven_days_ago = today_start - timedelta(days=7)
fourteen_days_ago = today_start - timedelta(days=14)
...
결과로 나온 토픽들은 growth_rate DESC로 정렬되어 UI의 "트렌딩 토픽 (Trending Topics)" 위젯을 채웁니다.
이력 스냅샷 생성 (Historical Snapshotting)
사용자가 페이지를 로드할 때마다 발생하는 CPU 집약적인 집계(aggregations)를 방지하기 위해, 애플리케이션은 APScheduler를 사용하여 자정 무렵에 generate_daily_snapshot을 실행합니다. 이 함수는 계산된 통계치를 JSON으로 직렬화(serialize)하여 analytics_snapshots 테이블에 저장함으로써, 30일간의 과거 타임라인을 빠르게 렌더링할 수 있게 합니다.
7. 프론트엔드 아키텍처 (Frontend Architecture) (React + Vite + Recharts)
프론트엔드는 현대적인 React + TypeScript + Vite 아키텍처를 기반으로 구축된 싱글 페이지 애플리케이션 (SPA)입니다.
UI 기능 (UI Features)
- 글래스모픽 테마 (Glassmorphic Theme): CSS 변수, 커스텀 Outfit 타이포그래피, 그리고 빛나는 테두리(glowing borders)를 기반으로 구축된 프리미엄 다크 모드 인터페이스입니다.
- 컴포넌트 레이아웃 (Component Layout): 사이드바와 메인 패널은 반응형 네비게이션을 위해
layouts/DashboardLayout.tsx에 구조화되어 있습니다. - 시각화 (Visualizations):
Recharts를 사용하여 동적 데이터를 표시합니다:- 일일 기사량을 나타내는 영역 차트 (Area charts).
- 30일 동안의 긍정/중립/부정 트렌드를 매핑하는 감성 타임라인 (Sentiment timelines).
- 차량 모델 및 기술 태그의 인기도를 비교하는 막대 차트 (Bar charts).
- 실시간 검색 (Real-time Search): 키 입력을 수집하고, API를 통해 SQLite FTS5 인덱스에 쿼리를 날리며, 밀리초 미만의 지연 시간으로 일치 항목을 강조 표시합니다.
8. Docker 오케스트레이션 및 DevSecOps 통찰 (Docker Orchestration & DevSecOps Insights)
이 플랫폼은 docker compose up --build라는 단일 명령어로 실행되도록 설계되었습니다. 오케스트레이션은 네트워크가 격리된 세 개의 종속적 컨테이너를 구성합니다:
- Ollama 컨테이너:
11434포트에서 LLM 모델을 가져오고 서빙합니다. - FastAPI 백엔드 컨테이너:
docker/backend.Dockerfile로부터 빌드됩니다. 데이터베이스를/app/data/autotrend.db위치의 영구적인 Docker 볼륨db_data에 매핑하여, 컨테이너가 재시작되어도 데이터베이스 레코드가 영구적으로 저장되도록 합니다. - Nginx 프론트엔드 컨테이너: React 애플리케이션을 컴파일하는 멀티 스테이지 빌드 (multi-stage build) 방식을 사용하며, Nginx를 통해
80포트에서 정적 자산을 서빙하고 백엔드로 요청을 프록시(proxying)합니다.
기술적 Docker 팁: Node.js 서브프로세스 요구 사항 (Technical Docker Tip: Node.js Subprocess Requirement)
Python 백엔드가 webcmd(npm 라이브러리)를 서브프로세스로 호출하기 때문에, 백엔드 Dockerfile은 멀티 런타임 환경을 지원하도록 구성되어야 합니다. Python slim 컨테이너 내부에 Node.js와 npm을 추가하고 @agentrhq/webcmd를 전역으로 설치하면 인제스션 파이프라인 (ingestion pipeline)이 오류 없이 컴파일됩니다:
FROM python:3.12-slim
WORKDIR /app
...
9. 확장성 및 향후 범위 (Extensibility & Future Scope)
AutoTrend 대시보드의 설계는 다음과 같이 쉽게 확장할 수 있도록 구성되었습니다:
- 새로운 뉴스 출처 추가 (Adding New News Outlets):
NewsProvider추상 기본 클래스 (Abstract Base Class)를 구현하는 새로운 클래스(예:AutoblogProvider)를 생성합니다. 스크래퍼 플러그인 명령(예:webcmd autoblog latest)을 매핑하고,run_ingestion_pipeline에 해당 프로바이더를 등록합니다. - 외부 LLM (External LLMs): [ai/client.py]의 AI 클라이언트는 API 키를 위한 환경 변수를 사용하여 원격 클라우드 모델(Gemini API 또는 OpenAI API 등)을 지원하도록 확장할 수 있습니다.
- 고급 감성 휴리스틱 (Advanced Sentiment Heuristics): 규칙 기반 엔진(Rule-based engine)을 업그레이드하여, 심층적인 오프라인 감성 추출을 위해 VADER 또는 트랜스포머 모델(Transformer models, 예: Hugging Face의
distilbert-base-uncased-finetuned-sst-2-english)을 지원할 수 있습니다.
BMW AutoTrend Dashboard 코드베이스를 위한 기술 문서로 작성되었습니다. Github 리포지토리: https://github.com/scha54/BMW-AutoTrend-Dashboard
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기