From b7d71d4220104597831bd1325593963b3fe3a023 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Wed, 25 Mar 2026 16:07:20 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(log-cleanup):=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EA=B4=80=EB=A6=AC=20=EC=A0=95=EC=B1=85=20?= =?UTF-8?q?=EC=88=98=EB=A6=BD=20=EB=B0=8F=20=EC=A0=95=EB=A6=AC=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EC=9E=91=EC=97=85=20=EA=B0=9C=EB=B0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LogCleanupJob: 보존 기간 초과 배치 로그 삭제 Tasklet Job - 대상: batch_api_log(30일), Spring Batch 메타(90일), batch_failed_record/RESOLVED(90일), batch_recollection_history(90일) - application.yml에서 테이블별 보존 기간 설정 가능 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../global/cleanup/LogCleanupConfig.java | 37 +++++ .../global/cleanup/LogCleanupJobConfig.java | 69 ++++++++ .../global/cleanup/LogCleanupTasklet.java | 148 ++++++++++++++++++ src/main/resources/application.yml | 7 + 4 files changed, 261 insertions(+) create mode 100644 src/main/java/com/snp/batch/global/cleanup/LogCleanupConfig.java create mode 100644 src/main/java/com/snp/batch/global/cleanup/LogCleanupJobConfig.java create mode 100644 src/main/java/com/snp/batch/global/cleanup/LogCleanupTasklet.java diff --git a/src/main/java/com/snp/batch/global/cleanup/LogCleanupConfig.java b/src/main/java/com/snp/batch/global/cleanup/LogCleanupConfig.java new file mode 100644 index 0000000..31561fb --- /dev/null +++ b/src/main/java/com/snp/batch/global/cleanup/LogCleanupConfig.java @@ -0,0 +1,37 @@ +package com.snp.batch.global.cleanup; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +/** + * 배치 로그 정리 설정 + * + * 로그 종류별 보존 기간(일) 설정 + * + * 설정 예시: + * app.batch.log-cleanup: + * api-log-retention-days: 30 + * batch-meta-retention-days: 90 + * failed-record-retention-days: 90 + * recollection-history-retention-days: 90 + */ +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "app.batch.log-cleanup") +public class LogCleanupConfig { + + /** batch_api_log 보존 기간 (일) */ + private int apiLogRetentionDays = 30; + + /** Spring Batch 메타 테이블 보존 기간 (일) */ + private int batchMetaRetentionDays = 90; + + /** batch_failed_record (RESOLVED) 보존 기간 (일) */ + private int failedRecordRetentionDays = 90; + + /** batch_recollection_history 보존 기간 (일) */ + private int recollectionHistoryRetentionDays = 90; +} diff --git a/src/main/java/com/snp/batch/global/cleanup/LogCleanupJobConfig.java b/src/main/java/com/snp/batch/global/cleanup/LogCleanupJobConfig.java new file mode 100644 index 0000000..1081c8a --- /dev/null +++ b/src/main/java/com/snp/batch/global/cleanup/LogCleanupJobConfig.java @@ -0,0 +1,69 @@ +package com.snp.batch.global.cleanup; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobExecutionListener; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * 배치 로그 정리 Job Config + * + * 스케줄: 매일 02:00 (0 0 2 * * ?) + * + * 동작: + * - 보존 기간이 지난 배치 로그 데이터를 삭제 + * - batch_api_log (30일), Spring Batch 메타 (90일), + * batch_failed_record/RESOLVED (90일), batch_recollection_history (90일) + */ +@Slf4j +@Configuration +public class LogCleanupJobConfig { + + private final JobRepository jobRepository; + private final PlatformTransactionManager transactionManager; + private final LogCleanupTasklet logCleanupTasklet; + + public LogCleanupJobConfig( + JobRepository jobRepository, + PlatformTransactionManager transactionManager, + LogCleanupTasklet logCleanupTasklet) { + this.jobRepository = jobRepository; + this.transactionManager = transactionManager; + this.logCleanupTasklet = logCleanupTasklet; + } + + @Bean(name = "logCleanupStep") + public Step logCleanupStep() { + return new StepBuilder("logCleanupStep", jobRepository) + .tasklet(logCleanupTasklet, transactionManager) + .build(); + } + + @Bean(name = "LogCleanupJob") + public Job logCleanupJob() { + log.info("Job 생성: LogCleanupJob"); + + return new JobBuilder("LogCleanupJob", jobRepository) + .listener(new JobExecutionListener() { + @Override + public void beforeJob(JobExecution jobExecution) { + log.info("[LogCleanupJob] 배치 로그 정리 Job 시작"); + } + + @Override + public void afterJob(JobExecution jobExecution) { + log.info("[LogCleanupJob] 배치 로그 정리 Job 완료 - 상태: {}", + jobExecution.getStatus()); + } + }) + .start(logCleanupStep()) + .build(); + } +} diff --git a/src/main/java/com/snp/batch/global/cleanup/LogCleanupTasklet.java b/src/main/java/com/snp/batch/global/cleanup/LogCleanupTasklet.java new file mode 100644 index 0000000..79f0da0 --- /dev/null +++ b/src/main/java/com/snp/batch/global/cleanup/LogCleanupTasklet.java @@ -0,0 +1,148 @@ +package com.snp.batch.global.cleanup; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.StepContribution; +import org.springframework.batch.core.scope.context.ChunkContext; +import org.springframework.batch.core.step.tasklet.Tasklet; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LogCleanupTasklet implements Tasklet { + + private final JdbcTemplate jdbcTemplate; + private final LogCleanupConfig config; + + @Value("${app.batch.target-schema.name}") + private String schema; + + @Override + public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception { + log.info("========================================"); + log.info("배치 로그 정리 Job 시작"); + log.info("========================================"); + + int totalDeleted = 0; + + // 1. batch_api_log 정리 + totalDeleted += cleanupApiLog(); + + // 2. Spring Batch 메타 테이블 정리 (FK 순서) + totalDeleted += cleanupBatchMeta(); + + // 3. batch_failed_record 정리 (RESOLVED만) + totalDeleted += cleanupFailedRecord(); + + // 4. batch_recollection_history 정리 + totalDeleted += cleanupRecollectionHistory(); + + log.info("========================================"); + log.info("배치 로그 정리 Job 완료 - 총 삭제: {} 건", totalDeleted); + log.info("========================================"); + + return RepeatStatus.FINISHED; + } + + private int cleanupApiLog() { + int days = config.getApiLogRetentionDays(); + String sql = String.format( + "DELETE FROM %s.batch_api_log WHERE created_at < NOW() - INTERVAL '%d days'", + schema, days); + int deleted = jdbcTemplate.update(sql); + log.info("[batch_api_log] 보존기간: {}일, 삭제: {}건", days, deleted); + return deleted; + } + + private int cleanupBatchMeta() { + int days = config.getBatchMetaRetentionDays(); + int totalDeleted = 0; + + // FK 의존 순서: step_execution_context → step_execution → job_execution_context → job_execution_params → job_execution → job_instance(orphan) + + // 1. batch_step_execution_context + String sql1 = String.format( + "DELETE FROM %s.batch_step_execution_context WHERE step_execution_id IN (" + + "SELECT se.step_execution_id FROM %s.batch_step_execution se " + + "JOIN %s.batch_job_execution je ON se.job_execution_id = je.job_execution_id " + + "WHERE je.create_time < NOW() - INTERVAL '%d days')", + schema, schema, schema, days); + int deleted = jdbcTemplate.update(sql1); + totalDeleted += deleted; + log.info("[batch_step_execution_context] 삭제: {}건", deleted); + + // 2. batch_step_execution + String sql2 = String.format( + "DELETE FROM %s.batch_step_execution WHERE job_execution_id IN (" + + "SELECT job_execution_id FROM %s.batch_job_execution " + + "WHERE create_time < NOW() - INTERVAL '%d days')", + schema, schema, days); + deleted = jdbcTemplate.update(sql2); + totalDeleted += deleted; + log.info("[batch_step_execution] 삭제: {}건", deleted); + + // 3. batch_job_execution_context + String sql3 = String.format( + "DELETE FROM %s.batch_job_execution_context WHERE job_execution_id IN (" + + "SELECT job_execution_id FROM %s.batch_job_execution " + + "WHERE create_time < NOW() - INTERVAL '%d days')", + schema, schema, days); + deleted = jdbcTemplate.update(sql3); + totalDeleted += deleted; + log.info("[batch_job_execution_context] 삭제: {}건", deleted); + + // 4. batch_job_execution_params + String sql4 = String.format( + "DELETE FROM %s.batch_job_execution_params WHERE job_execution_id IN (" + + "SELECT job_execution_id FROM %s.batch_job_execution " + + "WHERE create_time < NOW() - INTERVAL '%d days')", + schema, schema, days); + deleted = jdbcTemplate.update(sql4); + totalDeleted += deleted; + log.info("[batch_job_execution_params] 삭제: {}건", deleted); + + // 5. batch_job_execution + String sql5 = String.format( + "DELETE FROM %s.batch_job_execution WHERE create_time < NOW() - INTERVAL '%d days'", + schema, days); + deleted = jdbcTemplate.update(sql5); + totalDeleted += deleted; + log.info("[batch_job_execution] 삭제: {}건", deleted); + + // 6. batch_job_instance (참조 없는 인스턴스만) + String sql6 = String.format( + "DELETE FROM %s.batch_job_instance WHERE job_instance_id NOT IN (" + + "SELECT DISTINCT job_instance_id FROM %s.batch_job_execution)", + schema, schema); + deleted = jdbcTemplate.update(sql6); + totalDeleted += deleted; + log.info("[batch_job_instance] orphan 삭제: {}건", deleted); + + log.info("[Spring Batch 메타] 보존기간: {}일, 총 삭제: {}건", days, totalDeleted); + return totalDeleted; + } + + private int cleanupFailedRecord() { + int days = config.getFailedRecordRetentionDays(); + String sql = String.format( + "DELETE FROM %s.batch_failed_record WHERE status = 'RESOLVED' AND resolved_at < NOW() - INTERVAL '%d days'", + schema, days); + int deleted = jdbcTemplate.update(sql); + log.info("[batch_failed_record] 보존기간: {}일 (RESOLVED만), 삭제: {}건", days, deleted); + return deleted; + } + + private int cleanupRecollectionHistory() { + int days = config.getRecollectionHistoryRetentionDays(); + String sql = String.format( + "DELETE FROM %s.batch_recollection_history WHERE created_at < NOW() - INTERVAL '%d days'", + schema, days); + int deleted = jdbcTemplate.update(sql); + log.info("[batch_recollection_history] 보존기간: {}일, 삭제: {}건", days, deleted); + return deleted; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 49d7c50..7acd4b7 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -223,6 +223,13 @@ app: imo-column: imo_no # IMO/LRNO 컬럼명 (PK, NOT NULL) mmsi-column: mmsi_no # MMSI 컬럼명 (NULLABLE) + # 배치 로그 정리 설정 + log-cleanup: + api-log-retention-days: 30 # batch_api_log 보존 기간 + batch-meta-retention-days: 90 # Spring Batch 메타 테이블 보존 기간 + failed-record-retention-days: 90 # batch_failed_record (RESOLVED) 보존 기간 + recollection-history-retention-days: 90 # batch_recollection_history 보존 기간 + # 파티션 관리 설정 partition: # 일별 파티션 테이블 목록 (네이밍: {table}_YYMMDD) From 328f226356d663a481806d663b587787f3f45ffc Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Wed, 25 Mar 2026 16:07:57 +0900 Subject: [PATCH 2/5] =?UTF-8?q?docs:=20=EB=A6=B4=EB=A6=AC=EC=A6=88=20?= =?UTF-8?q?=EB=85=B8=ED=8A=B8=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/RELEASE-NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/RELEASE-NOTES.md b/docs/RELEASE-NOTES.md index 88995a1..f04c7c6 100644 --- a/docs/RELEASE-NOTES.md +++ b/docs/RELEASE-NOTES.md @@ -5,6 +5,7 @@ ## [Unreleased] ### 추가 +- 배치 로그 관리 정책 수립 및 정리 배치 작업 개발 (LogCleanupJob) (#100) - IMO Meta Table 관리 배치 작업 개발 (All IMO Import + Delete Flag Update) (#80) - Risk 상세 데이터 수집 배치 프로세스 추가 (RisksByImos API, 파티션 병렬 처리) (#65) - 배치 모니터링 React SPA 전환 및 10대 기능 강화 From 875ef2b7bc4110a1f395f56ba1b02a1f35562416 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Wed, 25 Mar 2026 16:55:11 +0900 Subject: [PATCH 3/5] =?UTF-8?q?refactor:=20AIS=20=EC=88=98=EC=A7=91=20?= =?UTF-8?q?=EB=B0=8F=20=EC=84=9C=EB=B9=84=EC=8A=A4=20API=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - aistarget, aistargetdbsync 패키지 전체 삭제 (34개 파일) - Kafka, JTS 의존성 제거 - API URL 환경별 중복 제거 (application.yml 공통 관리) - 프론트엔드 AIS 필터 버튼 제거 Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/pages/Executions.tsx | 23 - pom.xml | 11 - sql/chnprmship-cache-diag.sql | 149 -- .../com/snp/batch/SnpBatchApplication.java | 3 +- .../config/AisTargetImportJobConfig.java | 143 -- .../batch/dto/AisTargetApiResponse.java | 27 - .../aistarget/batch/dto/AisTargetDto.java | 167 -- .../batch/entity/AisTargetEntity.java | 129 -- .../processor/AisTargetDataProcessor.java | 137 -- .../batch/reader/AisTargetDataReader.java | 158 -- .../batch/repository/AisTargetRepository.java | 67 - .../repository/AisTargetRepositoryImpl.java | 409 ----- .../batch/writer/AisTargetDataWriter.java | 90 -- .../cache/AisTargetCacheManager.java | 272 ---- .../aistarget/cache/AisTargetFilterUtil.java | 229 --- .../aistarget/cache/SpatialFilterUtil.java | 317 ---- .../chnprmship/ChnPrmShipCacheManager.java | 131 -- .../chnprmship/ChnPrmShipCacheWarmer.java | 79 - .../chnprmship/ChnPrmShipProperties.java | 82 - .../classifier/AisClassTypeClassifier.java | 160 -- .../classifier/Core20CacheManager.java | 219 --- .../classifier/Core20Properties.java | 71 - .../aistarget/classifier/SignalKindCode.java | 118 -- .../aistarget/kafka/AisTargetKafkaConfig.java | 23 - .../kafka/AisTargetKafkaMessage.java | 55 - .../kafka/AisTargetKafkaProducer.java | 211 --- .../kafka/AisTargetKafkaProperties.java | 36 - .../web/controller/AisTargetController.java | 500 ------ .../web/dto/AisTargetFilterRequest.java | 165 -- .../web/dto/AisTargetResponseDto.java | 157 -- .../web/dto/AisTargetSearchRequest.java | 66 - .../aistarget/web/dto/NumericCondition.java | 90 -- .../web/service/AisTargetService.java | 422 ----- .../config/AisTargetDbSyncJobConfig.java | 92 -- .../ShipLastPositionSyncRepository.java | 16 - .../ShipLastPositionSyncRepositoryImpl.java | 185 --- .../batch/tasklet/AisTargetDbSyncTasklet.java | 121 -- .../tasklet/ShipLastPositionSyncTasklet.java | 134 -- src/main/resources/application-dev.yml | 69 +- src/main/resources/application-prod.yml | 69 +- src/main/resources/application.yml | 71 +- src/main/resources/chnprmship-mmsi.txt | 1402 ----------------- 42 files changed, 14 insertions(+), 7061 deletions(-) delete mode 100644 sql/chnprmship-cache-diag.sql delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/config/AisTargetImportJobConfig.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/dto/AisTargetApiResponse.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/dto/AisTargetDto.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/entity/AisTargetEntity.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/processor/AisTargetDataProcessor.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/reader/AisTargetDataReader.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/repository/AisTargetRepository.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/repository/AisTargetRepositoryImpl.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/batch/writer/AisTargetDataWriter.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/cache/AisTargetCacheManager.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/cache/AisTargetFilterUtil.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/cache/SpatialFilterUtil.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/chnprmship/ChnPrmShipCacheManager.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/chnprmship/ChnPrmShipCacheWarmer.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/chnprmship/ChnPrmShipProperties.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/classifier/AisClassTypeClassifier.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/classifier/Core20CacheManager.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/classifier/Core20Properties.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/classifier/SignalKindCode.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/kafka/AisTargetKafkaConfig.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/kafka/AisTargetKafkaMessage.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/kafka/AisTargetKafkaProducer.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/kafka/AisTargetKafkaProperties.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/web/controller/AisTargetController.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/web/dto/AisTargetFilterRequest.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/web/dto/AisTargetResponseDto.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/web/dto/AisTargetSearchRequest.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/web/dto/NumericCondition.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistarget/web/service/AisTargetService.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistargetdbsync/batch/config/AisTargetDbSyncJobConfig.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistargetdbsync/batch/repository/ShipLastPositionSyncRepository.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistargetdbsync/batch/repository/ShipLastPositionSyncRepositoryImpl.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistargetdbsync/batch/tasklet/AisTargetDbSyncTasklet.java delete mode 100644 src/main/java/com/snp/batch/jobs/aistargetdbsync/batch/tasklet/ShipLastPositionSyncTasklet.java delete mode 100644 src/main/resources/chnprmship-mmsi.txt diff --git a/frontend/src/pages/Executions.tsx b/frontend/src/pages/Executions.tsx index 0e77bf8..8c69792 100644 --- a/frontend/src/pages/Executions.tsx +++ b/frontend/src/pages/Executions.tsx @@ -93,9 +93,6 @@ export default function Executions() { return map; }, [displayNames]); - const aisJobs = useMemo(() => jobs.filter(j => j.toLowerCase().startsWith('ais')), [jobs]); - const nonAisJobs = useMemo(() => jobs.filter(j => !j.toLowerCase().startsWith('ais')), [jobs]); - const loadJobs = useCallback(async () => { try { const data = await batchApi.getJobs(); @@ -330,26 +327,6 @@ export default function Executions() { > 전체 - - {selectedJobs.length > 0 && (