[코틀린 동시성 프로그래밍] 2장 코루틴 인 액션 -2

Sdoubleu·2023년 1월 16일
0

코틀린 동시성

목록 보기
4/10
post-thumbnail

네트워킹 사용 권한 추가

  • 안드로이드에서는 앱이 다양한 기능에 접근하도록 하기 위해서 권한을 명시적으로 요청해야 함
    -> 사용자에게 특정 사용 권한을 거부하는 옵션을 제공하고 앱이 사용자가 예상한 것과 다른 일으 하지 못하게 하기 위함

  • 네트워크를 사용하도록 요청
    -> 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"/>

서비스 호출을 위한 코루틴 생성

  • 간단하게 시작하기 위해 자바의 DocumentBuilder를 사용해 RSS 피드를 호출
    -> 디스패처 아래에 DocumentBuilderFactory를 담을 변수 추가
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의 모든 요소들을 검사하면서 
피드에 있는 각 기사의 제목을 제외한 모든 것을 필터링한다.

UI 요소 추가

  • 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가 블로킹되면 발생하는 일

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 스레드에서 일어나야 하기 때문


UI 디스패처 사용

백그라운드에서 스레드를 실행하기 위해 CoroutineDispatcher를 사용했던 것과 같은 방식으로,
메인 스레드에서 작업을 수행토록 CoroutineDispatcher를 사용 가능

플랫폼별 UI 라이브러리

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 디스패처는 코루틴을 정식 지원하면서 안드로이드의 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) {
	//...
}
  • 호출자가 특정 CoroutineDispatcher로 코드를 실행할 수 있어서 좀 더 유연한 방식이지만, 함수에 적절한 이름이 주어졌을 때만 명시적이라는 단점

더 좋은 방식을 선택하기 위한 방법

  • 코루틴으로 감싼 동기 함수:
    가장 큰 장점은 정말로 명시적이라는 점이지만 꽤 장황하고 번거로워짐

  • 특정 디스패처를 갖는 비동기 함수:

  1. 내용이 덜 방황짐
  2. 함수를 호출하는 호출자가 어떤 디스패처를 사용해야 할지 결정할 수 없어서 유연성이 떨어짐
  • 유연한 디스패처를 갖는 비동기 함수:
  1. 함수를 호출하는 호출자가 어디서든 코루틴을 실행할 수 있음
  2. 여전히 함수에 적절한 이름을 부여하는 것은 개발자의 몫

최선의 결정은 상황에 따라 달라질 수 있으며 모든 시나리오에 딱 맞는 해결 방법은 없음


⭐정리

  • 안드로이드 앱은 네트워크 요청이 UI 스레드 상에서 수행된다면
    NetworkOnMainThreadException을 발생

  • 안드로이드 앱은 UI 스레드에서는 UI만 업데이트할 수 있으며,
    다른 스레드에서 수행하려고 하면 CalledFromWrongThreadException 발생

  • 네트워크 요청은 백그라운드 스레드에서 수행
    업데이트되는 뷰를 위한 정보는 UI 스레드로 전달

  • CoroutineDispatcher는 코루틴을 특정 스레드 또는 스레드 그룹에서 실행하도록 할 수 있음

  • 하나 이상의 코루틴을 launch or async로 스레드 실행할 수 있음

  • launch는 파이어-앤-포켓와 같은 시나리오에서 사용돼야 하는데,
    코루틴이 무언가를 반환할 것을 예상하지 않는 경우를 말함

  • 코루틴이 처리될 결과를 생성할 때 async를 사용
    결과를 처리 하지 않고 async를 사용하면 예외가 전파되지 않음

  • 코틀린은 안드로이드, Swing, JavaFX 등을 위한 특정 라이브러리를 갖고 있음
    각각은 UI 요소를 업데이트할 수 있는 적절한 코루틴 디스패처를 제공

  • 동시 코드를 작성하는 방법은 여러 가지 존재
    명확하고 안전하며 일관성 있게 코틀린의 유연성을 최대한 활용하는 방법을 이해하는 것이 중요!!

profile
개발자희망자

0개의 댓글