> For the complete documentation index, see [llms.txt](https://kopens.gitbook.io/plantpulse-platform/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kopens.gitbook.io/plantpulse-platform/developer/scheduler.md).

# 스케줄러 아키텍처

## 개요 <a href="#overview" id="overview"></a>

이 문서에서는 플랜트펄스 플랫폼의 Quartz 2.x 기반 스케줄러 아키텍처를 안내합니다. 스케줄러는 OPC 폴링, 태그 동기화, 시스템 유지보수 등 플랫폼의 주기적 작업을 관리하는 핵심 컴포넌트입니다.

## 아키텍처 <a href="#architecture" id="architecture"></a>

```mermaid
flowchart TB
  SF[JobSchedulerFactory]
  JM[JobManager<br/>잡 등록/해제/조회]
  DP[Deployer<br/>Quartz 배포]
  JS[JobSchedule]
  QT[Quartz Trigger]
  CE[Cron Expression]
  TP[JobThreadPool<br/>8 threads]
  HL[JobTriggerHistoryListener<br/>실행 이력 기록]

  SF --> JM
  JM --> DP --> JS
  JS --> QT
  JS --> CE
  JM --> TP
  JM --> HL
```

### 구성 요소

| 구성 요소                 | 역할                                   |
| --------------------- | ------------------------------------ |
| `JobSchedulerFactory` | Quartz 스케줄러 팩토리. 전체 스케줄러 인스턴스를 생성합니다 |
| `JobManager`          | 잡 등록/해제/조회를 관리합니다                    |
| `Deployer`            | 잡을 Quartz에 배포하고 트리거를 설정합니다           |
| `JobSchedule`         | 개별 잡의 스케줄 정보(Cron 표현식 등)를 보유합니다      |

## Term → Cron 변환 규칙 <a href="#term-to-cron" id="term-to-cron"></a>

플랫폼에서는 사용자 친화적인 Term(주기 용어)을 Quartz Cron 표현식으로 자동 변환합니다.

| Term  | Cron 표현식         | 설명             |
| ----- | ---------------- | -------------- |
| `1s`  | `* * * * * ?`    | 매 1초마다 실행합니다   |
| `5s`  | `0/5 * * * * ?`  | 매 5초마다 실행합니다   |
| `10s` | `0/10 * * * * ?` | 매 10초마다 실행합니다  |
| `30s` | `0/30 * * * * ?` | 매 30초마다 실행합니다  |
| `1m`  | `0 * * * * ?`    | 매 1분마다 실행합니다   |
| `5m`  | `0 0/5 * * * ?`  | 매 5분마다 실행합니다   |
| `10m` | `0 0/10 * * * ?` | 매 10분마다 실행합니다  |
| `30m` | `0 0/30 * * * ?` | 매 30분마다 실행합니다  |
| `1h`  | `0 0 * * * ?`    | 매 1시간마다 실행합니다  |
| `6h`  | `0 0 0/6 * * ?`  | 매 6시간마다 실행합니다  |
| `12h` | `0 0 0/12 * * ?` | 매 12시간마다 실행합니다 |
| `1d`  | `0 0 0 * * ?`    | 매일 자정에 실행합니다   |

## 스레드 풀 <a href="#thread-pool" id="thread-pool"></a>

### JobThreadPool

Quartz 잡 실행을 위한 전용 스레드 풀입니다.

| 항목       | 값           |
| -------- | ----------- |
| 기본 스레드 수 | 8개          |
| 최대 스레드 수 | 8개          |
| 타입       | 고정 크기 스레드 풀 |

### VirtualJobThreadPool (Java 21+)

Java 21 이상의 런타임에서는 가상 스레드(Virtual Thread) 기반 스레드 풀을 사용할 수 있습니다.

| 항목  | 설명                                   |
| --- | ------------------------------------ |
| 조건  | Java 21+ 런타임에서 자동 활성화됩니다             |
| 장점  | 스레드 수 제한 없이 경량 가상 스레드를 사용합니다         |
| 호환성 | 기존 `JobThreadPool`과 동일한 인터페이스를 제공합니다 |

## 잡 인벤토리 <a href="#job-inventory" id="job-inventory"></a>

플랫폼에는 28개 잡 클래스, 35개 등록 인스턴스가 운영됩니다.

### OPC 관련 잡 (1개 클래스)

| ID            | 클래스             | 주기        | 용도                   |
| ------------- | --------------- | --------- | -------------------- |
| `opc-polling` | `OPCPollingJob` | 설정에 따라 다름 | OPC 서버로부터 데이터를 폴링합니다 |

### Asset 관련 잡 (5개 클래스)

| ID                     | 클래스                     | 주기    | 용도              |
| ---------------------- | ----------------------- | ----- | --------------- |
| `asset-status-sync`    | `AssetStatusSyncJob`    | `1m`  | 에셋 상태를 동기화합니다   |
| `asset-health-check`   | `AssetHealthCheckJob`   | `5m`  | 에셋 헬스 체크를 수행합니다 |
| `asset-metric-collect` | `AssetMetricCollectJob` | `1m`  | 에셋 메트릭을 수집합니다   |
| `asset-alarm-evaluate` | `AssetAlarmEvaluateJob` | `10s` | 에셋 알람 조건을 평가합니다 |
| `asset-cache-refresh`  | `AssetCacheRefreshJob`  | `10m` | 에셋 캐시를 갱신합니다    |

### Tag/Point 관련 잡 (5개 클래스)

| ID                    | 클래스                    | 주기    | 용도                 |
| --------------------- | ---------------------- | ----- | ------------------ |
| `tag-sync`            | `TagSyncJob`           | `5m`  | 태그 메타정보를 동기화합니다    |
| `tag-status-update`   | `TagStatusUpdateJob`   | `1m`  | 태그 상태를 갱신합니다       |
| `point-timeout-check` | `PointTimeoutCheckJob` | `30s` | 포인트 수신 타임아웃을 검사합니다 |
| `point-map-recovery`  | `PointMapRecoveryJob`  | `5m`  | 포인트맵 불일치를 복구합니다    |
| `point-count-flush`   | `PointCountFlushJob`   | `1m`  | 포인트 카운트를 플러시합니다    |

### Site 관련 잡 (2개 클래스)

| ID                   | 클래스                   | 주기    | 용도                      |
| -------------------- | --------------------- | ----- | ----------------------- |
| `site-stats-collect` | `SiteStatsCollectJob` | `1m`  | 사이트 통계를 수집합니다           |
| `site-arch-sync`     | `SiteArchSyncJob`     | `10m` | 사이트 아키텍처(계층) 정보를 동기화합니다 |

### System 관련 잡 (9개 클래스)

| ID                        | 클래스                        | 주기    | 용도                           |
| ------------------------- | -------------------------- | ----- | ---------------------------- |
| `system-health`           | `SystemHealthJob`          | `30s` | 시스템 전체 헬스 체크를 수행합니다          |
| `system-metric-collect`   | `SystemMetricCollectJob`   | `10s` | 시스템 메트릭(CPU, 메모리 등)을 수집합니다   |
| `system-gc-monitor`       | `SystemGCMonitorJob`       | `1m`  | GC 상태를 모니터링합니다               |
| `system-disk-monitor`     | `SystemDiskMonitorJob`     | `5m`  | 디스크 사용량을 모니터링합니다             |
| `system-thread-monitor`   | `SystemThreadMonitorJob`   | `1m`  | 스레드 상태를 모니터링합니다              |
| `system-connection-check` | `SystemConnectionCheckJob` | `1m`  | 외부 연결(DB, Kafka 등) 상태를 점검합니다 |
| `system-log-cleanup`      | `SystemLogCleanupJob`      | `1d`  | 오래된 로그 파일을 정리합니다             |
| `system-temp-cleanup`     | `SystemTempCleanupJob`     | `6h`  | 임시 파일을 정리합니다                 |
| `system-backup-check`     | `SystemBackupCheckJob`     | `12h` | 백업 상태를 점검합니다                 |

### Report 관련 잡 (1개 클래스)

| ID             | 클래스              | 주기   | 용도                              |
| -------------- | ---------------- | ---- | ------------------------------- |
| `report-flush` | `ReportFlushJob` | `1m` | 인메모리 리포트 카운터를 Cassandra에 플러시합니다 |

### Edge 관련 잡 (1개 클래스)

| ID               | 클래스                | 주기    | 용도                   |
| ---------------- | ------------------ | ----- | -------------------- |
| `edge-heartbeat` | `EdgeHeartbeatJob` | `30s` | 엣지 디바이스의 하트비트를 확인합니다 |

### Plugin 관련 잡 (3개 클래스)

| ID                   | 클래스                   | 주기   | 용도                        |
| -------------------- | --------------------- | ---- | ------------------------- |
| `plugin-oee-calc`    | `PluginOEECalcJob`    | `1m` | OEE(설비종합효율)를 계산합니다        |
| `plugin-ram-calc`    | `PluginRAMCalcJob`    | `5m` | RAM(신뢰성/가용성/유지보수성)을 계산합니다 |
| `plugin-ems-collect` | `PluginEMSCollectJob` | `1m` | EMS(에너지 관리) 데이터를 수집합니다    |

## 이력 관리 <a href="#history-management" id="history-management"></a>

### JobTriggerHistoryListener

모든 잡 실행의 이력을 기록하는 Quartz `TriggerListener` 구현체입니다.

| 기록 항목  | 설명                  |
| ------ | ------------------- |
| 잡 ID   | 실행된 잡의 식별자입니다       |
| 시작 시간  | 잡 실행 시작 시간입니다       |
| 종료 시간  | 잡 실행 종료 시간입니다       |
| 실행 시간  | 잡 실행 소요 시간 (밀리초)입니다 |
| 실행 결과  | 성공/실패 여부입니다         |
| 에러 메시지 | 실패 시 예외 메시지입니다      |

이력 데이터는 Cassandra에 저장되며, 관리 콘솔에서 조회할 수 있습니다.

## 설정 프로퍼티 <a href="#configuration" id="configuration"></a>

| 프로퍼티                               | 기본값     | 설명                                             |
| ---------------------------------- | ------- | ---------------------------------------------- |
| `scheduler.enabled`                | `true`  | 스케줄러 활성화 여부입니다                                 |
| `scheduler.thread.count`           | `8`     | Quartz 스레드 풀 크기입니다                             |
| `scheduler.virtual.thread.enabled` | `auto`  | 가상 스레드 사용 여부입니다. `auto`이면 Java 21+ 시 자동 활성화됩니다 |
| `scheduler.misfire.threshold.ms`   | `60000` | Misfire 판단 임계값 (밀리초)입니다                        |
| `scheduler.history.enabled`        | `true`  | 실행 이력 기록 활성화 여부입니다                             |
| `scheduler.history.retention.days` | `30`    | 이력 보관 기간 (일)입니다                                |

## 잡 실행 중복 방지 <a href="#duplicate-prevention" id="duplicate-prevention"></a>

### JobStatus ConcurrentHashMap

동일한 잡이 이전 실행이 완료되지 않은 상태에서 다시 트리거되는 것을 방지합니다.

```
┌─────────────────────────────────────────────────┐
│  JobStatus (ConcurrentHashMap<String, Boolean>)  │
│                                                   │
│  잡 실행 시작 ──▶ map.put(jobId, true)            │
│  잡 실행 종료 ──▶ map.remove(jobId)               │
│                                                   │
│  트리거 시점:                                      │
│  if (map.containsKey(jobId)) {                    │
│      // 이전 실행 중 → 스킵                        │
│      log.warn("Job already running: {}", jobId);  │
│  }                                                │
└─────────────────────────────────────────────────┘
```

`ConcurrentHashMap`을 사용하여 다중 스레드 환경에서도 안전하게 중복 실행을 감지합니다.

## 주요 임계값 <a href="#thresholds" id="thresholds"></a>

| 임계값         | 값    | 설명                          |
| ----------- | ---- | --------------------------- |
| 스레드 풀 크기    | 8    | 동시 실행 가능한 최대 잡 수입니다         |
| Misfire 임계값 | 60초  | 이 시간 이상 지연되면 Misfire로 판정합니다 |
| 잡 타임아웃      | 300초 | 잡 실행이 이 시간을 초과하면 경고를 발생시킵니다 |
| 이력 보관 기간    | 30일  | 이 기간이 지난 이력은 자동 삭제됩니다       |
| 최대 재시도 횟수   | 3회   | 잡 실행 실패 시 최대 재시도 횟수입니다      |
