2020-10-19 TIL

7 minute read

요구사항 분석

Use-case (what)

  • 사용 사례
  • 액터가 시스템을 사용하여 달성하려는 업무 목표
  • use-case identity (식별방법)
    • 적정크기 (2주~6주) : RUP 개발 프로세스 <-> 에자일 개발 방법론(1시간~1일)
      • 개발 단위 : 분석 -> 설계 -> 구현 -> 테스트
    • (개발할) 시스템을 사용해서 처리하는 업무
    • 한 사람이 한 번에 한 순간에 수행하는 업무
      • 예) 메일 보내는 일
        • 메일 임시 보관하기
        • 메일 보내기
    • 카운트가 가능(업무의 시작과 끝이 명확)한 단위로 업무를 쪼갠다.
  • 특별한 예
    • 업무가 아닌데도 use-case로 식별하는 경우
      • 여러 use-case의 공통 시나리오 일 때
        • 게시글 등록, 변경, 삭제 : 로그인 <- 업무는 아니다 : 다른 use-case 들의 관리를 쉽게하기 위해 따로 추출한다 -> 개발 편의성
  • use-case 통합
    • CRUD 게시글 관리ㄴ
      • 게시글 등록 Create
      • 게시글 조회 Read / Retreive
      • 게시글 변경 Update
      • 게시글 삭제 Delete
        • 서로 관련된 업무인 경우 업무의 편의성을 위해 한개의 use-case로 합치기도 한다.
          • XxxManage, XxxHandler, XxxService
          • 단, 합친 use-case는 2주에서 6주 범위에서 개발 가능해야 한다.
  • use-case diagram
    • actor -> use-case -> secondary actor
      • 비회원 -> 회원 가입하기(join member) -> 카카오 주소검색 서비스
  • use-case diagram 2
    • 비회원 -> 회원가입 —>(include) 주소검색(유스케이스로 뽑음) -> 카카오 주소검색
      • 실행흐름
        • 회원가입 - 주소검색 - 끝 (주소 검색을 하지 않고 끝으로 가는 방법은 없는 경우 : Include)
      • 회원가입(주소 검색을 클릭) <— (extend : 선택사항) 주소검색 -> 카카오 주소검색
        • 회원가입 - 끝
        • 회원가입 주소검색 - 주소검색 - 끝
  • use-case 예)
    • 회원
      • -> 프로젝트 관리
      • -> 작업 관리
      • -> 게시글 관리
      • -> 회원 탈퇴
    • 관리자
      • -> 회원관리
      • 회원가입 <- 비회원
      • 로그인 <- 비회원

개발방법론 - RUP

  • 래셔널 통합 프로세스
  • Rational Unified Process
  • 소프트웨어 개발 과정에서 누가, 언제, 어떻게, 무엇을 수행해야 하는가를 기술한다.
    • 작업자(Workers) : ‘누가(who)’에 해당
    • 액티비티(Activities) : ‘어떻게(how)’에 해당
    • 산출물(Artifacts) : ‘무엇을(what)’에 해당
    • 웍플로우(Workflows) : ‘언제(when)’에 해당

스레드풀

ExcutorService

  • 1) Runnable을 상속하는 클래스를 만들어서 run() 메서드에서 별도의 스레드로 독립적으로 실행해야 하는 일을 정의한다.
  • 2) execute()를 호출하면
  • 3) ExecutorService가 스레드를 생성한다.
  • 4) 스레드가 클래스를 실행한다.
