feat(maintenance): 배치 로그 관리 정리 Job 구현 (#16) #20

병합
HYOJIN feature/ISSUE-16-batch-log-cleanup 에서 develop 로 2 commits 를 머지했습니다 2026-03-25 16:11:47 +09:00
4개의 변경된 파일160개의 추가작업 그리고 0개의 파일을 삭제

파일 보기

@ -4,6 +4,9 @@
## [Unreleased] ## [Unreleased]
### 추가
- 배치 로그 관리 정리 Job 구현: 90일 보관 기간 기준 자동 삭제 (#16)
## [2026-03-25] ## [2026-03-25]
### 추가 ### 추가

파일 보기

@ -0,0 +1,63 @@
package com.snp.batch.jobs.maintenance;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.batch.core.Job;
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.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.PlatformTransactionManager;
import java.time.LocalDateTime;
/**
* 배치 로그 정리 Job
* 보관 기간이 지난 Spring Batch 메타데이터를 삭제
*
* 실행 주기: 매주 일요일 00:00 (Quartz 스케줄)
*/
@Slf4j
@Configuration
@RequiredArgsConstructor
public class BatchLogCleanupJobConfig {
private final JobRepository jobRepository;
private final PlatformTransactionManager transactionManager;
private final BatchLogCleanupRepository cleanupRepository;
@Value("${app.batch.log-retention-days:90}")
private int retentionDays;
@Bean
public Job batchLogCleanupJob() {
return new JobBuilder("batchLogCleanupJob", jobRepository)
.start(batchLogCleanupStep())
.build();
}
@Bean
public Step batchLogCleanupStep() {
return new StepBuilder("batchLogCleanupStep", jobRepository)
.tasklet(batchLogCleanupTasklet(), transactionManager)
.build();
}
@Bean
public Tasklet batchLogCleanupTasklet() {
return (contribution, chunkContext) -> {
LocalDateTime before = LocalDateTime.now().minusDays(retentionDays);
log.info("[LogCleanup] 배치 로그 정리 시작 (보관 기간: {}일, 기준일시: {})", retentionDays, before);
int totalDeleted = cleanupRepository.deleteOldLogs(before);
log.info("[LogCleanup] 배치 로그 정리 완료 (총 {}건 삭제)", totalDeleted);
return RepeatStatus.FINISHED;
};
}
}

파일 보기

@ -0,0 +1,93 @@
package com.snp.batch.jobs.maintenance;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
/**
* 배치 로그 삭제 Repository
* FK 제약 조건 순서를 고려하여 삭제
*/
@Slf4j
@Repository
@RequiredArgsConstructor
public class BatchLogCleanupRepository {
private final JdbcTemplate jdbcTemplate;
/**
* 보관 기간이 지난 배치 로그를 삭제
* 삭제 순서: step_context step_execution job_context job_params job_execution job_instance(고아)
*
* @param before 시점 이전의 로그를 삭제
* @return 삭제된
*/
public int deleteOldLogs(LocalDateTime before) {
int total = 0;
// 1. batch_step_execution_context (step_execution FK)
int stepCtx = jdbcTemplate.update("""
DELETE FROM batch_step_execution_context
WHERE STEP_EXECUTION_ID IN (
SELECT se.STEP_EXECUTION_ID
FROM batch_step_execution se
INNER JOIN batch_job_execution je ON se.JOB_EXECUTION_ID = je.JOB_EXECUTION_ID
WHERE je.START_TIME < ?
)
""", before);
log.info("[LogCleanup] batch_step_execution_context: {}건 삭제", stepCtx);
total += stepCtx;
// 2. batch_step_execution
int stepExec = jdbcTemplate.update("""
DELETE FROM batch_step_execution
WHERE JOB_EXECUTION_ID IN (
SELECT JOB_EXECUTION_ID FROM batch_job_execution WHERE START_TIME < ?
)
""", before);
log.info("[LogCleanup] batch_step_execution: {}건 삭제", stepExec);
total += stepExec;
// 3. batch_job_execution_context
int jobCtx = jdbcTemplate.update("""
DELETE FROM batch_job_execution_context
WHERE JOB_EXECUTION_ID IN (
SELECT JOB_EXECUTION_ID FROM batch_job_execution WHERE START_TIME < ?
)
""", before);
log.info("[LogCleanup] batch_job_execution_context: {}건 삭제", jobCtx);
total += jobCtx;
// 4. batch_job_execution_params
int jobParams = jdbcTemplate.update("""
DELETE FROM batch_job_execution_params
WHERE JOB_EXECUTION_ID IN (
SELECT JOB_EXECUTION_ID FROM batch_job_execution WHERE START_TIME < ?
)
""", before);
log.info("[LogCleanup] batch_job_execution_params: {}건 삭제", jobParams);
total += jobParams;
// 5. batch_job_execution
int jobExec = jdbcTemplate.update("""
DELETE FROM batch_job_execution WHERE START_TIME < ?
""", before);
log.info("[LogCleanup] batch_job_execution: {}건 삭제", jobExec);
total += jobExec;
// 6. batch_job_instance (고아 인스턴스)
int jobInst = jdbcTemplate.update("""
DELETE FROM batch_job_instance
WHERE JOB_INSTANCE_ID NOT IN (
SELECT JOB_INSTANCE_ID FROM batch_job_execution
)
""");
log.info("[LogCleanup] batch_job_instance (고아): {}건 삭제", jobInst);
total += jobInst;
return total;
}
}

파일 보기

@ -104,6 +104,7 @@ app:
batch: batch:
chunk-size: 10000 chunk-size: 10000
sub-chunk-size: 5000 # Writer Sub-Chunk 분할 크기 sub-chunk-size: 5000 # Writer Sub-Chunk 분할 크기
log-retention-days: 90 # 배치 로그 보관 기간 (일)
source-schema: source-schema:
name: std_snp_data name: std_snp_data
tables: tables: