본문 바로가기

국비 교육

2020.10.19 일자 수업 : 사용 사례, executor 프레임워크

 요구사항 분석 - 사용 사례 

 

Actor(who)가 이 시스템을 사용하여 달성하려는 업무 목표(what)를 Use-case(사용사례)라고 한다. 분석 설계 과정에서 use-case을 식별하는 것을 use-case identify라고 한다. 다음은 use-case을 식별할 때 사용되는 use-case의 조건이다.

  • (개발할) 시스템을 사용해서 처리하는 업무
  • 한 사람이 한 번에 한 순간에 수행하는 업무
    예) 메일 전송 (메일 전송은 한 번에 한 순간에 하지 않을 수도 있다.) => 메일 임시보관 / 메일 전송
  • 카운트가 가능한 단위로 업무를 쪼갠다. 업무의 시작과 끝이 명확하다.

이 조건에 따라 use-case의 적정 크기를 지정해야 한다. RUP 개발 프로세스에서는 2주에서 6주 안에 개발할 수 있는 규모를 use-case의 적정 크기로 권장하고 있다.

 

그러나 Agile 개발 방법론은 개발의 단위를 1시간 ~1일 사이에 개발할 수 있는 규모로 권장하고 있다.
*개발 단위 - 분석 -> 설계 -> 구현 -> 테스트

 

쉽게 말하면 use-case는 개발자가 개발하게 될 기능의 단위이다. 버튼을 누르는 것 자체도 기능이긴 하지만 use-case는 어떤 단위의 업무여야 한다. 버튼 누르는 일 자체가 업무는 아니다.

너무 use-case를 크게 잡으면 각 기능을 구현하는 데 도움이 되지 않는다. 예를 들어 게시물 관리는 use-case에 비해 너무 큰 범위이다. 이것을 게시물 등록, 조회, 삭제 등등으로 나눠야 한다. 

 

use-case identify 관련 원서
www.amazon.com/Applying-Use-Cases-Addison-Wesley-Technology-ebook/dp/B003YMNVCK/ref=sr_1_1?dchild=1&keywords=applying+use+cases&qid=1603068754&sr=8-1

 

Amazon.com

Enter the characters you see below Sorry, we just need to make sure you're not a robot. For best results, please make sure your browser is accepting cookies.

www.amazon.com

use-case의 특별한 예

업무가 아닌데도 use-case로 식별하는 경우가 있는데 이것은 여러 다른 use-case의 공통 시나리오일 경우이다.

 

예를 들어 게시글 등록 / 게시글 변경 / 게시글 삭제 use-case들에서 공통적으로 수행해야할 첫 단계는 로그인이다.

로그인은 업무는 아니지만, 다른 use-case들의 관리를 쉽게 하기 위해, 혹은 개발의 편의성을 위해(세가지 use-case에서 로그인 기능을 세번 구현하는 것보다는 로그인 기능을 따로 구현하는 것이 편하므로) 따로 추출하기도 한다.

 

신입은 회사 들어가서 use-case를 식별할 일은 없지만 분석설계팀에서 그린 use-case을 알아볼 줄은 알아야 한다.

 

use-case 통합

게시글 등록(Create), 게시글 조회(Read/Retrieve), 게시글 변경(Update), 게시글 삭제(Delete) => CRUD 게시글 관리

-> 서로 관련된 업무인 경우, 관리의 편의성을 위해 한 개의 use-case로 합치기도 한다.

-> CRUD = XxxManage = XxxHandler = Xxxservice

단, 합친 use-case는 2주에서 6주 범위에서 개발 가능한 수준(어느정도 규모가 있는 기업 기준)이어야 한다. 합쳤을 때에 6주를 넘어가는 수준의 큰 범위라면 쪼개야 한다. 

 

