공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 09 / 22

@Scheduled 운영

fixedRate·fixedDelay·cron, 예외·겹침·다중 인스턴스·종료
섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

4. 응용 변형 예제

4.1 Spring Boot 로 옮기기

java
@Configuration
@EnableScheduling
public class SchedulingConfig {

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler s = new ThreadPoolTaskScheduler();
        s.setPoolSize(4);                              // 배치 수 + 여유 1
        s.setThreadNamePrefix("batch-");
        s.setErrorHandler(t ->
                log.error("스케줄 작업 실패", t));       // 예외 삼키지 않고 로그
        s.setWaitForTasksToCompleteOnShutdown(true);    // shutdown 시 진행 중 대기
        s.setAwaitTerminationSeconds(30);
        return s;
    }
}

@Component
public class SyncJob {

    @Scheduled(fixedDelayString = "${sync.delay:60000}")
    public void syncFromLegacy() { /* ... */ }

    @Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")
    public void dailyReport() { /* ... */ }
}

fixedDelayString 은 프로퍼티로 간격을 외부화할 때 씁니다. zone 을 명시했으므로 컨테이너의 OS 시간대가 UTC 여도 항상 KST 02시에 돕니다.

4.2 ShedLock 적용

xml
<!-- pom.xml -->
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-spring</artifactId>
</dependency>
<dependency>
    <groupId>net.javacrumbs.shedlock</groupId>
    <artifactId>shedlock-provider-jdbc-template</artifactId>
</dependency>
java
@Configuration
@EnableSchedulerLock(defaultLockAtMostFor = "10m")
public class ShedLockConfig {
    @Bean
    public LockProvider lockProvider(DataSource ds) {
        return new JdbcTemplateLockProvider(ds);
    }
}

@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")
@SchedulerLock(name = "dailyReport",
        lockAtMostFor = "30m", lockAtLeastFor = "1m")
public void dailyReport() { /* ... */ }
sql
-- Oracle 용 shedlock 테이블
CREATE TABLE shedlock (
    name       VARCHAR2(64)  NOT NULL PRIMARY KEY,
    lock_until TIMESTAMP(3)  NOT NULL,
    locked_at  TIMESTAMP(3)  NOT NULL,
    locked_by  VARCHAR2(255) NOT NULL
);

lockAtMostFor 는 작업이 실제로 걸리는 최대 예상 시간보다 넉넉히 잡아야 합니다. 짧게 잡으면 작업이 끝나기 전에 잠금이 풀려 다른 인스턴스가 같이 돕니다. lockAtLeastFor 는 인스턴스 간 시계 차이를 흡수하는 최소 보유 시간입니다.

4.3 관리 API 로 즉시 실행

java
@RestController
@RequestMapping("/admin/jobs")
public class JobAdminController {
    private final SafeTaskHolder dailyReport;   // 스케줄과 같은 인스턴스 공유

    @PostMapping("/{name}/run")
    public ResponseEntity<String> runNow(@PathVariable String name) {
        SafeTask task = dailyReport.find(name);
        if (task.isRunning())                    // 진행 중이면 겹치지 않게 거절
            return ResponseEntity.status(409).body("이미 실행 중입니다");
        new Thread(task).start();
        return ResponseEntity.accepted().body("실행을 시작했습니다");
    }
}

수동 트리거가 스케줄 실행과 겹치는 (a) 상황을 막으려면, 관리 API 가 부르는 것도 스케줄이 부르는 것과 같은 SafeTask 인스턴스 여야 합니다. 그래야 running 플래그가 공유돼 겹침이 막힙니다.

4.4 실행 이력 테이블

sql
CREATE TABLE job_run (
    job_name    VARCHAR2(64)  NOT NULL,
    started_at  TIMESTAMP     NOT NULL,
    finished_at TIMESTAMP,
    status      VARCHAR2(16)  NOT NULL,   -- RUNNING, OK, FAIL
    message     VARCHAR2(2000)
);

-- 마지막 성공 이후 N분: 알림 조건
SELECT job_name,
       ROUND((SYSDATE - MAX(finished_at)) * 24 * 60) AS minutes_since_ok
FROM   job_run
WHERE  status = 'OK'
GROUP  BY job_name
HAVING (SYSDATE - MAX(finished_at)) * 24 * 60 > 60;  -- 60분 이상 성공 없음

SafeTask.run 의 시작 시점에 RUNNING 행을 insert 하고, 종료 시점에 같은 행을 OK/FAIL 로 update 합니다. 이 테이블이 있으면 "배치가 죽었는지" 를 콘솔 로그를 뒤지지 않고 SQL 한 줄로 확인할 수 있습니다.

응용 변형 예제
  • 4.1 Spring Boot 로 옮기기
  • 4.2 ShedLock 적용
  • 4.3 관리 API 로 즉시 실행
  • 4.4 실행 이력 테이블
이전 섹션3 코드 예제4 / 7다음 섹션5 자주 하는 실수 (Tip)