infra
Platform

모듈 맵

[Infra Ops] Cron/Quartz 장애 분석과 배치 재처리 실무

0 / 52 완료

펼치기
0 / 52 완료0%

Infra-ops · 32 / 52

[Infra Ops] Cron/Quartz 장애 분석과 배치 재처리 실무

crontab 스케줄링, Quartz 장애 패턴(misfire/lock 충돌), Spring Batch 재처리, 배치 실패 감지 및 알림까지

🚨INCIDENT ALERT
HIGH

새벽 2시에 실행돼야 할 정산 배치가 안 돌았습니다. 원인을 찾아보니 Quartz가 "Unable to acquire lock"를 남기고 실행을 포기했습니다. 다음날 같은 파라미터로 Spring Batch를 수동 실행하려니 "already exists and is complete" 예외가 납니다. 배치 실패를 감지해 알림을 받는 구조도 없어서 새벽에 아무도 몰랐습니다.

cron 스케줄링 기본부터 Quartz 장애 패턴, Spring Batch 재처리, 배치 실패 감지까지 — 배치 운영 실무를 정리합니다.

이번 챕터에서 배울 것
  • 1crontab 표현식을 읽고 실행 스케줄을 설명할 수 있다
  • 2cron 실행 로그 확인 방법과 Quartz misfire 개념을 설명할 수 있다
  • 3Quartz Lock 충돌 장애를 진단하고 DB에서 수동으로 해제할 수 있다
  • 4Spring Batch 재실행이 안 될 때 run.id 파라미터 추가로 해결할 수 있다
  • 5배치 실패 감지 스크립트와 알림 구조를 설명할 수 있다

crontab 기본

💡개념

crontab 표현식과 실행 로그 확인

Linux 시스템에서 정기 실행 작업의 대부분은 cron을 씁니다. 표현식 읽는 법과 실행 결과를 로그에서 확인하는 방법을 알아야 배치 문제를 진단할 수 있습니다.

crontab 표현식과 실행 로그 확인

새벽 2시에 돌아야 할 정산 배치가 실행되지 않았습니다. 아침에 고객사 민원이 들어오고 나서야 알았습니다. crontab에 등록은 됐다고 하는데, 실행 여부를 확인하는 방법을 모릅니다. 배치 실패가 조용히 일어나고 아무도 모르는 상황이 반복되면 서비스 신뢰도에 직격탄이 됩니다. 배치 운영의 첫걸음은 "언제 실행됐는지, 실행됐는지 안 됐는지"를 로그에서 확인하는 능력입니다. crontab 표현식을 읽고 실행 이력을 추적하는 방법을 익히면 이런 장애를 빠르게 진단할 수 있습니다.

crontab 표현식 구조:

┌──── 분 (0-59)
│  ┌─── 시 (0-23)
│  │  ┌── 일 (1-31)
│  │  │  ┌─ 월 (1-12)
│  │  │  │  ┌ 요일 (0-7, 0과 7 모두 일요일)
│  │  │  │  │
*  *  *  *  *  실행할 명령어

자주 쓰는 cron 표현식:

로컬 터미널
# 매일 새벽 2시 정각
0 2 * * * /opt/batch/run-daily.sh

# 매 30분마다
*/30 * * * * /opt/batch/check-status.sh

# 매 2시간 정각
0 */2 * * * /opt/batch/sync-data.sh

# 평일(월-금) 오전 9시
0 9 * * 1-5 /opt/batch/business-report.sh

# 매월 1일 새벽 3시
0 3 1 * * /opt/batch/monthly-settle.sh

# 매일 자정, 새벽 6시, 정오, 오후 6시
0 0,6,12,18 * * * /opt/batch/refresh-cache.sh
로컬 터미널
# crontab 관리 명령어
crontab -l               # 현재 사용자의 crontab 목록
crontab -e               # crontab 편집 (기본 에디터)
crontab -l -u appuser    # 특정 사용자의 crontab 확인 (root 권한)
sudo crontab -l -u root  # root 사용자 crontab