use-case diagram 표기법

  • use-case 들에 대한 다이어그램을 그릴 때에는 이 use-case를 수행할 actor와 use-case를 화살표로 잇는다.
  • 로그인 기능과 같이 다른 use-case에서 필수적으로 수행해야할 use-case는 <<include>> 을 표기하고 점선 화살표로 잇는다. include의 목적어가 되는 쪽이 화살표가 향하는 방향이다.
                                                           <<include>>
    Primary Actor -------->(회원가입) - - - - - - - -> (주소 검색)-----> Secondary Actor
  • 또한 필수적이지는 않으나 선택적으로 다른 use-case에서 수행할 수 있는 use-case는 <<extend>>을 표기하고 점선 화살표로 잇는다. extend의 목적어가 되는 쪽이 화살표가 향하는 방향이므로 include와는 반대 방향을 취한다.
                                                              <<extend>>
    Primary Actor ---------> (회원가입)<- - - - - - - -(주소 검색)----> Secondary Actor
  • 위와 같이 선택적인 use-case를 수행할 수 있는 분기점을 extension point라고 한다.
  • 보통은 use-case의 이름을 영어로는 [동사 + 명사]로 짓고, 한국어로는 [명사 + 동사 (+하기)
  • primary actor는 다이어그램의 가장 왼편에 위치하고, 가장 오른편에서는 시스템이 의존하고 있는 secondary actor가 위치하게 된다.

설계팀에서 그려주는 이 다이어그램은 큰 틀일 뿐이다. 즉, 클래스들의 구조를 직접 설계하는 것이 아니기 때문에 개발자는 이것을 참고하여 클래스 구조를 만들거나 혹은 이것을 큰 틀을 벗어나지 않는 선에서 변형시켜 구조를 만들 수도 있다.

USE-CASES 명세서

더보기

[UCXX - 유스케이스명]

[유스 케이스에 대한 간단한 설명]

액터 (현재 유스케이스를 실행하는 액터를 적는다.)

  • 주 액터

    • [액터명] : [간단한 설명]

    • [액터명] : [간단한 설명]

  • 보조 액터

    • [액터명] : [간단한 설명]

    • [액터명] : [간단한 설명]

성공 시나리오

  • [액터가 시스템에 대해 하는 행위]

    • [액터의 요청에 대한 시스템의 반응]

  • [액터가 시스템에 대해 하는 행위]

    • [액터의 요청에 대한 시스템의 반응]

예외 시나리오

  • [예외 상황이 발생하는 경우]

    • [예외 상황 처리하는 시스템의 반응]

명세서 예시

더보기

[UC01 - 프로젝트 등록]

프로젝트 정보를 등록한다.

액터 (현재 유스케이스를 실행하는 액터를 적는다.)

  • 주 액터

    • 회원 : PMS 시스템에 가입되어있는 사람

  • 보조 액터

    • 없음

성공 시나리오

  • 회원이 프로젝트 목록에서 '새 프로젝트' 버튼을 클릭한다.

    • 시스템은 프로젝트 정보 입력화면을 제공한다.

  • 회원은 프로젝트 기본 정보(제목, 설명, 시작일, 종료일, 팀원)을 입력한다.

  • 회원은 '팀원 추가' 버튼을 클릭한다.

    • 시스템은 팀원 목록을 제공한다.

    • 회원은 추가할 팀원을 모두 선택한 후 '확인' 버튼을 클릭한다.

      • 시스템은 프로젝트 정보 입력 창에 회원이 선택한 팀원 정보를 출력한다.

    • 회원은 '취소' 버튼을 클릭한다.

      • 시스템은 팀원 목록 화면을 닫고 다시 프로젝트 정보 입력 화면을 제공한다.

  • 회원은 '등록' 버튼을 클릭한다.

    • 시스템은 프로젝트 정보를 저장한 후 프로젝트 목록 화면을 제공한다.

  • 회원은 '취소' 버튼을 클릭한다.

    • 시스템은 프로젝트 목록 화면으로 돌아간다.

예외 시나리오

  • [예외 상황이 발생하는 경우]

    • [예외 상황 처리하는 시스템의 반응]

*애자일 방법 - 명세서같은 문서를 최소화하고 오로지 사용자가 원하는 대로 만들기 위한 방법이다. 애자일 방법은 일단 최소한의 단위별로 프로젝트를 만들고 계속해서 고객에게 피드백을 계속해서 받았다. 문서 작성보다는 코딩에 집중한다. 

그러나 우리나라는 애자일 방법보다는 명세서를 보고 한다. 명세서는 고객과 회사 사이의 계약서와 같기 때문이다. 서비스 업체는 고객이 자기 회사이므로 개발팀이 개발하고 고객이 바로 보고하는 검토가 가능하기 때문에 애자일 방법론이 맞으나, 누군가에게 어플리케이션을 제공하는 SI 회사는 애자일 방법보다는 계약서의 형태를 띄는 명세서를 갖고 하게 된다.


 스레드풀 프레임워크 

 

git/eomcc-java-basic/src/main/java com.eomcs.concurrent.ex07

자바 API에서 java.util.cuncurrent 패키지의 Executors 프레임워크는 스레드풀 기능을 제공하고 있다. 이 프레임 워크를 통해 스레드 풀 기능을 사용해볼 것이다.

 

간단한 사용 방법은 다음과 같다.

  • Executors의 스태틱 메서드를 통해 스레드풀 기능을 갖는 ExecutorService 구현체를 리턴받는다.
  • ExecutorService 객체에 대하여 수행할 작업을 run 메서드로 갖는 Runnable 객체를 파라미터로 주고 execute 메서드를 호출하면, 스레드풀에서 스레드를 하나 꺼내어 작업을 바로 수행시킨다.
  • 생성된 스레드의 작업이 끝나도 스레드 자체는 종료되지 않으므로 메인 스레드가 종료되어도 프로그램은 종료되지 않는다.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0110 {

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    executorService.execute(() -> System.out.printf("%s - Hello!\n",
        Thread.currentThread().getName()));

    System.out.println("main() 종료!");
  }
}

