[Spring] @Scheduled, 잘 알고 사용하기

하원·2024년 12월 22일
post-thumbnail

안녕하세요, 하원입니다.
하반기 공채와 막학기를 마치고 다시 글을 쓰러 왔습니다.

오늘은 @Scheduled와 다양한 @Scheduled의 특징들에 대해 간단하게 소개해 보겠습니다.


@Scheduled란?

  • 메서드를 일정 시간 간격 또는 특정 시간에 실행하고 싶을 때 사용하는 어노테이션입니다.

  • 반복 작업, 로그 작업, 통계 등 특정 시간대나 일정한 시간을 간격으로 작업이 필요할 때 주로 사용됩니다.

  • 간단하게 일상생활에 비유하자면 타이머나 알림 서비스라고 말할 수 있습니다.

저는 프로젝트 내에서 @Scheduled를 통해 행사의 종료 날짜에 따라 해당 홍보 포스터가 종료 상태로 바뀔 수 있도록 구현하였습니다.
그러면 @Scheduled를 어떻게 사용하는지 간단하게 소개해 보겠습니다.


@Scheduled 사용법

1. Config 파일 생성

@Scheduled를 사용하기 위해서 스케줄링 기능을 활성화해 줍니다.

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulingConfig {

}

위와 같이 Config 파일을 하나 생성해 주면 이제 @Scheduled를 사용할 수 있게 됩니다.

이 Config 파일을 생성하지 않으면 @Scheduled가 동작하지 않게 됩니다. 경험담입니다..!


2. 스케줄링 메서드 정의

이제 스케줄링 메서드를 작성해 주면 됩니다.
@Scheduled를 지정해 줄 메서드를 작성하고, 알맞은 @Scheduled 속성을 부여해 보겠습니다.

    // 매일 정오마다 실행
    @Scheduled(cron = "0 0 12 * * ?")
    @Transactional
    public void checkEndedPoster() {
        // 종료된 포스터 조회
        List<Poster> endedPosters = posterRepository.findEndDateBeforeNow(LocalDateTime.now());

        // 종료된 포스터의 status를 END로 변경
        for (Poster poster : endedPosters) {
            Poster.endPoster(poster);
        }
    }

저는 위 코드와 같이 매일 정오마다 종료된 포스터를 검사하여 포스터의 상태를 END로 변경하는 메서드를 작성하였습니다.

여기서 핵심 코드는 @Scheduled(cron = "0 0 12 * * ?") 이 부분인데요, @Scheduled 어노테이션 안에 속성을 지정해서 특정 시간 또는 일정한 시간 간격으로 스케줄링을 설정할 수 있습니다.

저는 cron 표현식을 사용해서 매일 정오 때마다 해당 메서드가 실행되도록 설정하였습니다.

그럼 @Scheduled에 어떤 속성이 존재하는지 살펴보겠습니다.


@Scheduled 속성들

1. fixedRate

@Scheduled(fixedRate = 3000)
  • 일정 간격으로 메서드가 실행됩니다.
  • 단위는 milliseconds로, 위 예시에서는 3초마다 해당 메서드가 실행됩니다.
  • fixedRate는 해당 메서드가 완료되지 않아도 3초가 지났다면 메서드가 다시 실행됩니다.

2. fixedDelay

@Scheduled(fixedDelay = 3000)
  • 해당 메서드가 종료된 시점으로부터 3초 이후에 메서드가 실행됩니다.
  • 단위는 milliseconds로, 위 예시에서는 메서드가 끝난 시점으로부터 3초가 지난 이후에 다시 실행됩니다.
  • fixedDelay는 해당 메서드가 완료되어야 일정 시간을 지연시킨 후 메서드를 실행합니다.

3. cron

@Scheduled(cron = "0 0/30 * * * ?")
  • 크론 표현식에 따라 지정된 시간대에 메서드가 실행됩니다.
  • 위 예시에서는 매 30분마다 메서드를 실행시키게 됩니다.

cron 표현식

  • 초 (0-59) 분 (0-59) 시 (0-23) 일 (1-31) 월 (1-12) 요일 (0{일}-6{토})
  • * : 모든 값 (= 매일, 매초, 매분)
  • ? : 특정하지 않음을 뜻함
  • / : 간격을 지정, ex) 0 0/30 * * * * = 매 30분마다 실행
  • L : 마지막을 의미, ex) 0 0 12 L * ? = 매월 마지막 날 12시에 실행
  • 이외에도 더 많은 표현식이 있으니 찾아보시길 바랍니다.

위 3가지 속성 이외에도 fixedRateString, fixedDelayString, initialDelay, initialDelayString, 등 다양한 속성들이 존재합니다.

String이 붙은 속성들은 3000과 같은 숫자 표현이 아닌 "3000"과 같은 String 표현으로 값을 지정해 준다는 의미입니다.


@Scheduled 주의사항

1. 단일 스레드 내에서 동작

기본적으로 @Scheduled단일 스레드 내에서 실행됩니다. 즉, 진행 중인 메서드가 완료되지 않으면 다른 스케줄링 메서드가 동작하지 않는다는 의미입니다.

@Scheduled(fixedRate = 5000)
public void task() throws InterruptedException {
    Thread.sleep(10000);  // 10초 소요
    System.out.println("작업 실행");
}

위와 같은 예시를 들어보겠습니다.

fixedRate=5000로 지정했기 때문에 task라는 메서드는 5초마다 실행되어야 합니다. 그럼 위의 코드는 어떻게 실행될까요?


실행 결과

  1. 첫 번째 task 시작 (0초)
  2. Thread.sleep(10000)으로 인해 10초 동안 작업 진행
  3. 첫 번째 task 종료 (10초)
  4. 두 번째 task 시작 (10초)
    -> 결과적으로 의도했던 것과는 다르게 10초마다 메서드가 실행됩니다.

