FLOPI 확장성 분석 리포트

NiceGUI + FastAPI + 4-Agent Pipeline 아키텍처에 대한 면밀한 확장성 분석입니다.
48,000줄 코드 전수 분석을 기반으로 병목 지점, 프레임워크 한계, 단계별 해결 방안을 제시합니다.

2026-03-28 분석 48,359 LOC

architecture 현재 아키텍처

FLOPI는 두 개의 독립 서비스로 구성되며, HTTP로 통신합니다.

NiceGUI Dashboard (:18080) FastAPI API Server (:8600) 30개 페이지 ──httpx──▶ REST API (30+ 라우터) @ui.refreshable 폴링 APScheduler (Detection) WebSocket (캔버스 이벤트) 4-Agent Pipeline Workflow Engine │ ▼ Oracle DB (운영) / SQLite (시뮬레이터) pool: min=2, max=30

4-Agent Pipeline 흐름

이상 감지 (Detection Scheduler — 60초 틱) │ ▼ Sentinel Agent ─── 규칙 + AI 패턴 분석으로 이상 탐지 │ ▼ Diagnostician Agent ─── 플레이북 → 진단 케이스 매칭 │ ▼ Advisor Agent ─── 결정 가이드 참고 → 액션 + 파라미터 제안 │ ▼ Validator Agent ─── DT/시뮬/KB로 검증 → safe / caution / risky │ ▼ auto + safe → 즉시 실행 (auto_approved) approval/sign_off 또는 unsafe → 승인 대기

speed 현재 한계치

코드 분석 기반 추정치입니다. 실제 환경에서는 네트워크/LLM 응답 시간에 따라 변동됩니다.

~15명
동시 사용자
DB 커넥션 풀 max=30 (공유)
~8건
동시 이상 처리 (쾌적)
10건+ 시 DB/LLM 경합으로 성능 급락
~100개
Detection 룰
60초 틱 + Semaphore(20)
~30명
NiceGUI UI 사용자
서버사이드 UI + 싱글 프로세스
30
LLM 동시 호출
Semaphore(30) — 런타임 변경 가능

build_circle 자원 추가 vs 설계 변경

위 한계치 중 일부는 자원(DB 풀, LLM 키, 메모리)을 늘리면 즉시 해결됩니다. 그러나 나머지는 아무리 자원을 투입해도 코드 구조를 바꾸지 않으면 풀리지 않는 문제입니다.

paid 자원 추가로 해결 가능 (남은 항목)
문제해결
LLM rate limit키 로테이션 / 상위 플랜
메모리 부족RAM 추가
DB 풀 분리Detection/API 전용 풀 분리
check_circle
설정 변경이나 인프라 스케일업으로 즉시 개선 가능.
engineering 설계 변경이 필요 (자원으로 안 풀림)
문제이유
싱글 이벤트 루프12코어여도 1코어만 사용. workers 늘리면 APScheduler 중복 실행
NiceGUI 수평확장WebSocket 세션이 인스턴스에 바인딩 — 멀티 인스턴스 불가
태스크 유실프로세스 죽으면 메모리 태스크 소실 — 영속성 설계 필요
Scheduler 중복워커 분리 없이 워커 수 늘리면 Detection 사이클 N배 실행
warning
코드 구조를 바꿔야 하는 문제. 서버를 아무리 좋은 걸 써도 동일한 병목이 발생합니다.
tips_and_updates
참고: DB 커넥션 풀(max=30), Detection Semaphore(20), LLM Semaphore(30)는 이미 적절히 튜닝되어 있습니다. 남은 과제는 설계 변경이 필요한 구조적 문제입니다.

loop 싱글 이벤트 루프 — 왜 12코어인데 1코어만 쓸까?

FLOPI의 거의 모든 확장성 문제를 관통하는 핵심 원인입니다. 이 구조를 이해하면 나머지 이슈들이 왜 발생하는지 자연스럽게 이해됩니다.

1. asyncio 이벤트 루프란?

sync
단일 스레드에서 동시성을 구현하는 메커니즘

Python의 asyncio하나의 스레드에서 여러 작업을 번갈아 실행합니다. 실제로 동시에 2개가 돌아가는 게 아니라, "기다리는 시간"을 활용해 다른 작업을 처리하는 것입니다.