이렇게만 코드를 짜면 영원히 프로그램이 종료되지 않기 때문에 스레드풀에 있는 스레드들을 모두 종료시키고 싶다면, ExecutorService 객체에 대하여 shutdown 메서드를 호출하면 스레드들이 모두 종료될 것이다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Exam0120 {

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    executorService.execute(() -> System.out.printf("%s - Hello!\n",
        Thread.currentThread().getName()));

    executorService.shutdown();

    System.out.println("main() 종료!");
  }
}


ExecutorService 객체 생성하기

newFixedThreadPool()

Executor의 스태틱 메서드인 newFixedThreadPool 메서드를 호출하면, Executors는 파라미터로 지정된 크기의 빈 스레드풀 객체를 생성하여 리턴한다. 새로 생성된 스레드풀은 비어있으므로 처음으로 execute 를 호출하면 무조건 스레드를 생성하겠지만, 그 이후로는 스레드풀이 비어있는 순간에 execute 메서드를 호출하는 경우에만 스레드를 생성한다. 또한 NewFixedThredPool을 통해 만들어진 스레드풀은 지정된 스레드의 최대 개수까지만 스레드를 생성할 수 있기 때문에 그 이상으로는 스레드풀이 비어있어도 스레드를 생성하지 않고, 기존의 스레드가 작업 수행이 끝나 스레드풀로 돌아올 때까지 기다린다.

 

다음은 스레드의 작업이 시작되고 끝나는 시점에서 해당 스레드의 이름을 출력하고, 그 사이에 정해진 밀리초만큼 sleep하는 작업을 수행하는 run 메서드를 가진 Runnable 구현체이다. 이 객체를 execute로 주어 스레드풀에서 스레드가 생성되거나 재사용되는 상황을 살펴보겠다.

  static class MyRunnable implements Runnable {
    int millisec;

    public MyRunnable(int millisec) {
      this.millisec = millisec;
    }

    @Override
    public void run() {
      try {
        System.out.printf("%s 스레드 실행 중...\n",
            Thread.currentThread().getName());

        Thread.sleep(millisec);

        System.out.printf("%s 스레드 종료!\n",
            Thread.currentThread().getName());
      } catch (Exception e) {
        System.out.printf("%s 스레드 실행 중 오류 발생!\n", Thread.currentThread().getName());
      }
    }

다음은 크기를 3으로 지정한 스레드 풀을 생성하여 작업을 수행시키는 코드이다. 첫번째 execute 호출에서는 생성된 스레드가 하나도 없어 스레드가 당연히 생성될 것이고, execute의 세번째 호출까지는 앞 스레드들의 작업이 끝나지 않아 스레드풀이 비어있기 때문에 스레드를 생성할 것이다. 그러나 스레드 풀에 담을 수 있는 스레드의 최대 개수가 3개이므로, 그 다음부터는 앞에 만들어진 세개의 스레드 중에서 두 번째로 생성된 스레드의 작업이 가장 먼저 끝나기전까지는 작업이 수행되지 않을 것이다. 또한 두번째로 생성된 스레드가 다시 맡은 네번째 작업이 가장 먼저 끝나게 될 것이므로 다섯번째 작업도 두 번째 스레드가 수행할 것이다.

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(3000));
    executorService.execute(new MyRunnable(9000));

    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
  
