executorService 이용해서 스레드 사용하기

정 승 연·2024년 4월 17일
0

땅굴파는 이야기

목록 보기
5/5

참고한 글

https://jeong-pro.tistory.com/188
https://ho-log.tistory.com/109
https://engineerinsight.tistory.com/197
https://veneas.tistory.com/entry/Java-Executors-Thread-사용법
https://recordsoflife.tistory.com/1470
https://dev-coco.tistory.com/186
https://xxeol.tistory.com/44
https://yangbox.tistory.com/28
https://velog.io/@dailylifecoding/java-executor-service-weird-exit-methods#🥝-제대로-shutdown-시키려면

robotMoveService의 로봇 시뮬레이션 로직 재구성

  • thread pool 닫힘 확인 (자원해제)
  • thread scheduling 끝남 확인
  • 동시 실행 가능 여부 확인
## PR 설명
- robotMoveService의 로봇 시뮬레이션 로직 재구성
- Redisson 스레드 풀 설정
- 불필요한 distributeLock 삭제

## 작업사항
- php 서버의 toDestination 사용
  - php 서버의 toDestination에서 서빙 나갈 수 있는 로봇이 없으면 400 리턴. try-catch 문으로 예외처리 하는 과정에서 catch 부분에서 BaseException을 던지고 있었음
  - 이 BaseException 가 발생하면 while(true) 가 실행되지 않고 정지되었음.
  - 기존 php 서버에서 받아오던 toDestination 값을 robotService에 구현 해둔 것으로 사용
  - php 서버 응답 사용하지 않으므로 응답 대기를 위한 @Async 삭제
- while(true)와 thread.sleep()을 이용한 로직 수행
  - 기존에는 로봇 serving button을 실행한 후 sleep 5초, toDestination 값이 없으면 sleep 7초 수행
  - 로봇을 보내고 로봇이 움직이는 시간을 고려해 15초 주기로 toDesitnation-servingButton 로직 실행
  - scheduleExecutorService로 스레드 풀을 관리하고 자원 낭비를 하지 않도록 구성. sleep() 을 사용하는 스레드는 그 시간동안 스레드가 일시중지 되어있어 자원을 효율적으로 사용할 수 없음.
    - ExecutorService는 스레드 풀을 사용하여 스레드 관리를 추상화하고 최적화하여, 스레드 생성과 소멸에 드는 비용과 복잡성을 줄임.
- 대기중인 스레드
  - 메소드 실행 시 thread 생성, scheduleExecutor를 이용해 주기적으로 실행. 
  - 기본 30분 또는 지정 횟수 이상 로직을 돌리면 메소드 종료하고 thread 반납하도록 구현
  - 대기중인 스레드를 사용하는 것도 고려했으나 자주 사용하지 않는 기능에 대기 스레드를 두는 것이 비효율적이라 판단. 매번 새로 만들고 반납하는데 리소스가 들지만 생성/반납 방법 선택
  - 스레드 현황 확인하고 대기하는 스레드 없는 것 확인했음
  
## 연결 이슈 close
<!-- `close #이슈 번호`를 통해 PR 머지와 함께 이슈를 close 할 수 있습니다. -->
close #