이벤트 루프 동작 원리 (커피숍 비유) ☕ 바리스타 1명 (= 이벤트 루프 1개 = CPU 코어 1개) 손님A 주문 받기 (0.1초) → 에스프레소 머신에 넣기 → 추출 기다리는 동안 (= await) 손님B 주문 받기 (0.1초) → 머신에 넣기 → 기다리는 동안 손님A 에스프레소 완성 → 우유 스티밍 → 기다리는 동안 손님C 주문 받기 (0.1초) → ... ✅ I/O 대기(네트워크, DB)가 대부분이면 → 바리스타 1명으로 충분! ❌ 직접 원두를 갈아야 하면 (= CPU 작업) → 다른 손님 전부 대기

2. FLOPI에서의 문제

warning
모든 것이 하나의 루프를 공유
FLOPI 단일 프로세스 내부 ┌─────────────────────────────────────────────────────┐ │ asyncio Event Loop (1개) │ │ │ │ ┌─────────────┐ ┌──────────┐ ┌───────────────┐ │ │ │ FastAPI │ │ NiceGUI │ │ APScheduler │ │ │ │ 28개 API │ │ 29개 │ │ Detection │ │ │ │ 엔드포인트 │ │ 페이지 │ │ 60초 사이클 │ │ │ └──────┬──────┘ └────┬─────┘ └──────┬────────┘ │ │ │ │ │ │ │ ┌──────┴──────────────┴───────────────┴─────────┐ │ │ │ 전부 같은 이벤트 루프에서 순서대로 처리 │ │ │ └───────────────────────────────────────────────┘ │ │ │ │ M4 Pro 12코어 중... 단 1코어만 사용 │ │ 나머지 11코어: 😴 (NiceGUI가 멀티워커를 지원 안 함) │ └─────────────────────────────────────────────────────┘

3. 구체적 병목 시나리오

timeline
실제 일어나는 일: 타임라인
시나리오: Detection 사이클 + AI Chat + 대시보드 동시 접근 시간 ──────────────────────────────────────────────────→ [Detection] 규칙 100개 평가 시작 (Semaphore 20, DB 쿼리 20개 동시 await) ├── 각 DB 쿼리 await 중 다른 작업 처리 가능 ✅ └── 이상 발견! → Pipeline 트리거 → LLM 호출 (5~10초 await) [AI Chat] 사용자가 질문 입력 ├── KB 검색 (ChromaDB 쿼리 ~0.5초) ← 이건 빨리 됨 └── LLM 스트리밍 응답 (5~30초) ← SSE 연결 점유 [Dashboard] 30초마다 KPI 갱신 └── DB 쿼리 5개 → DB 풀(30) 경합 시 대기 문제 지점: • LLM 응답 대기 중 → await이라 다른 작업은 처리 가능 (여기까지는 OK) • 하지만 LLM 응답 도착 후 JSON 파싱 + 프롬프트 조립 = CPU 작업 • CPU 작업 중에는 이벤트 루프가 완전히 멈춤 • 이 순간 다른 모든 API 응답, WebSocket 메시지, SSE 스트림 중단

await 구간(네트워크 I/O)은 괜찮습니다. 문제는 await 사이의 동기 코드입니다. LLM 응답 JSON 파싱, 프롬프트 텍스트 조립, 로그 포매팅 등이 모두 이벤트 루프를 점유합니다. 개별로는 수십 ms에 불과하지만, 파이프라인 9라운드 × 동시 이상 8건 = 72회 누적되면 체감됩니다.

4. 왜 워커를 늘릴 수 없는가?

block
workers=4 → APScheduler 4배 실행
일반 FastAPI: workers=4 하면 성능 4배 Worker 1 ─── 이벤트 루프 1 → 코어 1 Worker 2 ─── 이벤트 루프 2 → 코어 2 Worker 3 ─── 이벤트 루프 3 → 코어 3 Worker 4 ─── 이벤트 루프 4 → 코어 4 → 요청이 4개 워커에 분산 ✅ FLOPI에서 workers=4 하면? Worker 1 ─── APScheduler ─── Detection 사이클 (60초마다) Worker 2 ─── APScheduler ─── Detection 사이클 (60초마다) ← 중복! Worker 3 ─── APScheduler ─── Detection 사이클 (60초마다) ← 중복! Worker 4 ─── APScheduler ─── Detection 사이클 (60초마다) ← 중복! → 이상 탐지가 4배로 실행 → DB 부하 4배 → 이상 알림 4배 중복 생성 NiceGUI도 문제: 사용자A의 WebSocket → Worker 1에 바인딩 사용자A가 새로고침 → Worker 3에 연결될 수 있음 → UI 상태 소실