// 결과!
// pool-1-thread-1 스레드 실행 중... 6초 시작
// pool-1-thread-3 스레드 실행 중... 9초 시작
// main() 종료!
// pool-1-thread-2 스레드 실행 중... 3초 시작
// pool-1-thread-2 스레드 종료!     3초 끝
// pool-1-thread-2 스레드 실행 중... 2초 시작
// pool-1-thread-2 스레드 종료!     2초 끝
// pool-1-thread-2 스레드 실행 중... 4초 시작
// pool-1-thread-1 스레드 종료!      6초 끝
// pool-1-thread-3 스레드 종료!      9초 끝
// pool-1-thread-2 스레드 종료!      4초 끝

newCachedThreadPool()

Executors의 newCachedThreadPool 스태틱 메서드를 호출하면 스레드의 크기가 지정되지 않은 스레드풀을 생성하여 리턴해준다. 따라서스레드풀은 풀이 비어있는 순간에 execute를 호출하면 무조건 스레드를 생성한다. 그러나 스레드 풀에 스레드가 하나라도 있다면 생성하지 않고 존재하는 스레드를 재사용한다.

 

다음 코드는 newCachedThreadPool을 통해 스레드풀을 리턴받아 5번의 execute을 통해 작업을 수행시킨 코드이다. 네번째 execute 호출까지는 스레드풀이 비어있는 순간의 호출이므로 4개의 스레드가 생성될 것이다. 그러나 마지막 execute 호출 전에 3초 동안이나 메인 스레드가 잠들어 그동안 적어도 네번째 스레드의 작업이 완료될 것이므로 마지막 execute는 새로운 스레드를 생성하지 않고, 네번째 스레드를 재사용할 것이다.

  public static void main(String[] args) throws Exception {

    ExecutorService executorService = Executors.newCachedThreadPool();

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(3000));
    executorService.execute(new MyRunnable(9000));
    executorService.execute(new MyRunnable(2000));

    Thread.sleep(3000);

    executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
  
// 결과!
// pool-1-thread-1 스레드 실행 중... 6초 시작
// pool-1-thread-2 스레드 실행 중... 3초 시작
// pool-1-thread-3 스레드 실행 중... 9초 시작
// pool-1-thread-4 스레드 실행 중... 2초 시작
// pool-1-thread-4 스레드 종료!     2초 끝
// main() 종료!
// pool-1-thread-4 스레드 실행 중... 4초 시작
// pool-1-thread-2 스레드 종료!     3초 끝
// pool-1-thread-1 스레드 종료!     6초 시작
// pool-1-thread-4 스레드 종료!     4초 끝
// pool-1-thread-3 스레드 종료!     9초 끝

newSingleThreadExecutor()