Thread Pool

  • thread pool 이용해 작업 후 반납(작업 종료) 해야함
    • 종료하지 않으면 무한 대기 상태
    • 단일/멀티 스레드는 executor.shutDown()
    • scheduled 은 shutDown() 하면 RejectedExecutionException 뜸 → 아마 Executor 선언을 전역으로 해서 그런듯
    • 스레드풀 자체 종료 대신 ScheduledFuture<?> 인스턴스
    • Runnable 구현체 안에서 scheduledFuture 저장할 필드 생성해서 외부에서 ScheduledFuture 설정해서 접근 가능하도록 설정
      • scheduled 는 future 객체의 cancel(true) 사용
        - true : 즉시 종료 / false : 대기하다 종료

        AtomicReference<ScheduledFuture<?>> futureRef = new AtomicReference<>();
        
        Runnable serving = () -> {
             if (!redisService.isKeyPresent(redisKey) || count < times.get()) { // key 가 비어있거나 times 가 충족되었을 때
                 redisService.deleteIntegerData(redisKey);
        
        	       **executorService.shutdownNow();**
        
                 ScheduledFuture<?> scheduledFuture = futureRef.get();
                 if (scheduledFuture != null) {
                    **scheduledFuture.cancel(true);**
                 }
             return;
        }
        

executorService.shutDown/shutDownNow

리턴 타입메소드명(매개 변수)설명
voidshutdown()현재 처리 중인 작업 뿐만 아니라 작업 큐에 대기하고 있는모든 작업을 처리한 뒤에 스레드풀을 종료시킨다.
ListshutdownNow()현재 작업 처리 중인 스레드를 interrupt 해서 작업 중지를 시도하고 스레드 풀을 종료 시킨다.리턴값을 작업 큐에 있는 미처리된 작업(Runnable)의 목록이다
booleanawaitTermination(long timeout, TimeUnit unit)shutdown()메소드 호출 이후 모든 작업처리를 timeout 시간 내에 완료하면 true를 리턴하고, 완료하지 못하면 작업 처리 중인 스레드를 interrupt하고 false를 리턴한다
  • shutdown()
    • shutdown() 메소드는 ExecutorService가 새 작업을 더 이상 받아들이지 않도록 합니다.
    • 이미 제출된 작업들은 계속 실행됩니다.
    • 이 메소드를 호출한 후에는 새로운 작업을 ExecutorService에 제출할 수 없으며, 만약 제출하려고 하면 RejectedExecutionException이 발생합니다.
    • 스레드 풀 내의 스레드들은 현재 처리 중인 작업을 완료한 후 종료됩니다.
  • shutdownNow()
    • shutdownNow() 메소드는 ExecutorService가 새 작업을 받아들이지 않을 뿐만 아니라, 현재 실행 중인 작업을 시도적으로 중단하고자 시도합니다.
    • 이 메소드는 현재 실행 중인 모든 작업에 대해 Thread.interrupt()를 호출하여 스레드가 중단될 수 있도록 요청합니다. 그러나 실제 작업이 중단되는지는 작업이 인터럽트에 어떻게 반응하는지에 달려 있습니다.
    • 이 메소드는 또한 대기 중인 작업 목록을 반환하며, 이 목록에는 시작되지 않은, 즉 실행되지 않은 작업들이 포함됩니다.
    • shutdownNow()를 호출한 후에도 새로운 작업을 제출하려고 하면 RejectedExecutionException이 발생합니다.
  • 사용 상황
    • shutdown()은 모든 작업이 평온하게 완료될 때까지 기다리고 싶을 때 사용합니다. 이는 일반적으로 서비스 종료 시 깔끔한 종료를 원할 때 적합합니다.
    • shutdownNow()는 가능한 빠르게 ExecutorService를 종료하고자 할 때 사용합니다. 이는 긴급하게 리소스를 해제해야 하거나 더 이상 실행 중인 작업을 유지할 필요가 없을 때 유용합니다.
    • 따라서 어떤 메소드를 사용할지는 애플리케이션의 요구 사항과 작업의 성격에 따라 결정해야 합니다. shutdownNow()는 강제적인 종료가 필요할 때 사용하고, shutdown()은 모든 작업이 완료될 때까지 기다리고자 할 때 사용하는 것이 일반적입니다.

executorService 선언하는 메소드

메소드명(매개)초기 스레드 수코어 스레드 수최대 스레드 수
newCachedThreadPool()00Integer.MAX_VALUE
newFixedThreadPool(int nThread)0nThreadsnThreads

초기 스레드 수은 ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수를 말하고,

코어 스레드 수는 스레드 수가 증가된 후 사용되지 않는 스레드를 스레드풀에서 제거할 때

최대 스레드 수는 스레드풀에서 관리하는 최대 스레드 수이다.

  • ScheduledThread 인 scheduleAtFixedRate를 실행 중 특정 조건에서 종료시키려면 어떻게해?

    ChatGPT

    ScheduledThreadPoolExecutorscheduleAtFixedRate 메서드를 사용할 때, 특정 조건에 따라 작업을 종료시키고 싶다면 작업 내에서 그 조건을 검사하고 조건이 충족될 때 작업을 적절히 종료시켜야 합니다. 이는 일반적으로 작업 내부에서 조건을 평가하고, 조건이 참이 되면 작업 실행을 멈추는 방식으로 구현됩니다. 다음은 간단한 예제 코드로, 어떻게 작업을 시작하고 특정 조건이 만족되면 중단시킬 수 있는지 보여줍니다:

    javaCopy code
    import java.util.concurrent.ScheduledThreadPoolExecutor;
    import java.util.concurrent.TimeUnit;
    
    public class Main {
        public static void main(String[] args) {
            ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
    
            // Runnable 작업 생성
            Runnable task = new Runnable() {
                private int count = 0;
    
                @Override
                public void run() {
                    count++;
                    System.out.println("Running task count: " + count);
    
                    // 특정 조건을 검사 (예: count가 10이 됐을 때)
                    if (count >= 10) {
                        System.out.println("Condition met, stopping.");
                        // 작업 취소
                        executor.shutdown();
                    }
                }
            };
    
            // scheduleAtFixedRate를 이용하여 작업을 주기적으로 실행
            executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
        }
    }
    

    이 예제에서는 ScheduledThreadPoolExecutor를 사용하여 매초마다 task를 실행합니다. task 내부에서는 실행 횟수를 계산하고, 이 횟수가 10회 이상이 되면 executor.shutdown()을 호출하여 모든 스케줄링된 작업을 종료하고 스레드 풀을 정리합니다.

    다음과 같은 방법으로도 작업을 종료할 수 있습니다:

    1. Future 객체 사용: scheduleAtFixedRateScheduledFuture 객체를 반환합니다. 이 객체를 사용하여 실행 중인 작업을 취소할 수 있습니다.

    2. 외부 변수 사용: 작업이 외부 변수의 상태를 검사하고 해당 변수에 따라 실행을 계속할지 결정할 수 있습니다.

      이러한 방법들은 각 상황에 맞게 조정할 수 있으며, 작업의 주기적 실행을 관리하는 데 매우 유용합니다.

      스레드풀을 정리하지 않고 할 수 있어? 저렇게 하면
      [2024-04-16 09:30:26][ThreadPoolTaskExecutor-2] ERROR o.s.a.i.SimpleAsyncUncaughtExceptionHandler - Unexpected exception occurred invoking async method: public void com.example.helper_spring_boot.src.robotMove.RobotMoveService.executeSimulator(java.lang.String,java.lang.Integer)
      java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.ScheduledThreadPoolExecutorScheduledFutureTask@2a69ff38[Notcompleted,task=java.util.concurrent.ExecutorsScheduledFutureTask@2a69ff38[Not completed, task = java.util.concurrent.ExecutorsRunnableAdapter@49c92de7[Wrapped task = com.example.helper_spring_boot.src.robotMove.RobotMoveService$$Lambda$2177/0x0000000801f68b50@7b8f63c8]] rejected from java.util.concurrent.ScheduledThreadPoolExecutor@6b133599[Terminated, pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 3]
      at java.base/java.util.concurrent.ThreadPoolExecutorAbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2081)atjava.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:841)atjava.base/java.util.concurrent.ScheduledThreadPoolExecutor.delayedExecute(ScheduledThreadPoolExecutor.java:340)atjava.base/java.util.concurrent.ScheduledThreadPoolExecutor.scheduleAtFixedRate(ScheduledThreadPoolExecutor.java:632)atcom.example.helperspringboot.src.robotMove.RobotMoveService.executeSimulator(RobotMoveService.java:148)atcom.example.helperspringboot.src.robotMove.RobotMoveServiceAbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2081) at java.base/java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:841) at java.base/java.util.concurrent.ScheduledThreadPoolExecutor.delayedExecute(ScheduledThreadPoolExecutor.java:340) at java.base/java.util.concurrent.ScheduledThreadPoolExecutor.scheduleAtFixedRate(ScheduledThreadPoolExecutor.java:632) at com.example.helper_spring_boot.src.robotMove.RobotMoveService.executeSimulator(RobotMoveService.java:148) at com.example.helper_spring_boot.src.robotMove.RobotMoveServiceFastClassBySpringCGLIBFastClassBySpringCGLIB$f8d25041.invoke()
      at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)

      이 에러나

      ChatGPT

      제시하신 에러 메시지는 java.util.concurrent.RejectedExecutionException을 보여주고 있습니다. 이 에러는 작업을 수행하려는 스레드 풀이 이미 종료되었거나 종료 과정에 있을 때 발생합니다. 즉, 스레드 풀이 더 이상 새로운 작업을 수용할 수 없는 상태입니다.

      스레드 풀을 종료하지 않고 주기적인 작업을 취소하려면, 스레드 풀 자체를 종료하는 대신 ScheduledFuture<?> 인스턴스를 사용하여 특정 작업만 취소할 수 있습니다. 다음은 scheduleAtFixedRate로 반환되는 ScheduledFuture를 사용하여 특정 조건이 만족되면 작업을 취소하고 스레드 풀은 계속 유지하는 방법을 보여주는 예제 코드입니다:

      javaCopy code
      import java.util.concurrent.ScheduledFuture;
      import java.util.concurrent.ScheduledThreadPoolExecutor;
      import java.util.concurrent.TimeUnit;
      
      public class Main {
          public static void main(String[] args) {
              ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
      
              // Runnable 작업 생성
              Runnable task = new Runnable() {
                  private int count = 0;
      
                  @Override
                  public void run() {
                      count++;
                      System.out.println("Running task count: " + count);
      
                      // 특정 조건을 검사 (예: count가 10이 됐을 때)
                      if (count >= 10) {
                          System.out.println("Condition met, cancelling the task.");
                          // 이 행은 더이상 작업을 수행하지 않도록 작업 취소
                          thisFuture.cancel(false);
                      }
                  }
              };
      
              // scheduleAtFixedRate를 이용하여 작업을 주기적으로 실행
              ScheduledFuture<?> thisFuture = executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
          }
      }
      

      이 코드에서는 thisFuture 변수를 사용하여 작업을 참조하고, 조건이 충족되면 thisFuture.cancel(false) 메서드를 호출하여 특정 작업을 취소합니다. 이 때 false는 작업이 현재 실행 중이라면 완료될 때까지 기다리라는 의미이고, true를 사용하면 실행 중인 작업도 즉시 중단됩니다.

      이 방법을 사용하면 스레드 풀은 계속 살아있으며, 필요에 따라 다른 작업을 추가하거나 스케줄을 조정할 수 있습니다.

      Cannot resolve symbol 'scheduledFuture'
      이렇게 뜨는데?

      ChatGPT

      위에서 보여준 코드 예제에는 scheduledFuture 변수를 적절하게 정의하지 않은 실수가 있었습니다. Runnable 내부에서 scheduledFuture의 참조를 사용하려고 했으나, 이는 람다 표현식이나 내부 클래스에서 사용할 때 적절히 final 이거나 effectively final이어야 합니다.

      여기서 사용할 수 있는 해결책 중 하나는 Runnable을 구현하는 클래스 안에서 ScheduledFuture를 저장할 수 있는 필드를 생성하는 것입니다. 그럼 이 필드를 통해 외부에서 ScheduledFuture를 설정할 수 있으며, 내부에서도 접근이 가능해집니다.

      다음은 수정된 코드 예제입니다:

      javaCopy code
      import java.util.concurrent.ScheduledFuture;
      import java.util.concurrent.ScheduledThreadPoolExecutor;
      import java.util.concurrent.TimeUnit;
      
      public class Main {
          public static void main(String[] args) {
              ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
      
              // Runnable 작업을 위한 클래스 정의
              class PeriodicTask implements Runnable {
                  private int count = 0;
                  private ScheduledFuture<?> future;
      
                  public void setFuture(ScheduledFuture<?> future) {
                      this.future = future;
                  }
      
                  @Override
                  public void run() {
                      count++;
                      System.out.println("Running task count: " + count);
      
                      // 특정 조건을 검사 (예: count가 10이 됐을 때)
                      if (count >= 10) {
                          System.out.println("Condition met, cancelling the task.");
                          // 이 행은 더 이상 작업을 수행하지 않도록 작업 취소
                          if (future != null) {
                              future.cancel(false);
                          }
                      }
                  }
              }
      
              // Runnable 작업 인스턴스 생성
              PeriodicTask task = new PeriodicTask();
      
              // scheduleAtFixedRate를 이용하여 작업을 주기적으로 실행
              ScheduledFuture<?> future = executor.scheduleAtFixedRate(task, 0, 1, TimeUnit.SECONDS);
      
              // Future 설정
              task.setFuture(future);
          }
      }
      

      이 코드에서는 PeriodicTask라는 내부 클래스를 정의하고, 이 클래스는 Runnable 인터페이스를 구현합니다. 이 클래스에는 ScheduledFuture<?> 타입의 필드와 이 필드를 설정하는 메서드가 포함되어 있습니다. 메인 메서드에서는 PeriodicTask 인스턴스를 생성하고, 스케줄러로부터 반환받은 ScheduledFuture<?> 인스턴스를 이 인스턴스에 설정합니다. 이렇게 하면 Runnable 내부에서도 스스로를 취소할 수 있습니다.

      https://lh3.googleusercontent.com/a/AGNmyxaOICM05O7g-k2so1l6JcfTKXoexlQ_oEOfdszc=s96-c

      Runnable serving = () -> {
      log.info("times"+String.valueOf(times.get()));

              if (!redisService.isKeyAbsent(redisKey) || count < times.get()) { // key 가 비어있거나 times 가 충족되었을 떄
                  log.info("simulator end .. ");
                  redisService.deleteIntegerData(redisKey);

      // executorService.shutdownNow();
      shutdownAndAwaitTermination(executorService);

                  scheduledFuture.cancel(false);
                  return ;
              }
      
              int randomIndex = (int) (Math.random() * tablesList.size());
              String destinationTable = tablesList.get(randomIndex).getTableName();
              Integer destinationNode = tablesList.get(randomIndex).getNodeId();
              try {
                  RobotsDestinationResDto resDto = robotsService.toDestinationV2(userId);
                  if (!resDto.isError()) {
                      String topic = resDto.getMapName() + "/" + resDto.getRobotId() + "/server/button";
                      MqttButtonReqDto buttonReqDto = new MqttButtonReqDto(destinationNode, destinationTable, "serving");
                      mqttPublishService.publishData(buttonReqDto, topic);
                      times.getAndIncrement();
                      redisService.updateValueRemainingExpiration(redisKey, times.get());
                  } else {
                      redisService.deleteIntegerData(redisKey);
                  }
              } catch (BaseException e) {
                  // to-desination 없음. 대기
              }
          };
      
          ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(serving, 10, 10, TimeUnit.SECONDS);
      
          log.info("future is cancelled :: "+ scheduledFuture.isCancelled());

      // scheduledFuture.cancel(true);

      이렇게 되어있는데 해결해줘

      ChatGPT

      주어진 코드에서 scheduledFuture를 사용하려고 하지만 Runnable 람다 표현식 내에서는 scheduledFuture가 아직 초기화되지 않았기 때문에 "Cannot resolve symbol 'scheduledFuture'" 오류가 발생합니다. 람다 표현식 내부에서는 외부의 로컬 변수에 접근할 때 그 변수가 final이거나 사실상 final(effectively final)인 경우에만 접근할 수 있습니다. 여기서 문제는 scheduledFuture가 람다 표현식 이후에 선언되고 초기화되기 때문에 람다 내에서 사용할 수 없는 것입니다.

      이 문제를 해결하기 위한 한 가지 방법은 Runnable을 별도의 클래스로 정의하는 것입니다. 이렇게 하면 Runnable의 인스턴스를 생성하기 전에 scheduledFuture를 저장할 필드를 설정할 수 있습니다. 다음은 코드를 수정한 예제입니다:

    이 접근 방식은 futureRunnable 구현 내부에서 참조하면서도 futurefinal 또는 effectively final을 만족하도록 해줍니다. ServingTask 클래스를 별도로 정의함으로써 더 많은 컨트롤을 할 수 있고, Runnable 내에서 외부 변수를 참조하는 문제를 해결할 수 있습니다.

    람다식을 사용할 수 있는 다른 방법은 없어?

    ChatGPT

    람다 표현식을 사용하면서 scheduledFuture 변수를 참조할 수 있는 한 가지 방법은 AtomicReference를 사용하는 것입니다. AtomicReferencefinal 또는 사실상 final(effectively final) 상태를 유지하면서, 참조하는 객체를 변경할 수 있도록 해줍니다. 이 방법을 사용하면 Runnable 내에서 scheduledFuture를 참조할 수 있게 됩니다.

  • ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0);
    이걸 0으로 설정했을 때랑 1로 설정했을 때랑 차이가 뭐야?

    ChatGPT
    
    **`ScheduledExecutorService`**의 **`newScheduledThreadPool`** 메소드를 이용하여 스레드 풀을 생성할 때 코어 스레드 수를 설정하는 것은 중요한 설정 중 하나입니다. 이 수는 스레드 풀이 유지할 수 있는 최소 스레드 수를 결정합니다. 여기서 **`0`**과 **`1`**로 설정하는 것의 차이를 살펴보겠습니다:
    
    ### **코어 스레드 수가 0일 때**
    
    - **스레드 풀 생성**: **`newScheduledThreadPool(0)`**을 호출하면 코어 스레드 수가 0인 스레드 풀이 생성됩니다. 이는 스레드 풀이 비활성 상태일 때 어떠한 스레드도 유지하지 않겠다는 것을 의미합니다. 즉, 스케줄된 작업이 없을 경우 스레드 풀 내에 스레드가 존재하지 않습니다.
    - **작업 실행**: 작업이 스케줄될 때, 필요한 스레드가 동적으로 생성됩니다. 작업이 완료된 후 일정 시간(기본적으로 60초) 동안 다른 작업이 발생하지 않으면 스레드는 종료되고 스레드 풀은 다시 스레드가 없는 상태가 됩니다.
    
    ### **코어 스레드 수가 1일 때**
    
    - **스레드 풀 생성**: **`newScheduledThreadPool(1)`**을 호출하면 코어 스레드 수가 1인 스레드 풀이 생성됩니다. 이는 스레드 풀이 항상 최소한 한 개의 스레드를 유지하도록 보장합니다.
    - **작업 실행**: 스레드 풀에 상시로 하나의 스레드가 존재하기 때문에, 스케줄된 작업은 이 스레드에서 실행됩니다. 만약 해당 스레드가 작업 중이 아닐 때 추가적인 작업이 스케줄된다면, 이 스레드에서 즉시 실행될 수 있습니다. 코어 스레드가 계속 유지되기 때문에, 작업 간의 전환 시간이 단축될 수 있습니다.
    
    ### **차이점**
    
    - **스레드 유지**: 0으로 설정하면 스레드가 필요할 때만 생성되고, 그 외의 시간에는 스레드를 유지하지 않습니다. 반면, 1로 설정하면 항상 최소 한 개의 스레드가 유지되어 빠른 작업 시작이 가능합니다.
    - **자원 사용**: 0으로 설정한 경우 자원 사용을 최소화할 수 있지만, 작업 시작 시 스레드 생성에 드는 비용이 발생합니다. 1로 설정한 경우 상시적인 자원 사용이 있지만, 작업 대기 시간이 줄어들 수 있습니다.
    
    스레드 풀의 크기를 0으로 설정하는 것은 일반적으로 추천되지 않습니다. 이는 스레드 풀을 효율적으로 사용하지 못하게 만들 수 있으며, 작업 처리 시 지연을 발생시킬 수 있습니다. 최소한 하나 이상의 스레드를 유지하는 것이 좋은 관행으로 간주됩니다.

