ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [사내 shedlock 적용기] Spring Scheduler로 Hard delete 구현하기
    Spring Boot/스케쥴러 2023. 1. 9. 21:41

    사내 프로젝트 마이그레이션을 진행하면서 이전에 Spring Quartz 로 되어있던 배치성 작업을 없애기로 결정했다.

     

    결정한 이유는 다음과 같다. 

    • 이전 사수분이 알림과 데이터 물리삭제 용도로 Quartz 테이블, 설정만 구성중에 퇴사하여 실제로는 동작하지 않던 상태.
      • 동작하도록 만들기 위해서는 Quartz에 대한 학습, 기존 구현 로직 분석 등의 학습 곡선
    • 현재는 정기적인 배치성 물리 삭제 기능만 요구사항으로 있는 상태

    즉, 미완성인 배치성 기능을 수정하여 고치기엔 학습이 필요한 무거운 작업인데,

    필요한 요구사항은 단순해서 현재의 최선은 Spring에서 제공하는 Scheduler 어노테이션을 활용해 해결하기로 했다. 


    1. Spring-annotations for scheduler 사용하기

    • 스프링에서 제공하는 스케줄러를 이용하는 방법이다. 

    장점

    • 초 간편하게 구성할 수 있다.

    단점

    • Clustering시 1개만 실행되게 못함 → 라이브러리로 해결할 수 있다. (이게 오늘의 main 주제 shedlock 라이브러리)
    • 실패 시 후처리 불가 (스케줄러가실패했을 때나 thread pool에서 사용할 수 있는 thread가 없는 경우에 발생할 수 있다.)
    • 수정사항이 생기면 배포를 통한 수정이 이루어져야 한다. 즉, 동작하는 주기를 변경 불가능, 올바른 방법은 개발자의 배포 없이, Admin 화면에서 수정을 할 수 있는 모습이겠다. (이를 Spring Batch나 Spring Quartz에서 제공한다고 한다.)

    1.1 Spring-annotations for scheduler 사용 방법

    @Configuration
    @EnableScheduling // 스케쥴러 사용을 위해서 선언
    public class AppConfiguration {
        // 하단에 서술할 cron 표현식으로 scheduling을 설정한다. (second, minute, hour, day, month, weekday)
    	@Scheduled(cron = "3 * * * * *")
    	public void sendMessageCron() {
            System.out.println("Hello I am scheduled cron method")
    	}
    }
    • @Scheduled 에서 자주 사용하는 옵션
    fixedRate 해당 배치 작업이 시작 시점에서 시간을 카운팅
    fixedDelay 해당 배치 작업이 끝난 시점에서 시간을 카운팅.
    cron cron expression을 따른다. (https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/support/CronSequenceGenerator.html)
    initialDelay 작업이 수행되기 전 쉬게 하는 시간

    CRON Expression

     ┌───────────── second (0-59)
     │ ┌───────────── minute (0 - 59)
     │ │ ┌───────────── hour (0 - 23)
     │ │ │ ┌───────────── day of the month (1 - 31)
     │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
     │ │ │ │ │ ┌───────────── day of the week (0 - 7)
     │ │ │ │ │ │          (or MON-SUN -- 0 or 7 is Sunday)
     │ │ │ │ │ │
     * * * * * *
    
    (값)
    * : 모든 값
    ? : 특정 값 없음
    - : 범위 지정에 사용
    , : 여러 값 지정 구분에 사용
    / : 초기값과 증가치 설정에 사용
    L : 지정할 수 있는 범위의 마지막 값
    W : 월~금요일 또는 가장 가까운 월/금요일
    # : 몇 번째 무슨 요일 2#1 => 첫 번째 월요일
    
    초 분 시 일 월 주(년)
     "0 30 4 * * *" : 매일 새벽 4시 30분 0초
     "0 0 12 * * ?" : 아무 요일, 매월, 매일 12:00:00
     "0 15 10 ? * *" : 모든 요일, 매월, 아무 날이나 10:15:00 
     "0 15 10 * * ?" : 아무 요일, 매월, 매일 10:15:00 
     "0 15 10 * * ? *" : 모든 연도, 아무 요일, 매월, 매일 10:15 
     "0 15 10 * * ? : 2005" 2005년 아무 요일이나 매월, 매일 10:15 
     "0 * 14 * * ?" : 아무 요일, 매월, 매일, 14시 매분 0초 
     "0 0/5 14 * * ?" : 아무 요일, 매월, 매일, 14시 매 5분마다 0초 
     "0 0/5 14,18 * * ?" : 아무 요일, 매월, 매일, 14시, 18시 매 5분마다 0초 
     "0 0-5 14 * * ?" : 아무 요일, 매월, 매일, 14:00 부터 매 14:05까지 매 분 0초 
     "0 10,44 14 ? 3 WED" : 3월의 매 주 수요일, 아무 날짜나 14:10:00, 14:44:00 
     "0 15 10 ? * MON-FRI" : 월~금, 매월, 아무 날이나 10:15:00 
     "0 15 10 15 * ?" : 아무 요일, 매월 15일 10:15:00 
     "0 15 10 L * ?" : 아무 요일, 매월 마지막 날 10:15:00 
     "0 15 10 ? * 6L" : 매월 마지막 금요일 아무 날이나 10:15:00 
     "0 15 10 ? * 6L 2002-2005" : 2002년~2005년까지 매월 마지막 금요일 10:15:00 
     "0 15 10 ? * 6#3" : 매월 3번째 금요일 아무 날이나 10:15:00
    

    멀티 스레드로 동작하도록 만들기

    • 스케줄러의 기본 설정은 싱글 스레드라는 한계가 있다. @Schueduled 작업이 여러개가 있다면. 제대로 동작하지 않을 수 있다. 따라서 스케줄러가 멀티 스레딩으로 작동하게 만들어 줄 필요가 있다.
    • mainClass에 @EnableAsync 추가
    • Scheduled로 동작하는 하는 메서드 위에 @Async 붙이기
    • Configuration 설정 추가
    @Configuration
    class SchedulerConfig: SchedulingConfigurer {
    
        companion object {
            const val POOL_SIZE = 10;
        }
    
        override fun configureTasks(taskRegistrar: ScheduledTaskRegistrar) {
    
            val threadPoolTaskScheduler = ThreadPoolTaskScheduler()
    
            threadPoolTaskScheduler.apply {
                poolSize = POOL_SIZE
            }.initialize()
    
            taskRegistrar.setTaskScheduler(threadPoolTaskScheduler)
        }
    }
    

    참고

    1.2 Spring scheduler annotations를 clustering 환경에서 사용 방법

    위에서 서술한 Scheduler를 그대로 사용하기에는 큰 문제가 하나 있다. 

     

    보통 서버는 트래픽, 부하에 따라서 Scaling을 한다는 것, 하나의 서버(노드)가 여러개의 노드로 확장했을때, 각각의 노드에서 중복으로 Scheduler 기능이 수행되며 버그가 발생할 수 있다. 

     

    여러개의 노드로 확장했을때,

    하나의 노드에서만 Scheduler 기능만 동작하게 만드는 여러 방법이 있는데,  

    그 중 하나의 노드 외의 노드에 Lock을 걸어서 해결하는 ShedLock이라는 라이브러리를 소개한다.

     

    ShedLock 라이브러리

    Clustering 환경에서 하나의 node에서 스케줄러가 실행되면, 다른 노드에서는 같은 작업을 skip하도록 도와주는 라이브러리 이다. 노드에 시간을 기준으로 Lock을 걸 수 있다. (서버 시간이 같다는 것을 가정한다)

    사용법

    • Build.gradle.kt 설정 ()
    implementation("net.javacrumbs.shedlock:shedlock-spring:4.42.0")
    implementation("net.javacrumbs.shedlock:ahedlock-provider-jdbc-template:4.42.0")
    • SechedulerConfig 
      • 스프링에 Scheduler 기능 설정
    @Configuration
    @EnableScheduling
    class SchedulerConfig: SchedulingConfigurer {
    
        companion object {
            const val POOL_SIZE = 10
        }
    
        override fun configureTasks(taskRegistrar: ScheduledTaskRegistrar) {
    
            val threadPoolTaskScheduler = ThreadPoolTaskScheduler()
            threadPoolTaskScheduler.apply {
                poolSize = POOL_SIZE
            }.initialize()
    
            taskRegistrar.setTaskScheduler(threadPoolTaskScheduler)
        }
    }
    
    • ### Configure Lock Provider
    • lock 정보를 저장할 저장소 설정이 필요하다.
      • Shedlock의 동작 히스토리를 기록할 테이블 생성이 필요하다. (저는 shedlock 이름으로 DB 테이블 생성)
      • dynamodb 사용할까 해봤지만, 학습곡선이 있는 것으로 보여서 기존 DB에 추가

    DB 테이블

    # MySQL
    create table shedlock
    (
        name varchar(64) not null primary key,
        lock_until timestamp(3) not null,
        locked_at timestamp(3) default CURRENT_TIMESTAMP(3) not null,
        locked_by varchar(255) not null
    );
    
    • application.properties
    shedlock.table=shedlock
    •  SchedulerShedLockConfig
      • DataSource에 ShedLock 설정 
    @Configuration
    @EnableSchedulerLock(defaultLockAtMostFor = "3m")
    class SchedulerShedLockConfig(
    
        @Value("\\${shedlock.table}")
        val shedLockTable: String
        
    ) {
    
        @Bean
        fun lockProvider(datasource: DataSource): LockProvider {
    
            return JdbcTemplateLockProvider(datasource, shedLockTable)
        }
    }
    • SchedulerTask
      • Soft 삭제된 테이블을 매일 주기적으로 삭제하기 위한 Scheduler Task 설정
    @Component
    class SchedulerTask(
        private val schedulerTaskService: SchedulerTaskService
    ) {
        @Scheduled(cron = "0 0 05 * * ?", zone = "Asia/Seoul")
        @SchedulerLock(name = "deleteHardly", lockAtLeastFor = "30s", lockAtMostFor = "1m")
        fun deleteHardly() {
    
            val targetAt = Instant.now().atOffset(ZoneOffset.UTC).minusMonths(1).toInstant()
    
            schedulerTaskService.deleteHardlyByLastModifiedAtBefore(targetAt)
        }
    }
    

    ###  Task에 적용할 수 있는 Annotation 설정 정보

    Task에 적용할 수 있는 Annotation 설정 정보

     

    • Application에 마지막으로 Scheduling을 사용하겠다는 어노테이션을 붙여야 정상 동작한다.
    @SpringBootApplication
    @EnableScheduling // ShedLock 사용하겠다. 
    class FFApiKotlinApplication
    
    fun main(args: Array<String>) {
        runApplication<FFApiKotlinApplication>(*args)
    }
    

    Reference

     

    ### ShedLock 이외의 옵션들

    1) Directly use Quartz (Quartz 라이브러리 사용하기)

    Scheduler annotation 방법은 동적으로 시작 시간을 설정할 수 없는 단점이 있다.

    • Quartz 라이브러리를 이용하면 동적으로 시작 시간를 설정할 수 있다. 클라이언트 화면을 보면서 배포 없이 스케줄러의 시간을 조작할 수 있다.
    • 그러나, 사용하기 위한 학습곡선이 있다.
    implementation 'org.springframework.boot:spring-boot-starter-quartz'
    

    참고 사이트

    핵심 개념

    • Job : 비즈니스 로직 (인터페이스)
    • JobDetails : job 인스턴스, 관련 데이터
    • Trigger : job이 실행될 시점을 정의
    • Scheduler : job, triggers 조작

    2) JobRunr

    3) 인프라에서 삭제

    반응형
Designed by Tistory.