๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

๊ฐœ๋ฐœ/๋Ÿฐ์„ธ๊ถŒ

ํ”„๋กœ์ ํŠธ์—์„œ Batch Job ๋กœ๊ทธ์— MDC๋ฅผ ์ ์šฉํ–ˆ๋‹ค.

slf4j, logback, log4j2 ๋“ฑ์˜ ๋กœ๊ทธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์—์„œ ์ œ๊ณตํ•˜๋Š” Map ํ˜•์‹์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์˜ ํŠน์ง•์ ์ธ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ๋งค์ปค๋‹ˆ์ฆ˜์ด๋‹ค. MDC๋Š” key / value ์ €์žฅ์†Œ๋ฅผ ์ง€์›ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์ด ์ €์žฅ์†Œ๋Š” ThreadContext์— ์˜์กดํ•œ๋‹ค.

 

MDC๊ฐ€ ๋„์ž…๋œ ์ด์œ 

์—ฌ๋Ÿฌ ์Šค๋ ˆ๋“œ๊ฐ€ ์‹คํ–‰๋˜๋Š” ์„œ๋ฒ„ ํ™˜๊ฒฝ์—์„œ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธธ ๋•Œ, ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜์ง€ ์•Š๋Š”๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด ๋™์‹œ์— 3๋ช…์˜ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์š”์ฒญํ–ˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๋ฉด, ์ด๋Ÿฌํ•œ ์Šค๋ ˆ๋“œ์˜ ์š”์ฒญ ์ฒ˜๋ฆฌ๋Š” ์ˆœ์ฐจ์ ์œผ๋กœ ์ด๋ค„์ง€์ง€ ์•Š์œผ๋ฉฐ ์‹ฌ์ง€์–ด๋Š” ์™ธ๋ถ€ API ํ˜ธ์ถœ ๊ฐ™์€ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ๋‹ค๋ฅธ ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ๋„ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ ํŠน์ • ์š”์ฒญ์— ๋Œ€ํ•œ ๋กœ๊ทธ๋ฅผ ์ถ”์ ํ•˜๊ธฐ๊ฐ€ ์–ด๋ ต๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด ํ˜„์žฌ ๋Ÿฐ์„ธ๊ถŒ ํ”„๋กœ์ ํŠธ์—์„œ Spring Batch Job์— ๋Œ€ํ•œ ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ณ  ์žˆ๋Š”๋ฐ, ์ด ์‚ฌ์ด์— ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ์ด ์˜ค๋ฉด ๋กœ๊ทธ ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ๋˜์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์— Spring Batch์™€ ๊ด€๋ จ๋œ ๋กœ๊ทธ๋ฅผ ์ˆ˜ ๋งŽ์€ ๋กœ๊ทธ๋“ค ์‚ฌ์ด์—์„œ ์ฐพ์•„์•ผํ•œ๋‹ค.

 

@Slf4j  
@Component  
public class CourseSyncJobListener implements JobExecutionListener {  
  
    @Override  
    public void beforeJob(JobExecution jobExecution) {  
        JobParameters params = jobExecution.getJobParameters();  
        String runId = params.getString("run.id");  
  
        log.info("[CourseSyncJob ์‹œ์ž‘] run-id: {} | startTime: {}", runId, jobExecution.getStartTime());  
    }  
  
    @Override  
    public void afterJob(JobExecution jobExecution) {  
        JobParameters params = jobExecution.getJobParameters();  
        String runId = params.getString("run.id");  
        BatchStatus status = jobExecution.getStatus();  
  
        Duration duration = Duration.between(jobExecution.getStartTime(), jobExecution.getEndTime());  
  
        log.info("[CourseSyncJob ์ข…๋ฃŒ] run-id: {} | endTime: {}", runId, jobExecution.getEndTime());  
        log.info("์ƒํƒœ: {}", status);  
        log.info("์ฒ˜๋ฆฌ ์‹œ๊ฐ„: {}์‹œ๊ฐ„ {}๋ถ„ {}์ดˆ", duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart());  
        jobExecution.getStepExecutions().forEach(stepExecution ->  
                log.info("Step [{}]: Read={}, Write={}, Skip={}",  
                stepExecution.getStepName(),  
                stepExecution.getReadCount(),  
                stepExecution.getWriteCount(),  
                stepExecution.getSkipCount()  
        ));  
    }  
}


