[코틀린 동시성 프로그래밍] 3장 라이프 사이클과 에러 핸들링 - 예시

Sdoubleu·2023년 2월 13일
0

코틀린 동시성

목록 보기
10/10
post-thumbnail

RSS - 여러 피드에서 동시에 읽기

피드 목록 지원

  • 가져올 피드를 담을 변경 불가능한 목록을 만든다.
    3개를 하려 했으나 사이트 오류로 인해 2개로 변경
class MainActivity : AppCompatActivity() {
	val feeds = listOf(
			"https://www.npr.org/rss/rss.php?id=1001",
            "https://feeds.foxnews.com/foxnews/politics?format=xml",
            //"https://rss.cnn.com/rss/cnn_topstories.rss"  <- 사이트 변경으로 인하여 오류 발생때문에 주석처리 
        )
  • fechRssheadlines() 함수를 수정해서 목적에 맞게 수정
private fun fetchRssHeadlines(feed: String, dispatcher: CoroutineDispatcher)
    = GlobalScope.async(dispatcher) {
        val builder = factory.newDocumentBuilder()
        val xml = builder.parse(feed)
        val news = xml.getElementsByTagName("channel").item(0)

        (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
			}
}
  • 함수는 단일 피드 대신 뉴스를 가져오는 데 사용할 URL로 feed 인수를 사용한다.
    -> 이를 통해 둘 이상의 피드에서 헤드라인을 가져올 수 있다.
    -> 디스패처를 인수로 사용하는 비동기 함수가 되도록 함수의 시그니처가 변경됐다.

스레드 풀 만들기

  • 디스패처를 업데이트하는 단계다. 크기가 2인 스레드 풀을 만들고 IO로 이름을 바꾼다.
val dispatcher = newFixedThreadPoolContext(2,"IO")

asyncFetchHeadlines() 는 서버에서 정보를 가져올 뿐 아니라 파싱도 하기 때문에 풀의 크기를 늘린다.
XML을 파싱하는 오버 헤드는 단일 스레드를 사용하는 경우
-> 성능에 영향을 준다.

때로는 다른 스레드의 파싱이 완료될 때까지 한 피드로부터 정보를 가져오는 것이 지연될 수 있다.

데이터를 동시에 가져오기

동시에 여러 피드에 요청을 보내기 위해 필요한 모든 것을 갖췄다.
목록에서 각 피드당 하나의 디퍼드를 생성한다.
먼저 asyncLoadNews() 함수를 수정해 대기하는 모든 디퍼드를 추적할 수 있는 목록을 만들어보자.

private fun asyncLoadNews() = GlobalScope.launch(dispatch) {
        val requests = mutableListOf<Deferred<List<String>>>()

        // 피드별로 가져온 요소를  피드 목록에 추가한다.
        feeds.mapTo(requests) {
            asyncFetchLeadlines(it, dispatch)
        }
       
        // 각 코드가 완료될 때까지 대기하는 코드를 추가한다.
        requests.forEach{
            it.await()
		}
	}

응답 병합

  • 헤드라인의 목록을 반환하기 때문에 이들을 하나의 리스트에 담고 싶을 것이다.
    -> 이를 위해 각 디퍼드의 내용을 플랫 맵(falt map)을 이용해 담을 수 있다.
val headlines = requests.flatMap { 
            it.getCompleted()
        }

2 개의 피드에서 동시에 가져온 모든 헤드라인을 포함하는 headlines 변수가 생겼다.
현재 asyncLoadNews()는 두 부분으로 구성돼 있다.
첫 번째는 데이터를 가져오고 구성하는 것이다.

두 번째는 첫 번째 바로 아래 부분으로 헤드 라인의 개수를 UI상에 표시한다.

val newCount = findViewById<TextView>(R.id.newsCount)

GlobalScope.launch(Dispatchers.Main) {
	newCount.text = "Found ${headlines.size} News"
}
  • 두 번째 부분을 수정해서 가져온 피드 개수가 출력한 메시지에 표시되도록 한다.
val newCount = findViewById<TextView>(R.id.newsCount)
       
launch(Dispatchers.Main) {
	newCount.text = "Found ${headlines.size} News" 
            + "in ${requests.size} feeds"
	}

동시 요청 테스트