일반적인 Stateless API 서버는 워커를 늘리면 되지만, FLOPI는 스케줄러(APScheduler) + 상태 유지 UI(NiceGUI)가 같은 프로세스에 있어서 워커를 늘리는 것 자체가 불가능합니다.

5. 해결 방향

lightbulb
근본적 해결: 프로세스를 역할별로 분리합니다.
목표 아키텍처: 3개 프로세스 ┌──────────────┐ ┌─────────────────┐ ┌───────────────┐ │ API 서버 │ │ 스케줄러 │ │ UI 서버 │ │ FastAPI │ │ APScheduler │ │ NiceGUI │ │ workers=4 │ │ 워커 1개 (고정) │ │ 워커 1개 │ │ 코어 4개 활용 │ │ Detection 전담 │ │ WebSocket │ │ Stateless │ │ 파이프라인 실행 │ │ 상태 유지 │ └──────┬───────┘ └────────┬────────┘ └──────┬────────┘ │ │ │ └───────────────────┴───────────────────┘ 공유: DB 풀 (분리) + Redis 큐

API는 멀티워커로 4코어 활용, 스케줄러는 단독 프로세스로 중복 실행 방지, NiceGUI는 현행대로 단일 프로세스. 프로세스 간 통신은 DB 큐 또는 Redis로 연결합니다.

sync_problem Fire-and-Forget 태스크 유실 CRITICAL

워크플로우와 플레이북 트리거가 결과를 추적하지 않습니다.

# detection/scheduler.py
asyncio.create_task(trigger_event_workflows(...))  # 결과 추적 없음
asyncio.create_task(trigger_playbooks(...))         # 실패해도 모름
report_problem
유실 시나리오
lightbulb
해결: DB 기반 Job Queue 도입 — jobs 테이블에 상태 추적 (pending → running → completed/failed) + 재시도 카운터 + 워커 폴링.

linear_scale 4-Agent 파이프라인 순차 블로킹 CRITICAL

파이프라인 내부(Diagnostician → Advisor → Validator)는 순차 실행이지만, 여러 이상 건의 파이프라인은 asyncio.create_task()로 동시에 뜰 수 있습니다. 문제는 DB 커넥션 풀(30개, 공유) + LLM Semaphore(30) + 단일 이벤트 루프를 공유하면서 발생하는 자원 경합입니다.

hourglass_top
파이프라인 1건당 자원 소모
AgentLLM 호출평균 시간최악
Diagnostician1~3 라운드3~5초15초
Advisor1~3 라운드3~5초15초
Validator1~3 라운드3~5초15초
합계최대 9회9~15초45초
stacked_line_chart
동시 처리 시 성능 변화
동시 이상LLM 호출DB 커넥션 압박체감 속도
1~5건~45회여유 (풀 30개 중 일부)쾌적 (10~20초)
8건~72회Detection과 경합 시작느려짐 (30~60초)
15건+~135회풀 경합 + LLM Semaphore(30) 포화수 분 지연, 타임아웃 발생

하드 리밋이 아니라 5건까지는 쾌적, 8건부터 성능 급락, 15건+는 시스템 불안정. DB 커넥션 풀 30개를 Detection(Semaphore 20) + 파이프라인 + API가 동시에 사용하면서 병목이 발생합니다. 에이전트 하나 실패 시 폴백 없이 해당 파이프라인 중단.

lightbulb
해결: (1) 독립적인 이상 건은 병렬 파이프라인 실행, (2) 에이전트별 타임아웃 + 서킷브레이커, (3) 실패 시 폴백 전략 (규칙 기반 즉시 대응).

storage DB 커넥션 풀 경합 (단일 풀 공유) HIGH

Oracle 커넥션 풀이 max=30으로 설정되어 있지만, Detection과 API가 동일 풀을 공유하므로 동시 부하 시 경합이 발생합니다.

