시스템 아키텍처 가이드
Caffeine 플랫폼의 시스템 아키텍처, 미들웨어 파이프라인, DTO 분리 패턴, DI 등록 전략에 대한 상세 가이드이다.
4-Tier 계층 구조
Caffeine은 명확한 4계층(Presentation, Engine, Edge, Driver) 아키텍처로 설계되어 있으며, 각 계층은 독립적으로 배포 및 확장이 가능하다.
계층별 역할
| 계층 | 프로젝트 | 역할 |
|---|---|---|
| Presentation | Admin, Mobile, CLI, Client SDK | 사용자 인터페이스 및 외부 연동 |
| Engine | Caffeine.Engine | 중앙 오케스트레이터. REST, gRPC, SignalR, GraphQL, MQTT, Kafka 통합 |
| Edge | Caffeine.Bridge.Host | 엣지 게이트웨이. 드라이버 동적 로드(Plugin Architecture) |
| Driver | Caffeine.Drivers.* | IDriverModule 인터페이스 구현. 산업 프로토콜 플러그인 |
미들웨어 파이프라인 (Engine)
Engine의 HTTP 요청 처리 파이프라인은 다음 순서로 구성된다.
ExceptionHandlingMiddleware
파이프라인 최상단에 위치하여 모든 처리되지 않은 예외를 catch한다. 내부 에러 상세(스택 트레이스, DB 쿼리 등)를 외부에 노출하지 않으며, 일관된 JSON 에러 응답을 반환한다.
위치: src/Caffeine.Engine/Middleware/ExceptionHandlingMiddleware.cs
동작 방식:
RequestDelegate를 호출하여 다음 미들웨어로 요청 전달- 예외 발생 시
ILogger로 에러 로깅 (경로 정보 포함) - 예외 타입별 HTTP 상태코드 분기 (v2.2.1+):
ValidationException→ 400 (Bad Request)KeyNotFoundException→ 404 (Not Found)UnauthorizedAccessException→ 403 (Forbidden)OperationCanceledException→ 499 (Client Closed Request)- 기타 예외 → 500 (Internal Server Error)
ApiErrorResponse형식의 JSON 응답 반환
에러 응답 형식 (ApiErrorResponse):
{
"code": "INTERNAL_SERVER_ERROR",
"message": "서버 내부 오류가 발생했습니다.",
"traceId": "00-abc123...",
"details": "..." // Development 환경에서만 포함 (v2.2.1+)
}
code: 에러 분류 코드 (문자열)message: 사용자에게 표시할 안전한 메시지 (내부 상세 미포함)traceId: OpenTelemetry Activity ID 또는 ASP.NET Core TraceIdentifierdetails: 스택 트레이스 (Development 환경에서만, v2.2.1+)
JWT Bearer Authentication (v2.2.1+)
모든 REST API 엔드포인트에 대해 JWT Bearer 토큰 인증을 강제한다.
위치: src/Caffeine.Engine/Program.cs (Middleware 등록)
동작 방식:
- HTTP 요청 헤더에서
Authorization: Bearer <token>추출 - JWT 서명 검증 (
Issuer,Audience,Secret) - 토큰 유효성 검사 (만료 시간, 클레임)
- 성공 시
HttpContext.User에 ClaimsPrincipal 설정 - 실패 시 401 Unauthorized 반환
설정 (appsettings.json):
{
"Jwt": {
"Issuer": "Caffeine.Engine",
"Audience": "Caffeine.Client",
"Secret": "your-secret-key-here"
}
}
Controller 인증 강제:
[Authorize] // v2.2.1+: 모든 Controller에 적용
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class AlarmController : ControllerBase
{
// 모든 Action 메서드에 인증 필수
}
파이프라인 등록 순서 (Program.cs)
1. ExceptionHandlingMiddleware (전역 예외 처리 -- 최우선)
2. Authentication Middleware (JWT Bearer 인증 -- v2.2.1+)
3. gRPC-Web Middleware (gRPC-Web 프로토콜 지원)
4. WebSocket Middleware (GraphQL Subscription용)
5. CORS Middleware (Admin UI 연동)
6. Authorization Middleware ([Authorize] 속성 검증 -- v2.2.1+)
7. Controller / gRPC / SignalR / GraphQL 매핑
상세한 미들웨어 사용 가이드는 미들웨어 가이드를 참조한다.
DTO 분리 패턴
Controller에서 도메인 엔티티를 직접 노출하지 않고, 전용 DTO(Data Transfer Object)를 사용하여 API 계약을 관리한다.
DTO 위치 및 구조
| DTO | 위치 | 용도 |
|---|---|---|
ApiErrorResponse | Caffeine.Core/Models/ | 통합 에러 응답 (모든 Controller 공용) |
CreateSilenceRequest | Caffeine.Engine/DTOs/ | Alert Silence 생성 요청 |
WebhookResponse | Caffeine.Engine/DTOs/ | Alertmanager Webhook 처리 결과 |
LicenseStatusDto, LicenseUploadRequest 등 | Caffeine.Engine/DTOs/ | 라이선스 관리 API |
배치 원칙
- Core 계층 DTO (
Caffeine.Core/Models/): 여러 계층에서 공유되는 공통 모델 (예:ApiErrorResponse) - Engine 계층 DTO (
Caffeine.Engine/DTOs/): Engine API에 특화된 요청/응답 모델 - 도메인 엔티티와 DTO 간 변환은 Controller 내부에서 직접 수행 (별도 Mapper 미사용)
Controller 표준 패턴
모든 REST Controller는 다음 표준을 따른다.
API Versioning
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[resource]")]
public class ResourceController : ControllerBase
Asp.Versioning패키지 사용- URL 경로 기반 버전 관리:
/api/v1/... - 기본 버전: 1.0 (미지정 시 자동 적용)
반환 타입
- 성공:
Ok(data),CreatedAtAction(...),NoContent() - 실패:
BadRequest(new { message = "..." }),NotFound(...),StatusCode(500, ...) ProducesResponseType어트리뷰트로 Swagger 문서 자동 생성
Controller 목록
| Controller | 경로 | 역할 |
|---|---|---|
AlarmController | /api/v1/alarms | 활성 알람 조회, 이력 조회, 알람 확인(Acknowledge) |
AlertHistoryController | /api/v1/alert-history | 알림 히스토리 페이징 조회, 심각도/소스별 통계 |
AlertSilenceController | /api/v1/alert-silence | Silence 규칙 CRUD |
AlertmanagerController | /api/v1/alertmanager | Prometheus Alertmanager Webhook 수신 |
ConfigController | /api/v1/config | 태그 설정 조회/일괄 등록 (Excel Import/Export) |
DriverController | /api/v1/drivers | 드라이버 설정 조회/업데이트 |
LicenseController | /api/v1/license | 라이선스 상태 조회, 업로드, 재검증, 기능/이력 조회 |
PredictiveMaintenanceController | /api/v1/predictive-maintenance | 고장 예측, RUL, 이상 탐지, 모델 재학습 |
DI 등록 전략
등록 위치 분리
Caffeine의 서비스 등록은 계층별로 분리되며, 명확한 소유권 원칙을 따른다.
CaffeineServiceCollectionExtensions.AddCaffeine() (Caffeine.Core):
- 데이터 코덱 (
IDataCodec<double>,IDataCodec<short>,IDataCodec<int>) Singleton 등록 - Big-Endian/Little-Endian 코덱 옵션 지원 (
CaffeineOptions.UseBigEndianCodecs) AddCaffeine(o => o.UseBigEndianCodecs = true)한 줄 호출로 코어 서비스 일괄 등록
InfrastructureExtensions.AddCaffeineInfrastructure():
- 외부 서비스 어댑터 (Redis, InfluxDB, TypeDB)
- 메시징 (NullMessageProducer, RxEventBus)
- ML 서비스 (PredictiveMaintenanceService 등)
- 알림 시스템 (AlertDeduplicator, AlertSilenceService 등)
Engine/Program.cs (Composition Root):
- 백그라운드 워커 (HostedService 등록)
- gRPC/SignalR 엔드포인트
- Engine 전용 서비스 (ReasoningEngine, AlarmManagerService 등)
- 라이선스 서비스
NullMessageProducer (Null Object 패턴)
Kafka 메시지 브로커가 구성되지 않은 환경에서 안전한 폴백을 제공한다.
위치: src/Caffeine.Infrastructure/Messaging/NullMessageProducer.cs
IMessageProducer (Core 인터페이스)
|
+-- KafkaMessageProducer (Kafka 연결 시)
+-- NullMessageProducer (Kafka 미연결 시 -- No-op)
InfrastructureExtensions에서 기본 등록:services.AddSingleton<IMessageProducer, NullMessageProducer>()ProduceAsync()호출 시 Debug 로그만 기록하고 즉시 반환- Engine이 Kafka 없이도 정상 기동 가능 (Graceful Degradation)
백그라운드 워커
Engine에는 13개의 BackgroundService 워커가 등록되어 있다.
| 워커 | 역할 |
|---|---|
IngestionWorker | 데이터 수집 파이프라인 구동 (Channel 기반) |
HistorianService | 시계열 데이터 InfluxDB 저장 |
ReasoningService | AI 추론 및 이상 감지 |
AlarmManagerService | 알람 관리 V2 (SQLite 기반) |
AlarmService | Reactive Extensions 기반 온도 알람 |
HealthCheckAlertService | Health Check 기반 알림 |
RedisBulkService | Redis 대량 쓰기 |
MqttListenerService | MQTT 메시지 리스너 |
SecsGemService | SECS/GEM 시뮬레이션 연동 |
ModelLoaderService | 시작 시 태그 모델 로드 |
TagStreamingService | 실시간 태그 값 SignalR 브로드캐스트 |
MetricReporterService | Prometheus 메트릭 수집 |
BackgroundLicenseMonitorService | 라이선스 상태 주기적 모니터링 |
외부 서비스 통합
Data Store
| 서비스 | 라이브러리 | 역할 | Graceful Degradation |
|---|---|---|---|
| Redis | StackExchange.Redis | 실시간 캐시, Shadow Repository | AbortOnConnectFail = false |
| InfluxDB | InfluxDB.Client | 시계열 저장, ML 피처 추출 | 토큰 미설정 시 NullTraceWriter |
| TypeDB | TypeDB Driver | 지식 그래프 | Lazy Connection |
| SQLite | EF Core | 알람 이력, 부품 교체 이력 | 로컬 파일 기반 |
Messaging
| 서비스 | 역할 | 폴백 |
|---|---|---|
| Kafka | 이벤트 스트리밍 | NullMessageProducer |
| MQTT | 장비 메시지 수신 | 환경변수 미설정 시 비활성화 |
| RxEventBus | In-Memory Reactive 이벤트 버스 | 항상 활성 |
통신 프로토콜
| 프로토콜 | 포트 | 용도 |
|---|---|---|
| HTTP/REST | 5001 | Swagger API, Admin UI 연동 |
| gRPC (h2c) | 5050 | Bridge 양방향 스트리밍 |
| SignalR | 5001 (/hubs/alarms, /hubs/monitoring) | 실시간 Push |
| GraphQL | 5001 (/graphql) | Query, Mutation, Subscription |
| MQTT | 외부 브로커 | 장비 메시지 수신 |
| Kafka | 외부 브로커 | 이벤트 스트리밍 |
멀티테넌시 아키텍처 (v2.4+)
Caffeine은 TenantScope(AsyncLocal) + ITenantContext(Scoped) 이중 구조로 멀티테넌시를 지원한다. 기존 싱글 테넌트 환경과 완전히 호환되며, MultiTenancy.Enabled = true 설정 시 활성화된다.
테넌트 해석 흐름
이중 컨텍스트 구조
| 구조 | 라이프타임 | 용도 | 접근 방법 |
|---|---|---|---|
TenantScope | AsyncLocal (static) | Singleton 서비스 (InfluxDB, Redis, TypeDB) | TenantScope.GetEffectiveTenantId() |
ITenantContext | Scoped (DI) | Controller, Blazor, Handler | 생성자 주입 |
이중 구조의 이유: InfluxDB 클라이언트, Redis ConnectionMultiplexer 등은 Singleton으로 등록되어 Scoped DI에 접근할 수 없다. AsyncLocal은 async/await 체인을 따라 자동 전파되므로 Singleton에서도 현재 테넌트를 읽을 수 있다.
Blazor Server 특수 처리
Blazor Server에서는 각 렌더링 사이클이 새로운 async 컨텍스트에서 실행되어 AsyncLocal이 초기화된다. TenantCircuitHandler가 CircuitHandler.OnCircuitOpenedAsync와 OnConnectionUpAsync에서 TenantScope를 자동 복원한다.
데이터 격리
- EF Core Global Query Filter:
IMultiTenantEntity를 구현한 엔티티에 자동WHERE TenantId = @tenantId적용 - 저장소 자동 할당:
JsonFileRepository,SqliteAlarmRepository가 저장 시TenantScope.GetEffectiveTenantId()로 자동 설정
Edge↔Cloud 통신 추상화 (v2.4+)
Bridge(Edge)와 Engine(Cloud) 간의 통신을 IEdgeCloudTransport 인터페이스로 추상화하여, 전송 프로토콜을 DI 등록만으로 교체할 수 있다.
통신 아키텍처
Transport 교체 전략
| Transport | 환경 | 프로토콜 | 장점 |
|---|---|---|---|
GrpcEdgeCloudTransport | LAN | gRPC h2c (양방향 스트리밍) | 고성능, 낮은 지연, Protobuf 직렬화 |
MqttEdgeCloudTransport | WAN | MQTT v5 (QoS 1) | 방화벽 관통, 테넌트 인식 토픽, LWT 지원 |
교체 방법: DI 등록만 변경하면 BridgeWorker 코드 수정 없이 전환된다.
// LAN 환경 (기본)
services.AddSingleton<IEdgeCloudTransport, GrpcEdgeCloudTransport>();
// WAN 환경
services.AddSingleton<IEdgeCloudTransport, MqttEdgeCloudTransport>();
오프라인 복원력
SqliteOfflineBuffer는 네트워크 단절 시 메시지를 SQLite에 영속 저장한다.
| 항목 | 기본값 | 설명 |
|---|---|---|
| 최대 크기 | 100 MB | 초과 시 FIFO 순서로 오래된 메시지 삭제 |
| 메시지 TTL | 24시간 | 만료 시 자동 정리 |
| Flush 전략 | FIFO | 순서 보장 전송, 실패 시 즉시 중단 |
복원 시나리오:
- 네트워크 단절 감지 → 전송 실패한 메시지를
IOfflineBuffer.EnqueueAsync()로 저장 - SQLite에 FIFO 순서로 영속 (프로세스 재시작에도 보존)
TransportState.Connected이벤트 수신 →FlushAsync()로 일괄 재전송- 전송 성공한 메시지만 삭제, 실패 시 중단하여 순서 보장
MQTT 테넌트 인식 토픽
MqttEdgeCloudTransport는 토픽에 테넌트 ID를 포함하여 멀티테넌트 환경에서 데이터를 격리한다.
caffeine/{tenantId}/{driverId}/data — 드라이버 데이터 (Outbound)
caffeine/{tenantId}/{driverId}/command — Engine 명령 (Inbound, 구독)
caffeine/{tenantId}/{driverId}/status — 온라인/오프라인 (Retained, LWT)
참고 자료
- 아키텍처 개요 -- 4-Tier 구조 및 프로젝트 의존성 상세
- 미들웨어 가이드 -- ExceptionHandlingMiddleware 상세
- 설정 가이드 -- 환경변수 및 appsettings 상세
- Best Practices -- 성능 최적화 및 보안 가이드
- 멀티테넌시 API -- 멀티테넌시 인터페이스 레퍼런스
- Edge↔Cloud 통신 API -- 통신 추상화 레퍼런스
작성일: 2026-02-11 버전: 2.4