Aspectran Scheduler는 애플리케이션 내의 특정 작업을 정해진 시간에 또는 주기적으로 실행할 수 있도록 하는 강력한 기능입니다. 이 기능을 통해 개발자는 배치(Batch) 작업, 데이터 동기화, 리포트 생성 등 다양한 백그라운드 태스크를 Aspectran의 핵심 컴포넌트인 Translet을 사용하여 손쉽게 구현하고 관리할 수 있습니다.
1. 핵심 아키텍처: 내장된 Quartz 기반 프레임워크
Aspectran Scheduler의 가장 중요한 특징은 Quartz 스케줄러 기반의 강력한 스케줄링 프레임워크를 코어에 내장하고 있다는 점입니다. 개발자는 별도의 스케줄링 라이브러리를 직접 통합하는 복잡한 과정 없이, Aspectran이 제공하는 설정 규칙만으로 즉시 스케줄링 기능을 활성화하고 사용할 수 있습니다.
핵심은 SchedulerService 인터페이스이며, Aspectran은 이 인터페이스의 기본 구현체로 Quartz를 사용하는 DefaultSchedulerService를 제공합니다. 따라서 개발자는 스케줄링의 복잡한 내부 동작을 신경 쓸 필요 없이, 자신이 가장 잘 아는 Translet을 사용하여 원하는 작업을 손쉽게 자동화할 수 있습니다.
2. 스케줄러 설정 방법
Aspectran은 스케줄러를 설정하는 두 가지 주요 방법을 제공합니다.
- XML/APON 기반 설정: 전통적이고 명시적인 방법으로, 모든 규칙을 설정 파일에 정의합니다.
- 어노테이션 기반 설정: Java 클래스 내에서 어노테이션을 사용하여 스케줄링 규칙을 직접 정의하는 현대적인 방법입니다.
방법 1: XML/APON 기반 설정
이 방식은 스케줄러 빈(Bean)과 스케줄 규칙(<schedule>)을 XML 또는 APON 파일에 명시적으로 정의합니다.
1단계: 스케줄러 빈(Bean) 정의하기
스케줄러 인스턴스를 생성하는 빈을 정의합니다. Aspectran이 제공하는 QuartzSchedulerFactoryBean을 사용하는 것이 가장 간결하고 권장되는 방법입니다.
<bean id="scheduler1" class="com.aspectran.core.scheduler.support.QuartzSchedulerFactoryBean">
<property type="properties" name="quartzProperties">
<entry name="org.quartz.scheduler.instanceName">MyScheduler</entry>
<entry name="org.quartz.threadPool.threadCount">10</entry>
<!-- 기타 모든 Quartz 속성... -->
</property>
</bean>
Quartz 속성 설정 스레드 풀(threadPool), 잡 스토어(jobStore) 등 Quartz 스케줄러의 세부 동작은 매우 다양한 속성을 통해 제어할 수 있습니다. 모든 속성에 대한 자세한 내용은 Quartz 공식 설정 문서를 참고하시기 바랍니다.
2단계: <schedule> 규칙 정의하기
정의된 스케줄러 빈을 사용하여, <schedule> 규칙 그룹을 정의합니다. 이 규칙은 하나의 실행 주기(트리거)를 공유하는 잡(Job)들의 그룹을 의미합니다.
<schedule id="my-schedule">
<!-- 1. 언제 실행할지 정의 (트리거) -->
<scheduler bean="scheduler1">
<trigger type="cron">
expression: 0 0 2 * * ?
</trigger>
</scheduler>
<!-- 2. 무엇을 실행할지 정의 (잡) -->
<job translet="/batch/daily-report"/>
<job translet="/batch/log-archive"/>
</schedule>
방법 2: 어노테이션 기반 설정
@Schedule 어노테이션을 사용하면, 스케줄러 빈 정의부터 잡(Job)과 트리거(Trigger) 설정까지 모든 것을 하나의 Java 클래스 안에서 선언적으로 처리할 수 있습니다. 이는 코드의 응집도를 높이고, XML 설정을 최소화하는 장점이 있습니다.
@Schedule 어노테이션 사용 예제
다음 예제는 AnnotatedScheduledTasks.java 클래스 하나에 스케줄러 빈, 스케줄 규칙, 그리고 잡이 실행할 Translet까지 모두 정의한 것입니다.
@Component
@Bean("annotatedScheduledTasks")
@Schedule(
id = "annotated-schedule",
scheduler = "annotatedScheduler", // 아래 @Bean 메소드에서 생성될 스케줄러 빈의 이름
cronTrigger = @CronTrigger(
expression = "0/10 * * * * ?" // 10초마다 실행
),
jobs = {
@Job(translet = "/annotated/job1") // 실행할 Translet 이름
}
)
public class AnnotatedScheduledTasks {
// 1. 잡(Job)이 실행할 Translet을 정의
@Request("/annotated/job1")
@Transform(FormatType.TEXT)
public String myScheduledTask() {
return "Annotation-based job executed at " + LocalDateTime.now();
}
// 2. 스케줄러 빈(Bean)을 정의
@Bean("annotatedScheduler")
public org.quartz.Scheduler createScheduler() throws SchedulerException {
// 필요에 따라 Quartz 속성을 로드하거나 생성
Properties props = new Properties();
props.put("org.quartz.scheduler.instanceName", "AnnotatedScheduler");
props.put("org.quartz.threadPool.threadCount", "5");
SchedulerFactory factory = new StdSchedulerFactory(props);
return factory.getScheduler();
}
}
@Schedule: 클래스 레벨에 붙여 스케줄 규칙을 정의합니다.id,scheduler빈 이름, 트리거(cronTrigger또는simpleTrigger), 그리고 실행할jobs배열을 속성으로 가집니다.@Job:@Schedule의jobs속성 내에서 실행할 Translet을 지정합니다.@Request:@Job에 지정된translet이름을 처리하는 실제 메소드를 정의합니다. 즉, 이 메소드가 스케줄링된 작업의 본체가 됩니다.@Bean메소드:@Schedule의scheduler속성에서 참조할 QuartzScheduler인스턴스를 생성하여 빈으로 등록합니다.
트리거(Trigger) 타입 상세 설명
트리거는 잡(Job) 그룹이 실행될 시점을 정교하게 제어합니다. XML 방식의 <trigger> 요소와 어노테이션 방식의 @SimpleTrigger, @CronTrigger 어노테이션을 통해 설정할 수 있습니다.
simple 트리거: 간격 기반 반복
simple 트리거는 “지금부터 10초 후에 시작해서, 1시간마다, 총 5번 실행”과 같이 단순한 간격으로 작업을 반복할 때 사용합니다. 특정 간격으로 정해진 횟수만큼 또는 무한히 작업을 반복하는 데 가장 적합합니다.
- 주요 속성:
startDelaySeconds: 스케줄러가 시작된 후, 첫 실행을 지연할 시간 (초 단위)intervalInSeconds,intervalInMinutes,intervalInHours: 반복할 시간 간격repeatCount: 첫 실행 후 추가로 반복할 횟수 (-1은 무한 반복)repeatForever:true로 설정 시 무한 반복
- 예제 (XML): 1시간마다 무한히 반복
<trigger type="simple"> startDelaySeconds: 10 intervalInHours: 1 repeatForever: true </trigger> - 예제 (어노테이션): 5분 후에 시작하여, 30초 간격으로 총 10번 실행
@Schedule( // ... simpleTrigger = @SimpleTrigger( startDelaySeconds = 300, intervalInSeconds = 30, repeatCount = 9 // 최초 1회 실행 + 9회 반복 = 총 10회 ) // ... )
cron 트리거: 시간표 기반 예약
cron 트리거는 “매주 금요일 오후 5시 30분” 또는 “매달 마지막 날 새벽 1시”와 같이 달력과 연관된 복잡한 시간표에 따라 작업을 실행할 때 사용합니다. 이는 강력한 Cron 표현식을 기반으로 동작하여 매우 유연하고 강력한 스케줄링을 제공합니다.
- 주요 속성:
expression: 실행 시간표를 정의하는 Cron 표현식 문자열.
- 예제 (XML): 매일 밤 11시 50분에 실행
<trigger type="cron"> expression: 0 50 23 * * ? </trigger> - 예제 (어노테이션): 매주 월요일부터 금요일까지, 오전 9시 30분에 실행
@Schedule( // ... cronTrigger = @CronTrigger(expression = "0 30 9 ? * MON-FRI") // ... )
오작동 정책(Misfire Policy): 누락된 실행 처리
오작동(misfire)은 스케줄러가 중지되어 있었거나 Quartz 스레드 풀에 여유 스레드가 없어 트리거가 예정된 시간에 실행되지 못했을 때 발생합니다. misfirePolicy 속성을 통해 이러한 상황을 어떻게 처리할지 정의할 수 있습니다.
오작동 감지 기준: misfireThreshold
트리거가 실제로 ‘오작동(misfire)’했는지 여부는 org.quartz.jobStore.misfireThreshold 속성(밀리초 단위)에 의해 결정됩니다. 이 임계값은 스케줄러가 트리거의 실행 지연을 허용하는 ‘유예 기간’을 의미합니다.
- 임계값 이내: 현재 시간이 예정된 실행 시간으로부터 임계값 이내라면, 스케줄러는 오작동 정책을 적용하지 않고 가능한 한 빨리 트리거를 실행합니다.
- 임계값 초과: 지연 시간이 임계값을 초과하면 트리거는 공식적으로 ‘오작동’으로 간주되며, 이때 설정된
misfirePolicy가 적용됩니다.
Quartz의 기본값은 60,000밀리초(1분)입니다. 스케줄러 빈 설정의 quartzProperties에서 이 값을 조정할 수 있습니다:
<bean id="scheduler1" class="com.aspectran.core.scheduler.support.QuartzSchedulerFactoryBean">
<property type="properties" name="quartzProperties">
<!-- 오작동 임계값을 30초로 설정 -->
<entry name="org.quartz.jobStore.misfireThreshold">30000</entry>
<!-- ... 기타 속성 ... -->
</property>
</bean>
cron 트리거에서 사용 가능한 정책
ignoreMisfires: 누락된 모든 실행을 즉시 실행합니다.smartPolicy(기본값): 누락된 실행 중 첫 번째 것만 즉시 실행하고 나머지는 폐기합니다. (fireOnceNow와 동일)fireOnceNow: 누락된 실행 중 첫 번째 것만 즉시 실행하고 나머지는 폐기합니다.doNothing: 누락된 모든 실행을 폐기하고 다음 예정된 시간까지 대기합니다.
simple 트리거에서 사용 가능한 정책
ignoreMisfires: 누락된 모든 실행을 가능한 한 빨리 실행합니다.smartPolicy(기본값):- 고정 반복(Fixed Repeat)인 경우:
rescheduleNowWithExistingRepeatCount와 동일. - 무한 반복(Infinite Repeat)인 경우:
rescheduleNextWithRemainingCount와 동일.
- 고정 반복(Fixed Repeat)인 경우:
fireNow: 누락된 첫 번째 실행을 즉시 수행하고 나머지는 폐기합니다.rescheduleNowWithExistingRepeatCount: 즉시 재실행하며, 남은 반복 횟수는 유지됩니다.rescheduleNowWithRemainingRepeatCount: 즉시 재실행하지만, 첫 번째 누락분이 반복 횟수에 포함됩니다.rescheduleNextWithExistingCount: 다음 예정된 시간까지 대기하며, 남은 반복 횟수는 유지됩니다.rescheduleNextWithRemainingCount: 누락된 실행을 모두 폐기하고 다음 예정된 시간까지 대기합니다.
설정 예제
```xml
<trigger type="cron">
expression: 0 0 2 * * ?
misfirePolicy: doNothing
</trigger>
```
- 예제 (어노테이션):
@CronTrigger( expression = "0 0 2 * * ?", misfirePolicy = MisfirePolicy.DO_NOTHING )
3. 스케줄 잡 로깅 및 모니터링
스케줄링된 작업의 실행 상태를 확인하고 디버깅하는 것은 매우 중요합니다. Aspectran은 스케줄 잡의 실행 이벤트를 Logback을 통해 상세하게 기록할 수 있도록 지원합니다.
로깅 메커니즘
Aspectran 스케줄러는 com.aspectran.core.scheduler.activity.ActivityJobReporter 클래스를 통해 잡의 시작, 성공, 실패 등의 이벤트를 로깅합니다. 이 리포터는 Quartz의 JobListener와 연동되어 잡의 생명주기 동안 발생하는 주요 정보를 기록합니다.
잡(Job) 실행 결과 로깅
스케줄 작업의 실제 실행 결과를 로그 메시지에 포함하고 싶다면, 잡이 실행하는 Translet에 응답 방식을 지정하면 됩니다. 메소드가 값을 반환하고 해당 Translet에 변환 작업(예: @Transform(FormatType.TEXT))이 정의되어 있으면, Aspectran은 이 응답 내용을 SUCCESS 로그 엔트리에 포함합니다.
예를 들어, 잡이 다음과 같이 설정되어 있다면:
@Request("test/schedule/count.job")
@Transform(FormatType.TEXT)
public String count() {
int count = counter.incrementAndGet();
return "Count: " + count;
}
성공 로그에는 다음과 같이 response 필드가 포함됩니다: DEBUG ... SUCCESS {group=countSchedule, name=test/schedule/count.job, ..., response=Count: 1}
로그 가독성을 위한 팁 Translet은 다양한 응답 형식을 지원하지만, 로그 메시지의 가독성을 유지하고 효율적인 로그 관리를 위해
FormatType.TEXT와 같은 간결한 텍스트 형식을 사용하는 것이 좋습니다.
Logback 설정 예제
스케줄러 로그를 별도의 파일로 분리하여 관리하려면 Logback의 SiftingAppender와 Aspectran의 LoggingGroupDiscriminator를 활용할 수 있습니다. 다음은 gs-scheduler 예제 프로젝트의 설정을 참고한 예시입니다.
<!-- app/config/logging/included/logback-scheduler.xml -->
<included>
<appender name="SIFT-SCHEDULER" class="ch.qos.logback.classic.sift.SiftingAppender">
<discriminator class="com.aspectran.logging.LoggingGroupDiscriminator">
<key>LOGGING_GROUP</key>
<defaultValue>root</defaultValue>
</discriminator>
<sift>
<appender name="FILE-SCHEDULER-${LOGGING_GROUP}" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${aspectran.basePath:-app}/logs/${LOGGING_GROUP}-scheduler.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${aspectran.basePath:-app}/logs/archived/${LOGGING_GROUP}-scheduler.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<charset>UTF-8</charset>
<pattern>%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %msg%n</pattern>
</encoder>
</appender>
</sift>
</appender>
<!-- ActivityJobReporter에서 발생하는 로그를 SIFT-SCHEDULER 앱펜더로 보냅니다. -->
<logger name="com.aspectran.core.scheduler.activity.ActivityJobReporter" level="debug" additivity="false">
<appender-ref ref="SIFT-SCHEDULER"/>
</logger>
</included>
메인 logback.xml 파일에서 위 설정을 포함하면 (<include file=".../logback-scheduler.xml"/>), ActivityJobReporter에서 발생하는 모든 로그는 LOGGING_GROUP 값에 따라 별도의 스케줄러 로그 파일로 기록됩니다. 기본 LOGGING_GROUP은 root이므로, 로그 파일은 app/logs/root-scheduler.log에 생성됩니다.
로그 파일 확인
애플리케이션 실행 후, 설정된 aspectran.basePath (기본값 app) 아래의 logs 디렉터리에서 root-scheduler.log 파일을 확인하여 스케줄 잡의 실행 로그를 모니터링할 수 있습니다.
4. 클러스터 환경에서의 분산 스케줄링
여러 노드가 하나의 클러스터로 동작하는 환경에서는 동일한 스케줄이 중복 실행되는 것을 방지하는 것이 매우 중요합니다. Aspectran은 Redis 기반의 분산 락(Distributed Lock) 기능을 통해 클러스터 전역에서 안전한 작업 실행을 보장합니다.
분산 락(Distributed Lock)의 작동 원리
Aspectran은 스케줄 작업 실행 직전, Redis에 아주 짧은 시간(TTL) 동안 해당 스케줄 고유의 락 키(Lock Key)를 생성하여 선점권을 확인합니다.
- 락 획득 시도: 작업을 수행하려는 노드는 Redis의
SET NX기능을 사용하여 락 획득을 시도합니다. - 작업 실행: 가장 먼저 실행 시점에 도달하여 락을 획득한 노드만 실제 작업을 수행합니다.
- 실행 스킵: 락 획득에 실패한 나머지 노드들은 해당 주기에서 작업을 자동으로 건너뜁니다.
- 안전성 보장: 작업 도중 노드가 비정상 종료되더라도, 락에 설정된 만료 시간(Expiration)이 지나면 자동으로 해제되어 다음 주기에서 다른 노드가 작업을 이어받을 수 있습니다.
isolated 속성을 통한 실행 제어
스케줄링 규칙에는 해당 작업이 분산 대상인지, 아니면 각 노드마다 독립적으로 항상 실행되어야 하는지를 결정하는 isolated 속성이 있습니다.
isolated="false"(기본값): 클러스터 전역에서 단 하나의 노드만 작업을 수행합니다. (분산 락 적용 대상)isolated="true": 클러스터 상태와 관계없이 모든 노드가 각자 정해진 시간에 작업을 수행합니다. 시스템 로그 수집이나 로컬 캐시 갱신과 같은 작업에 적합합니다.
실시간 제어 및 모니터링 (Scheduler Manager)
Aspectran Console은 클러스터 전역의 스케줄러를 한눈에 파악하고 조작할 수 있는 Scheduler Manager 화면을 제공합니다.
- 통합 뷰: 클러스터 내 모든 노드의 스케줄러 상태와 활성 잡 목록을 실시간으로 확인합니다.
- 동적 스위칭: 애플리케이션 재배포 없이 특정 스케줄이나 개별 잡을 즉시 활성화(Enable)하거나 비활성화(Disable)할 수 있습니다.
- 상태 가시성: 분산 락 기능이 정상적으로 작동하고 있는지, 어떤 노드에 연결되어 있는지를 시각적으로 보여줍니다.
5. 전체 설정 예제 및 참고 자료
Aspectran 스케줄러를 실제로 어떻게 구성하고 사용하는지에 대한 완전한 예제는 다음 GitHub 프로젝트에서 확인하실 수 있습니다. 이 프로젝트는 이 문서에서 설명한 모든 개념을 실제로 구현한 좋은 참고 자료입니다.
- Aspectran Scheduler 예제 프로젝트: https://github.com/aspectran/gs-scheduler
6. 결론
Aspectran Scheduler는 프레임워크의 핵심 사상인 추상화와 모듈화를 잘 보여주는 기능입니다. 개발자는 Quartz API의 복잡함을 신경 쓸 필요 없이, 자신이 가장 잘 아는 Translet을 사용하여 원하는 작업을 손쉽게 자동화하고, XML/APON 기반의 명시적 설정과 어노테이션 기반의 편리한 설정 중 프로젝트에 가장 적합한 방식을 선택하여 스케줄러를 애플리케이션의 일부로 완벽하게 통합하고 관리할 수 있습니다. 특히 Redis 기반의 분산 락 기능은 대규모 클러스터 환경에서도 데이터 무결성과 시스템 안정성을 완벽하게 보장합니다.