소개

8장_동시성 코드 테스트와 디버깅

개요

동시성의 가장 어려운 부분 중 하나는 개발과정의 후반부에 버그들이 발생하는 것
Why? → Happy case 가 아니라 Edge case 가 정말 많다
이번 챕터에서는 테스트 시나리오를 작성하는데 도움이 될만한 몇가지 방법/조언과 코루틴 로깅/디버깅과 관련된 내용을 포함

동시성 코드 테스트

테스트는 단순히 하는 것이 아니라 정확하게 해야 한다는 점이 정말 중요하다.

동시성 테스트의 원칙#1 - 가정을 버려라

suspend fun fetchData() { val dataFromDb = fetchFromDatabase() val dataFromCache = fetchFromCache() val dataFromApi = fetchFromApi() val result = Result(dataFromDb, dataFromCache, dataFromApi) }
Kotlin
복사
이 코드는 Db, Cache, Api 에서 데이터를 가져온다.
이것 자체가 가정이다. 가져오지 못할 수도 있다.
이 코드는 Db, Cache, Api 순서로 데이터를 가져온다.
각 fetch가 비동기로 동작한다면 가정이 될 수 있다.

동시성 테스트의 원칙#2 - 나무가 아닌 숲에 집중하라

기본적이지만 쉽게 간과될 수 있는 부분
Test Case - 애플리케이션의 기본 단위 검증
1.
DB로부터 성공적인 정보 조회
2.
캐시로부터 성공적인 정보 조회
3.
API로부터 성공적인 정보 조회
4.
DB로부터 불완전하거나 누락된 정보 조회
5.
캐시로부터 불완전하거나 누락된 정보 조회
6.
API로부터 불완전하거나 누락된 정보 조회
훌륭한 테스트 케이스이지만, 동시성 테스트에서는 숲이 아닌 나무를 보는 것밖에 되지 않는다.
DB가 API보다 더 오래 걸리고 불완전하며 누락된 정보가 있을 때 무슨 일이 발생하는지?
DB, 캐시 API로부터 딱 하나의 데이터만 가져오는데 성공한다면 무슨 일이 발생하는지?
위의 경우가 발생했을 때 얼마나 높은 회복력을 지니는지?
….
결국 기능 테스트를 작성해야만 한다.
기능 테스트? → 함수와 같이 작은 단위가 아니라 기능의 전체 동작을 테스트
기능 테스트는 기능을 전체적으로 실행해 애플리케이션이 비동기적으로 작업하는데 따르는 복잡성을 지닌 테스트를 잘 표현할 수 있다.
(동시성 코드라면) 기능성 테스트는 필수다.
기능 테스트를 하지 않은 동시성 애플리케이션은 취약하며, 코드 변경에 따라 문제가 발생한다.
문제#1 - 레이스 컨디션 발생 가능
문제#2 - 원자성 위반
문제#3 - 데드락

테스트에 대한 추가 조언

애플리케이션의 안정성을 보장하기 위한 유일한 방법은 정확한 테스트를 하는 것이다.
동시성과 직접적으로 연관있는 필수사항은 아니지만, 꼭 명심해야하는 중요한 조언이다.
<조언>
1.
버그 수정은 시나리오를 커버하는 테스트와 함께 수반되어야 한다.
2.
동시성 버그가 애플리케이션의 다른 부분에 어떠한 방법으로 영향을 줄지 생각해야 한다.
3.
동시성 작업을 위해 모든 값을 차례로 하는 테스트는 하지 말아야 한다.
4.
구현을 하기 전에 복원력에 대해 이야기 하고, 항상 복원력에 대한 테스트를 해야 한다.
5.
Edge case를 찾기 위해 노력해야 한다. → Coverage Report에서 Branch Analysis를 사용(활용)
6.
단위 테스트와 기능 테스트를 작성하는 시점에 대해 알아야 한다. → 기능 테스트는 종종 더 많은 노력이 필요해서 실제로 가치가 있을 때 수행해야 한다.
7.
인터페이스를 사용해 종속성을 연결한다. → 기능 테스트를 위해 복잡한 시나리오를 되풀이하기 위한 mock 작업이 쉬워진다.

디버깅

로그에서 코루틴 식별

쓰레드의 이름으로 식별: Thread.currentThread().name
코루틴이 다수라면 코루틴마다 이름을 넘겨주어 로깅을 해야하기 때문에 번거롭다
코루틴에 고유 식별자로 사용할 수 있는 것이 없다면, 특정 요소와 관련된 엔트리를 식별하기 어렵다
// 이 부분이 100회 반복된다면? 각 연산이 어느 시점에 이루어졌는지 식별하기 어렵다. withContext(ctx) { producer.produce(userName) // userName은 유니크하지 않은 값 log.debug(/* ... */ Thread.currentThread().name) }
Kotlin
복사

자동 이름 지정

코틀린이 코루틴에 자동 식별자를 할당하도록 하는 방법 → VM 플래그에 -Dkotlinx.corountines.debug 전달
Thread.currentThread().name 의 값에 영향을 미침 → grep 하기 용이
Procecesing 0 ... thread-pool-1 @corountine#1 Procecesing 1 ... thread-pool-2 @corountine#2 Procecesing 3 ... thread-pool-3 @corountine#2 Procecesing 2 ... thread-pool-2 @corountine#3 Procecesing 5 ... thread-pool-1 @corountine#4
Kotlin
복사

특정 이름 설정

Actor 나 프로듀서 같이 오래 지속되는 코루틴에 사용
코루틴 빌더에 파라미터 전달 → withContext(ctx + CoroutineName("somethingName"))
VM 플래그에 -Dkotlinx.corountines.debug 를 전달해야 동작

디버거에서 코루틴 식별

Debugger 의 Variable Watch 를 통해 식별 가능

복원력과 안정성

앞서 관련된 조언을 하기는 했지만, 그만큼 중요하다.
작성 중인 애플리케이션에서 발생할 수 있는 문제를 예상 했더라도, 특정 조건을 충족하지 않는다면 애플리케이션이 중단되는 것을 막을 순 없다.
복원력은 프로젝트 시작 초기에 고려해야 한다. → (기승전결) 애플리케이션이 안정적인지 보장하기 위한 유일한 방법은 제대로 된 테스트를 진행하는 것이다