원인

  • @Scheduled가 기본적으로 단일 스레드 내에서 실행되기 때문에 이전 메서드가 끝나지 않으면 다음 메서드가 실행되지 않습니다.

결과적으로 @Scheduled는 기본적으로 단일 스레드 내에서 동작하기 때문에, 메서드 내의 작업 상황 또는 다른 메서드 간의 관계로 인해 의도한 대로 동작하지 않을 수 있습니다.

해결 방법 - TaskScheduler

@Configuration
@EnableScheduling
public class SchedulerConfig {
    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5); // 스레드 풀 크기 설정
        return scheduler;
    }
}
  1. TaskScheduler를 스프링 빈으로 등록해 줍니다.
  2. 위 코드와 같이 ThreadPoolTaskScheduler 객체를 생성합니다.
  3. 스레드 풀의 크기를 지정해 줍니다.
    • 지정된 수만큼 스레드를 미리 만들어 놓고, 요청이 들어올 때 해당 스레드들을 사용하여 실행하게 됩니다.
    • 5개로 지정하면 스케줄 작업을 5개까지 병렬로 진행할 수 있음을 의미합니다.
  4. 스레드 풀을 5개로 지정했기 때문에, 최대 5개까지 스케줄 작업을 동시에 진행할 수 있습니다.

2. 예외 사항 발생 시, 중단

스케줄링 작업 도중, 예외가 발생하면 해당 스케줄링 메서드의 스레드가 더 이상 실행되지 않고 중단됩니다.
다시 말해, 해당 스레드는 더 이상 작동되지 않고 멈춤 상태를 유지하게 됩니다.

해당 오류가 반복적으로 발생하여 서버의 리소스를 고갈시킬 수 있다는 이유로 자동으로 복구되지 않고 중단된 상태로 남는다고 합니다.

그러면 어떻게 처리를 해줘야 할까요?


try-catch 예외 처리

@Scheduled(fixedRate = 5000)
public void task() {
    try {
        System.out.println("작업 시작");
        if (예외 발생 조건) {
        	throw new Exception("예외 발생");
        }
    } catch (Exception e) {
        System.err.println("작업 중 예외 발생: " + e.getMessage());
    }
}

위 코드와 같이 예외가 발생하는 코드를 try-catch로 처리해 주면, 스레드가 중단되지 않고 계속 실행됩니다.

장단점

  • 장점 : 예외로 인해 스레드가 중단되는 상황을 방지할 수 있습니다.
  • 단점 : 예외가 발생해도 빠르게 알아차리기 어렵기 때문에, 로그를 통해 예외사항을 주기적으로 관찰하고 대처해야 합니다.

3. JVM 종료 시, 함께 종료

JVM 종료 시, 발생하는 플로우

  1. JVM이 종료되면, 스프링 애플리케이션 컨텍스트도 함께 종료됩니다.
    • 컨텍스트가 종료되면 스레드 풀도 종료됩니다.
  2. @Scheduled 작업은 앞에서 언급한 ThreadPoolTaskScheduler와 같은 스레드 풀에서 실행됩니다.
  3. 따라서 JVM이 종료될 때, 스레드 풀도 함께 종료되기 때문에 스케줄링 작업도 중단됩니다.
  4. 스레드 풀이 종료되는 과정에서 실행 중인 작업도 중단될 수 있습니다.

JVM이 일반 스레드의 작업 종료를 기다려 준다고 하던데요?

  • @Scheduled에서 사용되는 스레드는 일반적으로 데몬 스레드가 아닌 일반 스레드입니다.
  • 데몬 스레드는 JVM 종료 시 강제 종료되지만, 일반 스레드는 JVM이 종료를 기다려 줍니다.
  • 하지만 스프링 애플리케이션이 JVM의 종료 신호를 받게 되면, 앞서 말했듯이 컨텍스트를 종료시키면서 스레드 풀도 함께 종료시키게 됩니다.
  • 따라서 일반 스레드도 실행 중에 종료될 수 있습니다.

해결 방법 - Grateful Shutdown

# application.yml
server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s
  • Grateful Shutdown현재 실행 중인 작업을 안전하게 종료할 수 있는 시간을 제공해 줍니다.
  • 위 예시에서는 종료 전 30초 시간을 제공하여 실행 중인 작업의 종료를 기다려 줍니다.
  • 하지만 30초를 초과하면 작업이 강제로 중단되고 애플리케이션이 종료됩니다.

기능의 단점도 살펴보기

사실 @Scheduled에 대한 포스팅을 작성하면서 앞서 말했던 주의사항들을 처음 접하게 되었습니다. 실제로 프로젝트 내에서 @Scheduled를 사용하고 있음에도 불구하고 주의사항들을 숙지하지 않고 사용하고 있었습니다.

그저 기능 구현에만 집중했었기 때문에 해당 기능이 잘 돌아가기만 하면 그 이외의 경우들을 고려하지 않았던 것 같습니다.

앞으로는 새로운 기능들을 사용하더라도 해당 기능의 단점이나 주의사항을 잘 파악하며 적용해야겠습니다.


마무리

기능 구현에만 집중하다 보면 예외사항들을 고려하지 못하는 경우가 발생하는 것 같다. 실제로 배포 이후, 예외나 오류를 해결하는 과정에 기능 구현보다 더 많은 시간을 소요하는데 말이다.

기능 구현을 하면서 해당 기능에서 발생할 수 있는 예외사항을 고려하는 습관이 굉장히 중요하다는 것을 느끼게 되었다.


참고

profile
호기심 저장소

0개의 댓글