모든 Android 앱은 기본 스레드를 사용하여 UI 작업을 처리합니다. 이 기본 스레드에서 장기 실행 작업을 호출하면 작업이 정지되고 응답하지 않을 수 있습니다. 예를 들어 앱이 기본 스레드에서 네트워크를 요청하면 네트워크 응답을 받을 때까지 앱의 UI가 정지됩니다. 기본 스레드에서 UI 업데이트를 계속 처리하는 동안 추가 백그라운드 스레드를 만들어 장기 실행 작업을 처리할 수 있습니다.
이 가이드에서는 Kotlin 및 자바 프로그래밍 언어 개발자가 스레드 풀을 사용하여 Android 앱에서 여러 스레드를 설정하고 사용하는 방법, 스레드에서 실행할 코드를 정의하는 방법 및 이러한 스레드 중 하나와 기본 스레드 간에 통신하는 방법을 보여줍니다.
예시 개요
이 주제의 예에서는 앱 아키텍처 가이드에 따라 네트워크를 요청하고 결과를 기본 스레드로 반환합니다. 그러면 앱이 그 결과를 화면에 표시할 수 있습니다.
구체적으로 ViewModel
은 기본 스레드의 저장소 레이어를 호출하여 네트워크 요청을 트리거합니다. 저장소 레이어는 네트워크 요청 실행을 기본 스레드 외부로 이동하고 콜백을 사용하여 결과를 기본 스레드에 다시 게시하는 역할을 합니다.
네트워크 요청 실행을 기본 스레드 외부로 이동하려면 앱에 다른 스레드를 만들어야 합니다.
다중 스레드 만들기
스레드 풀은 큐에서 작업을 동시에 실행하는 관리형 스레드 컬렉션입니다. 새 작업은 기존 스레드가 유휴 상태가 되면 이 스레드에서 실행됩니다. 작업을 스레드 풀로 보내려면 ExecutorService
인터페이스를 사용합니다. ExecutorService
는 Android 애플리케이션 구성요소인 서비스와 아무 관련이 없습니다.
스레드를 만드는 것은 비용이 많이 들기 때문에 앱이 초기화될 때 스레드 풀을 한 번만 만들어야 합니다. Application
클래스 또는 종속 항목 삽입 컨테이너에 ExecutorService
의 인스턴스를 저장해야 합니다.
다음 예에서는 백그라운드 작업을 실행하는 데 사용할 수 있는 4개의 스레드로 구성된 스레드 풀을 만듭니다.
Kotlin
class MyApplication : Application() { val executorService: ExecutorService = Executors.newFixedThreadPool(4) }
자바
public class MyApplication extends Application { ExecutorService executorService = Executors.newFixedThreadPool(4); }
예상되는 워크로드에 따라 스레드 풀을 구성할 수 있는 다른 방법이 있습니다. 자세한 내용은 스레드 풀 구성을 참조하세요.
백그라운드 스레드에서 실행
기본 스레드에서 네트워크를 요청하면 스레드가 응답을 받을 때까지 대기하거나 차단됩니다. 스레드가 차단되었기 때문에 OS는 onDraw()
를 호출할 수 없고 앱이 정지되며 이에 따라 애플리케이션 응답 없음(ANR) 대화상자가 표시될 수 있습니다. 대신 백그라운드 스레드에서 이 작업을 실행해 보겠습니다.
먼저 Repository
클래스를 살펴보고 네트워크 요청 방식을 알아보겠습니다.
Kotlin
sealed class Result<out R> { data class Success<out T>(val data: T) : Result<T>() data class Error(val exception: Exception) : Result<Nothing>() } class LoginRepository(private val responseParser: LoginResponseParser) { private const val loginUrl = "https://example.com/login" // Function that makes the network request, blocking the current thread fun makeLoginRequest( jsonBody: String ): Result<LoginResponse> { val url = URL(loginUrl) (url.openConnection() as? HttpURLConnection)?.run { requestMethod = "POST" setRequestProperty("Content-Type", "application/json; charset=utf-8") setRequestProperty("Accept", "application/json") doOutput = true outputStream.write(jsonBody.toByteArray()) return Result.Success(responseParser.parse(inputStream)) } return Result.Error(Exception("Cannot open HttpURLConnection")) } }
자바
// Result.java public abstract class Result<T> { private Result() {} public static final class Success<T> extends Result<T> { public T data; public Success(T data) { this.data = data; } } public static final class Error<T> extends Result<T> { public Exception exception; public Error(Exception exception) { this.exception = exception; } } } // LoginRepository.java public class LoginRepository { private final String loginUrl = "https://example.com/login"; private final LoginResponseParser responseParser; public LoginRepository(LoginResponseParser responseParser) { this.responseParser = responseParser; } public Result<LoginResponse> makeLoginRequest(String jsonBody) { try { URL url = new URL(loginUrl); HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection(); httpConnection.setRequestMethod("POST"); httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8"); httpConnection.setRequestProperty("Accept", "application/json"); httpConnection.setDoOutput(true); httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8")); LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream()); return new Result.Success<LoginResponse>(loginResponse); } catch (Exception e) { return new Result.Error<LoginResponse>(e); } } }
makeLoginRequest()
는 동기식이며 호출 스레드를 차단합니다. 네트워크 요청의 응답을 모델링하기 위해 자체 Result
클래스를 사용합니다.
ViewModel
은 예를 들어 사용자가 버튼을 탭할 때 네트워크 요청을 트리거합니다.
Kotlin
class LoginViewModel( private val loginRepository: LoginRepository ) { fun makeLoginRequest(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) } }
자바
public class LoginViewModel { private final LoginRepository loginRepository; public LoginViewModel(LoginRepository loginRepository) { this.loginRepository = loginRepository; } public void makeLoginRequest(String username, String token) { String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }"; loginRepository.makeLoginRequest(jsonBody); } }
위의 코드에서 LoginViewModel
은 네트워크를 요청할 때 기본 스레드를 차단합니다. 인스턴스화한 스레드 풀을 사용하여 실행을 백그라운드 스레드로 이동할 수 있습니다. 먼저 종속 항목 삽입 원칙에 따라 LoginRepository
는 코드를 실행하지만 스레드를 관리하지 않기 때문에 ExecutorService
와는 대조적으로 Executor
의 인스턴스를 사용합니다.
Kotlin
class LoginRepository( private val responseParser: LoginResponseParser private val executor: Executor ) { ... }
자바
public class LoginRepository { ... private final Executor executor; public LoginRepository(LoginResponseParser responseParser, Executor executor) { this.responseParser = responseParser; this.executor = executor; } ... }
Executor의 execute()
메서드는 Runnable
을 사용합니다.
Runnable
은 호출 시 스레드에서 실행되는 run()
메서드가 있는 단일 추상 메서드(SAM) 인터페이스입니다.
실행을 백그라운드 스레드로 이동하고 현재 응답을 무시하는 makeLoginRequest()
라는 또 다른 함수를 만들어 보겠습니다.
Kotlin
class LoginRepository( private val responseParser: LoginResponseParser private val executor: Executor ) { fun makeLoginRequest(jsonBody: String) { executor.execute { val ignoredResponse = makeSynchronousLoginRequest(url, jsonBody) } } private fun makeSynchronousLoginRequest( jsonBody: String ): Result<LoginResponse> { ... // HttpURLConnection logic } }
자바
public class LoginRepository { ... public void makeLoginRequest(final String jsonBody) { executor.execute(new Runnable() { @Override public void run() { Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody); } }); } public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) { ... // HttpURLConnection logic } ... }
execute()
메서드 내에서, 백그라운드 스레드에서 실행하려는 코드 블록으로 새 Runnable
을 만듭니다(이 경우 동기 네트워크 요청 메서드). 내부적으로 ExecutorService
는 Runnable
을 관리하며 사용 가능한 스레드에서 이 인터페이스를 실행합니다.
고려사항
앱의 모든 스레드는 기본 스레드를 포함하여 다른 스레드와 동시에 실행될 수 있으므로 코드가 스레드로부터 안전한지 확인해야 합니다. 이 예에서는 스레드 간에 공유되는 변수에 쓰는 것을 피하고 대신 변경할 수 없는 데이터를 전달합니다. 이는 각 스레드가 자체 데이터 인스턴스와 함께 작동하며 동기화의 복잡성을 방지하기 때문에 좋은 방법입니다.
스레드 간에 상태를 공유해야 한다면 잠금과 같은 동기화 메커니즘을 사용하여 스레드로부터의 액세스를 관리하는 데 주의해야 합니다. 이 내용은 이 가이드에서 다루지 않습니다. 일반적으로 가능한 한 스레드 간에 변경 가능한 상태를 공유하지 않아야 합니다.
기본 스레드와 통신
이전 단계에서는 네트워크 요청 응답을 무시했습니다. 화면에 결과를 표시하려면 LoginViewModel
에서 결과를 알아야 합니다. 콜백을 사용하면 됩니다.
makeLoginRequest()
함수는 값을 비동기적으로 반환할 수 있도록 콜백을 매개변수로 사용해야 합니다. 결과가 포함된 콜백은 네트워크 요청이 완료되거나 실패가 발생할 때마다 호출됩니다.
Kotlin에서는 고차 함수를 사용할 수 있습니다. 그러나 자바에서는 동일한 기능을 사용하기 위해 새 콜백 인터페이스를 만들어야 합니다.
Kotlin
class LoginRepository( private val responseParser: LoginResponseParser private val executor: Executor ) { fun makeLoginRequest( jsonBody: String, callback: (Result<LoginResponse>) -> Unit ) { executor.execute { try { val response = makeSynchronousLoginRequest(jsonBody) callback(response) } catch (e: Exception) { val errorResult = Result.Error(e) callback(errorResult) } } } ... }
자바
interface RepositoryCallback<T> { void onComplete(Result<T> result); } public class LoginRepository { ... public void makeLoginRequest( final String jsonBody, final RepositoryCallback<LoginResponse> callback ) { executor.execute(new Runnable() { @Override public void run() { try { Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody); callback.onComplete(result); } catch (Exception e) { Result<LoginResponse> errorResult = new Result.Error<>(e); callback.onComplete(errorResult); } } }); } ... }
ViewModel
은 이제 콜백을 구현해야 합니다. 그리고 다음과 같이 결과에 따라 서로 다른 로직을 실행할 수 있습니다.
Kotlin
class LoginViewModel( private val loginRepository: LoginRepository ) { fun makeLoginRequest(username: String, token: String) { val jsonBody = "{ username: \"$username\", token: \"$token\"}" loginRepository.makeLoginRequest(jsonBody) { result -> when(result) { is Result.Success<LoginResponse> -> // Happy path else -> // Show error in UI } } } }
자바
public class LoginViewModel { ... public void makeLoginRequest(String username, String token) { String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }"; loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() { @Override public void onComplete(Result<LoginResponse> result) { if (result instanceof Result.Success) { // Happy path } else { // Show error in UI } } }); } }
이 예에서는 콜백이 백그라운드 스레드인 호출 스레드에서 실행됩니다. 즉, 기본 스레드로 다시 전환할 때까지 직접 UI 레이어를 수정하거나 UI 레이어와 통신할 수 없습니다.
핸들러 사용
Handler
를 사용하여 다른 스레드에서 실행될 작업을 큐에 추가할 수 있습니다. 작업을 실행할 스레드를 지정하려면 스레드에 Looper
를 사용하여 Handler
를 구성합니다. Looper
는 연결된 스레드의 메시지 루프를 실행하는 객체입니다. Handler
를 만든 후에는 post(Runnable)
메서드를 사용하여 해당 스레드에서 코드 블록을 실행할 수 있습니다.
Looper
에는 기본 스레드의 Looper
를 검색하는 도우미 함수인 getMainLooper()
가 포함되어 있습니다. 이 Looper
를 사용하여 Handler
를 만들어서 기본 스레드에서 코드를 실행할 수 있습니다. 이는 매우 자주 실행할 수 있는 작업이므로 다음과 같이 ExecutorService
를 저장한 곳과 동일한 위치에 Handler
의 인스턴스를 저장할 수도 있습니다.
Kotlin
class MyApplication : Application() { val executorService: ExecutorService = Executors.newFixedThreadPool(4) val mainThreadHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper()) }
자바
public class MyApplication extends Application { ExecutorService executorService = Executors.newFixedThreadPool(4); Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper()); }
핸들러를 Repository
에 삽입하는 것이 좋습니다. 이 방법이 더 많은 유연성을 제공하기 때문입니다. 예를 들어 나중에 별도 스레드에서의 작업을 예약하기 위해 다른 Handler
를 전달하려고 할 수 있습니다. 항상 동일한 스레드와 다시 통신하는 경우 다음 예와 같이 Handler
를 Repository
생성자에 전달할 수 있습니다.
Kotlin
class LoginRepository( ... private val resultHandler: Handler ) { fun makeLoginRequest( jsonBody: String, callback: (Result<LoginResponse>) -> Unit ) { executor.execute { try { val response = makeSynchronousLoginRequest(jsonBody) resultHandler.post { callback(response) } } catch (e: Exception) { val errorResult = Result.Error(e) resultHandler.post { callback(errorResult) } } } } ... }
자바
public class LoginRepository { ... private final Handler resultHandler; public LoginRepository(LoginResponseParser responseParser, Executor executor, Handler resultHandler) { this.responseParser = responseParser; this.executor = executor; this.resultHandler = resultHandler; } public void makeLoginRequest( final String jsonBody, final RepositoryCallback<LoginResponse> callback ) { executor.execute(new Runnable() { @Override public void run() { try { Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody); notifyResult(result, callback); } catch (Exception e) { Result<LoginResponse> errorResult = new Result.Error<>(e); notifyResult(errorResult, callback); } } }); } private void notifyResult( final Result<LoginResponse> result, final RepositoryCallback<LoginResponse> callback, ) { resultHandler.post(new Runnable() { @Override public void run() { callback.onComplete(result); } }); } ... }
또는 더 많은 유연성을 원한다면 다음과 같이 핸들러를 각 함수에 전달할 수 있습니다.
Kotlin
class LoginRepository(...) { ... fun makeLoginRequest( jsonBody: String, resultHandler: Handler, callback: (Result<LoginResponse>) -> Unit ) { executor.execute { try { val response = makeSynchronousLoginRequest(jsonBody) resultHandler.post { callback(response) } } catch (e: Exception) { val errorResult = Result.Error(e) resultHandler.post { callback(errorResult) } } } } }
자바
public class LoginRepository { ... public void makeLoginRequest( final String jsonBody, final RepositoryCallback<LoginResponse> callback, final Handler resultHandler, ) { executor.execute(new Runnable() { @Override public void run() { try { Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody); notifyResult(result, callback, resultHandler); } catch (Exception e) { Result<LoginResponse> errorResult = new Result.Error<>(e); notifyResult(errorResult, callback, resultHandler); } } }); } private void notifyResult( final Result<LoginResponse> result, final RepositoryCallback<LoginResponse> callback, final Handler resultHandler ) { resultHandler.post(new Runnable() { @Override public void run() { callback.onComplete(result); } }); } }
이 예에서는 저장소의 makeLoginRequest
호출에 전달된 콜백이 기본 스레드에서 실행됩니다. 즉, 콜백에서 UI를 직접 수정하거나 LiveData.setValue()
를 사용하여 UI와 통신할 수 있습니다.
스레드 풀 구성
이전 코드 예와 같이 사전 정의된 설정으로 Executor
도우미 함수 중 하나를 사용하여 스레드 풀을 만들 수 있습니다.
또는 스레드 풀의 세부정보를 맞춤설정하려면 ThreadPoolExecutor
를 직접 사용하여 인스턴스를 만들면 됩니다. 다음 세부정보를 구성할 수 있습니다.
- 초기 및 최대 풀 크기
- 연결 유지 시간 및 시간 단위. 연결 유지 시간은 스레드가 종료되기 전에 유휴 상태로 유지될 수 있는 최대 기간입니다.
Runnable
작업을 보유하는 입력 큐. 이 큐는BlockingQueue
인터페이스를 구현해야 합니다. 앱의 요구사항에 맞도록 사용 가능한 큐 구현 중에서 선택할 수 있습니다. 자세한 내용은ThreadPoolExecutor
의 클래스 개요를 참조하세요.
다음은 총 프로세서 코어 수, 1초의 연결 유지 시간 및 입력 큐를 기반으로 스레드 풀 크기를 지정하는 예입니다.
Kotlin
class MyApplication : Application() { /* * Gets the number of available cores * (not always the same as the maximum number of cores) */ private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors() // Instantiates the queue of Runnables as a LinkedBlockingQueue private val workQueue: BlockingQueue<Runnable> = LinkedBlockingQueue<Runnable>() // Sets the amount of time an idle thread waits before terminating private const val KEEP_ALIVE_TIME = 1L // Sets the Time Unit to seconds private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS // Creates a thread pool manager private val threadPoolExecutor: ThreadPoolExecutor = ThreadPoolExecutor( NUMBER_OF_CORES, // Initial pool size NUMBER_OF_CORES, // Max pool size KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, workQueue ) }
자바
public class MyApplication extends Application { /* * Gets the number of available cores * (not always the same as the maximum number of cores) */ private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors(); // Instantiates the queue of Runnables as a LinkedBlockingQueue private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(); // Sets the amount of time an idle thread waits before terminating private static final int KEEP_ALIVE_TIME = 1; // Sets the Time Unit to seconds private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS; // Creates a thread pool manager ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor( NUMBER_OF_CORES, // Initial pool size NUMBER_OF_CORES, // Max pool size KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT, workQueue ); ... }
동시 실행 라이브러리
스레딩의 기본사항 및 기본 메커니즘을 이해하는 것이 중요합니다. 그러나 이러한 개념에 대한 높은 수준의 추상화를 제공하는 인기 있는 라이브러리는 물론, 스레드 간에 데이터를 전달하는 데 즉시 사용할 수 있는 유틸리티가 많이 있습니다. 이러한 라이브러리에는 자바 프로그래밍 언어 사용자를 위한 Guava 및 RxJava와 Kotlin 사용자에게 권장되는 코루틴이 포함되어 있습니다.
실제로 앱 및 개발팀에 가장 적합한 것을 선택해야 합니다. 하지만 스레딩 규칙은 동일하게 유지됩니다.
참고 항목
Android의 프로세스 및 스레드에 관한 자세한 내용은 프로세스 및 스레드 개요를 참조하세요.