๊ทธ๋ž˜์„œ MDC๋ผ๋Š” ๊ฐœ๋…์ด ๋‚˜์™”๊ณ , ์š”์ฒญ๋งˆ๋‹ค ๊ณ ์œ  ๊ฐ’์„ ํ• ๋‹นํ•˜๊ณ , ์ด๋ฅผ ํ†ตํ•ด ๋กœ๊ทธ๋ฅผ ๊ด€๋ฆฌํ•˜๊ณ ์ž ๋“ฑ์žฅํ•œ ๊ฒƒ์ด๋‹ค. ์ฆ‰, ๋กœ๊ทธ์— ๋งฅ๋ฝ์„ ๋ถ€์—ฌํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค!

 

 

๊ทธ๋ ‡๋‹ค๋ฉด, ํ”„๋กœ์ ํŠธ์—์„œ๋Š”?

์œ„์˜ ๋Ÿฐ์„ธ๊ถŒ ์ฝ”๋“œ ์˜ˆ์‹œ์—์„œ๋Š” MDC๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ๊ฐ€ ์กฐ๊ธˆ ํ๋ ค์ง„๋‹ค. ์™œ๋ƒ๋ฉด, ํ•ด๋‹น Batch Job์€ ๋‹จ์ผ ์Šค๋ ˆ๋“œ๋กœ ์‹คํ–‰๋˜๊ณ  ๋งค์ผ ์ƒˆ๋ฒฝ 2์‹œ์— ๋”ฑ ํ•œ ๋ฒˆ ์‹คํ–‰๋œ๋‹ค. ๋”๊ตฐ๋‹ค๋‚˜ `CourseSyncJobListener`๋ฅผ ์ƒ์†ํ•˜๋Š” `CourseSyncJobListener`๊ฐ€ ๋‚ด๋ถ€์ ์œผ๋กœ Batch Job Id๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

 

@Slf4j  
@Component  
public class CourseSyncJobListener implements JobExecutionListener {  
  
    @Override  
    public void beforeJob(JobExecution jobExecution) {  
        JobParameters params = jobExecution.getJobParameters();  
        String runId = params.getString("run.id");  
  
        log.info("[CourseSyncJob ์‹œ์ž‘] run-id: {} | startTime: {}", runId, jobExecution.getStartTime());  
    }  
  
    @Override  
    public void afterJob(JobExecution jobExecution) {  
        JobParameters params = jobExecution.getJobParameters();  
        String runId = params.getString("run.id");  
        BatchStatus status = jobExecution.getStatus();  
  
        Duration duration = Duration.between(jobExecution.getStartTime(), jobExecution.getEndTime());  
  
        log.info("[CourseSyncJob ์ข…๋ฃŒ] run-id: {} | endTime: {}", runId, jobExecution.getEndTime());  
        log.info("[run-id: {}] ์ƒํƒœ: {}", runId, status);  
        log.info("[run-id: {}] ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: {}์‹œ๊ฐ„ {}๋ถ„ {}์ดˆ", runId, duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart());  
        jobExecution.getStepExecutions().forEach(stepExecution ->  
                log.info("[run-id: {}] Step [{}]: Read={}, Write={}, Skip={}",
                runId,
                stepExecution.getStepName(),  
                stepExecution.getReadCount(),  
                stepExecution.getWriteCount(),  
                stepExecution.getSkipCount()  
        ));  
    }  
}


