result = new ArrayList<>();
+
+ aggregators.forEach((key, aggregator) -> {
+ if (aggregator.getTotalCount() > 0) {
+ result.add(aggregator.snapshot());
+ }
+ });
+
+ aggregators.clear();
+
+ log.info("Flushed all {} stats records", result.size());
+
+ return result;
+ }
+
+ /**
+ * 현재 집계 중인 키 개수 반환 (모니터링용)
+ */
+ public int getActiveAggregatorCount() {
+ return aggregators.size();
+ }
+}
diff --git a/src/main/java/com/eactive/eai/custom/statistics/collector/MinuteStatsAggregator.java b/src/main/java/com/eactive/eai/custom/statistics/collector/MinuteStatsAggregator.java
new file mode 100644
index 0000000..15b0e97
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/collector/MinuteStatsAggregator.java
@@ -0,0 +1,186 @@
+package com.eactive.eai.custom.statistics.collector;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.HdrHistogram.Histogram;
+
+import com.eactive.eai.custom.statistics.config.ApiStatsConfig;
+import com.eactive.eai.custom.statistics.dto.MinuteStatsKey;
+import com.eactive.eai.custom.statistics.dto.TransactionEvent;
+import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute;
+
+import lombok.Getter;
+
+/**
+ * 분단위 통계 집계기
+ * - HdrHistogram을 사용하여 응답시간 백분위 계산
+ * - Thread-safe한 집계 처리
+ */
+public class MinuteStatsAggregator {
+
+ @Getter
+ private final MinuteStatsKey key;
+
+ // 건수 메트릭
+ private final AtomicLong totalCnt = new AtomicLong(0);
+ private final AtomicLong successCnt = new AtomicLong(0);
+ private final AtomicLong timeoutCnt = new AtomicLong(0);
+ private final AtomicLong systemErrCnt = new AtomicLong(0);
+ private final AtomicLong bizErrCnt = new AtomicLong(0);
+
+ // 로그 시퀀스 900 구간 에러 카운트
+ private final AtomicLong seq900TimeoutCnt = new AtomicLong(0);
+ private final AtomicLong seq900SystemErrCnt = new AtomicLong(0);
+ private final AtomicLong seq900BizErrCnt = new AtomicLong(0);
+
+ // 응답시간 히스토그램 (1ms ~ 60초, 3자리 정밀도)
+ private final Histogram histogram;
+
+ // 응답시간 통계용
+ private final AtomicLong totalRespTime = new AtomicLong(0); // 평균 계산용
+ private volatile long minRespTime = Long.MAX_VALUE;
+ private volatile long maxRespTime = Long.MIN_VALUE;
+
+ public MinuteStatsAggregator(MinuteStatsKey key) {
+ this.key = key;
+ // HdrHistogram: PropManager에서 설정값 읽기
+ long minMs = ApiStatsConfig.getHistogramMinMs();
+ long maxMs = ApiStatsConfig.getHistogramMaxMs();
+ int precision = ApiStatsConfig.getHistogramPrecision();
+ this.histogram = new Histogram(minMs, maxMs, precision);
+ }
+
+ /**
+ * 거래 이벤트 기록
+ */
+ public synchronized void recordEvent(TransactionEvent event) {
+ int logSeq = event.getLogSequence();
+
+ // 로그 시퀀스 400: 일반 통계 집계
+ if (logSeq == 400) {
+ totalCnt.incrementAndGet();
+
+ if (event.isSuccess()) {
+ successCnt.incrementAndGet();
+ } else {
+ switch (event.getErrorType()) {
+ case TIMEOUT:
+ timeoutCnt.incrementAndGet();
+ break;
+ case SYSTEM_ERROR:
+ systemErrCnt.incrementAndGet();
+ break;
+ case BUSINESS_ERROR:
+ bizErrCnt.incrementAndGet();
+ break;
+ }
+ }
+
+ // 응답시간 기록 (400 구간만)
+ long respTime = event.getResponseTimeMs();
+ totalRespTime.addAndGet(respTime);
+
+ // 히스토그램에 기록
+ long maxMs = ApiStatsConfig.getHistogramMaxMs();
+ try {
+ long clippedRespTime = Math.min(Math.max(1, respTime), maxMs);
+ histogram.recordValue(clippedRespTime);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ histogram.recordValue(maxMs);
+ }
+
+ // Min/Max 업데이트
+ updateMinMax(respTime);
+ }
+ // 로그 시퀀스 900: 900 전용 에러 카운트만 집계
+ else if (logSeq == 900 && !event.isSuccess()) {
+ switch (event.getErrorType()) {
+ case TIMEOUT:
+ seq900TimeoutCnt.incrementAndGet();
+ break;
+ case SYSTEM_ERROR:
+ seq900SystemErrCnt.incrementAndGet();
+ break;
+ case BUSINESS_ERROR:
+ seq900BizErrCnt.incrementAndGet();
+ break;
+ }
+ }
+ // 그 외 로그 시퀀스는 무시
+ }
+
+ /**
+ * Min/Max 응답시간 업데이트 (thread-safe)
+ */
+ private synchronized void updateMinMax(long respTime) {
+ if (respTime < minRespTime) {
+ minRespTime = respTime;
+ }
+ if (respTime > maxRespTime) {
+ maxRespTime = respTime;
+ }
+ }
+
+ /**
+ * 현재 집계 결과를 Entity로 변환
+ */
+ public ApiStatsMinute snapshot() {
+ ApiStatsMinute stats = new ApiStatsMinute();
+
+ // 키 정보
+ stats.setStatTime(key.getStatTime());
+ stats.setApiName(key.getApiName());
+ stats.setGwInstanceId(key.getGwInstanceId());
+ stats.setBizDivCode(key.getBizDivCode());
+ stats.setClientId(key.getClientId());
+ stats.setInboundAdapter(key.getInboundAdapter());
+ stats.setOutboundAdapter(key.getOutboundAdapter());
+
+ // 건수 메트릭
+ stats.setTotalCnt(totalCnt.get());
+ stats.setSuccessCnt(successCnt.get());
+ stats.setTimeoutCnt(timeoutCnt.get());
+ stats.setSystemErrCnt(systemErrCnt.get());
+ stats.setBizErrCnt(bizErrCnt.get());
+
+ // 로그 시퀀스 900 구간 에러 카운트
+ stats.setSeq900TimeoutCnt(seq900TimeoutCnt.get());
+ stats.setSeq900SystemErrCnt(seq900SystemErrCnt.get());
+ stats.setSeq900BizErrCnt(seq900BizErrCnt.get());
+
+ // 응답시간 메트릭
+ long total = totalCnt.get();
+ if (total > 0) {
+ // 평균
+ double avg = (double) totalRespTime.get() / total;
+ stats.setAvgRespTime(BigDecimal.valueOf(avg).setScale(3, RoundingMode.HALF_UP));
+
+ // Min/Max
+ stats.setMinRespTime(BigDecimal.valueOf(minRespTime).setScale(3, RoundingMode.HALF_UP));
+ stats.setMaxRespTime(BigDecimal.valueOf(maxRespTime).setScale(3, RoundingMode.HALF_UP));
+
+ // 백분위 (HdrHistogram 사용)
+ long p50 = histogram.getValueAtPercentile(50.0);
+ long p95 = histogram.getValueAtPercentile(95.0);
+ stats.setP50RespTime(BigDecimal.valueOf(p50).setScale(3, RoundingMode.HALF_UP));
+ stats.setP95RespTime(BigDecimal.valueOf(p95).setScale(3, RoundingMode.HALF_UP));
+ } else {
+ stats.setAvgRespTime(BigDecimal.ZERO);
+ stats.setMinRespTime(BigDecimal.ZERO);
+ stats.setMaxRespTime(BigDecimal.ZERO);
+ stats.setP50RespTime(BigDecimal.ZERO);
+ stats.setP95RespTime(BigDecimal.ZERO);
+ }
+
+ return stats;
+ }
+
+ /**
+ * 집계된 총 건수 반환 (빈 집계기 체크용)
+ */
+ public long getTotalCount() {
+ return totalCnt.get();
+ }
+}
diff --git a/src/main/java/com/eactive/eai/custom/statistics/config/ApiStatsConfig.java b/src/main/java/com/eactive/eai/custom/statistics/config/ApiStatsConfig.java
new file mode 100644
index 0000000..bdb7e36
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/config/ApiStatsConfig.java
@@ -0,0 +1,95 @@
+package com.eactive.eai.custom.statistics.config;
+
+import java.util.Properties;
+
+import com.eactive.eai.common.property.PropManager;
+import com.eactive.eai.common.util.Logger;
+
+import lombok.experimental.UtilityClass;
+
+/**
+ * API 통계 설정 관리
+ * - PropManager를 통해 운영 중 동적 변경 가능한 설정값 관리
+ */
+@UtilityClass
+public class ApiStatsConfig {
+
+ private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
+
+ /** PropManager 그룹명 */
+ private static final String GROUP_NAME = "ApiStats";
+
+ /** 기본값 상수 */
+ private static final boolean DEFAULT_ENABLED = true;
+ private static final long DEFAULT_HISTOGRAM_MIN_MS = 1L;
+ private static final long DEFAULT_HISTOGRAM_MAX_MS = 60000L;
+ private static final int DEFAULT_HISTOGRAM_PRECISION = 3;
+
+ /**
+ * 통계 수집 활성화 여부
+ * - true: 통계 수집 활성화 (기본값)
+ * - false: 통계 수집 비활성화
+ */
+ public static boolean isEnabled() {
+ try {
+ Properties prop = PropManager.getInstance().getProperties(GROUP_NAME);
+ String value = prop.getProperty("enabled", String.valueOf(DEFAULT_ENABLED));
+ return Boolean.parseBoolean(value);
+ } catch (Exception e) {
+ logger.debug("Failed to get enabled from PropManager, using default: " + DEFAULT_ENABLED);
+ return DEFAULT_ENABLED;
+ }
+ }
+
+ /**
+ * 히스토그램 최소값 (ms)
+ */
+ public static long getHistogramMinMs() {
+ try {
+ Properties prop = PropManager.getInstance().getProperties(GROUP_NAME);
+ String value = prop.getProperty("histogram.min.ms", String.valueOf(DEFAULT_HISTOGRAM_MIN_MS));
+ return Long.parseLong(value);
+ } catch (Exception e) {
+ logger.debug("Failed to get histogram.min.ms from PropManager, using default: " + DEFAULT_HISTOGRAM_MIN_MS);
+ return DEFAULT_HISTOGRAM_MIN_MS;
+ }
+ }
+
+ /**
+ * 히스토그램 최대값 (ms)
+ */
+ public static long getHistogramMaxMs() {
+ try {
+ Properties prop = PropManager.getInstance().getProperties(GROUP_NAME);
+ String value = prop.getProperty("histogram.max.ms", String.valueOf(DEFAULT_HISTOGRAM_MAX_MS));
+ return Long.parseLong(value);
+ } catch (Exception e) {
+ logger.debug("Failed to get histogram.max.ms from PropManager, using default: " + DEFAULT_HISTOGRAM_MAX_MS);
+ return DEFAULT_HISTOGRAM_MAX_MS;
+ }
+ }
+
+ /**
+ * 히스토그램 정밀도 (유효숫자)
+ */
+ public static int getHistogramPrecision() {
+ try {
+ Properties prop = PropManager.getInstance().getProperties(GROUP_NAME);
+ String value = prop.getProperty("histogram.precision", String.valueOf(DEFAULT_HISTOGRAM_PRECISION));
+ return Integer.parseInt(value);
+ } catch (Exception e) {
+ logger.debug("Failed to get histogram.precision from PropManager, using default: " + DEFAULT_HISTOGRAM_PRECISION);
+ return DEFAULT_HISTOGRAM_PRECISION;
+ }
+ }
+
+ /**
+ * 설정값 로깅 (초기화 시)
+ */
+ public static void logCurrentConfig() {
+ logger.info("ApiStatsConfig] enabled = " + isEnabled());
+ logger.info("ApiStatsConfig] histogram.min.ms = " + getHistogramMinMs());
+ logger.info("ApiStatsConfig] histogram.max.ms = " + getHistogramMaxMs());
+ logger.info("ApiStatsConfig] histogram.precision = " + getHistogramPrecision());
+ }
+}
diff --git a/src/main/java/com/eactive/eai/custom/statistics/dto/MinuteStatsKey.java b/src/main/java/com/eactive/eai/custom/statistics/dto/MinuteStatsKey.java
new file mode 100644
index 0000000..f2d6797
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/dto/MinuteStatsKey.java
@@ -0,0 +1,90 @@
+package com.eactive.eai.custom.statistics.dto;
+
+import java.time.LocalDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.Objects;
+
+import lombok.Getter;
+
+/**
+ * 분단위 통계 집계 키
+ * - 동일 분의 동일 차원별로 집계하기 위한 키
+ */
+@Getter
+public class MinuteStatsKey {
+
+ /** 통계 시간 (분 단위, 초는 00) */
+ private final LocalDateTime statTime;
+
+ /** API명 */
+ private final String apiName;
+
+ /** API Gateway 인스턴스 ID */
+ private final String gwInstanceId;
+
+ /** 업무구분코드 */
+ private final String bizDivCode;
+
+ /** 클라이언트 ID */
+ private final String clientId;
+
+ /** Inbound Adapter */
+ private final String inboundAdapter;
+
+ /** Outbound Adapter */
+ private final String outboundAdapter;
+
+ public MinuteStatsKey(LocalDateTime statTime, String apiName, String gwInstanceId,
+ String bizDivCode, String clientId, String inboundAdapter, String outboundAdapter) {
+ // 초와 나노초를 0으로 절사하여 분 단위로 정규화
+ this.statTime = statTime.truncatedTo(ChronoUnit.MINUTES);
+ this.apiName = apiName;
+ this.gwInstanceId = gwInstanceId;
+ this.bizDivCode = bizDivCode;
+ this.clientId = clientId;
+ this.inboundAdapter = inboundAdapter;
+ this.outboundAdapter = outboundAdapter;
+ }
+
+ /**
+ * TransactionEvent로부터 키 생성
+ */
+ public static MinuteStatsKey from(TransactionEvent event, LocalDateTime eventTime) {
+ return new MinuteStatsKey(
+ eventTime,
+ event.getApiName(),
+ event.getGwInstanceId(),
+ event.getBizDivCode(),
+ event.getClientId(),
+ event.getInboundAdapter(),
+ event.getOutboundAdapter()
+ );
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ MinuteStatsKey that = (MinuteStatsKey) o;
+ return Objects.equals(statTime, that.statTime) &&
+ Objects.equals(apiName, that.apiName) &&
+ Objects.equals(gwInstanceId, that.gwInstanceId) &&
+ Objects.equals(bizDivCode, that.bizDivCode) &&
+ Objects.equals(clientId, that.clientId) &&
+ Objects.equals(inboundAdapter, that.inboundAdapter) &&
+ Objects.equals(outboundAdapter, that.outboundAdapter);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(statTime, apiName, gwInstanceId, bizDivCode,
+ clientId, inboundAdapter, outboundAdapter);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s|%s|%s|%s|%s|%s|%s",
+ statTime, apiName, gwInstanceId, bizDivCode,
+ clientId, inboundAdapter, outboundAdapter);
+ }
+}
diff --git a/src/main/java/com/eactive/eai/custom/statistics/dto/TransactionEvent.java b/src/main/java/com/eactive/eai/custom/statistics/dto/TransactionEvent.java
new file mode 100644
index 0000000..60a0793
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/dto/TransactionEvent.java
@@ -0,0 +1,58 @@
+package com.eactive.eai.custom.statistics.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+
+/**
+ * 거래 1건의 이벤트 정보
+ */
+@Getter
+@Builder
+public class TransactionEvent {
+
+ /** API명 */
+ private final String apiName;
+
+ /** API Gateway 인스턴스 ID */
+ private final String gwInstanceId;
+
+ /** 업무구분코드 (없으면 "NONE") */
+ private final String bizDivCode;
+
+ /** 클라이언트 ID (없으면 "NONE") */
+ private final String clientId;
+
+ /** Inbound Adapter (없으면 "NONE") */
+ private final String inboundAdapter;
+
+ /** Outbound Adapter (없으면 "NONE") */
+ private final String outboundAdapter;
+
+ /** 응답시간 (ms) */
+ private final long responseTimeMs;
+
+ /** 성공 여부 */
+ private final boolean success;
+
+ /** 오류 유형 (성공시 null) */
+ private final ErrorType errorType;
+
+ /** 로그 시퀀스 번호 (100, 200, 300, 400, 900) */
+ private final int logSequence;
+
+ /**
+ * 오류 유형
+ */
+ public enum ErrorType {
+ TIMEOUT, // Timeout 오류
+ SYSTEM_ERROR, // 시스템 오류
+ BUSINESS_ERROR // 업무 오류
+ }
+
+ /**
+ * 기본값을 "NONE"으로 정규화
+ */
+ public static String normalizeValue(String value) {
+ return (value == null || value.trim().isEmpty()) ? "NONE" : value;
+ }
+}
diff --git a/src/main/java/com/eactive/eai/custom/statistics/package-info.java b/src/main/java/com/eactive/eai/custom/statistics/package-info.java
new file mode 100644
index 0000000..a61de7a
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/package-info.java
@@ -0,0 +1,14 @@
+/**
+ * API Gateway 통계 수집 모듈
+ *
+ * 분단위로 API 거래 통계를 수집하고 DB에 저장합니다.
+ * HdrHistogram을 사용하여 메모리 효율적으로 응답시간 백분위(P50, P95)를 계산합니다.
+ *
+ *
주요 컴포넌트
+ *
+ * - ApiStatsMonitorImpl - StatisticsMonitor 구현체 (진입점)
+ * - ApiStatsCollector - 거래 이벤트 수집 및 메모리 집계
+ * - ApiStatsScheduler - 매 분 DB 저장 스케줄러
+ *
+ */
+package com.eactive.eai.custom.statistics;
\ No newline at end of file
diff --git a/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteLogger.java b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteLogger.java
new file mode 100644
index 0000000..9d79575
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteLogger.java
@@ -0,0 +1,16 @@
+package com.eactive.eai.custom.statistics.persistence;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute;
+import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinuteId;
+import com.eactive.eai.data.jpa.AbstractDataLogger;
+
+@Service
+@Transactional(propagation = Propagation.REQUIRES_NEW)
+public class ApiStatsMinuteLogger
+ extends AbstractDataLogger {
+
+}
diff --git a/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteRepository.java b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteRepository.java
new file mode 100644
index 0000000..e114c12
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/persistence/ApiStatsMinuteRepository.java
@@ -0,0 +1,9 @@
+package com.eactive.eai.custom.statistics.persistence;
+
+import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute;
+import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinuteId;
+import com.eactive.eai.data.jpa.BaseRepository;
+
+public interface ApiStatsMinuteRepository extends BaseRepository {
+
+}
diff --git a/src/main/java/com/eactive/eai/custom/statistics/scheduler/ApiStatsScheduler.java b/src/main/java/com/eactive/eai/custom/statistics/scheduler/ApiStatsScheduler.java
new file mode 100644
index 0000000..3918e73
--- /dev/null
+++ b/src/main/java/com/eactive/eai/custom/statistics/scheduler/ApiStatsScheduler.java
@@ -0,0 +1,119 @@
+package com.eactive.eai.custom.statistics.scheduler;
+
+import java.util.List;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import com.eactive.eai.custom.statistics.collector.ApiStatsCollector;
+import com.eactive.eai.custom.statistics.config.ApiStatsConfig;
+import com.eactive.eai.custom.statistics.persistence.ApiStatsMinuteLogger;
+import com.eactive.eai.data.entity.custom.statistics.ApiStatsMinute;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * API 통계 스케줄러
+ * - 매 분마다 실행되어 집계된 통계를 DB에 저장
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class ApiStatsScheduler {
+
+ private final ApiStatsCollector collector;
+ private final ApiStatsMinuteLogger logger;
+
+ /**
+ * 초기화 시 설정값 로깅
+ */
+ @PostConstruct
+ public void init() {
+ log.info("ApiStatsScheduler initialized");
+ ApiStatsConfig.logCurrentConfig();
+ }
+
+ /**
+ * 매 분마다 실행 (매 분 5초에 실행)
+ * - 이전 분의 완료된 통계를 DB에 저장
+ */
+ @Scheduled(cron = "5 * * * * ?")
+ public void saveMinuteStats() {
+ // 통계 수집 비활성화 체크
+ if (!ApiStatsConfig.isEnabled()) {
+ log.debug("API stats collection is disabled, skipping save");
+ return;
+ }
+
+ try {
+ log.debug("Starting minute stats flush...");
+
+ // 완료된 분의 통계 데이터 가져오기
+ List statsList = collector.flushCompletedMinutes();
+
+ if (statsList.isEmpty()) {
+ log.debug("No stats to save");
+ return;
+ }
+
+ // DB에 저장
+ int savedCount = 0;
+ int errorCount = 0;
+
+ for (ApiStatsMinute stats : statsList) {
+ try {
+ logger.log(stats);
+ savedCount++;
+
+ if (log.isDebugEnabled()) {
+ log.debug("Saved stats: {} - total={}, success={}, p95={}ms",
+ stats.getApiName(),
+ stats.getTotalCnt(),
+ stats.getSuccessCnt(),
+ stats.getP95RespTime());
+ }
+
+ } catch (Exception e) {
+ errorCount++;
+ log.error("Failed to save stats: {}", stats, e);
+ }
+ }
+
+ log.info("Minute stats saved: success={}, error={}, active_aggregators={}",
+ savedCount, errorCount, collector.getActiveAggregatorCount());
+
+ } catch (Exception e) {
+ log.error("Failed to flush minute stats", e);
+ }
+ }
+
+ /**
+ * 애플리케이션 종료 시 남은 통계 저장
+ */
+ @PreDestroy
+ public void shutdown() {
+ try {
+ log.info("Shutting down ApiStatsScheduler, flushing remaining stats...");
+
+ List statsList = collector.flushAll();
+
+ if (!statsList.isEmpty()) {
+ for (ApiStatsMinute stats : statsList) {
+ try {
+ logger.log(stats);
+ } catch (Exception e) {
+ log.error("Failed to save stats during shutdown: {}", stats, e);
+ }
+ }
+ log.info("Saved {} remaining stats during shutdown", statsList.size());
+ }
+
+ } catch (Exception e) {
+ log.error("Error during shutdown", e);
+ }
+ }
+}
diff --git a/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinute.java b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinute.java
new file mode 100644
index 0000000..919e792
--- /dev/null
+++ b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinute.java
@@ -0,0 +1,118 @@
+package com.eactive.eai.data.entity.custom.statistics;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.IdClass;
+import javax.persistence.Table;
+
+import org.hibernate.annotations.Comment;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * API Gateway 분단위 처리 통계
+ */
+@Getter
+@Setter
+@Entity
+@Table(name = "API_STATS_MINUTE")
+@org.hibernate.annotations.Table(appliesTo = "API_STATS_MINUTE", comment = "API Gateway 분단위 처리 통계")
+@IdClass(ApiStatsMinuteId.class)
+public class ApiStatsMinute implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @Id
+ @Column(name = "STAT_TIME", nullable = false)
+ @Comment("통계시간(분단위, 초는 00초로 절사)")
+ private LocalDateTime statTime;
+
+ @Id
+ @Column(name = "API_NAME", nullable = false, length = 100)
+ @Comment("API 명칭")
+ private String apiName;
+
+ @Id
+ @Column(name = "GW_INSTANCE_ID", nullable = false, length = 50)
+ @Comment("API Gateway 인스턴스 ID")
+ private String gwInstanceId;
+
+ @Id
+ @Column(name = "BIZ_DIV_CODE", nullable = false, length = 50, columnDefinition = "VARCHAR2(50) DEFAULT 'NONE'")
+ @Comment("업무구분코드 (없으면 NONE)")
+ private String bizDivCode = "NONE";
+
+ @Id
+ @Column(name = "CLIENT_ID", nullable = false, length = 100, columnDefinition = "VARCHAR2(100) DEFAULT 'NONE'")
+ @Comment("클라이언트 ID (없으면 NONE)")
+ private String clientId = "NONE";
+
+ @Id
+ @Column(name = "INBOUND_ADAPTER", nullable = false, length = 100, columnDefinition = "VARCHAR2(100) DEFAULT 'NONE'")
+ @Comment("Inbound Adapter (없으면 NONE)")
+ private String inboundAdapter = "NONE";
+
+ @Id
+ @Column(name = "OUTBOUND_ADAPTER", nullable = false, length = 100, columnDefinition = "VARCHAR2(100) DEFAULT 'NONE'")
+ @Comment("Outbound Adapter (없으면 NONE)")
+ private String outboundAdapter = "NONE";
+
+ // 건수 메트릭
+ @Column(name = "TOTAL_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("총 처리 건수")
+ private Long totalCnt = 0L;
+
+ @Column(name = "SUCCESS_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("성공 건수")
+ private Long successCnt = 0L;
+
+ @Column(name = "TIMEOUT_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("Timeout 오류 건수")
+ private Long timeoutCnt = 0L;
+
+ @Column(name = "SYSTEM_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("시스템 오류 건수")
+ private Long systemErrCnt = 0L;
+
+ @Column(name = "BIZ_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("업무 오류 건수")
+ private Long bizErrCnt = 0L;
+
+ @Column(name = "SEQ900_TIMEOUT_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("로그시퀀스900 Timeout 오류 건수")
+ private Long seq900TimeoutCnt = 0L;
+
+ @Column(name = "SEQ900_SYSTEM_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("로그시퀀스900 시스템 오류 건수")
+ private Long seq900SystemErrCnt = 0L;
+
+ @Column(name = "SEQ900_BIZ_ERR_CNT", nullable = false, precision = 12, columnDefinition = "NUMBER(12) DEFAULT 0")
+ @Comment("로그시퀀스900 업무 오류 건수")
+ private Long seq900BizErrCnt = 0L;
+
+ // 응답시간 메트릭 (ms, 소수점 3자리)
+ @Column(name = "AVG_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0")
+ @Comment("평균 응답시간(ms)")
+ private BigDecimal avgRespTime = BigDecimal.ZERO;
+
+ @Column(name = "MIN_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0")
+ @Comment("최소 응답시간(ms)")
+ private BigDecimal minRespTime = BigDecimal.ZERO;
+
+ @Column(name = "MAX_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0")
+ @Comment("최대 응답시간(ms)")
+ private BigDecimal maxRespTime = BigDecimal.ZERO;
+
+ @Column(name = "P50_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0")
+ @Comment("50 백분위 응답시간(ms)")
+ private BigDecimal p50RespTime = BigDecimal.ZERO;
+
+ @Column(name = "P95_RESP_TIME", precision = 10, scale = 3, columnDefinition = "NUMBER(10,3) DEFAULT 0")
+ @Comment("95 백분위 응답시간(ms)")
+ private BigDecimal p95RespTime = BigDecimal.ZERO;
+}
diff --git a/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinuteId.java b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinuteId.java
new file mode 100644
index 0000000..1d0a266
--- /dev/null
+++ b/src/main/java/com/eactive/eai/data/entity/custom/statistics/ApiStatsMinuteId.java
@@ -0,0 +1,26 @@
+package com.eactive.eai.data.entity.custom.statistics;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * API 통계 분단위 복합키
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class ApiStatsMinuteId implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private LocalDateTime statTime;
+ private String apiName;
+ private String gwInstanceId;
+ private String bizDivCode;
+ private String clientId;
+ private String inboundAdapter;
+ private String outboundAdapter;
+}