Spring Batch 도입부터 운영까지 - #1
(시한폭탄 제조)

Spring Batch
by hwaneehwanee·Oct 27, 2025

새로운 회사에 입사한 지 얼마 되지 않았을 때, 배치 시스템을 구축해야 하는 상황이 생겼다. 사내 메인 서비스는 .NET 기반이었지만, 당시 나는 .NET 경험이 많지 않았다. 그래서 익숙한 Java 생태계를 활용해 Spring Batch로 배치 처리를 진행하기로 결정했다.

문제는 Spring Batch 역시 업무에는 처음 적용해보는 기술이었다는 점이다.

요구사항

당시 환경은 다음과 같았다.

  • 여러 종류의 Database를 사용하고 있어 Multi DataSource 구성이 필요
  • 운영 서버의 OS가 Windows
  • 배포 시 다른 Job 프로세스에 영향을 주면 안 됨

나는 그동안 주로 Linux 환경에서 서버를 운영해왔기 때문에 비교적 단순하게 접근했다.

"Quartz로 스케줄링을 하고, 배포 시 다른 Job에 영향이 없도록 배치들을 멀티 모듈 구조로 분리하면 되겠네."

초기 설계

스케줄러는 Quartz를 사용하기로 했다.
다만 한 가지 조건을 추가했다.

  • Cron Expression을 소스 코드에 하드코딩하지 않는다.
  • Job 실행 여부와 상관없이 언제든지 스케줄을 제어할 수 있어야 한다.

이를 위해 Spring Batch 기본 테이블인 batch_job_instance에 아래 두 개의 커스텀 컬럼을 추가했다.

create table batch_job_instance
(
    JOB_INSTANCE_ID     bigint            not null primary key,
    VERSION             bigint            null,
    JOB_NAME            varchar(100)      not null,
    JOB_KEY             varchar(32)       not null,
    IS_SCHEDULER_ACTIVE tinyint default 1 null comment '스케줄링 실행 여부 (0: 비활성화, 1: 활성화)',
    CRON_EXPRESSION     varchar(100)      null comment 'Cron Expression',
    DESCRIPTION         varchar(255)      null comment 'JOB 설명',
    constraint JOB_INST_UN unique (JOB_NAME, JOB_KEY)
);

추가한 컬럼

컬럼설명
IS_SCHEDULER_ACTIVE스케줄 활성화 여부
CRON_EXPRESSION런타임에 변경 가능한 Cron 설정

이렇게 하면 DB 값만 수정해도 배치 실행 여부나 실행 주기를 즉시 변경할 수 있었다.

프로젝트 구조

최종적으로 프로젝트는 다음과 같은 멀티 모듈 구조로 구성했다.

├─Root Project
│
├─module-core
│  ├─config
│  ├─dto
│  ├─scheduler
│  ├─service
│
├─module-batch-1
├─module-batch-2
├─...
└─module-batch-n

module-core

공통 기능을 담당하는 모듈이다.

  • config : DataSource 및 공통 설정
  • dto : Core 모듈에서 사용하는 DTO
  • scheduler : Quartz Scheduler 관련 공통 클래스
  • service : 각 모듈에서 사용하는 공통 서비스

모든 하위 배치 모듈은 module-core를 의존성으로 포함하여 빌드된다.

Quartz와 Spring Batch 연결

애플리케이션이 실행되면 자동으로 스케줄이 등록되어야 하고, 이후 Quartz가 주기적으로 Batch Job을 실행해야 했다.

또한 모든 배치 모듈이 동일한 방식으로 스케줄링되도록 강제하고 싶었다. 배치마다 스케줄 등록 방식이 달라지면 유지보수가 어려워질것으로 보였다.

이를 위해 module-core에 두 개의 추상 클래스를 만들었다.

CustomJobRunner

애플리케이션 시작 시 실행되는 클래스

역할

  • 초기 Cron 설정
  • Quartz Scheduler 등록

CustomJobLauncher

Quartz가 주기적으로 호출하는 클래스

역할

  • 배치 실행
  • 스케줄 활성화 여부 확인
  • Cron 변경 여부 감지
  • Trigger 재등록

각 배치 모듈은 이 두 추상 클래스를 상속받아 구현하도록 하여, 스케줄 관련 로직을 공통화하고 동일한 개발 패턴을 따르도록 강제하였다.

전체 동작 흐름

[애플리케이션 시작]

module-batch(n)
   ↓
CustomJobRunner.run()
   ↓
Quartz Scheduler 등록

----------------------------

[스케줄 실행]

Quartz Trigger
   ↓
CustomJobLauncher.execute()
   ↓
Spring Batch Job 실행

당시에는 꽤 괜찮은 구조(?)라고 생각했다.
실제로도 문제없이 동작했고, 운영 환경에서도 안정적으로 실행되었다.

문제점 발견

처음에는 전혀 문제를 느끼지 못했다. 배치도 잘 돌아갔고 장애도 없었다.
하지만 시간이 지나면서 배치 Job 수가 늘어나고, 배치 모듈도 하나둘씩 증가하기 시작했다.

그러던 중 모니터링 과정에서 몇 가지 문제가 눈에 띄기 시작했다.

1. 구조가 직관적이지 않다

Spring Batch 자체도 처음 접하면 학습 난이도가 있는 편이다.

그런데 여기에 Quartz 스케줄링을 추가하면서

  • 추상 클래스 이해
  • CustomJobRunner 구현
  • CustomJobLauncher 구현
  • Quartz 동작 방식 이해

까지 요구하게 되었다.

결과적으로 팀원이 프로젝트에 참여했을 때 구조를 이해하는 데 생각보다 많은 시간이 필요했다.


2. 서버 메모리 사용량 증가

더 큰 문제는 리소스 사용량이었다.
현재 구조에서는 스케줄링이 애플리케이션 내부에서 동작한다.

즉, 실제 배치가 실행되지 않더라도 스케줄러를 유지하기 위해 애플리케이션은 항상 살아 있어야 한다.

배치 모듈 증가
   ↓
실행 중인 프로세스 증가
   ↓
메모리 사용량 증가
   ↓
서버 자원 낭비

초기에는 모듈 수가 적어 문제가 되지 않았다.

하지만 시간이 지나면서 배치 애플리케이션이 하나둘 늘어나자 서버 리소스를 지속적으로 점유하기 시작했다.

다행히 당시 서버 메모리에 여유가 있어 큰 장애로 이어지지는 않았지만, 언젠가는 문제가 발생할 수 있는 구조라는 생각이 들었다.

결국 구조 개선이 불가피하다고 판단했다.

구조 개편이 필요해졌다

처음에는 배치도 정상적으로 동작했고, 운영 환경에서도 안정적으로 실행되었다. 하지만 시간이 지나면서 배치 모듈이 늘어나고 운영 기간이 길어질수록 구조적인 한계가 조금씩 드러나기 시작했다.

특히 배치 실행 여부와 관계없이 애플리케이션이 항상 실행되어야 하는 점은 서버 리소스 측면에서 비효율적이었고, Quartz를 중심으로 구성된 구조 역시 생각보다 복잡했다.

결국 "배치를 실행하기 위해 정말 애플리케이션이 항상 살아 있어야 할까?"라는 고민을 하게 되었고, 보다 단순하고 효율적인 구조로 개선하기로 결정했다.

다음 글에서는 이러한 문제를 해결하기 위해 어떤 방향으로 구조를 개편했는지, 그리고 그 과정에서 얻은 경험을 정리해보려고 한다.

2편에서 계속...

연관 포스트