์ฝ”๋“œ๋กœ ๋‚˜ํƒ€๋‚ด๋ฉด ์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ ๋กœ๊ทธ์— ๋Œ€ํ•œ ๊ณ ์œ ๊ฐ’์„ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ ์•„๋‹Œ๊ฐ€? ํ•˜์ง€๋งŒ, ์—ฌ๊ธฐ์„œ ๋‚ด๊ฐ€ ๊ฐ„๊ณผํ•œ๊ฒŒ ์žˆ์—ˆ๋‹ค. ํ˜„์žฌ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” Batch Job์„ ์ž๋™ ์‹คํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•๋ฟ ์•„๋‹ˆ๋ผ ์ˆ˜๋™ ํ˜ธ์ถœ ํ•  ์ˆ˜ ์žˆ๋Š” API๊ฐ€ ์žˆ๋‹ค๋Š” ์ ์ด๋‹ค. ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น API๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ `CourseSyncService`๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

 

@Slf4j  
@Component  
@RequiredArgsConstructor  
public class CourseSyncService {  
  
    private final JobLauncher jobLauncher;  
    private final Job courseSyncJob;  
  
    @Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")  
    public void runScheduledCourseSyncJob() {  
        try {  
            log.info("CourseSyncJob ์ž๋™ ์‹œ์ž‘");  
            JobParameters jobParameters = new JobParametersBuilder()  
                    .addString("run.id", "scheduled-" + System.currentTimeMillis())  
                    .toJobParameters();  
            jobLauncher.run(courseSyncJob, jobParameters);  
        } catch (Exception e) {  
            log.warn("[EXCEPTION] CourseSyncJob ์ž๋™ ์‹คํ–‰ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ", LogContent.exception(e));  
        }
    }  
  
    @Async("asyncExecutor")  
    public void runCourseSyncJob() {  
        try {  
            log.info("CourseSyncJob ์ˆ˜๋™ ์‹œ์ž‘");  
            JobParameters jobParameters = new JobParametersBuilder()  
                    .addString("run.id", "manual-" + System.currentTimeMillis())  
                    .toJobParameters();  
            jobLauncher.run(courseSyncJob, jobParameters);  
        } catch (Exception e) {  
            log.warn("[EXCEPTION] CourseSyncJob ์ˆ˜๋™ ์‹คํ–‰ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ", LogContent.exception(e));  
        }
    }  
}

 

 

์—ฌ๊ธฐ์„œ ์ˆ˜๋™ ์‹œ์ž‘๊ณผ ์ž๋™ ์‹œ์ž‘์„ ๊ตฌ๋ถ„ํ•˜๊ธฐ ์œ„ํ•ด ์ง„์ž… ๋กœ๊ทธ๋ฅผ ๋‚จ๊ธฐ๊ณ  ์žˆ๋‹ค. ํ•˜์ง€๋งŒ, `CourseSyncJobListener`๊ฐ€ ์ง„์ž… ๋กœ๊ทธ๋ฅผ ๋‹ค์‹œ ๋‚จ๊ธฐ๊ณ  ์žˆ์–ด ์ค‘๋ณต๋œ ๋กœ๊ทธ์ผ ๋ฟ ์•„๋‹ˆ๋ผ ์ˆ˜๋™์ธ์ง€ ์ž๋™์ธ์ง€ ๋ถ„๊ฐ„ํ•  ์ˆ˜๊ฐ€ ์—†๋‹ค. ๊ทธ๋ž˜์„œ ๊ธฐ์กด Listener์˜ ์ง„์ž… ๋กœ๊ทธ๋ฅผ ์ง€์šฐ๊ณ , `CourseSyncService`์—์„œ run.id์™€ ํ•จ๊ป˜ ๋‚จ๊ธธ ํ•„์š”๊ฐ€ ์žˆ์—ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด run.id์— ๋“ค์–ด๊ฐ€๋Š” ๋‚ด์šฉ์„ ๋ณ€์ˆ˜๋กœ ๋ถ„๋ฆฌํ•˜๊ณ , Job ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ฒจ๋„ ๋˜์ง€๋งŒ ๋‚˜์ค‘์— ๋‹ค๋ฅธ ์—ฐ๋™๋˜๋Š” ๋ถ€๋ถ„๋“ค์ด ์ƒ๊ธฐ๋ฉด ๋˜‘๊ฐ™์ด ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ๋„˜๊ฒจ์ค˜์•ผ ํ•œ๋‹ค! ์‹ฌ์ง€์–ด ๋งค๋ฒˆ ๋กœ๊ทธ ์•ž์— run.id๋ฅผ ๋‚จ๊ฒจ์•ผ ํ•˜๋Š” ๊ท€์ฐฎ์Œ๊นŒ์ง€ ์žˆ๋‹ค. ๊ทธ๋ž˜์„œ ์ฐจ๋ผ๋ฆฌ MDC๋ฅผ ์ด์šฉํ•˜๋Š”๊ฒŒ ํ›จ์”ฌ ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ๋‹ค.

 