Executors의 newSingleThreadExecutor가 생성하여 리턴하는 스레드풀은 오로지 한개의 스레드만 가질 수 있다. 사용할 수 있는 스레드가 한 개이기 때문에 모든 execute를 순차적으로 실행한다.

  public static void main(String[] args) throws Exception {

    ExecutorService executorService = Executors.newSingleThreadExecutor();

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(3000));
    executorService.execute(new MyRunnable(9000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
  
// 결과!
// main() 종료!
// pool-1-thread-1 스레드 실행 중... 6초 시작
// pool-1-thread-1 스레드 종료!     6초 끝
// pool-1-thread-1 스레드 실행 중... 3초 시작
// pool-1-thread-1 스레드 종료!     3초 끝
// pool-1-thread-1 스레드 실행 중... 9초 시작
// pool-1-thread-1 스레드 종료!     9초 끝
// pool-1-thread-1 스레드 실행 중... 2초 시작
// pool-1-thread-1 스레드 종료!     2초 끝
// pool-1-thread-1 스레드 실행 중... 4초 시작
// pool-1-thread-1 스레드 종료!     4초 끝

스레드 실행 방법

ExecutorService 인터페이스에는 각 구현체에서 유용하게 사용될 메서드들을 정의하고 있다. 그 중 가장 대표적인 메서드를 소개한다.

execute

스레드풀에 수행할 작업을 작업 큐에 등록하는 메서드이다. 이 메서드를 호출하여 작업을 큐에 등록하면 스레드풀은 대기하는 스레드를 갖고 큐에 등록된 순서대로 작업을 수행할 것이다.

 

만약 스레드풀에 대기중인 스레드가 없다면 새로 스레드를 생성하여 작업을 수행시킬 것이며, 스레드가 이미 지정된 최대 개수까지 생성된 상태라면 기존 스레드가 작업을 끝낼 때까지 기다릴 것이다. execute 메서드로는 수행하는 작업의 종료 여부나 시점을 알 수가 없다. 가장 흔하게 사용되는 메서드 중 하나이다.

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));

    System.out.println("main() 종료!");
  }

submit

execute 메서드와 동일한 역할을 수행하나 Future 객체를 리턴한다. Future 객체에 대하여 get을 호출하면 Future를 리턴했던 그 스레드가 작업을 끝날때까지 get메서드에서 블러킹(blocking pending)되어있다가 해당 스레드의 작업이 끝나면 null을 리턴한다.

  public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    Future<?> future1 = executorService.submit(new MyRunnable(2000));
    Future<?> future2 = executorService.submit(new MyRunnable(4000));

    future1.get();
      System.out.println("첫 번째 작업이 끝났음");
      
    future2.get();
      System.out.println("두 번째 작업이 끝났음");

    System.out.println("main() 종료!");
  }
  
  // 결과!
  // pool-1-thread-1 스레드 실행 중...
  // pool-1-thread-2 스레드 실행 중...
  // pool-1-thread-1 스레드 종료!
  // 첫번째 작업이 끝났음
  // pool-1-thread-2 스레드 종료!
  // 두번째 작업이 끝났음
  // main() 종료!

스레드풀 종료 방법

shutdown()

ExecutorService의 객체에 대하여 shutdown 을 호출하면 더 이상의 작업 요청을 받지 않고 스레드를 바로 종료시킨다. 다만 shutdown은 모든 스레드가 종료될때까지 기다리지 않고 지시만 한뒤 바로 다음코드를 이어 실행한다.

 

다음 코드는 여러번의 작업 요청 후 shutdown을 호출한 코드이다. 작업이 완료되어 스레드들이 종료되기 전에 이미 shutdown이 리턴되어 main 스레드가 종료된 것을 확인할 수 있다.

  public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(3);
    
    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));
    
    executorService.shutdown();
    
    System.out.println("main() 종료!");

  }

// 결과!
// pool-1-thread-2 스레드 실행 중...
// main() 종료!
// pool-1-thread-1 스레드 실행 중...
// pool-1-thread-3 스레드 실행 중...
// pool-1-thread-2 스레드 종료!
// pool-1-thread-2 스레드 실행 중...
// .
// .
// .

shutdown 이 호출된 이후에 execute를 호출하면 RejectedExecutionException  예외가 발생한다.

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(4000));

    executorService.shutdown();

    executorService.execute(new MyRunnable(4000));

    System.out.println("main() 종료!");
  }
  
  // 결과!
  // RejectedExecutionException 발생
  

shutdownNow()

ExecutorService의 객체에 대하여 shutdownNow 을 호출하면 현재 대기중인 스레드들과 작업을 실행하고 있는 스레드들까지 모두 강제 종료(interrupt)된다. 

 

