김코딩

[Spring Batch] 2편. Job, JobInstance, JobExecution의 관계 본문

Spring Batch

[Spring Batch] 2편. Job, JobInstance, JobExecution의 관계

김코딩딩 2026. 4. 18. 16:36

들어가며

1편에서 Spring Batch가 왜 필요한지 알아보았다. 그중 "실패 시 어디까지 처리했는지 기억하고 재시작할 수 있다"는 장점이 있었는데, 이게 어떻게 가능한지 내부를 들여다보면 Job, JobInstance, JobExecution이라는 세 가지 개념이 등장한다.

이 세 용어는 이름이 비슷해서 처음엔 굉장히 헷갈린다. 하지만 한 번 제대로 정리해두면 Spring Batch의 재시작 메커니즘, 중복 실행 방지, 메타데이터 관리가 전부 한 줄기로 꿰어진다.

이 글에서는 내가 학습하면서 가장 헷갈렸던 이 세 개념을 정리한다.


Job이 곧 JobInstance일까?

처음 공식 문서를 봤을 때 가장 먼저 든 생각은 이거였다.

"Job과 JobInstance가 이름도 비슷한데, 같은 거 아닌가?"

결론부터 말하면 다르다. 그리고 이 둘의 관계는 Java의 클래스-객체 관계와 거의 동일하다.

// Java에서 클래스는 설계도, 객체는 그 설계도에서 찍어낸 실체
class Member { ... }
Member m1 = new Member("철수", 28);  // 객체 1
Member m2 = new Member("영희", 27);  // 객체 2

Spring Batch도 똑같다. Job은 코드로 정의하는 "설계도"이고, 실행할 때마다 JobParameters와 조합되어 "JobInstance"라는 실체가 만들어진다.

A Job defines what a job is and how it is to be executed, and a JobInstance is a purely organizational object to group executions together, primarily to enable correct restart semantics.
Spring Batch Reference: The Domain Language of Batch

💭 나의 생각
이 비유를 스스로 찾아냈을 때 막혔던 퍼즐이 풀리는 느낌이었다. 공식 문서에서도 "Job과 JobInstance는 다르다"라고는 명확히 나와 있지만, 왜 나눴는지에 대한 직관적인 설명은 부족했다. Java 개발자에게는 클래스-객체 비유가 가장 빠른 이해 경로인 것 같다.


JobInstance의 유일성은 어떻게 결정될까

Job이 클래스고 JobInstance가 객체라면, 하나의 Job으로 여러 JobInstance를 만들 수 있다는 뜻이다. 그렇다면 무엇이 각 JobInstance를 구분 짓는가?

답은 JobParameters다. 공식 문서의 표현을 빌리면:

Thus, the contract can be defined as: JobInstance = Job + identifying JobParameters.
Spring Batch Reference: The Domain Language of Batch

예를 들어 dailySettlementJob이라는 Job을 매일 다른 날짜 파라미터로 실행하면, 매일 다른 JobInstance가 생성된다.

// 4/18 실행 - 4/17 데이터 정산
JobParameters params1 = new JobParametersBuilder()
    .addString("date", "2026-04-17")
    .toJobParameters();

// 4/19 실행 - 4/18 데이터 정산
JobParameters params2 = new JobParametersBuilder()
    .addString("date", "2026-04-18")
    .toJobParameters();

Job 이름은 dailySettlementJob으로 동일하지만 date 파라미터 값이 다르므로, 두 실행은 서로 다른 JobInstance로 취급된다.

💭 나의 생각
"파라미터 이름"이 아니라 "파라미터 값"이 유일성을 결정한다는 점이 중요하다. 두 번 모두 date라는 동일한 키를 쓰지만, 값(2026-04-17 vs 2026-04-18)이 다르기 때문에 다른 JobInstance가 된다. 처음에는 이걸 잘못 이해해서 "같은 파라미터 이름을 쓰면 같은 Instance인가?" 라고 생각했다.


JobExecution은 또 뭘까

JobInstance가 "해야 할 작업 1건"이라면, JobExecution은 그 작업을 실제로 돌린 시도다.

A JobExecution refers to the technical concept of a single attempt to run a Job.
Spring Batch Reference: The Domain Language of Batch

