코루틴: Channel(채널) & Flow(플로우)

김명진·2021년 1월 3일
4

안드로이드

목록 보기
6/25

주로 연속적으로 값을 가져와야 하는 상황에서는 코루틴으로 코드를 짜기 쉽지가 않다.

예를들어, 시시각각 변화하는 온도를 가져오는 올때 코드를 보자.

lifecycleScope.launch {
	val temperatures = withContext(Dispactchers.IO){
		getTemperature()
	}
	for(temperature in temperatures) {
		Log.d("TEST", "$temperature")
	}
}

fun getTemperature(): MutableList<Int> {
	val list = mutableListOf<Int>()
	repeat(5){
		list.add(20 + it)
	}
	return list
}

리스트를 반환하게 했지만 값을 모두 가져온 후에 출력하는 문제가 생긴다.

이 문제를 해결하기 위해서는 코드 블록 사이의 구성을 복잡하게 만들어야 한다. 코루틴은 값을 반복적으로 가져오는 그것을 처리하는 구조보다는 한번에 무언가를 하는 것에 적합하다.

💡 Channel (채널)


채널은 일종의 파이프라인이다. 채널을 열고 한쪽에서 값을 보내면 다른 쪽에서 수신하는 개념이다.

위에 코루틴으로 짠 코드를 채널을 사용해 코드를 짜보자

val channel = Channel<Int>()

lifecycleScope.launch {
	repeat(5){
		channel.send(20 + it)
	}
}

lifecycleScope.launch {
	for(temperature in channel) {
		Log.d("TEST", "$temperature")
	}
}

이 코드를 실행하면 모두 받고 출력하는 이전 코드와 달리 값을 받을때마다 출력해준다.

두 코루틴이 채널을 통해서 값을 주고 받을 수 있다. 채널에 send 메서드를 통해 값을 보내면 반대편에서 값을 받을 수 있다. 값을 받기 위해서는 receive 메서드를 받아야하는데 for in 문에서는 자동으로 값을 수신해서 전달해준다. for in은 채널이 닫힐 때까지 값을 받는다. 채널을 닫기 위해서는 명시적으로 close 메서드를 호출할 수 있다.

채널을 만들고 코루틴을 만들어서 값을 전달하는 과정은 반복적인 과정이다. 이 과정을 간단히 만들어주는 빌더 produce가 있다.

val channel = lifecycleScope.produce<Int>{
	repeat(5){
		channel.send(20 + it)
	}
}

lifecycleScope.launch {
	for(temperature in channel) {
		Log.d("TEST", "$temperature")
	}
}

💡 Flow(플로우)


하지만 채널은 우리가 수신을 하기 전에도 데이터를 보내기 시작한다. 이런 특성 때문에 채널을 Hot이라고 부른다. 반면에 우리가 원하는 시점에 데이터를 가져오는 도구는 Cold라고 말한다. Flow는 cold 도구이다.

Flow로 짠 코드를 보자.

val flow: Flow<Int> = flow {
	repeat(5){
		emit(20 + it)
	}
}

lifecycleScope.launch {
	flow.collect { temperature ->
		Log.d("TEST", "$temperature")
	}
}

flow 빌더를 이용해서 블록을 열고 여기에 emit을 수행하는 것이 전부다. 이 블록은 collect를 호출하기 전까지 수행되지 않기 때문에 cold라고 한다.

collect 확장 함수가 호출되면 flow가 차례대로 수행된다. 단 flow 빌더 블록 내에서 다른 suspend 함수나 delay 등을 호출할 수 있지만 코루틴 빌더를 호출해서 디스패처를 부분적으로 변경할 수는 없다. Flow 가 수행되는 스레드를 변경하고 싶으면 flowOn 메서드를 사용한다.

val flow: Flow<Int> = flow {
	repeat(5){
		emit(20 + it)
	}
}.flowOn(Dispatchers.IO)

lifecycleScope.launch {
	flow.collect { temperature ->
		Log.d("TEST", "$temperature")
	}
}

만약 flow로 전달된 값을 조작하고 싶다면 map을 사용할 수 있다.

val flow: Flow<Int> = flow {
	repeat(5){
		emit(20 + it)
	}
}.flowOn(Dispatchers.IO)

lifecycleScope.launch {
	flow.map {
		it * 2
	}.collect { temperature ->
		Log.d("TEST", "$temperature")
	}
}

map 람다를 넣어 값을 조작했다.

map 도 다른 스레드에서 동작시킬수 있다.

val flow: Flow<Int> = flow {
	repeat(5){
		emit(20 + it)
	}
}.flowOn(Dispatchers.IO)

lifecycleScope.launch {
	flow.map {
		it * 2
	}.flowOn(Dispatchers.Default)
		.collect { temperature ->
		Log.d("TEST", "$temperature")
	}
}

예외가 발생했을 때는 catch 오퍼레이터를 통해 처리할 수 있다.

val flow: Flow<Int> = flow {
	repeat(5){
		emit(20 + it)
	}
	throw java.lang.RuntimeException("error")
}.catch { exception ->
	Log.d("TEST", "$exception")
}.flowOn(Dispatchers.IO)
profile
꿈꾸는 개발자

1개의 댓글

comment-user-thumbnail
2022년 2월 2일

정리 감사합니다!

답글 달기