# cron 실행 로그 확인
grep "CRON" /var/log/syslog | tail -20          # Ubuntu/Debian
grep "CRON" /var/log/cron | tail -20            # RHEL/CentOS
journalctl -u cron --since "today" --no-pager  # systemd 환경

# 특정 스크립트 실행 기록
grep "run-daily.sh" /var/log/syslog | tail -10

Quartz 스케줄러

💡개념

Quartz 구성 요소와 misfire 처리

Quartz는 Java 애플리케이션에서 쓰는 스케줄링 라이브러리입니다. Spring Boot와 결합해 자주 사용합니다. 세 가지 핵심 개념만 이해하면 됩니다.

Quartz 구성 요소와 misfire 처리

Quartz 핵심 구성요소:

구성요소역할
Job실제로 실행할 작업 (execute 메서드 구현)
TriggerJob을 언제 실행할지 스케줄 정의 (CronTrigger, SimpleTrigger)
SchedulerJob과 Trigger를 관리하고 실행 조율

Misfire 정책:

Java
// CronTrigger에서 misfire 정책 설정 예시
CronTrigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("dailyJob", "batch")
    .withSchedule(CronScheduleBuilder
        .cronSchedule("0 2 * * * ?")
        // misfire 정책 선택
        .withMisfireHandlingInstructionFireAndProceed()  // 즉시 한 번 실행
        // .withMisfireHandlingInstructionDoNothing()    // 스킵 (다음 스케줄 대기)
        // .withMisfireHandlingInstructionIgnoreMisfires() // 밀린 모든 실행
    )
    .build();
Misfire 정책동작사용 상황
FIRE_AND_PROCEED즉시 한 번 실행 후 정상 스케줄 복귀정산, 집계 배치
DO_NOTHING밀린 실행 스킵주기적 상태 체크
IGNORE_MISFIRES밀린 횟수만큼 연속 실행각 실행이 독립적인 경우만
💡개념

Quartz Cluster 모드와 Lock 충돌

여러 WAS 인스턴스가 같은 Quartz Job을 중복 실행하지 않도록 DB Lock을 사용합니다. 이 Lock이 제대로 해제되지 않으면 장애가 납니다.

YAML
# Spring Boot + Quartz Cluster 설정 예시
spring:
  quartz:
    job-store-type: jdbc
    properties:
      org.quartz.scheduler.instanceId: AUTO
      org.quartz.jobStore.isClustered: true
      org.quartz.jobStore.clusterCheckinInterval: 20000
      org.quartz.jobStore.misfireThreshold: 60000

Quartz DB 테이블에서 Lock 확인:

SQL
-- QRTZ_LOCKS 테이블에서 현재 Lock 상태 확인 (MySQL)
SELECT * FROM QRTZ_LOCKS;

-- QRTZ_SCHEDULER_STATE로 클러스터 노드 상태 확인
SELECT SCHED_NAME, INSTANCE_NAME, LAST_CHECKIN_TIME, CHECKIN_INTERVAL
FROM QRTZ_SCHEDULER_STATE;

-- 오래된 노드 확인 (LAST_CHECKIN_TIME이 오래된 경우 죽은 노드)
SELECT *, FROM_UNIXTIME(LAST_CHECKIN_TIME/1000) AS last_seen
FROM QRTZ_SCHEDULER_STATE
ORDER BY LAST_CHECKIN_TIME ASC;

-- 죽은 노드의 Lock 수동 해제 (신중하게)
DELETE FROM QRTZ_FIRED_TRIGGERS WHERE SCHED_NAME='scheduler' AND INSTANCE_NAME='dead-node-id';
DELETE FROM QRTZ_SCHEDULER_STATE WHERE SCHED_NAME='scheduler' AND INSTANCE_NAME='dead-node-id';