Thread.sleep() 대신 ExecutorService 를 사용한 이유

  1. 스레드 관리: while(true) 루프와 Thread.sleep을 사용하는 방식은 스레드를 직접 관리해야 하며, 이는 종종 비효율적이고 오류를 일으키기 쉬울 수 있습니다. 반면, ExecutorService는 스레드 풀을 사용하여 스레드 관리를 추상화하고 최적화하여, 스레드 생성과 소멸에 드는 비용과 복잡성을 줄여줍니다.
  2. 정확성과 예측성: Thread.sleep은 지정된 시간 동안 스레드를 일시 중지합니다만, 실행 준비가 되었을 때 스레드가 즉시 실행되는 것은 보장되지 않습니다. 스케줄이 지연될 수 있으며, sleep 후의 실제 실행 시간은 예측하기 어려울 수 있습니다. 반면, scheduledFixedRate는 지정된 주기에 따라 작업을 실행하도록 예약하며, 주어진 주기가 일정하게 유지되도록 노력합니다. 이는 특히 주기적인 작업 실행이 중요한 어플리케이션에 유리합니다.
  3. 작업 지연 관리: scheduledFixedRate는 이전 작업의 실행이 지연되더라도 정해진 주기를 유지하려고 시도합니다. 만약 작업이 예상보다 오래 걸리면, ExecutorService는 다음 작업 시작 시간을 조정하여 빠진 시간을 보충하려고 합니다. while 루프와 sleep을 사용하는 경우, 각 반복 사이의 지연은 처리 시간에 더해져서 전체 스케줄에 영향을 미칠 수 있습니다.
  4. 자원 활용: ExecutorService는 여러 작업을 스레드 풀에 효율적으로 할당하여 자원을 더욱 효과적으로 활용할 수 있습니다. 이는 특히 여러 작업을 동시에 실행해야 하는 경우 유용합니다.