shutdownNow 메서드가 호출되면 현재 수행 중인 작업들은 모두 예외가 발생하며 중단되고, 작업큐에서 대기 중인 작업들의 목록이 리턴된다. 물론 shutdownNow 호출 이후에 요청되는 작업도 예외를 던지며 모두 거절한다.

  public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(5000));
    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(7000));

    List<Runnable> tasks = executorService.shutdownNow();
    for (Runnable task : tasks) {
      System.out.println(((MyRunnable) task).millisec);
    }
    System.out.println("main() 종료!");
  }
  
  // 결과!
  // pool-1-thread-1 스레드 실행 중...
  // 5000
  // 6000
  // 7000
  // pool-1-thread-2 스레드 실행 중...
  // pool-1-thread-3 스레드 실행 중...
  // pool-1-thread-2 스레드 실행 중 오류 발생!
  // main() 종료!
  // pool-1-thread-1 스레드 실행 중 오류 발생!
  // pool-1-thread-3 스레드 실행 중 오류 발생!

 


스레드 풀 종료 대기

awaitTermination(int, TimeUnit)

executorService 객체에 대하여 awaitTermination 메서드를 호출하면 첫번쨰 파라미터로 준 초시간 안에 스레드들이 모두 종료될 시 바로 true를 리턴한다. 만약 첫번째 파라미터로 준 초시간까지 종료되지 않은 스레드가 하나라도 있다면 자동으로 false를 리턴한다.

 

두번째 파라미터로 주고 있는 TimeUnit.SECONDS는 초단위로 타임아웃을 할 때에 유용하게 사용되는 객체이다. TimeUnit은 enum 타입이다.

 

다음 코드는 크기가 3인 스레드풀을 생성하고 execute를 호출하여 여러 작업을 등록한 후, shutdown 과 awitTermination을 차례로 호출한 코드이다. shutdown 이 호출되면 가장 마지막으로 작업이 완료되어 스레드가 종료되는 시점은 17초이다. 즉 awaitTermination이 기다리는 10초를 훌쩍 넘는 시간이다. 따라서 이 메서드는 false를 리턴할 것이고, 조건문에 의해 아직 스레드가 종료되지 않았다는 메시지를 출력할 것이다.

  public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(15000));

    executorService.shutdown();

    if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
      System.out.println("아직 종료 안된 작업이 있다.");
    } else {
      System.out.println("모든 작업을 종료하였다.");
    }

    System.out.println("main() 종료!");
  }

이 메서드를 활용하면 스레드 풀의 강제 종료를 확실히 하는 데 도움이 된다.

 

예를 들면 일단 shutdown을 호출했으나 정해진 작업을 수행 완료하고 스레드를 종료하는 데 시간이 과도하게 걸리는 경우를 대비하여, awaitTermination 메서드를 통해 10초를 기다리다가 그래도 종료되지 않은 스레드에 한해, shutdownNow를 호출하여 강제 종료를 하는 것이다. 이렇게 하고도 스레드를 강제 종료를 하지 못한 경우를 또 다시 대비하여 다시 awaitTermination  메서드로 5초를 대기한 후, 그래도 종료되지 않은 스레드가 있다면 강제 종료를 하지 못했다는 메시지를 띄우는 것으로 마무리한다.

  public static void main(String[] args) throws Exception {
    ExecutorService executorService = Executors.newFixedThreadPool(3);

    executorService.execute(new MyRunnable(6000));
    executorService.execute(new MyRunnable(2000));
    executorService.execute(new MyRunnable(4000));
    executorService.execute(new MyRunnable(20000));

    executorService.shutdown();

    if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
      System.out.println("아직 종료 안된 작업이 있다.");
      System.out.println("남아 있는 작업의 강제 종료를 시도하겠다.");
      executorService.shutdownNow();

      if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
        System.out.println("스레드풀의 강제 종료를 완료하지 못했다.");
      } else {
        System.out.println("모든 작업을 강제 종료했다.");
      }
    }
    System.out.println("main() 종료!");
  }