@Slf4j  
@Component  
@RequiredArgsConstructor  
public class CourseSyncService {  
  
    private final JobLauncher jobLauncher;  
    private final Job courseSyncJob;  
  
    @Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")  
    public void runScheduledCourseSyncJob() {  
        String runId = "scheduled-" + System.currentTimeMillis();  
        MDC.put("run.id", runId);  
        try {  
            log.info("CourseSyncJob ์ž๋™ ์‹œ์ž‘");  
            JobParameters jobParameters = new JobParametersBuilder()  
                    .addString("run.id", runId)  
                    .toJobParameters();  
            jobLauncher.run(courseSyncJob, jobParameters);  
        } catch (Exception e) {  
            log.warn("[EXCEPTION] CourseSyncJob ์ž๋™ ์‹คํ–‰ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ", LogContent.exception(e));  
        } finally {  
            MDC.remove("run.id");  
        }  
    }  
  
    @Async("asyncExecutor")  
    public void runCourseSyncJob() {  
        String runId = "manual-" + System.currentTimeMillis();  
        MDC.put("run.id", runId);  
        try {  
            log.info("CourseSyncJob ์ˆ˜๋™ ์‹œ์ž‘");  
            JobParameters jobParameters = new JobParametersBuilder()  
                    .addString("run.id", runId)  
                    .toJobParameters();  
            jobLauncher.run(courseSyncJob, jobParameters);  
        } catch (Exception e) {  
            log.warn("[EXCEPTION] CourseSyncJob ์ˆ˜๋™ ์‹คํ–‰ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ", LogContent.exception(e));  
        } finally {  
            MDC.remove("run.id");  
        }  
    }  
}

 

๊ทธ๋ž˜์„œ CourseSyncService๋ฅผ ์ด๋ ‡๊ฒŒ ๊ฐœ์„ ํ–ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” runId๋ฅผ ์ƒ์„ฑํ•˜๊ณ  MDC์— ๋„ฃ๋Š”๋‹ค. ๊ทธ๋Ÿผ ํ•ด๋‹น ์Šค๋ ˆ๋“œ๊ฐ€ ์‹คํ–‰๋  ๋•Œ MDC๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์ด์— run.id๋ฅผ key๋กœ ๊ฐ€์ง€๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ๋Š”๋‹ค. ํ•˜์ง€๋งŒ, CourseSyncService์˜ ์Šค๋ ˆ๋“œ์™€ Batch Job ์‹คํ–‰ ์Šค๋ ˆ๋“œ๋Š” ๋‹ค๋ฅด๊ธฐ ๋•Œ๋ฌธ์— ์Šค๋ ˆ๋“œ ๋กœ์ปฌํ•œ MDC๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜๊ฐ€ ์—†์—ˆ๋‹ค!


