# 시스템 아키텍처

> X-Edge AI 박스가 어떻게 구성되는지 — 서비스, 데이터소스 플러그인, 추론 파이프라인, 실시간 이벤팅, 로깅.

하나의 X-Edge AI 박스는 자기 완결적인 3개 서비스 스택을 실행합니다. 모든 것 — 구성, 모델
버전, 이력, 로그 — 이 박스에 존재하며, 런타임에 클라우드 의존성이 없습니다.

```mermaid
flowchart LR
  subgraph Box["X-Edge AI Box (Docker)"]
    FE["Frontend\nnginx + React"] -->|REST + SignalR| BE["Backend\nASP.NET orchestration"]
    BE -->|gRPC :50051| INF["Inference runtime\nPython + ONNX Runtime"]
    BE --- DB[("SQLite\nconfig · history · logs")]
    PLUG["Datasource plugins\nOPC UA · MQTT · CSV"] --- BE
  end
  SENSORS["PLCs · brokers · devices"] --> PLUG
  PLUG -->|predictions written back| SENSORS
  USER["Operator browser"] --> FE
```

| 서비스 | 역할 | 포트 |
|---|---|---|
| Frontend | 대시보드 UI, nginx로 제공 | 80 (TLS 사용 시 443) |
| Backend | 오케스트레이션: 데이터소스, 모델, 추론 수명 주기, 인증, 영속화 | 5000 |
| Inference | CPU/CUDA/TensorRT에서 ONNX 모델 실행 | 50051 (gRPC) |

## 데이터소스 플러그인

산업 프로토콜은 백엔드가 시작 시 로드하는 **플러그인**으로 구현됩니다. 각 플러그인은 자신의
연결 설정을 타입이 지정된 스키마로 서술하며, UI는 이를 동적 폼으로 렌더링합니다 — 프로토콜을
추가해도 새 UI 빌드가 필요하지 않습니다.

- **입력(읽기 경로):** OPC UA(매 틱마다 폴링), MQTT(오래됨 방지 가드가 있는 푸시),
  CSV 재생.
- **출력(쓰기 경로):** OPC UA, MQTT, 회전형 CSV 파일. 출력 태그의 주소는 그대로
  사용되며(MQTT 토픽, OPC UA NodeId, CSV 열), 각 값은 `value × scale + offset`으로
  스케일링됩니다.

두 경로는 **의도적으로** 서로 다르게 실패합니다: 죽은 *입력* 태그는 추론을 눈에 띄게
멈춥니다(모델에 오래된 데이터를 공급하는 것은 이상 탐지에서 최악의 실패 모드입니다). 반면
실패하는 *출력* 싱크는 결코 추론을 멈추지 않으며, 대신 합쳐진 **Output write failed** 알림을
발생시킵니다.

## 추론 파이프라인

활성화된 하나의 입력 데이터소스가 페어링된 하나의 모델에 공급합니다:

1. 백엔드는 데이터소스의 **샘플링 속도**로 매핑된 태그를 샘플링하고, 이를 gRPC를 통해 추론
   런타임으로 스트리밍합니다.
2. 런타임은 `window size × tag count` 샘플의 슬라이딩 윈도우를 유지합니다. 가득 차면 새 샘플
   하나마다 예측(`health_score`, `failure_probability`, `rul_normalized`)을 생성합니다.
3. 예측은 다시 흘러나와 대시보드(SignalR), 이력 저장소, 구성된 출력 싱크, 그리고 박스 내
   SQLite 감사 추적으로 분배됩니다.

따라서 처리량은 발행자의 속도가 아니라 **샘플링 속도 × 데이터소스**에 따라 확장됩니다 — 100Hz
센서를 10Hz로 샘플링하면 초당 10건의 예측이 산출됩니다.