왜 이렇게 둘로 나눴을까? 바로 재시작 때문이다.

실무에서 배치는 실패할 수 있다. 네트워크가 끊기거나, DB가 순간 포화되거나, 예상하지 못한 데이터가 들어올 수 있다. 그런 일이 일어났을 때 "어떤 작업의 몇 번째 시도였는지"를 추적할 구조가 필요하다.

위 그림을 보면 이해가 쉽다:

  • Job: dailySettlementJob 하나
  • JobInstance: 날짜별로 3개 (4/17, 4/18, 4/19)
  • JobExecution:
    • Instance A (4/17): 첫 시도에 성공 → Execution 1개
    • Instance B (4/18): 첫 시도 실패, 두 번째 시도에 성공 → Execution 2개
    • Instance C (4/19): 첫 시도에 성공 → Execution 1개

중요한 건 Instance B의 경우, 같은 JobInstance 아래에 두 개의 JobExecution이 쌓였다는 점이다. 첫 실패가 기록으로 남아있기 때문에 두 번째 시도에서 "30만 건 처리 완료된 상태였지" 하고 이어서 갈 수 있는 것이다.

💭 나의 생각
이 구조는 장애 복구 관점에서 굉장히 잘 설계되어 있다. 운영 중 장애가 나면 실패한 Execution의 이력을 보고 원인을 파악하고, 같은 Instance로 재실행하면 이어서 처리된다. 결제나 정산처럼 절대 중복 처리되면 안 되는 도메인에서 Spring Batch를 쓰는 이유가 여기에 있다.


같은 파라미터로 재실행하면 어떻게 될까

위 그림에서 Instance B가 "실패 → 재시도"로 Execution 2개를 가진 것을 보면, 이런 의문이 생긴다.

"그럼 이미 성공한 Instance A(4/17)를 실수로 또 실행하면 Execution이 2개 생기는 건가?"

답은 아니다. Spring Batch는 이런 경우 아예 실행 자체를 차단한다.

규칙을 정리하면:

이전 실행 상태 재실행 시도 결과
COMPLETED (성공) JobInstanceAlreadyCompleteException 발생, 실행 차단
FAILED (실패) 같은 JobInstance에 새 JobExecution 생성, 이어서 실행

공식 문서의 표현은 다음과 같다.

If it is run again with the same identifying job parameters as the first run, a new JobExecution is created. However, there is still only one JobInstance.
Spring Batch Reference: The Domain Language of Batch

(위 문장은 실패 후 재실행 시나리오에 대한 설명이다. 성공한 Instance를 재실행하려 하면 예외가 발생한다.)

실무에서 만날 수 있는 상황

이 규칙이 왜 중요한지 실무 관점에서 생각해보자.

새벽에 정산 배치가 성공적으로 끝났는데, 코드에 버그가 있어서 정산 금액이 잘못 계산됐다는 게 오전에 발견됐다. CTO가 "어제 날짜로 다시 정산 돌려!"라고 지시한다. 같은 파라미터로 재실행 버튼을 누르면?

답: JobInstanceAlreadyCompleteException이 발생하며 실행이 차단된다.

이럴 때는 파라미터에 식별자를 추가하여 새로운 JobInstance로 만들거나, 별도의 보정 Job을 작성해서 실행하는 방식으로 해결한다.

// 방법 1: 파라미터에 식별자 추가
JobParameters params = new JobParametersBuilder()
    .addString("date", "2026-04-17")
    .addString("reason", "bugfix-2026-04-18")  // 새 Instance가 됨
    .toJobParameters();

💭 나의 생각
처음엔 "왜 성공한 걸 다시 못 돌리게 막아놨지?"라고 의아했다. 하지만 정산, 결제, 포인트 지급 같은 도메인을 생각해보면 당연한 설계다. 실수로 한 번 더 돌렸다가는 돈이 두 번 빠져나가는 사고가 날 수 있다. Spring Batch는 "프레임워크가 개발자의 실수를 막아주는" 사례라고 느꼈다. Saga 패턴에서 보상 트랜잭션(Compensating Transaction)을 별도로 설계하는 철학과도 비슷한 결이다.