주기적인 작업

  • scheduledExecutorService 를 이용해 주기적인 작업을 진행함. 특정 조건일 때 작업을 종료하도록 구현하려고 했는데 작업만 종료될 뿐 주기적인 작업은 종료되지 않았음 → 그럼 제대로 된 종료가 아님!
    • `ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0); 를 클래스단에 선언하고 {실행 → stop → 다시 실행} 하면 **java.util.concurrent.RejectedExecutionException`** 예외 발생
  • 또한 runnable 객체를 scheduleAtFixedRate 로 실행하고 생성된 Scheduled 객체를 정지해야했음. 기존 executor.shutDown 만 하면 10초 주기로 예약해둔 작업이 중지되지 않는 느낌 → 정확히 확인하기 어려움
    • 그래서 scheduled 작업을 종료하기 위해 Future 객체의 cancel 을 사용해야 했음
    • 코드에서 ScheduledExecutorService를 사용하여 정해진 간격으로 작업을 반복 실행합니다. 특정 조건이 충족되거나 오류가 발생하여 더 이상 작업을 실행할 필요가 없을 경우, 예약된 작업을 명시적으로 취소하는 것이 시스템 자원을 해방하는 데 도움이 됩니다.
    • 예약된 작업을 확인하고, 있을 경우 해당 작업을 취소합니다. 예를 들어, Redis 키가 더 이상 존재하지 않거나 특정 카운트에 도달했을 때 작업이 계속 실행되지 않도록 보장하는 데 유용합니다. 이 블록이 없다면, 작업이 불필요하게 계속 실행될 수 있어 자원을 낭비하게 됩니다.
  • 결론적으로, executorService.shutDownNow() 와 scheduledFuture.cancel(true) 모두 실행해야 작업이 필요한 시점에 제대로 종료되도록 보장하고 시스템의 부담을 줄이는 데 중요한 역할을 합니다.
    • executor.shutdown()
      • ExecutorService의 실행을 점진적으로 종료하도록 요청합니다. 이 메소드를 호출하면 더 이상 새로운 작업을 수락하지 않으며, 이미 제출된 작업들은 완료될 때까지 계속 실행됩니다. 하지만 새로운 작업을 시작할 수는 없습니다.
      • 이 메소드는 이미 스케줄링된 모든 작업을 취소하지 않으며, ScheduledExecutorService의 경우 이미 예약된 반복 작업도 완료될 때까지 계속 실행될 수 있습니다.
    • scheduledFuture.cancel(true)
      • 특정 ScheduledFuture 작업을 취소하려고 시도합니다. 이 메소드에 true를 인자로 전달하면, 실행 중인 작업이 있을 경우 강제로 중단을 시도합니다 (즉, 스레드를 interrupt 시킵니다).

      • 이 작업은 해당 Future에 의해 나타내진 하나의 작업에만 영향을 미치며, 다른 스케줄된 작업이나 실행자 서비스 전체에는 영향을 주지 않습니다.

            /** 로봇 시뮬레이션 로직 */
            public void executeSimulator(String userId, Integer count) {
        
                ScheduledExecutorService executorService = Executors.newScheduledThreadPool(0);
        
                Runnable serving = () -> {
                    if (@@@@) { // key 가 비어있거나 times 가 충족되었을 때        
                        executorService.shutdownNow();
        
                        ScheduledFuture<?> scheduledFuture = futureRef.get();
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
        
                   @@@
                };
        
                ScheduledFuture<?> scheduledFuture = executorService.scheduleAtFixedRate(serving, 1, 15, TimeUnit.SECONDS);
                futureRef.set(scheduledFuture);
        
            }

@Scheduled vs scheduledExecutorService

  • 개발 환경: 스프링 기반의 애플리케이션에서는 @Scheduled를 사용하는 것이 일반적으로 더 편리합니다. 스프링의 구성 요소와 잘 통합되고, 개발자가 스프링의 다른 기능과 함께 쉽게 사용할 수 있도록 설계되었습니다.
  • 성능과 제어가 중요한 시나리오: 특히 성능 최적화와 스레드 동작의 세밀한 제어가 필요한 시나리오에서는 ScheduledExecutorService가 더 적합할 수 있습니다. 복잡한 스레드 관리 요구사항이 있거나 특정 작업에 대한 매우 세밀한 스케줄링 조정이 필요한 경우 이를 사용하는 것이 좋습니다.

0개의 댓글