public class Exam0110 {
    public static void main(String[] args) {
      // 스레드풀을 생성한다.
      // - 최대 3개의 스레드를 생성한다.
      ExecutorService executorService = Executors.newFixedThreadPool(3);
  
      // 스레드풀에 작업 수행을 요청한다.
      // - 작업은 Runnable 구현체로 작성하여 넘겨준다.(람다 문법 사용)
      // - 스레드풀은 스레드를 생성하여 작업을 수행시킨다.
  
    //    class X implements Runnable {
    //      @Override
    //      public void run() {
    //        System.out.printf("%s 스레드 실행", Thread.currentThread().getName());
    //      }
    //    }

    //    new Runnable() {
    //      @Override
    //      public void run() {
    //        System.out.printf("%s 스레드 실행", Thread.currentThread().getName());
    //      }
    //    };

    //    Runnable obj = new Runnable() {
    //      @Override
    //      public void run() {
    //        System.out.printf("%s 스레드 실행", Thread.currentThread().getName());
    //      }
    //    };
    //
    //    threadPool.execute(obj);

    threadPool.execute(() ->
    System.out.printf("%s 스레드 실행", Thread.currentThread().getName()));

    System.out.println("main() 종료");
  
      System.out.println("main() 종료!");
      // JVM은 main 스레드가 종료하더라도 나머지 스레드가 종료할 때까지 기다린다.
      // 스레드풀에서 생성한 스레드가 요청한 작업을 마치더라도
      // 다음 작업을 수행하기 위해 계속 실행된 채로 대기하고 있기 때문에
      // JVM은 종료하지 않는다.
    }
}

ExecutorExcutorService - 스레드풀 종료

  • jvm 종료
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()));

    // 스레드풀에 있는 모든 스레드들이 요청한 작업을 끝내면 종료하게 한다.
    // 모든 스레드가 종료될 때까지 기다리지 않고 바로 리턴한다.
    // shutdown() 호출 이후에는 새 작업 요청을 받지 않는다.
    // 즉 execute()를 호출하면 예외가 발생한다.
    executorService.shutdown();

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

고정 크기 스레드풀 - newFixedThreadPool(숫자)

public class Exam0210 {
    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());
        }
      }
    }
    public static void main(String[] args) {
      ExecutorService executorService = Executors.newFixedThreadPool(3);
  
      // 일단 스레드풀의 크기(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() 종료!");
    }
}

가변 크기 스레드풀 - newCachedThreadPool()

  • 문제점 : 한번에 백만명이 접속하면 스레드가 한번에 백만개가 생성된다.
public class Exam0220 {
.
.
.
    public static void main(String[] args) throws Exception {
  
      // 스레드의 수를 고정하지 않고 필요할 때마다 스레드를 생성하는 스레드풀이다.
      // 물론 작업을 끝낸 스레드는 다시 사용할 수 있도록 pool에 보관한다.
      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() 종료!");
    }
}

한 개의 스레드를 갖는 스레드풀 - newSingleThreadExecutor()

public class Exam0230 {
.
.
.
  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() 종료!");
  }
}

작업 실행 : execute() 와 submit()

  • execute()
    • 리턴 타입이 void
    • 스레드풀에 수행할 작업을 등록한다.
    • 스레드풀은 execute()를 호출한 순서대로 작업큐에 작업을 보관한다.
    • 그리고 놀고 있는 스레드가 있다면, 작업큐에서 작업을 꺼 수행시킨다.
    • 놀고 있는 스레드가 없으면, 새로 스레드를 생성한다.
    • 놀고 있는 스레드가 없으면, 새로 스레드를 생성한다.
      • 수행한 작업의 종료 여부를 확인할 수 없다.
  • submit()
    • execute()와 같다.
      • 단 작업의 종료 상태를 확인할 수 있는 Future 객체를 리턴한다.
        • 리턴 타입이 future
      • 특정 작업이 끝날 때까지 기다리고 싶을 때 future를 쓴다.
public class Exam0320 {
.
.
.
 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));

    // Future.get()
    // => 요청한 작업이 완료될 때 까지 기다린다.(pending)
    // => 요청한 작업이 완료되면 null을 리턴한다.
    
    // if 문은 해당 작업이 끝나면 null을 리턴하는 것을 확인시키기 위해 사용하는 것이다.
    if (future2.get() == null)
      System.out.println("두 번째 작업이 끝났음");

    if (future1.get() == null)
      System.out.println("첫 번째 작업이 끝났음");

    //    future2.get(); // 해당 작업이 끝날 때까지 기다리다가, 끝나 null을 리턴한다.
    //    System.out.println("두 번째 작업이 끝났음");
    //
    //    future1.get(); // // 해당 작업이 끝날 때까지 기다리다가, 끝나 null을 리턴한다.
    //    System.out.println("첫 번째 작업이 끝났음");

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

