프로젝트에서 배치가 어떻게 동작하는지에 대한 흐름을 정리해봤다.
1. 배포 및 실행 방법
JAR 파일로 배포
Spring Boot 프로젝트를 gradle bootJar로 빌드하면 실행 가능한 JAR 파일이 생성된다. 이 JAR에는 내장 Tomcat과 모든 의존성이 포함되어 있어 별도의 웹 서버 설치 없이 실행할 수 있다.
NCP(Naver Cloud Platform) 서버에서 다음과 같이 실행한다:
java -jar project-common-batch.jar
이 명령어가 실행되면 Spring Boot 애플리케이션이 시작되며, 내장 웹 서버(포트 58080)가 함께 올라간다.
실행 주체: Java 프로세스가 CommonBatchApplication.main() 메서드를 실행한다.
2. 전체 동작 흐름
1단계: 애플리케이션 시작
서버 시작
↓
CommonBatchApplication.main() 실행
↓
Spring Boot 컨테이너 초기화
↓
모든 Job 빈 생성 (sampleJob, employeeJob, checkupPlanJob 등)
↓
RefreshScheduler 빈 생성 (모든 Job들을 Map으로 자동 주입)
Spring Boot 컨테이너란?
Spring Boot 컨테이너는 객체를 생성하고 관리하는 저장소다. @Bean, @Component 등이 붙은 클래스를 찾아 객체를 생성하고, 필요한 곳에 자동으로 주입한다.
일반적인 Java 프로그램 vs Spring Boot 컨테이너
일반적인 Java 프로그램:
┌─────────────────┐
│ 개발자가 직접 │
│ new 객체() │ ← 매번 직접 만들어야 함
│ 객체.메서드() │
└─────────────────┘
Spring Boot 컨테이너:
┌─────────────────────────────┐
│ Spring 컨테이너 (저장소) │
│ ┌───────────────────────┐ │
│ │ 객체1 (자동 생성) │ │
│ │ 객체2 (자동 생성) │ │ ← Spring이 알아서 생성/관리
│ │ 객체3 (자동 생성) │ │
│ │ ... │ │
│ └───────────────────────┘ │
└─────────────────────────────┘
Job 빈이란?
빈(Bean) = Spring 컨테이너가 관리하는 객체
Job 빈 = 배치 작업을 실행하는 객체
실제 코드 예시:
@Configuration // 이 클래스는 설정 클래스
public class SampleJobConfig {
@Bean // 이 메서드가 반환하는 객체가 "빈"이 됨
public Job sampleJob() { // 빈 이름: "sampleJob"
return new JobBuilder("sampleJob", jobRepository)
.start(step1())
.build();
}
}
동작 과정:
1. Spring 컨테이너 시작
↓
2. SampleJobConfig 클래스 발견 (@Configuration)
↓
3. sampleJob() 메서드 발견 (@Bean)
↓
4. sampleJob() 메서드 실행
↓
5. Job 객체 생성 ← 애플리케이션 시작 시 여기서 한 번만 생성!
↓
6. 컨테이너에 저장 (이름: "sampleJob")
컨테이너에 저장되는 모습:
Spring 컨테이너 (메모리)
├── "sampleJob" → Job 객체 (실제 배치 작업)
├── "employeeJob" → Job 객체
├── "checkupPlanJob" → Job 객체
├── "messageAtJob" → Job 객체
└── ... (다른 빈들)
2단계: 스케줄 등록 (30초마다 반복)
RefreshScheduler.onReady() 실행
↓
DB에서 활성 스케줄 조회 (hm_batch_job_schedule 테이블)
↓
각 스케줄마다:
- job_name으로 Job 객체 찾기
- cron_expr로 CronTrigger 생성
- TaskScheduler에 등록
onReady()
@EventListener(ApplicationReadyEvent.class)
public void onReady() {
running.set(true);
// 기동 직후 한 번 동기화
refreshSchedules();
}
핵심 포인트:
- 애플리케이션이 시작되면 ApplicationReadyEvent가 발생하여 onReady() 메서드가 호출된다.
- 이후 @Scheduled(cron = "30 * * * * *") 어노테이션으로 인해 매 분 30초마다 refreshSchedules() 메서드가 자동 실행된다.
- DB에서 활성 스케줄을 조회하고, 각 스케줄을 ThreadPoolTaskScheduler에 등록한다.
public interface BatchJobMapper {
@Select("""
SELECT
job_schedule_seq, job_name, cron_expr, job_params_json, start_dtm, end_dtm, use_yn
FROM hm_batch_job_schedule
WHERE use_yn='Y'
AND (start_dtm is null OR start_dtm <= now())
AND (end_dtm is null OR end_dtm >= now())
""")
List<BatchSchedule> findActive();
}
ApplicationReadyEvent란?
Spring Boot 애플리케이션이 완전히 시작되어 요청을 받을 준비가 되었을 때 발생하는 이벤트다.
Spring Boot는 시작 과정에서 여러 이벤트를 순차적으로 발생시킨다.
1. ApplicationStartingEvent - 애플리케이션 시작 직후
2. ApplicationEnvironmentPreparedEvent - Environment 준비 완료
3. ApplicationContextInitializedEvent - ApplicationContext 초기화
4. ApplicationPreparedEvent - ApplicationContext 준비 완료
5. ContextRefreshedEvent - ApplicationContext 리프레시 완료
6. ApplicationStartedEvent - 애플리케이션 시작 완료 (아직 준비 안됨)
7. ApplicationReadyEvent - 애플리케이션 완전히 준비됨 ✅
ApplicationReadyEvent를 왜 사용할까?
모든 Bean이 생성되고 초기화된 후 실행되기 때문에 외부 서비스 연결, 캐시 초기화, 스케줄러 시작 등 초기화 작업에 적합하다.
3단계: 시간이 되면 자동 실행
CronTrigger가 계산한 시간 도달
↓
TaskScheduler 스레드 풀에서 자동 실행
↓
runJob() 호출
↓
JobLauncher가 Spring Batch Job 실행
↓
Job → Step → Tasklet 순차 실행
↓
실제 비즈니스 로직 수행 (DB 조회, API 호출 등)
실행 메커니즘:
- ThreadPoolTaskScheduler가 등록된 CronTrigger를 모니터링한다.
- 설정된 시간(예: 매일 새벽 2시)이 되면 스레드 풀의 스레드가 자동으로 람다 함수를 실행한다.
- runJob() 메서드가 호출되어:
- 동일 Job이 이미 실행 중인지 확인 (중복 실행 방지)
- DB에서 읽은 job_params_json을 파싱하여 Job 파라미터로 변환
- JobLauncher.run()을 통해 Spring Batch Job을 실행한다.
실제 코드:
private void runJob(Job job, BatchSchedule s) {
// 동일 JobName이 이미 실행 중이면 스킵
if (isJobRunning(job)) {
log.info("[scheduler] job={} 이미 실행중 → skip", job.getName());
return;
}
// JSON → Map 변환
Map<String, Object> paramMap = parseParams(s.getJobParamsJson());
// JobParameters 구성
JobParametersBuilder builder = new JobParametersBuilder();
paramMap.forEach((k, v) -> builder.addString(k, String.valueOf(v)));
builder.addLong("run.ts", System.currentTimeMillis());
// 실행
jobLauncher.run(job, builder.toJobParameters());
}
3. 핵심 메커니즘
데이터베이스 기반 동적 스케줄링
- 스케줄 정보는 hm_batch_job_schedule 테이블에 저장된다.
- 30초마다 DB를 조회하여 스케줄을 갱신하므로, 재시작 없이 스케줄 변경이 반영된다.
DB 스케줄 예시:
INSERT INTO hm_batch_job_schedule
(job_name, cron_expr, job_params_json, use_yn)
VALUES
('employeeJob', '0 0 2 * * ?', '{"companySeq": 3}', 'Y');
-- 매일 새벽 2시에 employeeJob 실행
자체 스케줄링 시스템
- Spring의 @Scheduled와 ThreadPoolTaskScheduler를 사용한다.
- 외부 cron 데몬이나 스케줄러가 필요 없다.
중복 실행 방지
- 동일한 Job이 이미 실행 중이면 스킵한다.
- JobExplorer를 통해 실행 상태를 확인한다.
4. 요약
- 배포: JAR 파일로 NCP 서버에 배포
- 실행: java -jar 명령어로 시작
- 스케줄링: DB 기반으로 30초마다 갱신
- 실행 주체: Spring의 TaskScheduler가 자동 실행
- 특징: 외부 스케줄러 불필요, 재시작 없이 스케줄 변경 가능
이 구조로 서버가 실행 중이면 DB에 등록된 스케줄에 따라 배치 작업이 자동으로 실행된다.
'Dev Log' 카테고리의 다른 글
| 로그 관리와 디버깅을 빠르게 만드는 방법 (0) | 2026.01.11 |
|---|---|
| 「기획자의 SQL」을 읽으며 정리한 개념 노트와 생각들 (2) | 2026.01.04 |
| “Swagger 사용해보셨나요?”라는 질문에 제대로 답하기2 (0) | 2025.12.21 |
| “Swagger 사용해보셨나요?”라는 질문에 제대로 답하기 (1) | 2025.12.14 |
| API 개발 과정에서 반복되는 SQL 질문 (1) | 2025.12.07 |