ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Scheduler
    🌱 spring 2022. 10. 24. 17:08

    📌 Spring Scheduler


    @Scheduled

    Spring Scheduler는 @Scheduled 어노테이션을 명시해 사용할 수 있다.

    보통 실행하고자 하는 메소드명 위에 명시

    Scheduler가 정상 작동하기 위해서는 스프링 어플리케이션에서 Scheduling을 활성화 시켜주어야 한다.

    • @EnableScheduling 어노테이션을 클래스 위에 명시해 활성화 시킨다.
      • 사용하고자 하는 클래스 위에 명시할 수 도 있고
      • @SpringBootApplication 이 위치한 클래스 위에 명시해도 된다.

    동작

    Spring Scheduler는 동기적으로 스케줄러를 실행한다.

    옵션

    • fixedRate
    • fixedDelay
    • Cron
    1. fixedRate
      • 작업의 시작부터 시간을 카운트한다.
      @Scheduled(fixedRate=1000) // 단위: ms
      public void fixedRateScheduler() {
        System.out.println("작업이 시작하고 1000ms 마다 실행");
      }
    2. fixedDelay
      • 작업이 끝난 시점부터 시간을 카운트
      @Schedueld(fixedDelay=1000) // 단위: ms 
      public void fixedDelayScheduler() {
        System.out.println("작업이 끝나고 나서 다시 1000ms 후에 실행");
      }
    3. cron
      • 개발자가 초, 분, 시, 일, 월, 주, (년)을 지정해 스케줄러를 동작 시킨다.
      • 정확히 지정한 시간에서만 실행됨을 보장한다.
      @Scheduled(cron="0/60 * * * * ?") 
      public void cronScheduler() {
        System.out.println("시스템 시간을 기준으로 1분 마다 주기적으로 실행");
      }

    스레드 풀 설정

    기본적으로 모든 @Scheduled 작업은 Spring에 의해 생성된 한 개의 스레드 풀에서 실행된다.

    따라서, 하나의 Scheduled가 돌고 있다면 그 작업이 다 끝나야만 다음 Scheduled가 실행된다.

    @Component
    public class TestScheduler {
    
      @Scheduled(fixedDelay = 1000)
      public void testFixedDelay() throws InterruptedException {
        Thread.sleep(5000);
        System.out.println("fixedDelay Scheduler with : " + Thread.currentThread().getName());
      }
    
      @Scheduled(fixedDelay = 1000)
      public void testFixedDelay2() {
        System.out.println("fixedDelay Scheduler2 with : " + Thread.currentThread().getName());
      }
    
    }
    • 위 코드에서 testFixedDelay 메서드와 별개로 testFixedDelay2() 가 1초마다 수행되길 바랬지만 실제 결과는 그렇지 않다.

    fixedDelay Scheduler with : scheduling - 1
    fixedDelay Scheduler2 with : scheduling - 1
    fixedDelay Scheduler with : scheduling - 1
    fixedDelay Scheduler2 with : scheduling - 1
    fixedDelay Scheduler with : scheduling - 1
    fixedDelay Scheduler2 with : scheduling - 1
    fixedDelay Scheduler with : scheduling - 1
    fixedDelay Scheduler2 with : scheduling - 1
    ...
    
    • 두 task가 같은 스레드에서 처리되기 때문! →  한 test가 끝나야 다른 test가 실행되는 구조이다.

    해결 방법

    서로 다른 thread를 사용하면 기다리지 않고 요청을 수행할 수 있다.

    따라서, SchedulingConfigurer를 통해 thread pool size를 설정

    • 스레드 풀에서 스레드를 가져와 테스크를 처리

    ❓ Thread pool

    • thread pool은 작업처리에 사용되는 스레드를 제한된 개수만큼 정해 놓고 작업 큐(queue)에 들어오는 작업들을 하나씩 스레드가 맡아 처리하게 된다.
    • 작업 처리 요청이 폭증해도 스레드의 전체 개수가 늘어나지 않아 시스템 성능이 급격하게 저하되지 않는다.
    • 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리한다.
    • 따라서, 설정된 크기의 스레드를 만들어 놓고 해당 스레드들을 계속해서 재사용한다.

    Thread Pool의 적정 사이즈

    • 스레드를 불 필요하게 많이 만들면 메모리 낭비가 심해진다.
    • 너무 적은 수를 만들면 효율성이 떨어진다.
    • 따라서, 서비스가 실행되는 CPU의 코어 개수에 따라 유동적으로 생성될 수 있도록 하는 것이 좋다.
      • CPU 처리가 많은 경우
        • CPU의 코어를 N개라 하면 N + 1 만큼의 스레드를 생성해주면 최적에 가까운 성능을 낼 수 있다고 본다.
      • I / O 작업이 많은 경우
        • N * 2 만큼의 스레드를 생성해주기도 한다.

    thread pool 설정

    @Configuration
    public class SchedulerConfig implements AsyncConfigurer, SchedulingConfigurer {
    	
    	public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(Runtime.getRuntime().availableProcessors() * 2);
        scheduler.initialize();
        return scheduler;
      }
    }

    ⇒ 설정후 실행 결과

    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-2
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-3
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-2
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-4
    fixedDelay Scheduler with : ThreadPoolTaskScheduler-1
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-3
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-2
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-6
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-4
    fixedDelay Scheduler2 with : ThreadPoolTaskScheduler-7
    

    @Async가 필요한 이유?

    스레드 풀을 설정했으니 스케줄러가 잘 동작하는지 확인

    test1 만 실행하는 경우

    @Component
    public class TestScheduler {
    
      @Scheduled(fixedDelay = 1000)
      public void testFixedDelay() throws InterruptedException {
        System.out.println("fixedDelay Scheduler start with : " + Thread.currentThread().getName());
        Thread.sleep(5000);
        System.out.println("fixedDelay Scheduler end with : " + Thread.currentThread().getName());
      }
    }

    ⇒ 원하는 것은 1초마다 스케줄러가 작동하는 것이지만 결과는 그렇지 않았다.

    fixedDelay Scheduler end with : ThreadPoolTaskScheduler-1
    fixedDelay Scheduler start with : ThreadPoolTaskScheduler-1
    fixedDelay Scheduler end with : ThreadPoolTaskScheduler-1
    fixedDelay Scheduler start with : ThreadPoolTaskScheduler-2
    fixedDelay Scheduler end with : ThreadPoolTaskScheduler-2
    fixedDelay Scheduler start with : ThreadPoolTaskScheduler-1
    fixedDelay Scheduler end with : ThreadPoolTaskScheduler-1
    fixedDelay Scheduler start with : ThreadPoolTaskScheduler-3
    fixedDelay Scheduler end with : ThreadPoolTaskScheduler-3
    

    스레드 풀 설정을 마쳐도 그저 다른 스레드로 처리할 뿐 각 작업은 동기적으로 처리한다.

    따라서, 작업 수행에 걸리는 시간과 상관없이 원하는 시간마다 작업 수행을 시작하고 싶다면 비동기로 처리를 해야 한다.

     

    @Async를 활용하면 다음 스케줄러가 이전 스케줄러의 작업이 끝날 때까지 기다리지 않고 자신의 작업을 처리할 수 있게 된다.

    • 해당 어노테이션을 붙이면 각기 다른 스레드로 실행이 된다.

    @EnableAsync로 활성화를 시켜주어야 한다.

    • @EnableAsync 가 붙어 있는 Configuration 클래스가 필요하다.

    @EnableAsync 어노테이션을 사용하면 SimpleAsyncTaskExecutor 를 사용하도록 설정되어 있다.

    • SimpleAsyncTaskExecutor는 매번 스레드를 생성하는 방식이다.
    • 따라서, 설정을 오버라이딩해서 사용하는 것이 좋다.
      • @EnableAsync 를 Application 클래스가 아닌 Async 설정 클래스에 붙인다.

    ⇒ AsyncConfigurerSupport를 상속받는 Class를 직접 작성

    @Configuration
    @EnableAsync
    public class AsyncConfig extends AsyncConfigurerSupport {
        
        @Override
        public Executor getAsyncExecutor() {
            ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
            executor.setCorePoolSize(5);
            executor.setMaxPoolSize(30);
            executor.setQueueCapacity(50);
            executor.setThreadNamePrefix("Beomsic-");
            executor.initialize();
            return executor;
        }
    }
    • CorePoolSize : 기본 실행 대기하는 thread 수
    • MaxPoolSize : 동시 동작하는 최대 thread 수
    • QueueCapacity
      • MaxPoolSize 초과 요청에서 thread 생성 요청시, 해당 요청을 queue에 저장하는데, 이때 최대 수용 가능한 queue 수
    • ThreadNamePrefix : 생성되는 thread 접두사

    위 처럼 설정한 후 비동기 방식 사용을 원하는 method에 @Async 어노테이션을 지정해준다.

    @Async
    @Scheduled(fixedDelay = 1000)
    public void testFixedDelay() throws InterruptedException {
      System.out.println("fixedDelay Scheduler start with : " + Thread.currentThread().getName());
      Thread.sleep(5000);
      System.out.println("fixedDelay Scheduler end with : " + Thread.currentThread().getName());
    }

    결과

    fixedDelay Scheduler start with : Beomsic-1
    fixedDelay Scheduler start with : Beomsic-2
    fixedDelay Scheduler start with : Beomsic-3
    fixedDelay Scheduler start with : Beomsic-4
    fixedDelay Scheduler start with : Beomsic-5
    fixedDelay Scheduler end with : Beomsic-1
    fixedDelay Scheduler start with : Beomsic-1
    fixedDelay Scheduler end with : Beomsic-2
    fixedDelay Scheduler start with : Beomsic-2
    

    @Async 어노테이션의 2가지 제약조건

    1. public 메서드여야 한다.
      • @Async의 동작은 AOP가 적용되어 Spring Context에서 등록된 Bean Object의 method가 호출될 시에, Spring이 확인하고 @Async가 적용된 method인 경우 Spring이 method를 가로채 다른 thread에서 실행 시켜주는 방식이다.
      • 따라서, Spring이 해당 method를 가로챈 후, 다른 class에서 호출이 가능해야 하므로 private method는 사용할 수 없다.
    2. 스스로 호출하여 동작시킬 수 없다.(self-invocation)
      • Spring Context에 등록된 Bean의 method 호출이어야 Proxy 적용이 가능하므로, inner method의 호출은 Proxy 영향을 받지 않기 때문에 self-invocation이 불가능하다.

    참고 자료

    https://cheershennah.tistory.com/170

    https://ecsimsw.tistory.com/entry/Scheduler-적용-배경과-구조-Spring-Scheduler

    https://bepoz-study-diary.tistory.com/399

    https://velog.io/@gillog/Spring-Async-Annotation비동기-메소드-사용하기

    '🌱 spring' 카테고리의 다른 글

    JPA - @ElementCollection  (0) 2022.11.02
    JPA - Embedded Type  (0) 2022.11.02
    Filter, Interceptor  (0) 2022.10.11
    OSIV 🧐  (0) 2022.10.11
    JPA - N + 1 문제  (0) 2022.10.11

    댓글

Designed by Tistory.