From 57baf4d911f1b53072970201cb7adfce6630ac6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dean=5B=EB=B0=B1=EB=B3=91=EB=82=A8=5D?= Date: Wed, 4 Mar 2026 19:58:02 +0900 Subject: [PATCH 1/4] =?UTF-8?q?=EA=B5=AD=EC=9C=A0=EC=9D=B8=EC=9D=98?= =?UTF-8?q?=ED=83=80=EC=9E=85=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cd/kamcoback/common/enums/LayerType.java | 5 ++- .../kamcoback/layer/service/LayerService.java | 36 +++++++++---------- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java index 99816656..f4fba751 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java @@ -13,7 +13,10 @@ public enum LayerType implements EnumType { TILE("배경지도"), GEOJSON("객체데이터"), WMTS("타일레이어"), - WMS("지적도"); + WMS("지적도") + , KAMCO_WMS("국유인WMS") + , KAMCO_WMTS("국유인WMTS") + ; private final String desc; diff --git a/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java b/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java index e486b446..3372c7c7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java +++ b/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java @@ -58,11 +58,11 @@ public class LayerService { @Transactional public UUID saveLayers(String type, LayerDto.AddReq dto) { LayerType layerType = - LayerType.from(type) - .orElseThrow(() -> new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST)); + LayerType.from(type) + .orElseThrow(() -> new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST)); switch (layerType) { - case TILE -> { + case TILE, KAMCO_WMS, KAMCO_WMTS -> { return mapLayerCoreService.saveTile(dto); } @@ -169,21 +169,21 @@ public class LayerService { public List findLayerMapList(String type) { List layerMapDtoList = mapLayerCoreService.findLayerMapList(type); layerMapDtoList.forEach( - dto -> { - if (dto.getLayerType().equals("WMS")) { - dto.setUrl( - String.format( - "%s/%s/%s", - trimSlash(geoserverUrl), trimSlash(wmsPath), dto.getLayerType().toLowerCase())); - } else if (dto.getLayerType().equals("WMTS")) { - dto.setUrl( - String.format( - "%s/%s/%s", - trimSlash(geoserverUrl), - trimSlash(wmtsPath), - dto.getLayerType().toLowerCase())); - } - }); + dto -> { + if (dto.getLayerType().equals("WMS")) { + dto.setUrl( + String.format( + "%s/%s/%s", + trimSlash(geoserverUrl), trimSlash(wmsPath), dto.getLayerType().toLowerCase())); + } else if (dto.getLayerType().equals("WMTS")) { + dto.setUrl( + String.format( + "%s/%s/%s", + trimSlash(geoserverUrl), + trimSlash(wmtsPath), + dto.getLayerType().toLowerCase())); + } + }); return layerMapDtoList; } From 5d7cb18fb88f31bf7a714c0b82a932b5b6d36180 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dean=5B=EB=B0=B1=EB=B3=91=EB=82=A8=5D?= Date: Wed, 4 Mar 2026 20:05:00 +0900 Subject: [PATCH 2/4] =?UTF-8?q?=EC=A4=84=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 6 ++ .../cd/kamcoback/common/enums/LayerType.java | 7 +-- .../kamcoback/layer/service/LayerService.java | 34 +++++------ .../postgres/entity/GpuMetricEntity.java | 57 +++++++++++++++++++ .../postgres/entity/SystemMetricEntity.java | 53 +++++++++++++++++ 5 files changed, 136 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index db9bd29a..5970b8e8 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ Thumbs.db .env.*.local application-local.yml application-secret.yml +metrics-collector/.env ### Docker (local testing) ### .dockerignore @@ -72,3 +73,8 @@ docker-compose.override.yml *.swo *~ !/CLAUDE.md + +### Metrics Collector ### +metrics-collector/venv/ +metrics-collector/*.pid +metrics-collector/wheels/ diff --git a/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java b/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java index f4fba751..4cff54f7 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/enums/LayerType.java @@ -13,10 +13,9 @@ public enum LayerType implements EnumType { TILE("배경지도"), GEOJSON("객체데이터"), WMTS("타일레이어"), - WMS("지적도") - , KAMCO_WMS("국유인WMS") - , KAMCO_WMTS("국유인WMTS") - ; + WMS("지적도"), + KAMCO_WMS("국유인WMS"), + KAMCO_WMTS("국유인WMTS"); private final String desc; diff --git a/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java b/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java index 3372c7c7..a6849862 100644 --- a/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java +++ b/src/main/java/com/kamco/cd/kamcoback/layer/service/LayerService.java @@ -58,8 +58,8 @@ public class LayerService { @Transactional public UUID saveLayers(String type, LayerDto.AddReq dto) { LayerType layerType = - LayerType.from(type) - .orElseThrow(() -> new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST)); + LayerType.from(type) + .orElseThrow(() -> new CustomApiException("BAD_REQUEST", HttpStatus.BAD_REQUEST)); switch (layerType) { case TILE, KAMCO_WMS, KAMCO_WMTS -> { @@ -169,21 +169,21 @@ public class LayerService { public List findLayerMapList(String type) { List layerMapDtoList = mapLayerCoreService.findLayerMapList(type); layerMapDtoList.forEach( - dto -> { - if (dto.getLayerType().equals("WMS")) { - dto.setUrl( - String.format( - "%s/%s/%s", - trimSlash(geoserverUrl), trimSlash(wmsPath), dto.getLayerType().toLowerCase())); - } else if (dto.getLayerType().equals("WMTS")) { - dto.setUrl( - String.format( - "%s/%s/%s", - trimSlash(geoserverUrl), - trimSlash(wmtsPath), - dto.getLayerType().toLowerCase())); - } - }); + dto -> { + if (dto.getLayerType().equals("WMS")) { + dto.setUrl( + String.format( + "%s/%s/%s", + trimSlash(geoserverUrl), trimSlash(wmsPath), dto.getLayerType().toLowerCase())); + } else if (dto.getLayerType().equals("WMTS")) { + dto.setUrl( + String.format( + "%s/%s/%s", + trimSlash(geoserverUrl), + trimSlash(wmtsPath), + dto.getLayerType().toLowerCase())); + } + }); return layerMapDtoList; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java index 23f74a52..064553ac 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/GpuMetricEntity.java @@ -11,37 +11,94 @@ import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.ColumnDefault; +/** + * GPU 메트릭 엔티티 + * + *

서버의 GPU 성능 및 자원 사용량 메트릭 데이터를 저장하는 JPA 엔티티입니다. GPU 연산 사용률 및 메모리 사용량 등 GPU 리소스 모니터링 데이터를 관리합니다. + * + *

데이터 소스: nvidia-smi 명령어 또는 NVML (NVIDIA Management Library) + * + *

활용 사례: + * + *

    + *
  • AI/ML 학습 모니터링: 딥러닝 작업 중 GPU 활용도 추적 + *
  • 리소스 최적화: GPU 메모리 부족 또는 유휴 상태 감지 + *
  • 용량 계획: GPU 추가 필요 시점 예측 + *
  • 알림 설정: gpuUtil > 95% 또는 gpuMemUsed/gpuMemTotal > 90% 시 경고 + *
+ */ @Getter @Setter @Entity @Table(name = "gpu_metrics") public class GpuMetricEntity { + /** 기본 키 (UUID, 자동 생성) */ @Id @ColumnDefault("gen_random_uuid()") @Column(name = "uuid", nullable = false) private UUID id; + /** 시퀀스 기반 보조 ID */ @NotNull @ColumnDefault("nextval('gpu_metrics_id_seq')") @Column(name = "id", nullable = false) private Integer id1; + /** 메트릭 수집 시각 (시간대 포함, 기본값: 현재 시각) */ @NotNull @ColumnDefault("now()") @Column(name = "\"timestamp\"", nullable = false) private OffsetDateTime timestamp; + /** 모니터링 대상 서버 이름 */ @NotNull @Column(name = "server_name", nullable = false, length = Integer.MAX_VALUE) private String serverName; + /** + * GPU 연산 사용률 (백분율) + * + *

GPU 코어의 연산 처리 활용도를 나타냅니다. + * + *

범위: 0.0 ~ 100.0 + * + *

예시: 85.5 = GPU가 85.5% 활용되어 연산 중 + * + *

데이터 소스: nvidia-smi의 'utilization.gpu' 또는 NVML의 nvmlDeviceGetUtilizationRates + * + *

참고: 높은 사용률(>90%)은 GPU가 충분히 활용되고 있음을 의미하며, 낮은 사용률은 병목 지점이 다른 곳(CPU, I/O)에 있을 수 있음 + */ @Column(name = "gpu_util") private Float gpuUtil; + /** + * GPU 메모리 사용량 (MB 단위) + * + *

현재 GPU에 할당되어 사용 중인 메모리 양 + * + *

예시: 10240.0 = 약 10GB의 GPU 메모리 사용 중 + * + *

데이터 소스: nvidia-smi의 'memory.used' 또는 NVML의 nvmlDeviceGetMemoryInfo + * + *

용도: 딥러닝 모델 크기, 배치 사이즈 최적화, OOM(Out Of Memory) 에러 예측 + */ @Column(name = "gpu_mem_used") private Float gpuMemUsed; + /** + * GPU 총 메모리 용량 (MB 단위) + * + *

GPU에 장착된 전체 메모리 용량 + * + *

예시: 16384.0 = 16GB VRAM 장착 + * + *

데이터 소스: nvidia-smi의 'memory.total' 또는 NVML의 nvmlDeviceGetMemoryInfo + * + *

계산식: 메모리 사용률(%) = (gpuMemUsed / gpuMemTotal) × 100 + * + *

활용: 여유 메모리 = gpuMemTotal - gpuMemUsed + */ @Column(name = "gpu_mem_total") private Float gpuMemTotal; } diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java index 05e5bb02..27e8d67b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/SystemMetricEntity.java @@ -11,48 +11,101 @@ import lombok.Getter; import lombok.Setter; import org.hibernate.annotations.ColumnDefault; +/** + * 시스템 메트릭 엔티티 + * + *

서버 시스템의 성능 메트릭 데이터를 저장하는 JPA 엔티티입니다. CPU 및 메모리 사용량 등 시스템 리소스 모니터링 데이터를 관리합니다. + * + *

데이터 소스: Linux sar 명령어 또는 /proc/meminfo 파일 + * + *

활용 사례: + * + *

    + *
  • 용량 계획: 메모리 추가 필요 시점 예측 + *
  • 성능 모니터링: 메모리 부족 상황 감지 + *
  • 트렌드 분석: 시간대별 메모리 사용 패턴 파악 + *
  • 알림 설정: memused > 90% 시 경고 + *
+ */ @Getter @Setter @Entity @Table(name = "system_metrics") public class SystemMetricEntity { + /** 기본 키 (UUID, 자동 생성) */ @Id @ColumnDefault("gen_random_uuid()") @Column(name = "uuid", nullable = false) private UUID id; + /** 시퀀스 기반 보조 ID */ @NotNull @ColumnDefault("nextval('system_metrics_id_seq')") @Column(name = "id", nullable = false) private Integer id1; + /** 메트릭 수집 시각 (시간대 포함) */ @NotNull @Column(name = "\"timestamp\"", nullable = false) private OffsetDateTime timestamp; + /** 모니터링 대상 서버 이름 */ @NotNull @Column(name = "server_name", nullable = false, length = Integer.MAX_VALUE) private String serverName; + /** 사용자 프로세스가 사용한 CPU 사용률 (%) - 응용 프로그램 실행 */ @Column(name = "cpu_user") private Float cpuUser; + /** 시스템 프로세스가 사용한 CPU 사용률 (%) - 커널 작업 */ @Column(name = "cpu_system") private Float cpuSystem; + /** I/O 대기로 소모된 CPU 사용률 (%) - 디스크/네트워크 대기 */ @Column(name = "cpu_iowait") private Float cpuIowait; + /** 유휴 상태 CPU 사용률 (%) - 사용 가능한 여유 CPU */ @Column(name = "cpu_idle") private Float cpuIdle; + /** + * 사용 가능한 여유 메모리 (KB 단위) + * + *

시스템에서 즉시 사용 가능한 물리 메모리 양 + * + *

예시: 4194304 = 약 4GB의 여유 메모리 + * + *

데이터 소스: /proc/meminfo의 MemFree + */ @Column(name = "kbmemfree") private Long kbmemfree; + /** + * 현재 사용 중인 메모리 (KB 단위) + * + *

시스템이 현재 할당하여 사용 중인 물리 메모리 양 + * + *

예시: 8388608 = 약 8GB의 사용 중인 메모리 + * + *

계산: MemTotal - MemFree + */ @Column(name = "kbmemused") private Long kbmemused; + /** + * 메모리 사용률 (백분율) + * + *

전체 메모리 대비 사용 중인 메모리 비율 + * + *

계산식: (kbmemused / (kbmemused + kbmemfree)) × 100 + * + *

예시: 66.7 = 전체 메모리의 66.7% 사용 중 + * + *

관계식: 총 메모리 = kbmemused + kbmemfree + */ @Column(name = "memused") private Float memused; } From 9b79f31d7b700a372e7ce1609927d0bb1cdb34ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dean=5B=EB=B0=B1=EB=B3=91=EB=82=A8=5D?= Date: Fri, 6 Mar 2026 16:01:28 +0900 Subject: [PATCH 3/4] =?UTF-8?q?=EC=A4=84=EB=A7=9E=EC=B6=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../inference/utils/GeoJsonValidator.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java index d0a750ba..42f3737b 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java @@ -147,7 +147,7 @@ public class GeoJsonValidator { Set foundUnique = new HashSet<>(); // 중복된 scene_id 목록 (샘플 로그 출력용이라 순서 유지 가능한 LinkedHashSet 사용) - Set duplicates = new LinkedHashSet<>(); +// Set duplicates = new LinkedHashSet<>(); // scene_id가 null 또는 blank인 feature의 개수 (데이터 이상) int nullIdCount = 0; @@ -179,9 +179,9 @@ public class GeoJsonValidator { } // foundUnique.add(sceneId)가 false면 "이미 같은 값이 있었다"는 뜻 => 중복 - if (!foundUnique.add(sceneId)) { - duplicates.add(sceneId); - } +// if (!foundUnique.add(sceneId)) { +// duplicates.add(sceneId); +// } } // ========================================================= @@ -225,13 +225,13 @@ public class GeoJsonValidator { requested.size(), // 요청 도엽 유니크 수 foundUnique.size(), // GeoJSON에서 발견된 scene_id 유니크 수 nullIdCount, // scene_id가 비어있는 feature 수 - duplicates.size(), // 중복 scene_id 종류 수 + 0, // 중복 scene_id 종류 수 missing.size(), // 요청했지만 빠진 도엽 수 extra.size()); // 요청하지 않았는데 들어온 도엽 수 // 중복/누락/추가 항목은 전체를 다 찍으면 로그 폭발하므로 샘플만 - if (!duplicates.isEmpty()) - log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList()); +// if (!duplicates.isEmpty()) +// log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList()); if (!missing.isEmpty()) log.warn("missing sample: {}", missing.stream().limit(50).toList()); @@ -250,12 +250,12 @@ public class GeoJsonValidator { // - 요청 문법은 맞지만(파일은 있고 JSON도 읽힘), // 내용(정합성)이 요구사항을 만족하지 못하는 경우에 적합. // ========================================================= - if (!missing.isEmpty() || !extra.isEmpty() || !duplicates.isEmpty() || nullIdCount > 0) { + if (!missing.isEmpty() || !extra.isEmpty() || nullIdCount > 0) { throw new ResponseStatusException( HttpStatus.UNPROCESSABLE_ENTITY, String.format( "GeoJSON validation failed: missing=%d, extra=%d, duplicates=%d, nullId=%d", - missing.size(), extra.size(), duplicates.size(), nullIdCount)); + missing.size(), extra.size(), 0, nullIdCount)); } // 모든 조건을 통과하면 정상 From 65f9026922687baa56c2c703d00d94ec28b06832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dean=5B=EB=B0=B1=EB=B3=91=EB=82=A8=5D?= Date: Fri, 6 Mar 2026 16:02:18 +0900 Subject: [PATCH 4/4] =?UTF-8?q?=ED=95=99=EC=8A=B5=EC=84=9C=EB=B2=84?= =?UTF-8?q?=EB=B0=B0=ED=8F=AC=EC=A0=95=EB=B3=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/inference/utils/GeoJsonValidator.java | 14 +++++++------- .../common/service/ExternalJarRunner.java | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java index 42f3737b..53c97490 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/inference/utils/GeoJsonValidator.java @@ -147,7 +147,7 @@ public class GeoJsonValidator { Set foundUnique = new HashSet<>(); // 중복된 scene_id 목록 (샘플 로그 출력용이라 순서 유지 가능한 LinkedHashSet 사용) -// Set duplicates = new LinkedHashSet<>(); + // Set duplicates = new LinkedHashSet<>(); // scene_id가 null 또는 blank인 feature의 개수 (데이터 이상) int nullIdCount = 0; @@ -179,9 +179,9 @@ public class GeoJsonValidator { } // foundUnique.add(sceneId)가 false면 "이미 같은 값이 있었다"는 뜻 => 중복 -// if (!foundUnique.add(sceneId)) { -// duplicates.add(sceneId); -// } + // if (!foundUnique.add(sceneId)) { + // duplicates.add(sceneId); + // } } // ========================================================= @@ -230,8 +230,8 @@ public class GeoJsonValidator { extra.size()); // 요청하지 않았는데 들어온 도엽 수 // 중복/누락/추가 항목은 전체를 다 찍으면 로그 폭발하므로 샘플만 -// if (!duplicates.isEmpty()) -// log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList()); + // if (!duplicates.isEmpty()) + // log.warn("duplicates sample: {}", duplicates.stream().limit(20).toList()); if (!missing.isEmpty()) log.warn("missing sample: {}", missing.stream().limit(50).toList()); @@ -250,7 +250,7 @@ public class GeoJsonValidator { // - 요청 문법은 맞지만(파일은 있고 JSON도 읽힘), // 내용(정합성)이 요구사항을 만족하지 못하는 경우에 적합. // ========================================================= - if (!missing.isEmpty() || !extra.isEmpty() || nullIdCount > 0) { + if (!missing.isEmpty() || !extra.isEmpty() || nullIdCount > 0) { throw new ResponseStatusException( HttpStatus.UNPROCESSABLE_ENTITY, String.format( diff --git a/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java b/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java index 94bc2cb7..5caaa5ba 100644 --- a/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java +++ b/src/main/java/com/kamco/cd/kamcoback/common/service/ExternalJarRunner.java @@ -28,7 +28,8 @@ public class ExternalJarRunner { * @param mode *

MERGED - batch-ids 에 해당하는 **모든 데이터를 하나의 Shapefile로 병합 생성, *

MAP_IDS - 명시적으로 전달한 map-ids만 대상으로 Shapefile 생성, - *

RESOLVE - batch-ids 기준으로 **JAR 내부에서 map_ids를 조회**한 뒤 Shapefile 생성 + *

RESOLVE - batch-ids 기준으로 **JAR 내부에서 map_ids를 조회**한 뒤 Shapefile 생성 java -jar + * build/libs/shp-exporter.jar --spring.profiles.active=prod */ public void run(String jarPath, String batchIds, String inferenceId, String mapIds, String mode) { List args = new ArrayList<>();