안드로이드에서는 앱이 다양한 기능에 접근하도록 하기 위해서 권한을 명시적으로 요청해야 함
-> 사용자에게 특정 사용 권한을 거부하는 옵션을 제공하고 앱이 사용자가 예상한 것과 다른 일으 하지 못하게 하기 위함
네트워크를 사용하도록 요청
-> AndroidManifest.xml 파일에 수정
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET"/>
private val dispatch = newSingleThreadContext(name = "ServiceCall") private val factory = DocumentBuilderFactory.newInstance() 두 번째는 실제 호출을 수행할 함수를 만드는 단계 private fun fetchRssHeadlines(): List<String> { val builder = factory.newDocumentBuilder() val xml = builder.parse("https://www.npr.org/rss/rss.php??id=1001") return emptyList() }
아이디어는 주어진 피드의 헤드라인을 반환하도록 이 함수를 구현하는 것
해드라인 가져오기가 디스패처의 스레드에서 실행
-> 다음 단계는 실제로 응답의 본문을 읽고 헤드라인을 반환
예제에서는 XML 파싱을 할 때 라이브러리를 사용하는 대신 직접 파싱하도록 함 private fun fetchRssHeadlines(): List<String> { val builder = factory.newDocumentBuilder() val xml = builder.parse("https://www.npr.org/rss/rss.php?id=1001") val news = xml.getElementsByTagName("channel").item(0) return (0 until news.childNodes.length) .map { news.childNodes.item(it) } .filter { Node.ELEMENT_NODE == it.nodeType } .map { it as Element } .filter { "item" == it.tagName } .map { it.getElementsByTagName("title").item(0).textContent } } 코드는 단순히 XML의 모든 요소들을 검사하면서 피드에 있는 각 기사의 제목을 제외한 모든 것을 필터링한다.
MainAcitivy의 레이아웃을 수정
화면 정중앙에 ProgressBar만 추가 -> ConstraintLayout 사용
<ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
UI 스레드가 블로킹되면 안 되는 이유를 더 자세히 이해하기 위해
MainActivity에 코드 추가
override fun onResume() { super.onResume() Thread.sleep(5000) } 이렇게 하면 5초간 UI 스레드가 블로킹
⚡
UI 스레드는 블로킹되지 않아야 할 뿐 아니라 CPU 사용량이 많은 작업도 수행해서는 안되는데, 이는 사용자에게 유사한 경험을 줄 수 있기 때문이다.
뷰를 만들고 업데이트를하려면 UI 스레드를 사용해야 하며 그 사이의 모든 것은 백그라운드 스레드에서 수행해야 한다.
레이아웃에 TextView를 배치
<TextView android:id="@+id/newsCount" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" app:layout_constraintTop_toBottomOf="@+id/progressBar" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent"/>
뉴스의 수량을 표시하기 위해서 텍스트 설정
GlobalScope.launch(dispatch) { val headlines = fetchRssHeadlines() val newCount = findViewById<TextView>(R.id.newsCount) newCount.text = "Found ${headlines.size} News" }
↪ 이 장의 앞 부분에서 설명한 것처럼 코드가 실행되면
-> CalledFromWrongThreadException과 함께 앱이 중단
-> 코루틴의 모든 내용이 백그라운드 스레드에서 실행중이며 UI 업데이트는 UI 스레드에서 일어나야 하기 때문
백그라운드에서 스레드를 실행하기 위해 CoroutineDispatcher를 사용했던 것과 같은 방식으로,
메인 스레드에서 작업을 수행토록 CoroutineDispatcher를 사용 가능
JVM을 위한 GUI 앱이 많다는 점을 감안해서 코틀린은 플랫폼별 코루틴 기능을 라이브러리로 분리
kotlinx-coroutines-android
kotlinx-coroutines-javafx
kotlinx-coroutines-swing
이러한 플랫폼은 동일한 UI 모델을 갖고 있음
-> UI 스레드에서만 뷰를 생성하고 업데이트할 수 있음
-> 작은 라이브러리들은 코루틴을 UI 스레드로 제한하기 위해 구현된
-> CoroutineDispatcher
build.gradle(Module: App)에 코루틴 의존성을 추가
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
이렇게 하면 다른 것을 사용하던 방식과 똑같이 디스패처를 사용할 수 있음
안드로이드 UI 디스패처는 코루틴을 정식 지원하면서 안드로이드의 UI ㅣ디스패처는 Dispatchers.Main을 통해서 사용하도록 변경됨
GlobalScope.launch(dispatch) { val headlines = fetchRssHeadlines() val newCount = findViewById<TextView>(R.id.newsCount) GlobalScope.launch(Dispatchers.Main) { newCount.text = "Found ${headlines.size} News" } }
뉴스의 수량을 요청하고 표시하는 코드의 상당 부분은 onCreate() 함수 안에 존재
activity 생성 부분과 혼재돼 있을 뿐 아니라, 코드를 재사용하기 어려움
코루틴을 별도 함수로 분리하는 것을 고려한다면 접근 방법
fetchRssHeadlines() 함수를 직접 호출해서 그 결과를 이전과 같은 방식으로 표시하는 loadNews() 함수를 만들 수 있음
private fun loadNews(){ val headlines = fetchRssHeadlines() val newCount = findViewById<TextView>(R.id.newsCount) GlobalScope.launch(Dispatchers.Main) { newCount.text = "Found ${headlines.size} News" } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) loadNews() }
↪ loadNews()는 호출된 스레드와 같은 스레드를 사용
-> 피드를 가져오는 요청이 UI 스레드에서 일어나기 때문에
-> NetworkOnMainThreadException이 발생
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) GlobalScope.launch(dispatch) { loadNews() } }
비동기로 실행되는 코드라는 것을 명시적으로 나타내는 좋은 사례
loadNews()를 호출하는 호출자가 이미 백그라운드 스레드에 있다면
launch()나 async()빌더를 사용할 필요 없이, 같은 백그라운드 스레드에서 뉴스를 가져올 수 있기 때문에 유연
💣 그러나 UI 스레드에서 loadNews()를 호출하는 부분이 많으면
-> 가시성이 떨어짐
launch()를 포함하고 결과인 Job을 반환하는 함수인 asyncLoadNews() 함수를 작성
함수는 스레드와 상관없이 launch() 블록이 없는 상태로 호출될 수 있고
Job을 반환해서 호출자가 취소할 수 있음
private fun asyncLoadNews() = GlobalScope.launch(dispatch) { val headlines = fetchRssHeadlines() val newCount = findViewById<TextView>(R.id.newsCount) launch(Dispatchers.Main) { newCount.text = "Found ${headlines.size} News" } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) asyncLoadNews() }
함수가 여러 곳에서 호출될 경우 코드를 단순화하지만 백그라운드 스레드에서 강제로 실행되기 때문에 함수의 유연성이 줄어듦
코드 가독성에 있어서 함수 이름을 지정해야 한다는 단점
함수를 호출하는 호출자는 이 기능이 비동기로 실행될지 모를 수 있고 함수가 완료되는 것을 기다리지 않을 수 있음
-> 이는 레이스 컨디션이나 기타 동시성 문제로 이어짐
디스패처를 함수의 선택적 파라미터로 설정해서 함수에 어느 정도 유연성을 줄 수 있음
private val defDsp = newSingleThreadContext(name = "ServiceCall")
private fun asyncLoadNews(dispatcher: CoroutinDispatcher = defDsp) =
GlobalScope.launch(dispatch) {
//...
}
코루틴으로 감싼 동기 함수:
가장 큰 장점은 정말로 명시적이라는 점이지만 꽤 장황하고 번거로워짐
특정 디스패처를 갖는 비동기 함수:
최선의 결정은 상황에 따라 달라질 수 있으며 모든 시나리오에 딱 맞는 해결 방법은 없음
안드로이드 앱은 네트워크 요청이 UI 스레드 상에서 수행된다면
NetworkOnMainThreadException을 발생
안드로이드 앱은 UI 스레드에서는 UI만 업데이트할 수 있으며,
다른 스레드에서 수행하려고 하면 CalledFromWrongThreadException 발생
네트워크 요청은 백그라운드 스레드에서 수행
업데이트되는 뷰를 위한 정보는 UI 스레드로 전달
CoroutineDispatcher는 코루틴을 특정 스레드 또는 스레드 그룹에서 실행하도록 할 수 있음
하나 이상의 코루틴을 launch or async로 스레드 실행할 수 있음
launch는 파이어-앤-포켓와 같은 시나리오에서 사용돼야 하는데,
코루틴이 무언가를 반환할 것을 예상하지 않는 경우를 말함
코루틴이 처리될 결과를 생성할 때 async를 사용
결과를 처리 하지 않고 async를 사용하면 예외가 전파되지 않음
코틀린은 안드로이드, Swing, JavaFX 등을 위한 특정 라이브러리를 갖고 있음
각각은 UI 요소를 업데이트할 수 있는 적절한 코루틴 디스패처를 제공
동시 코드를 작성하는 방법은 여러 가지 존재
명확하고 안전하며 일관성 있게 코틀린의 유연성을 최대한 활용하는 방법을 이해하는 것이 중요!!