diff --git a/src/main/java/com/kamco/cd/kamcoback/geojson/config/GeoJsonMonitorConfig.java b/src/main/java/com/kamco/cd/kamcoback/geojson/config/GeoJsonMonitorConfig.java index cf99cd49..93c55425 100644 --- a/src/main/java/com/kamco/cd/kamcoback/geojson/config/GeoJsonMonitorConfig.java +++ b/src/main/java/com/kamco/cd/kamcoback/geojson/config/GeoJsonMonitorConfig.java @@ -22,8 +22,8 @@ public class GeoJsonMonitorConfig { /** 처리 실패 파일을 이동할 폴더 경로 */ private String errorDirectory = "~/geojson/error"; - /** 파일 모니터링 스케줄 (cron 표현식) 기본값: 매 30초마다 실행 */ - private String cronExpression = "0/30 * * * * *"; + /** 파일 모니터링 스케줄 (cron 표현식) 기본값: 매 1분마다 실행 */ + private String cronExpression = "0 * * * * *"; /** 지원하는 압축파일 확장자 */ private String[] supportedExtensions = {"zip", "tar", "tar.gz", "tgz"}; diff --git a/src/main/java/com/kamco/cd/kamcoback/geojson/service/GeoJsonDataService.java b/src/main/java/com/kamco/cd/kamcoback/geojson/service/GeoJsonDataService.java index 5659a3ac..273db83c 100644 --- a/src/main/java/com/kamco/cd/kamcoback/geojson/service/GeoJsonDataService.java +++ b/src/main/java/com/kamco/cd/kamcoback/geojson/service/GeoJsonDataService.java @@ -27,43 +27,57 @@ public class GeoJsonDataService { private final GeoJsonReader geoJsonReader = new GeoJsonReader(); /** GeoJSON 파일들을 데이터베이스에 저장 */ - @Transactional public List processGeoJsonFiles( - Map geoJsonContents, String archiveFileName) { + Map geoJsonContents, String archiveFileName) { List savedIds = new ArrayList<>(); log.info("GeoJSON 파일 처리 시작: {} ({}개 파일)", archiveFileName, geoJsonContents.size()); + // 개별 파일별로 독립적인 트랜잭션으로 처리 for (Map.Entry entry : geoJsonContents.entrySet()) { String fileName = entry.getKey(); String geoJsonContent = entry.getValue(); try { - Long savedId = processGeoJsonFile(fileName, geoJsonContent, archiveFileName); + Long savedId = processGeoJsonFileWithTransaction(fileName, geoJsonContent, archiveFileName); if (savedId != null) { savedIds.add(savedId); log.debug("GeoJSON 파일 저장 성공: {} (ID: {})", fileName, savedId); - - // 학습 모델 결과 파일인지 확인하여 geometry 데이터 처리 - if (isLearningModelResult(fileName, geoJsonContent)) { - processLearningModelGeometry(savedId, geoJsonContent, fileName); - } } } catch (Exception e) { log.error("GeoJSON 파일 처리 실패: {}", fileName, e); // 개별 파일 처리 실패는 전체 처리를 중단시키지 않음 } + + // 메모리 정리 + System.gc(); } log.info( - "GeoJSON 파일 처리 완료: {} (성공: {}개, 전체: {}개)", - archiveFileName, - savedIds.size(), - geoJsonContents.size()); + "GeoJSON 파일 처리 완료: {} (성공: {}개, 전체: {}개)", + archiveFileName, + savedIds.size(), + geoJsonContents.size()); return savedIds; } + /** 개별 파일을 별도 트랜잭션으로 처리 */ + @Transactional + public Long processGeoJsonFileWithTransaction( + String fileName, String geoJsonContent, String archiveFileName) { + try { + Long savedId = processGeoJsonFile(fileName, geoJsonContent, archiveFileName); + if (savedId != null && isLearningModelResult(fileName, geoJsonContent)) { + processLearningModelGeometryOptimized(savedId, geoJsonContent, fileName); + } + return savedId; + } catch (Exception e) { + log.error("파일 처리 중 트랜잭션 에러: {}", fileName, e); + throw e; // 트랜잭션 롤백을 위해 재throw + } + } + /** 개별 GeoJSON 파일을 MapSheetLearnDataEntity로 변환하여 저장 */ private Long processGeoJsonFile(String fileName, String geoJsonContent, String archiveFileName) { try { @@ -74,7 +88,7 @@ public class GeoJsonDataService { // 파일이 이미 처리되었는지 확인 String dataPath = generateDataPath(archiveFileName, fileName); Optional existingData = - mapSheetLearnDataRepository.findByDataPath(dataPath); + mapSheetLearnDataRepository.findByDataPath(dataPath); if (existingData.isPresent()) { log.warn("이미 처리된 파일입니다: {}", dataPath); @@ -83,7 +97,7 @@ public class GeoJsonDataService { // 새 엔티티 생성 및 저장 MapSheetLearnDataEntity entity = - createMapSheetLearnDataEntity(fileName, geoJsonContent, archiveFileName, geoJsonNode); + createMapSheetLearnDataEntity(fileName, geoJsonContent, archiveFileName, geoJsonNode); MapSheetLearnDataEntity savedEntity = mapSheetLearnDataRepository.save(entity); return savedEntity.getId(); @@ -108,7 +122,7 @@ public class GeoJsonDataService { /** MapSheetLearnDataEntity 생성 */ private MapSheetLearnDataEntity createMapSheetLearnDataEntity( - String fileName, String geoJsonContent, String archiveFileName, JsonNode geoJsonNode) { + String fileName, String geoJsonContent, String archiveFileName, JsonNode geoJsonNode) { MapSheetLearnDataEntity entity = new MapSheetLearnDataEntity(); @@ -273,8 +287,8 @@ public class GeoJsonDataService { JsonNode properties = firstFeature.get("properties"); // 학습 모델 특화 필드 확인 return properties.has("cd_prob") - || properties.has("class") - || (properties.has("before") && properties.has("after")); + || properties.has("class") + || (properties.has("before") && properties.has("after")); } } } @@ -285,9 +299,9 @@ public class GeoJsonDataService { return false; } - /** 학습 모델 결과의 geometry 데이터 처리 */ - @Transactional - public void processLearningModelGeometry(Long dataUid, String geoJsonContent, String fileName) { + /** 학습 모델 결과의 geometry 데이터 처리 - 최적화된 배치 처리 */ + public void processLearningModelGeometryOptimized( + Long dataUid, String geoJsonContent, String fileName) { try { log.info("학습 모델 geometry 데이터 처리 시작: {} (dataUid: {})", fileName, dataUid); @@ -295,7 +309,7 @@ public class GeoJsonDataService { // 메타데이터 추출 String mapSheetName = - rootNode.has("name") ? rootNode.get("name").asText() : fileName.replace(".geojson", ""); + rootNode.has("name") ? rootNode.get("name").asText() : fileName.replace(".geojson", ""); // 파일명에서 연도 및 지도번호 추출 (캠코_2021_2022_35813023) String[] parts = mapSheetName.split("_"); @@ -318,36 +332,75 @@ public class GeoJsonDataService { return; } - List geomEntities = new ArrayList<>(); + // 소규모 배치로 나누어 처리 + int totalFeatures = features.size(); + int batchSize = 10; // 작은 배치 크기 int processedCount = 0; - for (JsonNode feature : features) { - try { - MapSheetLearnDataGeomEntity geomEntity = + log.info("총 {}개 feature를 {}개씩 배치로 나누어 처리", totalFeatures, batchSize); + + for (int i = 0; i < totalFeatures; i += batchSize) { + int endIndex = Math.min(i + batchSize, totalFeatures); + List batch = new ArrayList<>(); + + for (int j = i; j < endIndex; j++) { + try { + JsonNode feature = features.get(j); + MapSheetLearnDataGeomEntity geomEntity = createGeometryEntity(feature, dataUid, beforeYear, afterYear, mapSheetNum); - if (geomEntity != null) { - geomEntities.add(geomEntity); - processedCount++; + if (geomEntity != null) { + batch.add(geomEntity); + } + } catch (Exception e) { + log.warn("Feature geometry 처리 실패 (feature {}): {}", j + 1, e.getMessage()); } - } catch (Exception e) { - log.warn("Feature geometry 처리 실패 (feature {}): {}", processedCount, e.getMessage()); } + + // 배치별 저장 + if (!batch.isEmpty()) { + saveBatchGeometry(batch); + processedCount += batch.size(); + log.debug("배치 {}-{} 처리 완료 ({}개)", i + 1, endIndex, batch.size()); + } + + // 메모리 정리 + batch.clear(); } - // 배치 저장 - if (!geomEntities.isEmpty()) { - mapSheetLearnDataGeomRepository.saveAll(geomEntities); - log.info("학습 모델 geometry 데이터 저장 완료: {} ({}개 feature)", fileName, geomEntities.size()); - } + log.info("학습 모델 geometry 데이터 저장 완료: {} ({}개 feature)", fileName, processedCount); } catch (Exception e) { log.error("학습 모델 geometry 데이터 처리 실패: {}", fileName, e); + throw new RuntimeException("Geometry 처리 실패: " + fileName, e); + } + } + + /** 배치별 geometry 저장 - 별도 트랜잭션 */ + @Transactional(propagation = org.springframework.transaction.annotation.Propagation.REQUIRES_NEW) + public void saveBatchGeometry(List batch) { + try { + if (batch == null || batch.isEmpty()) { + return; + } + mapSheetLearnDataGeomRepository.saveAll(batch); + } catch (Exception e) { + log.error("배치 geometry 저장 실패: {}", e.getMessage()); + // 개별 저장 시도 + for (MapSheetLearnDataGeomEntity entity : batch) { + try { + if (entity != null) { + mapSheetLearnDataGeomRepository.save(entity); + } + } catch (Exception individualError) { + log.warn("개별 geometry 저장 실패: {}", individualError.getMessage()); + } + } } } /** 개별 feature에서 geometry entity 생성 */ private MapSheetLearnDataGeomEntity createGeometryEntity( - JsonNode feature, Long dataUid, String beforeYear, String afterYear, String mapSheetNum) { + JsonNode feature, Long dataUid, String beforeYear, String afterYear, String mapSheetNum) { JsonNode properties = feature.get("properties"); JsonNode geometry = feature.get("geometry"); diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataEntity.java index e49a1833..0224dd26 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataEntity.java @@ -2,7 +2,10 @@ package com.kamco.cd.kamcoback.postgres.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; import java.time.ZonedDateTime; @@ -20,6 +23,11 @@ import org.hibernate.type.SqlTypes; public class MapSheetLearnDataEntity { @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "tb_map_sheet_learn_data_id_gen") + @SequenceGenerator( + name = "tb_map_sheet_learn_data_id_gen", + sequenceName = "tb_map_sheet_learn_data_data_uid", + allocationSize = 1) @Column(name = "data_uid", nullable = false) private Long id; diff --git a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java index 72ef0304..6d7cebcb 100644 --- a/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java +++ b/src/main/java/com/kamco/cd/kamcoback/postgres/entity/MapSheetLearnDataGeomEntity.java @@ -2,7 +2,10 @@ package com.kamco.cd.kamcoback.postgres.entity; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; import jakarta.validation.constraints.Size; import java.time.ZonedDateTime; @@ -17,6 +20,13 @@ import org.locationtech.jts.geom.Geometry; public class MapSheetLearnDataGeomEntity { @Id + @GeneratedValue( + strategy = GenerationType.SEQUENCE, + generator = "tb_map_sheet_learn_data_geom_id_gen") + @SequenceGenerator( + name = "tb_map_sheet_learn_data_geom_id_gen", + sequenceName = "tb_map_sheet_learn_data_geom_geom_uid", + allocationSize = 1) @Column(name = "geo_uid", nullable = false) private Long id;