  • 새로 구현한 소스를 실행하면 훨씬 더 많은 뉴스를 얻을 수 있다.

넌 해피 패스 - 예기치 않은 중단

  • 각 디퍼드가 완료될 때까지 대기하기 위한 방법을 살펴보자.
private fun asyncLoadNews() = GlobalScope.launch(dispatch) {
	val requests = mutableListOf<Deferred<List<String>>>()

	feeds.mapTo(requests) {
		asyncFetchLeadlines(it, dispatch)
	}

	requests.forEach{
		it.await()
	}
    ...
}

↪ 코루틴이 완료될 때까지 await() 를 사용해 대기하므로, 코루틴 내부에서 발생하는 예외는 현재 스레드로 전파된다. 다음과 같이 애플리케이션이 쉽게 중단될 수 있는 두 개의 시나리오가 존재한다는 것을 의미한다.

  • 인터넷이 연결되지 않은 경우
  • 하나 이상의 피드 URL이 유요하지 않거나 잘못된 경우
  • 그중 하나를 시험해보자. 다음과 같이 피드 목록에 잘못된 URL을 추가한다.
 val feeds = listOf(
        "https://www.npr.org/rss/rss.php?id=1001",
        "https://feeds.foxnews.com/foxnews/politics?format=xml",
        "htt://myNewsFeed"

↪ 이 피드를 가져오자마자 애플리케이션을 중단한다.

디퍼드가 예외를 갖도록 하기

  • 예외를 처리하기 쉬운 방법은 await() 대신 join() 을 사용해 디퍼드를 기다리는 것이다.
    이렇게 하면 대기할 때 예외가 전파되지 않는다.
    수정된 코드는 다음과 같다.
private fun asyncLoadNews() = GlobalScope.launch {
	val requests = mutableListOf<Deferred<List<String>>>()

	feeds.mapTo(requests) {
		asyncFetchLeadlines(it, dispatch)
	}

	requests.forEach{
		it.join()
	}
    ...
}
  • 애플리케이션을 실행해보면 여전히 중단되는 모습을 확인할 수 있따.
    디퍼드를 기다릴 때 예외를 전파하지 않더라도 요청을 읽을 때 예외를 전파해서 이런 일이 발생한다.
    디퍼드에서 getCompleted()를 호출하면 디퍼드는 예외로 인해 취소됐기 때문에 예외가 발생한다.

  • 디퍼드가 실패하지 않았을 때만 getCompleted()가 호출되도록 코드를 바꿔야 한다.

val headlines = requests
	.filter { !it.isCancelled }
    .flatMap { it.getCompleted() }

예외를 무시하지 말 것!

  • 예외를 무시하는 것은 나쁜 관행이며 고쳐야 한다.
    앱에 라벨을 추가해서 가져오지 못한 피드의 수를 표시한다.
    먼저 액티비티 XML로 이동해 현재 라벵에 이어 두 번째 라벨을 추가한다.
<TextView
	android:id="@+id/warnings"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"
	android:layout_marginTop="20dp"
	app:layout_constraintTop_toBottomOf="@+id/newsCount"
	app:layout_constraintLeft_toLeftOf="parent"
	app:layout_constraintRight_toRightOf="parent"/>
val headlines = requests
	.filter { !it.isCancelled }
	.flatMap { it.getCompleted() }

val failed = requests
	.filter { it.isCancelled }
	.size

 val newCount = findViewById<TextView>(R.id.newsCount)
 val warnings = findViewById<TextView>(R.id.warnings)
 val obtained = requests.size - failed

 GlobalScope.launch(Dispatchers.Main) {
	newCount.text = "Found ${headlines.size} News in ${requests.size} feeds"
          
  	if (failed > 0) {
		warnings.text = "Failed to fetch $failed feeds"  
		}
}

profile
개발자희망자

0개의 댓글