Files
eapim-online/src/main/java/com/eactive/eai/custom/statistics

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() 호출:

@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 추가:

@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 설정 예시:

-- 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 (긴급 상황 시)

-- 통계 수집 비활성화
UPDATE TSEAIRM24
SET PROP_VALUE = 'false'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled';
// PropManager 리로드 (즉시 적용)
PropManager.getInstance().reload("ApiStats");

통계 수집 ON (다시 활성화)

-- 통계 수집 재활성화
UPDATE TSEAIRM24
SET PROP_VALUE = 'true'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled';
// PropManager 리로드
PropManager.getInstance().reload("ApiStats");

히스토그램 범위 변경

-- 최대값을 300초로 변경
UPDATE TSEAIRM24
SET PROP_VALUE = '300000'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'histogram.max.ms';
// 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 표현식 수정:

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

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

모니터링

활성 집계기 개수 확인

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

로그 레벨 조정

# 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에 없으면 기본값 사용

테스트

단위 테스트 예제

@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") 실행

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

운영 중 성능 이슈 발생 시:

-- DB에서 비활성화
UPDATE TSEAIRM24 SET PROP_VALUE = 'false'
WHERE GROUP_NAME = 'ApiStats' AND PROP_KEY = 'enabled';
// 즉시 적용
PropManager.getInstance().reload("ApiStats");

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