이 모든 상태는 어디에 저장될까

Job, JobInstance, JobExecution의 상태 정보는 어디엔가 영구적으로 저장되어야 한다. 메모리에만 있으면 프로세스가 죽는 순간 날아가서 재시작이 불가능하다.

Spring Batch는 이를 위해 9개의 메타데이터 테이블을 자동으로 생성한다.

각 테이블의 역할은 다음과 같다.

Job 레벨

  • BATCH_JOB_INSTANCE: JobInstance 정보 (Job 이름, 파라미터 해시)
  • BATCH_JOB_EXECUTION: JobExecution 정보 (시작/종료 시간, 상태)
  • BATCH_JOB_EXECUTION_PARAMS: 전달받은 JobParameters의 실제 값
  • BATCH_JOB_EXECUTION_CONTEXT: Job 단위 상태 저장소 (재시작용)

Step 레벨

  • BATCH_STEP_EXECUTION: Step 실행 정보 (read/write/skip/commit 카운트)
  • BATCH_STEP_EXECUTION_CONTEXT: Step 단위 상태 저장소 (재시작용, 예: 마지막 처리 ID)

시퀀스

  • BATCH_JOB_INSTANCE_SEQ, BATCH_JOB_EXECUTION_SEQ, BATCH_STEP_EXECUTION_SEQ: 각 테이블의 PK 생성용 시퀀스

실무에서 유용한 쿼리

배치 운영 중 이 테이블들은 가장 먼저 조회하게 된다.

-- 최근 실행된 Job 이력 확인
SELECT
    ji.JOB_NAME,
    je.JOB_EXECUTION_ID,
    je.START_TIME,
    je.END_TIME,
    je.STATUS,
    je.EXIT_CODE
FROM BATCH_JOB_INSTANCE ji
JOIN BATCH_JOB_EXECUTION je ON ji.JOB_INSTANCE_ID = je.JOB_INSTANCE_ID
ORDER BY je.START_TIME DESC
LIMIT 20;

-- 특정 실행의 Step별 처리 건수 확인
SELECT
    STEP_NAME,
    STATUS,
    READ_COUNT,
    WRITE_COUNT,
    COMMIT_COUNT,
    ROLLBACK_COUNT,
    READ_SKIP_COUNT,
    WRITE_SKIP_COUNT
FROM BATCH_STEP_EXECUTION
WHERE JOB_EXECUTION_ID = ?;

💭 나의 생각
장애 대응 시 가장 먼저 이 테이블을 뒤진다고 한다. 로그보다 빠를 때가 많다는 얘기를 여러 곳에서 봤는데, 실제 구조를 보니 그럴 만하다고 느꼈다. "read_count = 300,000, write_count = 299,998" 같은 숫자만 보면 "어디서 2건이 날아갔는지" 즉시 파악할 수 있는 구조다.


정리

이번 편에서 정리한 내용을 한 문장으로 요약하면 다음과 같다.

Job은 작업의 설계도, JobInstance는 파라미터로 구체화된 작업 1건, JobExecution은 그 작업의 실제 시도다.

그리고 이 세 개념의 상태는 모두 메타데이터 테이블 9개에 자동으로 기록된다. 이 기록이 Spring Batch의 재시작, 중복 실행 방지, 실행 이력 관리를 가능하게 한다.

💭 나의 생각
1편을 쓸 때 "Spring Batch는 대용량 데이터를 안전하고 재시작 가능하게 처리하는 프레임워크"라고 정리했었다. 2편을 쓰면서 보니 "안전성"과 "재시작성"이 전부 Job-Instance-Execution 구조와 메타데이터 테이블에서 나온다는 걸 명확히 알게 됐다. 다음 편에서는 이제 드디어 실제 코드로 들어가서, Chunk 기반 처리와 Reader/Processor/Writer를 다룬다.


다음 편 예고

다음 편에서는 Spring Batch 실무에서 가장 많이 쓰는 Chunk 기반 처리 구조를 다룬다. ItemReader, ItemProcessor, ItemWriter 세 요소가 각각 어떤 역할을 하고, 실제 코드가 어떻게 생겼는지 회원 등급 재계산 배치 예제로 알아본다.


참고 자료