배치 실행 관리

1crontab 설정 및 실행 확인

crontab을 조회하고 실행 이력을 로그에서 확인합니다.

로컬 터미널
# 현재 crontab 확인
crontab -l
# 출력 예시:
# 0 2 * * * /opt/batch/run-daily.sh >> /opt/batch/logs/daily.log 2>&1
# */30 * * * * /opt/batch/sync.sh >> /opt/batch/logs/sync.log 2>&1

# 오늘 실행된 cron 작업 확인
grep "CRON" /var/log/syslog | grep "$(date +%b %e)" | tail -20
# 출력 예시:
# May 30 02:00:01 server CRON[12345]: (appuser) CMD (/opt/batch/run-daily.sh)

# cron이 실행했는지 확인 (스크립트 이름으로 검색)
grep "run-daily" /var/log/syslog | tail -5

# 배치 스크립트 직접 로그 확인
tail -50 /opt/batch/logs/daily.log
crontab -l
🔍확인 포인트
  • crontab -l 출력에 등록된 스케줄 표현식이 의도한 실행 시각과 일치하는지 확인
  • /var/log/syslog 또는 /var/log/cron에서 오늘 날짜로 해당 스크립트 실행 기록이 있는지 확인
  • 배치 로그 파일 마지막 줄에 성공 또는 실패 메시지가 기록됐는지 확인
2배치 프로세스 상태 확인

실행 중인 배치 프로세스를 확인하고, 실패 로그를 분석합니다.

로컬 터미널
# 실행 중인 배치 프로세스 확인
ps aux | grep batch
# 출력 예시:
# appuser  12345  95.0  2.1 2048000 345678 ?  R  02:00   5:23 java -jar batch.jar

# 프로세스가 없으면 완료됐거나 비정상 종료
# 최근 exit code 확인 (cron 스크립트에서 기록한 경우)
grep -E "exit code|FAILED|ERROR|SUCCESS" /opt/batch/logs/daily.log | tail -10

# Java 배치 실패 로그 확인
grep -E "ERROR|FAILED|Exception|caused by" /opt/batch/logs/batch.log | tail -20

# Spring Batch — 실행 이력 DB 확인
mysql -h db-server -u appuser -p -e "
  SELECT JOB_INSTANCE_ID, JOB_NAME, JOB_KEY, CREATE_TIME
  FROM BATCH_JOB_INSTANCE
  ORDER BY CREATE_TIME DESC LIMIT 10;
"

# 최근 실행 결과 확인
mysql -h db-server -u appuser -p -e "
  SELECT e.JOB_EXECUTION_ID, i.JOB_NAME,
         e.START_TIME, e.END_TIME,
         e.STATUS, e.EXIT_CODE, e.EXIT_MESSAGE
  FROM BATCH_JOB_EXECUTION e
  JOIN BATCH_JOB_INSTANCE i ON e.JOB_INSTANCE_ID = i.JOB_INSTANCE_ID
  ORDER BY e.START_TIME DESC LIMIT 5;
"
ps aux | grep batch
🔍확인 포인트
  • ps aux | grep batch 결과에 실행 중인 배치 프로세스가 표시되는지 또는 이미 종료됐는지 확인
  • BATCH_JOB_EXECUTION 테이블에서 최근 실행의 STATUS 컬럼이 COMPLETED 또는 FAILED인지 확인
  • EXIT_CODEEXIT_MESSAGE 컬럼에서 오류 내용이 있는지 확인
3Spring Batch 재실행 — run.id 파라미터 추가

COMPLETED된 Spring Batch Job을 재실행할 때 고유한 파라미터를 추가합니다.

로컬 터미널
# 재실행 실패 에러 메시지
# org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException:
#   A job instance already exists and is complete for parameters={date=20260530}.
#   If you want to run this job again, change the parameters.