warning
문제 시나리오
Detection 사이클과 API 요청이 동시에 발생할 때
Detection Cycle (60초마다) └── asyncio.Semaphore(20) 으로 최대 20개 규칙 동시 평가 └── 각 규칙이 DB 커넥션 1개 사용 └── 풀 30개 중 20개 점유 가능 동시에 들어오는 API 요청 ├── 대시보드 갱신 (KPI 쿼리) ├── AI Chat 세션 (도구 호출 + 결과 저장) ├── Tool Studio 실행 └── 남은 10개로 처리 — 부하 집중 시 대기 발생
error
영향 범위: 동시 사용자 15명까지는 안정적이나, 규칙 수 증가 + 동시 사용자 20명+이면 경합 발생. Detection과 API가 동일 풀을 공유하므로 탐지가 바쁜 시간대에 API 응답이 느려집니다.
lightbulb
해결: (1) Detection 전용 풀과 API 전용 풀 분리, (2) 커넥션 획득 실패 시 exponential backoff 재시도 로직 추가.

web NiceGUI 프레임워크 한계 HIGH

NiceGUI는 Python만으로 빠르게 UI를 만들 수 있지만, 구조적으로 수평 확장이 불가능합니다.

1. 서버사이드 UI 상태

memory
모든 UI 엘리먼트가 서버 메모리에 존재
일반 SPA (React / Next.js) 서버: HTML/JSON 보내고 끝 → Stateless 브라우저: 모든 UI 상태 관리 사용자 추가 = 서버 부하 거의 없음 NiceGUI 서버: 버튼, 테이블, 차트, 다이얼로그... 전부 Python 객체로 유지 브라우저: 서버가 보내는 DOM diff만 적용 사용자 추가 = 서버 메모리 선형 증가

FLOPI는 30개 페이지가 있고, 각 페이지마다 state 딕셔너리 + UI 컴포넌트를 서버에 유지합니다. 특히 Workflow 캔버스 (SVG + 7개 이벤트 핸들러)가 가장 무겁습니다.

2. 싱글 프로세스, 싱글 코어

error
ui.run()이 내부적으로 Uvicorn을 단일 워커로 실행합니다. 멀티워커는 NiceGUI가 공식적으로 지원하지 않습니다 — WebSocket 세션이 특정 워커에 바인딩되기 때문입니다. M4 Pro 12코어 중 NiceGUI는 1코어만 사용.

3. WebSocket 상시 연결

NiceGUI는 모든 클라이언트와 WebSocket을 상시 유지합니다. 이것이 @ui.refreshable, ui.timer(), emitEvent()의 작동 기반입니다.

사용자 수탭 수상시 WebSocket서버 영향
5명2개10개안정
20명2개40개메모리 주의
50명2개100개불안정

4. 브로드캐스트 없음

# 모든 페이지에서 이런 식으로 개별 폴링
ui.timer(30.0, header_status.refresh)  # 사용자마다 30초마다 API 호출

A가 이상 처리 완료해도 B 화면에 30초 후에야 반영. 사용자 50명이면 분당 100회 동일 API 중복 호출.

api FastAPI 운영 이슈 HIGH

FastAPI 자체는 확장성이 뛰어나지만, FLOPI에서의 사용 방식에 문제가 있습니다.

1. Uvicorn 단일 워커

bolt
워커 1개 = asyncio 이벤트 루프 1개

LLM 호출(Gemini)이 5~10초 동안 이벤트 루프를 점유하면, 같은 시간 동안 다른 API 응답도 지연됩니다.

info
workers=4로 늘리면? → APScheduler가 워커마다 중복 실행됩니다. Detection 사이클이 4배로 돌아가면서 DB 부하 폭증 + 이상 중복 생성.

2. SSE 스트리밍 연결 점유

# AI Chat 스트리밍 — 응답 올 때까지 연결 유지
@router.post("/api/ai-chat/stream")
async def chat(body):
    async def generate():
        # LLM 응답 5~30초간 이 연결 유지
        yield sse_event(...)
    return EventSourceResponse(generate())

AI Chat 사용자 5명이 동시에 질문하면 SSE 연결 5개가 수십 초간 점유. 이 동안 DB 커넥션도 물고 있을 수 있어 풀 고갈에 기여.

compare NiceGUI vs SPA 비교

