Kotlin_09

김재현·2023년 5월 18일
0

이 책에 대한 내용을 적었다.


1장

  • 애플리케이션에는 하나 이상의 프로세스가 있다. 각각은 적어도 하나의 스레드를 갖고 있고 코루틴은 스레드 안에서 실행된다.

  • 코루틴은 재개될 대마다 다른 스레드에서 실행될 수 있지만 특정 스레드에만 국한될 수도 있다.

  • 애플리케이션이 하나 이상의 스레드에 중첩돼 실행되는 경우는 동시적 실행이다.

  • 올바른 동시성 코드를 작성하려면 서로 다른 스레드 간의 통신과 동기화 방법을 배워야 하며, 코틀린에서는 코루틴의 통신과 동기화 방법의 학습을 의미한다.

  • 병렬 처리는 동시 처리 애플리케이션이 실행되는 동안 적어도 두 개 이상의 스레드가 같이 실행될 때 발생하다.

  • 동시 처리는 병렬 처리 없이 일어날 수 있다. 현대적 처리 장치는 스레드 간에서 교차 배치할 것이고 효과적으로 스레드를 중첩시킬 것이다.

  • 동시성 코드를 작성하는 데에는 어려움이 많다. 대부분 올바른 통신과 스레드 동기화와 관련이 있는데 레이스 컨디션, 원자성 위반, 교착 상태 및 라이브 락이 가장 일반적인 문제점이다.

  • 코틀린은 동시성에 대해 현대적이고 신선한 접근 방식을 취했다. 코틀린을 사용하면 넌 블로킹이며, 가독성 있게 활용될 뿐만 아니라 유연한 동시성 코드를 작성할 수 있다.


2장

비동기 함수는 어떻게 정의해 놓을 것인가?

  1. 코루틴으로 감싼 동기 함수 : 가장 큰 장점은 정말로 명시적이라는 점이지만 이렇게 하면 꽤 장황하고 번거로워진다.
private fun loadNews() {
	val headlines = fetchRssHeadlines()
    val newsCount = findViewById<TextView>(R.id.newsCount)
    GlobalScope.launch(Dispatchers.Main){
    	newsCount.text = "Found ${headlines.size} News"
        }
}

override fun onCreate(...){
	...
    GlobalScope.launch(dispatcher){
    	loadNews()
    }
}   
  1. 특정 디스패처를 갖는 비동기 함수: 내용이 덜 장황해지는 반면에, 함수를 호출하는 호출자가 어떤 디스패처를 사용해야 할지 경정할 수 없어서 유연성이 떨어진다. (특정 스레드에 강제하고 싶다면 유용할 수는 있다.) 비동기적인 함수라고 명시적으로 정의하는 것은 개발자에게 달려있는 부분으로, 그다지 이상적이지는 않다.
private fun asyncLoadNews() = GlobalScope.launch(dispatcher){
	val headlines = fetchRssHeadlines()
    val newsCount = findViewById<TextView>(R.id.newsCount)
    launch(Dispatchers.Main){
    	newsCount.text = "Found ${headlines.size} News"
        }
    }
    
override fun onCreate(...){
	...
    asyncLoadNews()
}
  1. 유연한 디스패처를 갖는 비동기 함수 : 함수를 호출하는 호출자가 어디서든 코루틴을 실행할 수 있지만 여전히 함수에 적절한 이름을 부여하는 것은 개발자의 몫이다.
private val defDsp = newSingleThreadContext(name = "ServiceCall")
private fun asyncLoadNews(dispatcher: CoroutineDispatcher = defDsp) = 
GlobalScope.launch(dispatcher){
	...
}

적절한 시나리오에 맞춰서 해결

  • 플랫폼 제약이 있는가? 안드로이드에서는 UI 스레드에서 네트워크 요청을 할 수 없음을 알고 있기 때문에 네트워킹을 할 때 코드가 잘못된 스레드에서 호출을 시도하지 않도록 비동기 함수를 사용하는 것이 유용하다.

  • 함수가 여러 곳에서 호출되는가? 여러 번 호출돼야 한다면 launch()나 async()블록으로 동기 함수를 감싸는 것이 좋다. 적당한 가독성과 함께 동시성을 명확히 해주지만, 같은 코드 조각을 모든 클래스에 전체적으로 적용해야 할 때는 비동기 함수에 만드는 편이 가독성을 높일 수 있다.

  • 함수 호출자가 어떤 디스패처를 사용할지 결정하기를 원하는가? 경우에 따라 호출자가 무엇을 하려고 하는지 상관없이 특정 코드가 특정 디스패치에서 실행되도록 강제하기를 원한다(예: 원자성 위반을 피하기 위해)면, 이때 특정 디스패처를 갖는 비동기 함수가 필요하다.

  • 이름이 정확하다고 보장할 수 있는가? 모든 팀이 비동기 함수임을 명확히 하기 위해 async 접두사의 사용을 강제하지 못한다면 비동기 함수의 사용을 피한다. 동시성 함수의 명칭이 명확하지 않아서 코드가 중단되는 것이 이름을 장황해지는 것보다 더 나쁘다.

  • 동기와 비동기 구현을 동일한 함수에서 모두 제공할 필요는 없으므로 어떠한 비용이 들더라도 이러한 경우는 피해야 한다. 이 방식의 나쁜 측면이 결국은 발생하리라 확신한다.

  • 같은 프로젝트에서 이러한 방법들을 과하게 혼용하지 않는다. 모든 방법은 유효하지만, 일관성을 위해서 코드 베이스에서 하나의 접근 방법을 사용하도록 최선을 다해야 한다. 표준의 부재와 일관성의 부재는 가독성에도 영향을 주고 많은 버그를 초래하기도 한다.

요약

  • 안드로이드 애플리케이션은 네트워크 요청이 UI 스레드 상에서 수행된다면 NetworkOnMainThreadException을 발생시킨다.

  • 안드로이드 애플리케이션은 UI 스레드에서는 UI만 업데이트 할 수 있으며, 다른 스레드에서 수행하려고 하면, CalledFromWrongThreadException을 발생시킨다.

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

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

  • 하나 이상의 코루틴을 launch 도는 async로 스레드에서 실행할 수 있다.

  • launch는 fire-and-forget와 같은 시나리오에서 사용돼야 하는데, 코루틴이 무언가를 반환할 것을 예상하지 않는 경우를 말한다.

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

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


3장

요약

  • 잡(Job)은 아무것도 반환하지 않는 백그라운드 작업에 사용된다.

  • 디퍼드(Deffered)는 백그라운드 작업이 수신하려는 것을 반환할 때 사용된다.

  • 잡은 다양한 상태값을 갖는다. New, Active, Cancelling, Cancelled, Completed

  • 잡의 현재 상태를 파악하기 위해 isActive, isCancelled 및 isCompleted 속성을 사용할 수 있다.

  • 디퍼드는 잡을 확장해 무언가를 반환할 가능성을 높인다.

  • 디버프가 가질 수 있는 상태는 잡의 상태와 같다.

  • 잡 상태는 앞으로만 이동할 수 있다. 이전 상태로 되돌릴 수 없다.

  • 최종 상태는 잡이 이동할 수 없는 상태 중 하나다.

  • 잡의 최종 상태는 Cancelled 및 Completed 이다.

  • join()을 사용해 디퍼드가 대기된 경우, 예외가 전파되지 않도록 값을 읽기 전에 취소됐는지 여부를 확인해야 한다.

  • 항상 잡에 예외를 기록하거나 표시하자.


4장

  • 인터페이스를 통해 일시 중단 함수를 만들어서 async나 await를 안써도 되는 방법이 있다.

  • 비동기 함수(Job 구현을 반환하는 함수)는 특정 구현을 강요하는 위험을 피하기 위해 withContext() 공개 API의 일부사 돼서는 안된다.

  • 디스패처를 시작으로 예외 처리와 취소 불가능한 고유한 컨텍스트로 옮겨가는 다양한 유형의 코루틴 컨텍스트가 있다.

  • 많은 컨텍스트를 하나로 결합하기 위해 +를 사용할 수 있다.

  • withContext()는 프로세스에 잡을 포함시키지 않고도 다른 컨텍스트로 전환할 수 있게 해주는 일시 중단 함수이다.


7장

  • 공유 상태를 가지면 동시성 코드에서 문제가 될 수 있다. 스레드의 캐시와 메모리 액세스의 원자성으로 인해 다른 스레드에서 수행한 수정 사항이 유실될 수 있다. 상태의 일관성을 해치는 원인이 된다.

  • 이러한 문제를 피하는 주요한 방법이 두 가지 있다. 하나의 스레드만 상태와 상호 작용하도록 보장해서 쓰기가 아닌 읽기 전용으로만 공유할 수 있게 하는 것과, 코드 블록을 원자적으로 만들기 위해서 잠금을 사용해 코드 블록을 실행하려는 모든 스레드의 동기화를 강제하는 것이다.

  • CoroutineContext를 하나의 스레드로 된 디스패처와 함께 사용해 코루틴의 실행을 단일 스레드에서 사용되도록 강제한다. 이를 스레드 한정이라 한다. (newSingleThreadContext)

  • 액터는 송신 채널과 코루틴의 쌍이다. 액터를 단일 스레드로 한정해 메시지를 기반으로 하는, 보다 강력한 동기화 메커니즘을 구축할 수 있다. 원하는 스레드에서 메시지를 보내 변경을 요청할 수 있지만, 변경은 특정 스레드에서 실행될 것이다.

  • 액터는 특히 코루틴의 스레드 제한과 쌍을 이뤄 이용하면 좋다. 예를 들어 액터가 스레드 풀에서 실행하도록 액터가 사용할 CoroutineContext를 지정할 수 있다.

  • 액터는 코루틴이기 때문에 여러 방식으로 시작할 수 있다. 가령 지연되도록 시작된 액터를 가질 수 있다.

  • 잠금을 사용해 코루틴을 동기화하기 위해 뮤텍스를 사용할 수 있다. 이렇게 하면 코루틴이 동기화된 작업을 수행할 수 있도록 기다리는 동안 코루틴을 일시 중단할 수 있다.

  • JVM은 스레드의 캐시에 저장되지 않는 변수인 휘발성 변수를 제공한다. 스레드 간에 공유되는 변수가 두 가지 특성이 있다면 기본 동시성 문제를 해결하는 데 도움이 될 수 있다. 즉 수정될 때 새 값은 이전 값에 의존하지 않으며 휘발성 변수의 상태는 다른 속성에 의존하지 않거나 영향을 미치지 않는 경우다.

  • 원자적 변수들이 있는데, 이 변수들은 변수의 값을 증가시키고 감소시키는 것과 같은 일반적인 작업에 원자적 구현을 제공하는 객체다.

  • 원자적 변수는 단순한 경우에 유용하지만 공유되는 상태가 하나 이상의 여러 변수인 경우 확장하기가 어려울 것이다.


8장

  • 버그 수정은 시나리오를 커버하는 테스트와 함께 수반돼야 한다. 오류가 반복적으로 발생하는 것을 통제할 수 있는 유일한 방법이다. 방금 만든 수정사항에 대한 테스트를 추가하지 않으면 결국 버그가 발생했던 코드로 다시 돌아간다.

  • 동시성 버그가 애플리케이션의 다른 부분에 어떠한 방법으로 영향을 줄 것인지 항상 생각해야 한다. 버그는 특정 기능에 대해서만 가끔 보고되지만, 유사한 방식으로 많은 기능들이 구현되기 때문에 같은 버그가 다른 곳에도 존재할 수 있다. 다른 곳에서 버그가 발생할 수 있다는 일말의 의심이 든다면 버그가 존재하는지 검증하기 위한 테스트를 추가하고, 그렇지 않다면 앞으로 발생할 일을 방지하기 위한 테스트를 추가한다.

  • 동시성 작업을 위해 모든 값을 차례로 하는 테스트를 하지 말아야 한다. 테스트의 목적은 모든 시나리오를 다루려는 것이 아니며, 문제를 야기할 수 있는 값을 추가하면서 가정에 도전하는 시나리오를 찾는 것이다.

  • 구현을 하기 전에 복원력에 대해서 이야기하고, 항상 복원력을 위한 테스트를 해야 한다.

  • 에지 케이스를 찾기 위해서 커버리지 보고서에서 분기 분석을 사용한다. 동시성 코드를 테스트할 때 항상 유용하지는 않지만 시도할 가치가 있다. 분기 분석 보고서는 테스트가 항상 같은 시나리오를 수행하는지 알려준다.

  • 단위 테스트와 기능 테스트를 작성하는 시점에 대해 알아야 한다. 기능 테스트는 종종 더 많은 노력이 필요해서 실제로 가치가 있을 때 수행해야 한다.

  • 인터페이스를 사용해 종속성을 연결한다. 기능 테스트를 위한 복잡한 시나리오를 되풀이하기 위한 모의(mock) 작업이 쉬워진다.


9장

  • 동시성 코드 테스트에 관한 원칙. 1. 가정을 버리는 것, 즉 앱의 복원력을 보장하기 위해 발생해서는 안 되는 시나리오에 대한 테스트를 작성하는 것을 의미한다. 2. 나무가 아닌 숲에 초점을 맞출 것, 동시성 테스트를 할 때 예상한 부분과 예상하지 못한 부분을 높은 수준에서 재현할 수 있는 기능 테스트를 수행하는 것을 의미한다.

  • 버그를 수정할 때 항상 테스트를 작성한다. 보고되지 않은 곳에서 재현될 수 있는 버그인지 분석하고, 이러한 시나리오를 대비하기 위한 테스트를 작성한다. 복원력 요구사항을 복원력에 대한 설계와 테스트의 일부로 고려한다. 분기 분석을 이용해 다양한 조건에 대한 충분한 테스트가 진행되는지를 확인한다. 언제 기능 테스트를 작성하도 단위 테스트를 작성하는지를 배운다. 항상 인터페이스 뒤에 의존성을 숨긴다. 인터페이스를 사용하는 잘 알려진 또 다른 이점은 의존성을 주입하고 기능의 새로운 구현을 쉽게 해주는 것이다.

  • 로그를 보다 쉽게 분석하기 위해서 코틀린이 코루틴의 ID를 Thread.currentThread().name에 추가하기 위한 -Dkotlinx.coroutines.debug 디버그 플래그를 사용한다.

  • 디버깅을 쉽게 하기 위해 항상 오래 지속되는 코루틴에는 이름을 지정ㅇ한다. 짧은 시간 그리고 오래 지속되는 코루틴을 모두 테스트할 때 조건부 브레이크 포인트를 사용한다. 그렇게 하면 관심을 두고 확인하려는 코루틴에만 분석을 집중할 수 있다.

  • 애플리케이션이 안정적인지 보장하기 위한 유일한 방법은 제대로 된 테스트를 진행하는 것이다. 애플리케이션의 품질을 기대하는 수준으로 맞추고 프로젝트에 진정한 가치를 더하기 위한 테스트를 작성하고 유지하는 것은 전적으로 개발자에 달려 있다.

profile
배운거 정리하기

0개의 댓글