# 해결: run.id를 고유값으로 추가
java -jar /opt/batch/batch.jar \
  --spring.batch.job.names=dailySettleJob \
  date=20260530 \
  run.id=$(date +%s%N)

# run.id=$(date +%s%N) → 나노초 단위 timestamp (매번 고유)
# 또는
run.id=$(uuidgen)

# 특정 날짜 재처리 (파라미터 추가)
java -jar /opt/batch/batch.jar \
  --spring.batch.job.names=monthlyReportJob \
  targetMonth=202605 \
  run.id=$(date +%s%N)

# 재실행 후 결과 확인
mysql -h db-server -u appuser -p -e "
  SELECT STATUS, EXIT_CODE, EXIT_MESSAGE
  FROM BATCH_JOB_EXECUTION
  ORDER BY START_TIME DESC LIMIT 3;
"
java -jar batch.jar --spring.batch.job.names=dailyJob run.id=$(date +%s%N)
🔍실행 후 확인할 것
  • crontab -l에 스케줄이 올바르게 등록됐는가
  • cron 로그에 오늘 날짜로 실행 기록이 있는가
  • BATCH_JOB_EXECUTION 테이블에서 최근 실행의 STATUS가 COMPLETED인가
  • run.id 파라미터 추가 후 재실행 시 이미 존재 예외가 해소됐는가
  • 배치 로그 파일에 오류 없이 정상 종료 메시지가 있는가

배치 실패 감지와 알림

💡개념

배치 실패 감지 — exit code와 로그 기반 알림

배치가 실패해도 아무도 모르는 구조는 운영 장애입니다. 최소한 exit code 기반 알림 스크립트를 crontab에 함께 등록해야 합니다.

로컬 터미널
#!/bin/bash
# /opt/batch/scripts/run-with-alert.sh
# 배치 실행 + 실패 시 알림 통합 스크립트

BATCH_JAR="/opt/batch/batch.jar"
JOB_NAME="dailySettleJob"
TARGET_DATE=$(date +%Y%m%d)
LOG_FILE="/opt/batch/logs/batch_${TARGET_DATE}.log"
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

echo "[$(date)] 배치 시작: ${JOB_NAME} / ${TARGET_DATE}" >> "$LOG_FILE"

# 배치 실행
java -jar "$BATCH_JAR" \
  --spring.batch.job.names="$JOB_NAME" \
  targetDate="$TARGET_DATE" \
  run.id="$(date +%s%N)" \
  >> "$LOG_FILE" 2>&1

EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
    echo "[$(date)] 배치 성공 (exit code: 0)" >> "$LOG_FILE"
else
    echo "[$(date)] 배치 실패 (exit code: ${EXIT_CODE})" >> "$LOG_FILE"

    # Slack 알림 (curl로 Webhook 호출)
    LAST_ERROR=$(grep -E "ERROR|Exception" "$LOG_FILE" | tail -3 | tr '\n' ' ')
    curl -s -X POST "$SLACK_WEBHOOK" \
      -H "Content-Type: application/json" \
      -d "{
        \"text\": \":rotating_light: 배치 실패 알림\n*Job:* ${JOB_NAME}\n*날짜:* ${TARGET_DATE}\n*Exit Code:* ${EXIT_CODE}\n*마지막 오류:*\n${LAST_ERROR}\"
      }"

    exit 1
fi
로컬 터미널
# crontab 등록 (run-with-alert.sh 사용)
0 2 * * * /opt/batch/scripts/run-with-alert.sh

배치 실행 이력 모니터링 쿼리:

SQL
-- 최근 7일간 배치 실패 목록
SELECT i.JOB_NAME, e.START_TIME, e.END_TIME,
       e.STATUS, e.EXIT_CODE,
       LEFT(e.EXIT_MESSAGE, 200) AS EXIT_MESSAGE
