Simplify

Java Executor 에 대해 - About Java Executor 본문

Other/Java

Java Executor 에 대해 - About Java Executor

Simplify - Jonghun 2020. 1. 3. 15:59

Java에서 이야기하는 Executor 에 대해서 잘 정리되어 있는 글이 있어, 학습하고자 참고하여 문서를 남깁니다. 모든 내용을 담고 있지는 않으므로, 아래 출처 / 참고자료 부분에 있는 링크를 따라가서 보시기 바랍니다.

 

사실 Executor 까지 참고하여 구현하게 되는 상황이 얼마나 될까 싶긴 합니다. 특히 저 처럼 업무(기능) 개발 위주로 업무를 진행하는 사람들은 기능 개발에 급급할 뿐 그 이상의 깊이있는 내용을 찾아보는 것이 쉽지는 않습니다. 물론, 다양한 부분에 대해서 찾아보고, 깊이있는 학습을 진행하는 것은 업무 개발에 있어서도 도움은 많이 될 것입니다.

 

일반적으로..

아래와 같은 소스 코드.. 아마도 서버 측 프로그램을 조금이라도 들여다 본 분이라면 이와 같은 형태를 접했을 것입니다. 

while(true){
  Request request = acceptRequest();
  Runnable requestHAndler = new RequestHandler(request);
  new Thread(requestHandler).start();
}

클라이언트에서 오는 요청 정보를 받아서, 이 요청 정보를 처리하는 handler 에 정보를 담고, 그 handler(runnable)를 Thread 를 생성하여 실행하는 소스입니다. 

 

위와 같이 처리하면 개발 과정에서 몇 번의 테스트에서는 정상적으로 잘 동작하는 것 처럼 보입니다. 물론 이 코드에 문제가 있다기 보다는 서버 측 개발에서는 아래와 같은 점이 전혀 고려되지 않았다는 점이 문제입니다. 

  • Thread 를 매 번 new 하기 때문에 resource가 무한정 생성되어 Memory leak 이 발생할 수 있습니다. 물론 이 문제는 아주 천천히 - JVM이 handle할 수 있는 수준에서의 요청에 대해서는 GC가 동작하여 적절하게 메모리를 관리해 줄 수 있습니다. 
  • Thread 생성 및 종료에 따른 오버헤드가 발생하고, 많은 수의 Thread 가 생성되는 경우 이를 스케쥴링 하는 데 있어서 오버헤드도 발생합니다. 단순히 매번 new 하는 것이 속도 저하에 문제가 되고, 서버 운영 측면에서는 JVM에 부하를 주는 요소일 수 있습니다. 

이를 해결하기 위해서 아래와 같은 부분이 서버 프로그래밍에는 추가되어 있습니다. 

  • Map 과 같은 Thread Pool 을 이용하여 총 생성될 수 있는 Thread 개수를 제한하고, 유휴 상태인 Thread를 이용하여 다음 작업을 수행합니다.

이러한 해결방법과 유사하게, 이를 더 편하게 하고 관리 측면에서도 유리하게 하기 위해 Java5 부터 Executor 인터페이스(concurrency api)를 제공하고 있으며, 그 구현체로 쓰레드 풀, 큐등을 다양하게 제공하고 있습니다. 

 

Executor 에 대해..

Java Executor 는 위와 같은 구조로 되어 있습니다. 이제 각각의 요소들이 어떻게 이루어져 있는 지 조금 더 상세하게 살펴보려고 합니다.

 

Executor 인터페이스 

제공된 작업(Runnable 구현체)을 실행 하는 객체가 구현해야할 인터페이스입니다. 이 인터페이스는 작업을 제공 하는 코드와 작업을 실행 하는 매커니즘 사이의 커플링을 제거해 줍니다.

package java.util.concurrent.Executor;
  
public interface Executor {
    void excute(Runnable command);
}

 

ExecutorService 인터페이스 

Executor 의 라이프 사이클을 관리할 수 있는 기능을 정의 합니다. Runnable 뿐만 아니라 Callable 도 작업으로 사용할 수 있습니다.

  • void shutdown(); 
    이미 Executor 에 전달된 작업은 실행되지만, 새로운 작업은 받지 않습니다.
  • List< Runnable > shutdownNow(); 
    현재 실행되고 있는 작업을 모두 중지 시키고, 현재 대기 하고 있는 작업을 멈추게 합니다. 그리고 현재 실행되기 위해 대기중인 작업 목록 리스트를 반환합니다.
  • boolean isShutdown(); 
    Executor 가 셧다운 되었는지의 여부를 반환합니다.
  • boolean isTerminated(); 
    shutdown() 실행 후, 모든 작업이 종료되었는지 여부를 확인한다.
  • boolean awaitTermination(long timeout, TimeUnit unit); 
    셧다운을 실행한 뒤, 지정한 시간 동안 모든 작업이 종료될 때 까지 대기한다. 지정한 시간 이내에서 실행중인 모든 작업이 종료되면 true를 리턴하고, 여전히 실행중인 작업이 남아 있다면 false를 리턴한다.
excutor.shutdown(); 

try{ 
    if(!excutor.awaitTermination(5, TimeUnit.SECONDS)){ 
        System.out.println("아직 처리중인 작업 존재"); 
        System.out.println("작업 강제 종료 실행"); 
        excutor.shutdownNow(); 
        if(!executor.awaitTermination(5, TimeUnit.SECONDS)) { 
            System.out.println("여전히 종료하지 않은 작업 존재"); 
        } 
    } 
} catch (InterruptedException e) { 
    excutor.shutdownNow(); 
    Thread.currentThread().interrupt(); 
} 

System.out.println("서버 셧다운 완료"); 

ExecutorService 인터페이스는 작업 수행과 관련해서 추가적으로 메소드를 제공하고 있습니다.

 

  • < T > Future< T >submit(Callable< T > task)
    결과값을 리턴하는 작업을 추가한다.
  • Future<?> submit(Runnable task)
    결과값이 없는 작업을 추가한다.
  • < T > Future< T > submit(Runnable task, T result) 
    새로운 작업을 추가한다. result는 작업이 성공적으로 수행될 때 사용될 리턴 값을 의미한다.
  • < T > List<Future< T > > invokeAll(Collections<? extends Callable< T > > tasks) 
    주어진 작업을 모두 실행한다. 각 실행 결과값을 구할 수 있는 Future의 list 를 리턴한다.
  • < T > List<Future< T > > invokeAll(Collections<? extends Callable< T > > tasks , long timeout, TimeUnit unit)
    위의 invokeAll() 과 동일하다. 지정한 시간 동안 완료되지 못한 작업은 취소되는 차이점이 있다.
  • < T > invokeAny(Collection<? extends Callable< T > > tasks) 
    작업을 수행하고, 작업 결과 중 성공적으로 완료된 것의 결과를 리턴한다. 정상적으로 수행된 결과가 발생하거나 예외가 발생하는 경우 나머지 완료되지 않은 작업은 취소된다.
  • T invokeAny(Collections<? extends Callable< T > > tasks, long timeout, TimeUnit unit) 
    invokeAny() 와 동일하다. 지정한 시간 동안만 대기한다는 차이점이 있다.

Callable 은 Runnable 과 비슷한데, 차이점이 있다면 결과 값을 리턴할 수 있다는 점이다. Future 는 Callable의 결과값을 구하는 데 사용됩니다.

 

ScheduledExecutorService 인터페이스

 

Executor 구현체는 전달받은 작업을 큐에 넣은 후, 사용할 수 있는 스레드가 존재하면, 해당 스레드에 작업을 실행 하도록 합니다.

 

Executor 를 이용하여 스레드를 실행하는 코드

// Excutor 를 이용한 스레드 실행.
Excutor excutor = new SomeExcutor();

while(true) {
    Request request = acceptRequest();
    Runnable requestHandler = new RequestHandler(request);
    excutor.excute(requestHandler);
}

 

출처 / 참고자료

Comments