์ด๋Ÿด ๋•Œ๋Š” ์Šค๋ ˆ๋“œ๊ฐ€ ์ „ํ™˜๋  ๋•Œ MDC๋ฅผ ์ „ํ™˜๋˜๋Š” ์Šค๋ ˆ๋“œ์— ๋„˜๊ฒจ์ฃผ์–ด์•ผ ํ•œ๋‹ค. ์ด๋Š” Spring์—์„œ ์ง€์›ํ•˜๊ณ  ์žˆ๋Š” `TaskDecorator`๋ฅผ ์ƒ์†ํ•˜์—ฌ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

private static class MdcTaskDecorator implements TaskDecorator {  
  
    @Override  
    public Runnable decorate(Runnable runnable) {  
        Map<String, String> contextMap = MDC.getCopyOfContextMap();  
  
        return () -> {  
            Map<String, String> previous = MDC.getCopyOfContextMap();  
            try {  
                if (contextMap != null) {  
                    MDC.setContextMap(contextMap);  
                } else {  
                    MDC.clear();  
                }  
                runnable.run();  
            } finally {  
                if (previous != null) {  
                    MDC.setContextMap(previous);  
                } else {  
                    MDC.clear();  
                }  
            }  
        };  
    }  
}
@Slf4j  
@Component  
public class CourseSyncJobListener implements JobExecutionListener {  
  
    @Override  
    public void afterJob(JobExecution jobExecution) {  
        BatchStatus status = jobExecution.getStatus();  
        Duration duration = Duration.between(jobExecution.getStartTime(), jobExecution.getEndTime());  
  
        StringBuilder stepInfo = new StringBuilder();  
        jobExecution.getStepExecutions().forEach(stepExecution ->  
                stepInfo.append(String.format(" | Step [%s]: Read=%d, Write=%d, Skip=%d",  
                        stepExecution.getStepName(),  
                        stepExecution.getReadCount(),  
                        stepExecution.getWriteCount(),  
                        stepExecution.getSkipCount()  
                ))  
        );  
  
        log.info("[CourseSyncJob ์ข…๋ฃŒ] ์ƒํƒœ: {} | ์ฒ˜๋ฆฌ ์‹œ๊ฐ„: {}์‹œ๊ฐ„ {}๋ถ„ {}์ดˆ{}",  
                status,  
                duration.toHoursPart(),  
                duration.toMinutesPart(),  
                duration.toSecondsPart(),  
                stepInfo);  
    }  
}


์ด๋ฅผ ํ†ตํ•ด์„œ CourseSyncJobListener๋ฅผ ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค. MDC๋ฅผ ํ†ตํ•œ ๊ฐœ์„ ์„ ํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ์ค„๋กœ ๋‚˜๋‰œ ๋กœ๊ทธ๋ฅผ ํ•˜๋‚˜์˜ ๋กœ๊ทธ๋กœ ํ•ฉ์น˜๊ธฐ๋„ ํ–ˆ๋‹ค. ์ข€ ๋” ์‰ฝ๊ฒŒ ์ถ”์ ํ•˜๊ธฐ ์œ„ํ•จ์ด์—ˆ๋‹ค. ๊ทธ๋ฆฌ๊ณ , ์ด๋Ÿฌํ•œ ๋ชฉ์ ์„ ์ข€ ๋” ์ž˜ ๋‚˜ํƒ€๋‚ด๊ธฐ ์œ„ํ•ด์„œ Logback ์„ค์ •์„ ํ†ตํ•ด Batch Job์˜ ๊ฒฝ์šฐ ๊ธฐ์กด ๋กœ๊ทธ์™€ ํ•จ๊ป˜ ํŒŒ์ผ๋กœ ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ ๋”ฐ๋กœ batch.log๋ผ๋Š” ํŒŒ์ผ์— ๊ธฐ๋กํ•˜๋„๋ก ๋ณ€๊ฒฝํ–ˆ๋‹ค~!