FROM BATCH_JOB_EXECUTION e
JOIN BATCH_JOB_INSTANCE i ON e.JOB_INSTANCE_ID = i.JOB_INSTANCE_ID
WHERE e.STATUS != 'COMPLETED'
  AND e.START_TIME > DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY e.START_TIME DESC;

트러블슈팅

원인: 이전 WAS 인스턴스가 비정상 종료되면서 Quartz DB Lock을 해제하지 못했습니다. 다른 인스턴스가 같은 Job을 실행하려 하지만 Lock이 잠겨 있어 실행하지 못합니다.

로컬 터미널
# 1. 에러 로그 확인
grep -E "Unable to acquire lock|Couldn't rollback" /opt/app/logs/app.log | tail -10

# 2. Quartz DB Lock 테이블 확인
mysql -h db-server -u appuser -p quartz_db << 'SQL'
-- 현재 Lock 상태
SELECT * FROM QRTZ_LOCKS;

-- 죽은 인스턴스 확인 (마지막 체크인 시간이 오래된 경우)
SELECT INSTANCE_NAME,
       FROM_UNIXTIME(LAST_CHECKIN_TIME/1000) AS last_checkin,
       CHECKIN_INTERVAL
FROM QRTZ_SCHEDULER_STATE
ORDER BY LAST_CHECKIN_TIME ASC;
SQL

# 3. 죽은 인스턴스의 잔여 데이터 정리 (신중하게)
# 실행 중인 WAS 인스턴스 ID 먼저 확인 후 제거 대상 결정
mysql -h db-server -u appuser -p quartz_db << 'SQL'
-- 죽은 노드 정보 제거 (INSTANCE_NAME은 실제 죽은 노드 ID로 변경)
DELETE FROM QRTZ_FIRED_TRIGGERS
WHERE SCHED_NAME = 'MyScheduler' AND INSTANCE_NAME = 'dead-node-20260530';

DELETE FROM QRTZ_SCHEDULER_STATE
WHERE SCHED_NAME = 'MyScheduler' AND INSTANCE_NAME = 'dead-node-20260530';
SQL

# 4. Quartz 스케줄러 재시작
sudo systemctl restart app-server

# 5. Lock 재발 방지 — clusterCheckinInterval 확인
grep "clusterCheckinInterval" /opt/app/config/application.yml
# 20000 (20초) 권장 — 너무 길면 죽은 노드 감지가 늦음

원인: 동일한 JobParameters 조합으로 이미 COMPLETED된 JobInstance가 있어 Spring Batch가 중복 실행을 차단하고 있습니다. 재처리가 필요한 경우 파라미터를 다르게 만들어야 합니다.

DB 클라이언트
# 1. 어떤 파라미터로 실행됐는지 DB에서 확인
mysql -h db-server -u appuser -p -e "
  SELECT JIP.JOB_INSTANCE_ID, JI.JOB_NAME,
         JIP.KEY_NAME, JIP.STRING_VAL, JIP.DATE_VAL, JIP.LONG_VAL
  FROM BATCH_JOB_INSTANCE JI
  JOIN BATCH_JOB_EXECUTION_PARAMS JIP
    ON JI.JOB_INSTANCE_ID = (
      SELECT JOB_INSTANCE_ID FROM BATCH_JOB_EXECUTION
      ORDER BY START_TIME DESC LIMIT 1
    )
  WHERE JI.JOB_NAME = 'dailySettleJob';
"

# 2. run.id를 추가해 재실행
java -jar /opt/batch/batch.jar \
  --spring.batch.job.names=dailySettleJob \
  targetDate=20260530 \
  run.id=$(date +%s%N)

# 3. 코드 레벨 해결 — JobLauncher에서 run.id 자동 추가
# Spring Batch XML 설정:
# <bean id="jobLauncher" ...>
#   <property name="jobParametersIncrementer">
#     <bean class="org.springframework.batch.core.launch.support.RunIdIncrementer"/>
#   </property>
# </bean>