스레드풀 종료 - shutdown()

  • shutdown을 호출하면 이전에 요청한 작업들이 완료될 때 까지 기다렸다가 스레드풀이 종료된다.
  • shutdown을 호출한 다음에 작업을 요청하면 예외가 발생된다.
public class Exam0410 {
.
.
.
  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));

    // 위에서 예외가 발생했기 때문에 "main() 종료" 는 출력되지 않는다.
    System.out.println("main() 종료!");
  }
}

스레드풀 즉시 종료 - shutdownNow()

  • 실행중인 작업은 즉시 멈추고 대기중인 작업은 리턴한다.
public class Exam0420 {
.
.
.
 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);
    }
    // 물론 새 작업 요청도 거절한다.
    // => 예외 발생!
    executorService.execute(new MyRunnable(4000));

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

shutdown() vs shutdownNow()

  • shutdown() : 진행 중인 작업을 완료하고 대기 중인 작업도 완료한 다음 종료.
  • shutdownNow() : 진행 중인 작업은 즉시 완료하고 대기 중인 작업은 리턴한다.

스레드풀 종료 대기 - awaitTermination()

  • 지정한 시간이 경과될 때까지 종료되지 않고 기다렸다가 지정된 시간이 지나면 더 이상 기다리지 않고 메인을 종료하고 종료되지 않은 스레드가 마감될 때 까지 기다린다.
public class Exam0510 {
.
.
.
 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(3000));

    executorService.shutdown();

    // 스레드풀의 모든 스레드가 종료되면 즉시 true를 리턴한다.
    // 만약 지정된 시간이 경과할 때 까지 종료되지 않았다면
    // false를 리턴한다.
    if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
      System.out.println("아직 종료 안된 작업이 있다.");
    } else {
      System.out.println("모든 작업을 종료하였다.");
    }

    System.out.println("main() 종료!");
  }
}
  • 결과
    pool-1-thread-1 스레드 실행 중...
    pool-1-thread-3 스레드 실행 중...
    pool-1-thread-2 스레드 실행 중...
    pool-1-thread-2 스레드 종료!
    pool-1-thread-2 스레드 실행 중...
    pool-1-thread-3 스레드 종료!
    pool-1-thread-1 스레드 종료!
    아직 종료 안된 작업이 있다.
    main() 종료!
    pool-1-thread-2 스레드 종료!
    

awaitTermination()과 shutdownNow()를 함께 사용했을 때

public class Exam0520 {
.
.
.
  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("남아 있는 작업의 강제 종료를 시도하겠다.");
      // => 만약 10초가 경과될 때까지 종료되지 않으면,
      //    수행 중인 작업을 강제 종료하라고 지시하고,
      //    대기 중인 작업은 취소한다.
      executorService.shutdownNow();

      // 그리고 다시 작업이 종료될 때까지 기다린다.
      if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
        System.out.println("스레드풀의 강제 종료를 완료하지 못했다.");
      } else {
        System.out.println("모든 작업을 강제 종료했다.");
      }

    }

    System.out.println("main() 종료!");
  }
}
  • 결과
    pool-1-thread-2 스레드 실행 중...
    pool-1-thread-3 스레드 실행 중...
    pool-1-thread-1 스레드 실행 중...
    pool-1-thread-2 스레드 종료!
    pool-1-thread-2 스레드 실행 중...
    pool-1-thread-3 스레드 종료!
    pool-1-thread-1 스레드 종료!
    아직 종료 안된 작업이 있다.
    남아 있는 작업의 강제 종료를 시도하겠다.
    pool-1-thread-2 스레드 실행 중 오류 발생!
    모든 작업을 강제 종료했다.
    main() 종료!
    

Categories:

Updated: