# API Gateway 통계 수집 모듈

분단위로 API 거래 통계를 수집하고 DB에 저장하는 모듈입니다.

## 아키텍처

```
거래 발생
    ↓
TransactionEvent 생성
    ↓
ApiStatsCollector.recordEvent()  ← 거래마다 호출
    ↓
MinuteStatsAggregator (메모리 집계)
    ↓
ApiStatsScheduler (매 분 5초 실행)
    ↓
DB 저장 (API_STATS_MINUTE 테이블)
```

## 패키지 구조

```
com.eactive.eai.custom.statistics/
├── dto/
│   ├── TransactionEvent.java      - 거래 이벤트 DTO
│   └── MinuteStatsKey.java        - 집계 키 (시간 + 차원)
├── collector/
│   ├── MinuteStatsAggregator.java - HdrHistogram 기반 집계
│   └── ApiStatsCollector.java     - 메인 수집기 (ConcurrentHashMap)
├── scheduler/
│   └── ApiStatsScheduler.java     - 스케줄러 (매 분 실행)
├── ApiStatsMinuteRepository.java  - JPA Repository
└── ApiStatsMinuteLogger.java      - DB 저장 Logger
```

## 사용법

### 1. 거래 처리 시 통계 수집

거래가 발생할 때마다 `ApiStatsCollector.recordEvent()` 호출:

```java
@Service
public class ApiGatewayService {

    @Autowired
    private ApiStatsCollector statsCollector;

    public void processTransaction(ApiRequest request) {
        long startTime = System.currentTimeMillis();

        try {
            // 거래 처리
            ApiResponse response = executeApi(request);

            // 통계 수집 (성공)
            recordStats(request, startTime, true, null);

            return response;

        } catch (TimeoutException e) {
            // 통계 수집 (Timeout)
            recordStats(request, startTime, false, ErrorType.TIMEOUT);
            throw e;

        } catch (SystemException e) {
            // 통계 수집 (System Error)
            recordStats(request, startTime, false, ErrorType.SYSTEM_ERROR);
            throw e;

        } catch (BusinessException e) {
            // 통계 수집 (Business Error)
            recordStats(request, startTime, false, ErrorType.BUSINESS_ERROR);
            throw e;
        }
    }

    private void recordStats(ApiRequest request, long startTime,
                           boolean success, ErrorType errorType) {
        long responseTime = System.currentTimeMillis() - startTime;

        TransactionEvent event = TransactionEvent.builder()
            .apiName(request.getApiName())
            .gwInstanceId(getInstanceId())
            .bizDivCode(TransactionEvent.normalizeValue(request.getBizDivCode()))
            .clientId(TransactionEvent.normalizeValue(request.getClientId()))
            .inboundAdapter(TransactionEvent.normalizeValue(request.getInboundAdapter()))
            .outboundAdapter(TransactionEvent.normalizeValue(request.getOutboundAdapter()))
            .responseTimeMs(responseTime)
            .success(success)
            .errorType(errorType)
            .build();

        statsCollector.recordEvent(event);
    }
}
```

### 2. 스케줄러 자동 실행

`ApiStatsScheduler`가 자동으로 매 분 5초에 실행되어 DB에 저장합니다.

```
매 분 5초 (예: 10:00:05, 10:01:05, ...)
  ↓
완료된 분의 통계를 flush (예: 10:00:00 분 데이터)
  ↓
DB 저장
```

### 3. 스케줄러 활성화

Spring Boot 애플리케이션에 `@EnableScheduling` 추가:

```java
@SpringBootApplication
@EnableScheduling  // ← 스케줄러 활성화
public class EapimOnlineApplication {
    public static void main(String[] args) {
        SpringApplication.run(EapimOnlineApplication.class, args);
    }
}
```

## 주요 클래스 설명

### TransactionEvent

거래 1건의 정보를 담는 DTO:
- API명, Instance ID, 업무구분코드, Client ID
- Inbound/Outbound Adapter
- 응답시간, 성공 여부, 오류 유형

### MinuteStatsAggregator

분단위 집계를 수행:
- **HdrHistogram**: P50, P95 백분위 계산
- **AtomicLong**: 건수 카운터 (Thread-safe)
- **snapshot()**: 집계 결과를 Entity로 변환

### ApiStatsCollector

메모리에서 통계 수집:
- **ConcurrentHashMap**: 멀티스레드 안전
- **computeIfAbsent()**: Lock-free 집계
- **flushCompletedMinutes()**: 완료된 분의 데이터 반환

### ApiStatsScheduler

매 분마다 DB 저장:
- **@Scheduled(cron = "5 * * * * ?")**: 매 분 5초 실행
- **@PreDestroy**: 종료 시 남은 데이터 저장

## 성능 특징

### 메모리 효율

- **HdrHistogram**: 수만 건을 2~4KB로 압축
- **ConcurrentHashMap**: 차원별로 독립적인 집계
- **분 단위 flush**: 메모리 누적 방지

### Thread-safe

- **ConcurrentHashMap**: Lock-free read
- **AtomicLong**: Lock-free increment
- **synchronized**: HdrHistogram 기록 시만 사용

### 성능 영향 최소화

- 거래당 오버헤드: < 1ms (메모리 연산만)
- DB 저장: 비동기 (스케줄러)
- Lock contention: 최소화

## 설정

### PropManager를 통한 동적 설정 (권장)

운영 중에 변경 가능한 설정값을 PropManager로 관리합니다.

#### 1. DB에 설정값 등록

PropManager 그룹명: **`ApiStats`**

| 키 | 설명 | 기본값 | 예시 |
|---|---|---|---|
| `enabled` | 통계 수집 활성화 여부 | true | true: 활성화, false: 비활성화 |
| `histogram.min.ms` | 히스토그램 최소값 (ms) | 1 | 응답시간 최소 기록값 |
| `histogram.max.ms` | 히스토그램 최대값 (ms) | 60000 | 60초 초과 시 클리핑 |
| `histogram.precision` | 히스토그램 정밀도 (유효숫자) | 3 | 3자리 정밀도 |

**DB 설정 예시:**
```sql
-- TSEAIRM24 테이블에 설정값 추가
INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION)
VALUES ('ApiStats', 'enabled', 'true', '통계 수집 활성화 여부');

INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION)
VALUES ('ApiStats', 'histogram.min.ms', '1', '히스토그램 최소값(ms)');

INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION)
VALUES ('ApiStats', 'histogram.max.ms', '60000', '히스토그램 최대값(ms)');

INSERT INTO TSEAIRM24 (GROUP_NAME, PROP_KEY, PROP_VALUE, DESCRIPTION)
VALUES ('ApiStats', 'histogram.precision', '3', '히스토그램 정밀도');
```

#### 2. 설정 변경 방법

**통계 수집 OFF (긴급 상황 시)**
```sql
-- 통계 수집 비활성화
UPDATE TSEAIRM24
SET PROP_VALUE = 'false'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled';
```

```java
// PropManager 리로드 (즉시 적용)
PropManager.getInstance().reload("ApiStats");
```

**통계 수집 ON (다시 활성화)**
```sql
-- 통계 수집 재활성화
UPDATE TSEAIRM24
SET PROP_VALUE = 'true'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled';
```

```java
// PropManager 리로드
PropManager.getInstance().reload("ApiStats");
```

**히스토그램 범위 변경**
```sql
-- 최대값을 300초로 변경
UPDATE TSEAIRM24
SET PROP_VALUE = '300000'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'histogram.max.ms';
```

```java
// PropManager 리로드
PropManager.getInstance().reload("ApiStats");
```

**방법 2: 관리 화면을 통한 수정**
- EMS 관리 화면에서 PropManager 설정 수정
- 저장 시 자동으로 메모리에 반영

#### 3. 설정값 확인

애플리케이션 시작 시 로그에서 확인:
```
ApiStatsScheduler initialized
ApiStatsConfig] enabled = true
ApiStatsConfig] histogram.min.ms = 1
ApiStatsConfig] histogram.max.ms = 60000
ApiStatsConfig] histogram.precision = 3
```

**비활성화 시 로그:**
```
ApiStatsConfig] enabled = false
API stats collection is disabled, skipping save
```

### 스케줄러 주기 변경

`ApiStatsScheduler.java`의 cron 표현식 수정:

```java
@Scheduled(cron = "5 * * * * ?")  // 매 분 5초
@Scheduled(cron = "0 * * * * ?")  // 매 분 0초
@Scheduled(cron = "0 */5 * * * ?")  // 5분마다
```

**주의:** 스케줄러 주기는 코드 변경 후 재배포 필요

## 모니터링

### 활성 집계기 개수 확인

```java
int count = statsCollector.getActiveAggregatorCount();
log.info("Active aggregators: {}", count);
```

### 로그 레벨 조정

```properties
# application.properties
logging.level.com.eactive.eai.custom.statistics=DEBUG
```

## 주의사항

1. **NULL 값 처리**: 차원 값이 NULL이면 "NONE"으로 변환 필수
2. **응답시간 범위**: PropManager 설정값 범위 초과 시 클리핑됨 (기본: 1ms ~ 60초)
3. **스케줄러 활성화**: `@EnableScheduling` 필수
4. **DB 저장 실패**: 로그로 기록되며 다음 분에 재시도하지 않음
5. **PropManager 설정**: `ApiStats` 그룹이 DB에 없으면 기본값 사용

## 테스트

### 단위 테스트 예제

```java
@Test
public void testStatsCollection() {
    ApiStatsCollector collector = new ApiStatsCollector();

    // 거래 이벤트 생성
    TransactionEvent event = TransactionEvent.builder()
        .apiName("TEST_API")
        .gwInstanceId("GW01")
        .bizDivCode("NONE")
        .clientId("CLIENT01")
        .inboundAdapter("HTTP")
        .outboundAdapter("REST")
        .responseTimeMs(100)
        .success(true)
        .build();

    // 이벤트 기록
    LocalDateTime now = LocalDateTime.of(2026, 1, 8, 10, 0, 30);
    collector.recordEvent(event, now);

    // Flush (10시 1분 이전)
    LocalDateTime flushTime = LocalDateTime.of(2026, 1, 8, 10, 1, 0);
    List<ApiStatsMinute> result = collector.flushBefore(flushTime);

    // 검증
    assertEquals(1, result.size());
    ApiStatsMinute stats = result.get(0);
    assertEquals("TEST_API", stats.getApiName());
    assertEquals(1L, stats.getTotalCnt());
    assertEquals(1L, stats.getSuccessCnt());
}
```

## 문제 해결

### 통계가 저장되지 않음

1. `@EnableScheduling` 확인
2. 로그에서 스케줄러 실행 확인
3. DB 연결 및 권한 확인

### 메모리 증가

1. 활성 집계기 개수 확인
2. 스케줄러 정상 동작 확인
3. 차원 카디널리티 확인 (너무 많은 고유 키)

### P50, P95 값이 이상함

1. 응답시간 단위 확인 (ms)
2. PropManager 설정값 확인 (`ApiStats` 그룹)
3. 히스토그램 범위 확인 (기본: 1~60000ms)
4. 데이터 건수 확인 (최소 1건 이상)

### PropManager 설정이 적용되지 않음

1. DB에 `ApiStats` 그룹 설정 확인
2. PropManager 초기화 확인
3. 로그에서 설정값 로딩 확인
4. 설정 변경 후 `PropManager.reload("ApiStats")` 실행

### 통계 수집을 비활성화하고 싶음

**운영 중 성능 이슈 발생 시:**
```sql
-- DB에서 비활성화
UPDATE TSEAIRM24 SET PROP_VALUE = 'false'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled';
```

```java
// 즉시 적용
PropManager.getInstance().reload("ApiStats");
```

이후 모든 `recordEvent()` 호출과 스케줄러 실행이 즉시 중단됩니다.
메모리에 쌓인 데이터는 유지되며, 다시 활성화 시 계속 수집됩니다.