web NiceGUI (현재)
UI 상태 위치서버 메모리
사용자당 서버 부하WebSocket + Python 객체
멀티워커불가
실시간 업데이트내장 (WebSocket)
수평 확장매우 어려움
개발 속도빠름 (Python만)
적정 사용자~30명
code Next.js + API (대안)
UI 상태 위치브라우저
사용자당 서버 부하Stateless (거의 없음)
멀티워커자유롭게 확장
실시간 업데이트별도 구현 필요 (SSE/WS)
수평 확장쉬움
개발 속도느림 (TS + Python)
적정 사용자수천 명
info
판단 기준: FAB 내부 운영 도구 (~30명) → NiceGUI 유지. 외부 서비스 / 상용화 (100명+) → SPA 전환 고려.

timer Detection 틱 60초 고정 MEDIUM

schedule
틱 겹침 (Tick Overlap)

현재 60초마다 run_detection_cycle()이 실행되어 due 규칙들을 평가합니다. 규칙이 100개로 늘어나면 한 틱에 평가할 양이 폭증하고, 평가가 60초를 넘으면 다음 틱과 겹칩니다.

refresh UI 30초 폴링 MEDIUM

NiceGUI 대시보드의 모든 데이터가 30초 간격 HTTP 폴링으로 갱신됩니다. 사용자 50명 × 페이지 평균 2개 타이머 = 분당 200회 API 호출 (대부분 중복).

lightbulb
개선: SSE 기반 서버 푸시로 전환 — 이상 발생/상태 변경 시에만 이벤트 전송. 비핵심 데이터(LLM 사용량, 활동 로그)는 5분 간격으로 완화.

psychology LLM 라운드 제한 MEDIUM

컨텍스트최대 라운드위험
Detection Agent3회복잡한 이상 → 불완전 진단
AI Chat5회다단계 도구 체인 중단
라운드당 타임아웃없음느린 도구가 전체 에이전트 블로킹

에이전트 수가 늘면 각각의 LLM 호출이 누적되어 Gemini API rate limit에 도달할 수 있습니다.

memory 글로벌 상태 의존 MEDIUM

# 모듈 레벨 전역 변수들
_pool = None           # Oracle 커넥션 풀 (싱글톤)
_thick_done = False    # Thick client 초기화 플래그
_current_module = ""   # LLM 호출자 컨텍스트

Uvicorn 멀티워커 전환 시 각 워커가 독립된 _pool을 생성합니다. _current_modulecontextvars가 아니라 일반 변수이므로 동시 요청 간 충돌 가능.

emergency Phase 1: 안정성 확보

1
DB 풀 분리 (Detection / API)
현재 max=30 단일 풀 → 역할별 분리
# 현재: 단일 풀 max=30 (Detection + API 공유)
# 개선: Detection 전용 풀과 API 전용 풀 분리
detection_pool = create_pool_async(min=2, max=15)
api_pool = create_pool_async(min=2, max=20)
2
DB 기반 Job Queue
fire-and-forget → 영구 추적
-- 새 테이블
CREATE TABLE job_queue (
    id           NUMBER GENERATED ALWAYS AS IDENTITY,
    job_type     VARCHAR2(50),    -- 'workflow' | 'playbook'
    payload      CLOB,             -- JSON (anomaly_id, params...)
    status       VARCHAR2(20),    -- pending | running | completed | failed
    retry_count  NUMBER DEFAULT 0,
    max_retries  NUMBER DEFAULT 3,
    created_at   TIMESTAMP DEFAULT SYSTIMESTAMP,
    started_at   TIMESTAMP,
    completed_at TIMESTAMP,
    error_msg    VARCHAR2(1000)
);
3
에이전트 타임아웃 + 서킷브레이커
LLM 장애 시 전체 파이프라인 보호
# 에이전트당 타임아웃
result = await asyncio.wait_for(
    diagnostician.run(anomaly),
    timeout=30  # 30초 초과 시 TimeoutError
)

# 서킷브레이커 (연속 5회 실패 → 60초간 우회)
from pybreaker import CircuitBreaker
llm_breaker = CircuitBreaker(fail_max=5, reset_timeout=60)

trending_up Phase 2: 처리량 개선

4
파이프라인 병렬화
독립적 이상은 동시 처리