# Java Config:
# @Bean
# public Job dailyJob(JobBuilderFactory factory, Step step) {
#     return factory.get("dailySettleJob")
#         .incrementer(new RunIdIncrementer())  // run.id 자동 증가
#         .start(step)
#         .build();
# }

# 4. FAILED 상태의 Job은 같은 파라미터로 재시작 가능
# FAILED → 동일 파라미터로 재실행하면 중단 지점부터 재시작
mysql -h db-server -u appuser -p -e "
  SELECT STATUS, EXIT_CODE FROM BATCH_JOB_EXECUTION
  WHERE JOB_INSTANCE_ID = 12345 ORDER BY START_TIME DESC LIMIT 1;
"
# STATUS=FAILED → 동일 파라미터로 재실행 가능 (자동 재시작)
# STATUS=COMPLETED → run.id 필요
💼
실무 맥락
현업 패턴

실제 업무에서 이 지식이 쓰이는 상황:

배치 운영은 야간 장애의 주요 원인입니다. 인프라 엔지니어가 반복적으로 만나는 세 가지 상황입니다.

1. "배치가 안 돌았어요" 신고 접수 시:

로컬 터미널
# 5분 안에 원인 파악 루틴
# 1) cron 실행 여부 확인
grep "run-daily.sh" /var/log/syslog | grep "$(date +%b %e)"

# 2) 배치 로그 확인
tail -100 /opt/batch/logs/batch_$(date +%Y%m%d).log

# 3) Spring Batch 실행 이력 DB 확인
# SELECT STATUS, EXIT_CODE FROM BATCH_JOB_EXECUTION ORDER BY START_TIME DESC LIMIT 5;

# 4) Quartz Lock 확인 (Quartz 사용 시)
# SELECT * FROM QRTZ_FIRED_TRIGGERS WHERE FIRED_TIME < (UNIX_TIMESTAMP()-3600)*1000;

2. 운영 배치 스케줄 변경 요청 처리:

로컬 터미널
# 현재 스케줄 확인
crontab -l -u appuser

# 스케줄 변경 (예: 2시 → 1시 30분)
crontab -e -u appuser
# 0 2 → 30 1

# 변경 후 다음 실행 시간 계산 확인
# 온라인 cron 표현식 검증: https://crontab.guru

3. 재처리 요청 처리 체크리스트:

로컬 터미널
# □ 재처리 대상 날짜/범위 확인
# □ DB에서 기존 실행 결과 확인 (COMPLETED vs FAILED)
# □ 재처리 시 기존 데이터 중복 여부 확인 (멱등성)
# □ run.id 파라미터 추가
# □ 재실행 후 BATCH_JOB_EXECUTION STATUS 확인
# □ 처리 결과 데이터 정합성 검증

다음 모듈에서는 dev/stg/prod 환경변수와 설정 파일 분리 전략 — 환경별 설정 관리 실무를 다룹니다.

지식 확인

퀴즈 — 4문제

Q1

Quartz misfire가 발생하는 조건은?

Q2

cron 표현식 '0 */2 * * *'의 의미는?

Q3

Spring Batch에서 동일한 JobParameters로 재실행이 안 되는 이유는?

Q4

배치 프로세스가 실행 중인지 확인하는 가장 직접적인 방법은?

0 / 4 답변

🧪 실습으로 확인하기

Nginx 설치 및 기동

초급

Linux 서버에 Nginx를 설치하고 systemd 서비스로 등록하여 80포트에서 응답하는 상태까지 만든다.

30📋 3단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요

infra-ops중급 · 50
[Infra Ops] Git/GitLab 브랜치 전략과 릴리즈 관리
인프라 서비스 운영 트랙 계속
linux입문 · 30
[Linux] 개발자가 왜 리눅스 서버와 커맨드라인을 반드시 배워야 하는가
Linux 트랙 시작점