**배압(Backpressure)**은 소비자별로 적용됩니다: 느린 대시보드가 추론을 스로틀하도록 결코
허용되지 않으며(그들의 이벤트가 먼저 드롭됨), 출력 싱크는 싱크가 따라잡지 못할 때 파이프라인을
엔드투엔드로 늦추는 경계가 있는 무손실 큐를 받고, 비유한(NaN/Inf) 예측은 필터링되고
카운트됩니다. 이 모두는 카운터로 확인할 수 있습니다 — [모니터링](/ko/operate/monitoring/)을
참조하세요.

## 모델 수명 주기

모델은 [modelctl](/ko/configure/prepare-models/)로 박스 밖에서 작성되고(변환 → 검증 → 양자화
→ 패키징), 그다음 UI를 통해 업로드됩니다. 업로드는 로드하지 않고 추론 런타임을 통해 ONNX
계약(형상 + SHA-256)을 검증합니다. 모델은 데이터소스가 **활성화**될 때만 엔진에 진입합니다.
로딩은 무중단 핫스왑이며, 버전은 불변입니다 — 같은 버전으로 변경된 파일을 다시 업로드하면
거부됩니다. 롤백은 이전의 유효한 버전을 재활성화합니다.

## 실시간 이벤팅

라이브 대시보드 업데이트(센서 배치, 예측, 헬스, 상태 변경, 알림)는 SignalR WebSocket 허브를
통해 이동합니다. 이 스트림은 **발사 후 망각(fire-and-forget) 및 인메모리** 방식입니다 — 결코
진실의 원천이 아닙니다. 브라우저가 연결을 끊으면 놓친 이벤트는 재전송되지 않으며, 권위 있는
상태는 항상 REST API와 박스 내 데이터베이스에서 옵니다.

이벤팅 백플레인은 인메모리이며, 이는 **단일 노드 박스 배포에서만** 올바릅니다. 하나의 박스에
대해 두 개의 백엔드 인스턴스를 실행하지 마세요(예: 로드 밸런서 뒤에서) — 클라이언트가 조용히
이벤트를 놓칠 것입니다. 여러 박스의 플릿은 괜찮습니다: 각 박스는 자체 단일 노드 시스템입니다.

## 로깅 — 세 개의 경계가 있는 계층

| 계층 | 목적 | 경계 |
|---|---|---|
| 컨테이너 stdout(`docker logs`) | 라이브 디버깅 | 서비스당 10 MB × 3 파일 |
| 영속 볼륨의 회전형 파일(Warning+) | 사후 포렌식 — 컨테이너 재생성 및 데이터베이스 손상에서 살아남음 | 10 MB × 7(백엔드) / × 6(추론) |
| SQLite 로그 저장소 | LogViewer 페이지 및 알림 경보 | 10,000개 항목, 60초마다 정리 |

틱별 파이프라인 라인은 Debug 레벨로 로깅됩니다. 기본 Info 스트림은 수명 주기 이벤트, 하트비트,
경고만 전달합니다.

## 구성 가져오기 / 내보내기

관리자는 전체 추론 구성(태그 매핑을 포함한 입력 및 출력 데이터소스)을 하나의 버전 관리된 JSON
엔벨로프로 내보내고 다른 박스에서 다시 가져올 수 있습니다. 비밀은 내보내기 시 마스킹됩니다.
가져오기는 단일 트랜잭션으로 실행됩니다 — 항목별 실패가 하나라도 있으면 파일 전체를 롤백하고
전체 오류 목록을 반환합니다.

## 다음 단계

  - [제품 개요](/ko/product-overview/) — 박스가 하는 일과 대상 사용자.
  - [입력 데이터소스 연결](/ko/configure/input-datasources/) — OPC UA, MQTT, CSV를 파이프라인에 연결합니다.
  - [모니터링](/ko/operate/monitoring/) — 지연 시간 분석, 배압 카운터, 헬스.
  - [gRPC 추론 API](/ko/api-reference/grpc-inference/) — 백엔드 ↔ 추론 런타임 계약.