현재 이상 10건이 들어오면 순차 처리 → 병렬화하면 동시 3~5건 처리 가능.

# 이상 건별 파이프라인 병렬 실행
pipeline_semaphore = asyncio.Semaphore(5)

async def run_with_limit(anomaly):
    async with pipeline_semaphore:
        return await pipeline.execute(anomaly)

await asyncio.gather(*[run_with_limit(a) for a in anomalies])
5
폴링 → SSE 푸시
NiceGUI 30초 폴링 제거

이상 발생/상태 변경 이벤트를 SSE로 대시보드에 즉시 전송. 비핵심 데이터는 5분 간격으로 완화하여 API 부하 80% 감소.

6
Detection 틱 최적화
30초 + 우선순위 스케줄링

틱 간격 60초 → 30초 단축. 규칙 우선순위 기반 스케줄링: Critical 규칙 먼저, Low 규칙은 유휴 시간에 평가. 동일 데이터 소스 규칙을 그룹화하여 쿼리 1회로 다수 규칙 평가.

cloud Phase 3: 멀티 인스턴스 확장

7
Redis 도입
세션 캐시 + 분산 락
8
메시지 큐 (RabbitMQ / Kafka)
fire-and-forget → 내구성 있는 이벤트 버스

DB 기반 Job Queue를 전용 메시지 큐로 교체. 워크플로우/플레이북을 별도 컨슈머 프로세스로 분리.

9
워커 프로세스 분리
역할별 독립 스케일링
API Server (Uvicorn 4 workers) └── REST API + SSE 전용, APScheduler 없음 Detection Worker (단일 프로세스) └── APScheduler + 규칙 평가 전용 Pipeline Worker (2~4 프로세스) └── 4-Agent 파이프라인 실행 전용 NiceGUI UI (1 프로세스) └── 대시보드 전용, API Server와 HTTP 통신
10
프론트엔드 SPA 전환 (선택)
100명+ 외부 서비스 시

NiceGUI를 Next.js 등 SPA로 교체. FastAPI는 API 서버로 유지. 서버 Stateless 달성 → 수평 확장 자유. 단, 개발 비용이 가장 큼.

gavel 최종 판단

check_circle
현재 상태: FAB 내부 도구로서는 충분

운영자 5~10명이 사용하는 FAB 내부 도구로서는 현재 아키텍처가 작동합니다. NiceGUI의 빠른 개발 속도가 큰 장점이고, 프로세스 분리(NiceGUI / FastAPI)도 잘 되어 있습니다.

priority_high
단, Phase 1은 지금 해야 합니다

에이전트와 규칙이 늘어나면 태스크 유실DB 풀 경합이 가장 먼저 터집니다. 이건 사용자 수와 무관하게, 시스템 자체의 안정성 문제입니다.

rocket_launch
상용화를 노린다면

Phase 2~3를 순차적으로 진행하되, NiceGUI → SPA 전환 시점을 미리 정해두세요. 사용자 30명을 넘는 순간이 전환점입니다. FastAPI API는 그대로 유지하면서 프론트엔드만 교체하면 됩니다.

남은 이슈

영역현재 상태병목?리스크
태스크 내구성fire-and-forgetYesCRITICAL
파이프라인순차 블로킹YesCRITICAL
DB 풀 경합max=30, 단일 풀 (Detection/API 공유)규칙 증가 시HIGH
NiceGUI 확장싱글 프로세스 + 서버 상태30명+HIGH
FastAPI 워커단일 워커 + Scheduler동시 요청 많을 때HIGH
Detection 틱60초 고정규칙 200개+MEDIUM
UI 폴링30초 간격사용자 50명+MEDIUM
LLM 라운드3~5회 제한복잡한 이상MEDIUM
글로벌 상태모듈 레벨 변수멀티워커 시MEDIUM
ChromaDB로컬 디스크문서 대량 시LOW

해결 완료

영역현재 값설정 위치런타임 변경
DB 커넥션 풀 크기max=30config.py:24 / 환경변수 ORACLE_MAX_POOL환경변수
Detection 동시성Semaphore(20)detection/scheduler.py:19 / DB detection.max_concurrentDB 설정
LLM 동시 호출Semaphore(30)core/llm/client.py:85 / DB llm.max_concurrentDB 설정