최근 Google Architecture MVP Sample project를 참고하여 MVP를 공부하던 도중 Executor
의 클래스를 처음 보게 되었다. 그 후 구글링을 통해 네트워크 IO 통신과 디스크 IO 작업, UI 작업등 Background 작업을 위해 해당 클래스가 사용되었고 kotlin의 coroutine
이 출시되기 이전 Thread를 유용하게 사용한 방법이었다.
- Executor Inferface
import java.util.concurrent
interface Executor {
void execute(Runnable command);
}
구조로만 보면 Runnable 객체를 실행하는 단순한 구조이다. 클래스 자체로 어떤 Background 프로세스가 생성되는 것이 아닌 틀만 제공해준다. 따라서 대부분의 사용자는 Executor
를 implements하여 Background 작업을 수행하는 클래스를 정의한다.
- Executor 호출
Executor를 알기 전 Background 작업을 수행하기 위해 단순 Thread 객체를 선언하고 호출 하였다.
new Thread(new Runnable()).start();
만약, 여러 작업을 실행해야 한다면 그때마다 Thread 객체를 생성하는 것은 매우 비효율적일 것이다. (작업마다 2개 객체가 생성되니? 이건 추측) Executor 구조는 해당 관점에서 효율적인 호출 방법을 가지고 있다.
Executor executor = new CustomExecutor()
executor.excute( new RunnableTask1());
executor.excute( new RunnableTask2());
executor.excute( new RunnableTask3());
- ExecutorService
Executor 구조를 보면 별도의 Thread를 생성하지 않는 것을 볼 수 있다. 그 말은 해당 객체를 호출한 Thread에서 Runnable을 실행한다는 뜻. 따라서 Network IO 작업을 MainThread에서 진행할 수 없고 UI가 멈춘 채로 Runnable이 실행되면 사용자에게 불친절하므로 별도의 Thread pool를 가지고 있어야 한다.
Executors
클래스는 ExecutorService라는 Executor 인터페이스를 확장하고 Thread의 cycle을 관리하는 Thread pool객체를 만들어주는 여러 Factory 함수를 가지고 있다.
동시에 실행되는 Thread의 개수를 고정
open class MyExecutor implements Executor {
private Executor executor;
MyExecutor() {
this(Executors.newFixedThreadPool(2));
}
MyExecutor(Executor executor) {
this.executor = executor;
}
public void execute(Runnable command) {
executor.execute(command);
}
}
MyExecutor executor = new MyExecutor();
executor.excute( new RunnableTask1());
executor.excute( new RunnableTask2());
//Thread pool의 크기가 2이므로 앞의 작업이 끝날 때 까지 기다림
executor.excute( new RunnableTask3());
스레드의 개수가 제한없이 무분별하게 늘어나다 보면 그걸 관리하는 cpu의 과부하가 생깁니다. Executors.newFixedThreadPool(int poolSize)는 동시에 실행되는 쓰레드의 수를 매개변수로 넘겨진 Size만큼 조정합니다. Size가 2로 선언이 된다면 3개의 작업이 들어왔을 때 마지막으로 들어온 1개의 작업은 앞의 2개 작업이 끝날 때까지 대기합니다.
하나의 스레드만 실행 가능한 Thread pool
open class MyExecutor implements Executor {
private Executor executor;
MyExecutor() {
this(Executors.newSingleThreadExecutor());
}
MyExecutor(Executor executor) {
this.executor = executor;
}
public void execute(Runnable command) {
executor.execute(command);
}
}
MyExecutor executor = new MyExecutor();
executor.excute( new RunnableTask1());
executor.excute( new RunnableTask2());
Executors.newSingleThreadExecutor()은 오직 하나의 Thread만 실행가능한 Thread pool을 생성합니다. 그래서 synchronized가 중요한 작업에서 안전하게 사용할 수 있습니다.
스레드의 개수를 제한하지 않는 경우
ExecutorService execService = Executors.newCachedThreadPool();
위 방법으로 생성된 Thread pool은 요청해오는 작업을 바로바로 실행합니다. 따라서 실행 가능한 Thread 개수의 제한이 없습니다. 하지만 제한이 없다는 것은 Thread의 관리를 개발자 관리에 맡긴다는 뜻으로 더 세심히 설계해야 합니다.
- Executor의 값 반환
Executor의 execute로 실행된 작업들은 값을 반환하지 않습니다. 만약 값의 반환이 필요하다면 submit 함수를 사용해야 합니다.
abstract Future submit(Callable<T> task)
abstract Future<T> submit(Runnable task, T result)
abstract Future<?> submit(Runnable task)
Callable<T> task = new Callable<T>(){
@Override
public T call() throws Exception{
// 작업 내용
return T;
}
}
- Google Architecture에서 Executor를 사용한 방법
import android.os.Handler
import android.os.Looper
import java.util.concurrent.Executor
import java.util.concurrent.Executors
const val THREAD_COUNT = 3
open class AppExecutor constructor(
val diskIO: Executor = DiskIOThreadExecutor(),
val networkIO: Executor = Executors.newFixedThreadPool(THREAD_COUNT),
val mainThread: Executor = MainThreadExecutor()
) {
//UI 작업을 실행하는 Thread
private class MainThreadExecutor : Executor {
private val mainThreadHandler = Handler(Looper.getMainLooper())
override fun execute(command: Runnable) {
mainThreadHandler.post(command)
}
}
}
/////////////////////////////////////////////////////////////
import java.util.concurrent.Executor
import java.util.concurrent.Executors
class DiskIOThreadExecutor : Executor {
private val diskIO = Executors.newSingleThreadExecutor()
override fun execute(command: Runnable) { diskIO.execute(command) }
}
앞서 설명한 여러 Executors 메서드를 활용하여 내부 Sqlite에서 Data를 가져오는 DiskIO Executor, NetworkIO를 수행하는 Executor, Handler를 사용해 UI 로직을 MainThread에서 수행하는 것으로 기능별로 분리했다.
Executor의 분리는 Background 작업을 수행하는 코드의 가독성을 증진시켜 주었다.
appExecutor.diskIO.execute { //DiskIO Thread
val accounts = accountDao.getAccounts()
appExecutor.mainThread.execute { //MainThread
if (accounts.isEmpty()) {
//테이블이 비어 있거나 데이터베이스가 존재하지 않을 때
callback.onDataNotAvailable()
} else {
callback.onAccountsLoaded(accounts)
}
}
}
참고
'Android > Common' 카테고리의 다른 글
[Android] Drawable color 속성을 코드로 변경하기 (0) | 2020.08.24 |
---|---|
[Android] MVP 적용해보기 - View와 Presenter (0) | 2020.08.11 |
[Android] MVC 적용하기 (0) | 2020.07.07 |
[Android] 내부 DB - Room (0) | 2020.06.03 |
[Android] Google Calendar API 사용법 (4) | 2020.05.02 |