From b7d71d4220104597831bd1325593963b3fe3a023 Mon Sep 17 00:00:00 2001 From: HYOJIN Date: Wed, 25 Mar 2026 16:07:20 +0900 Subject: [PATCH] =?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)