[인프런] 2시간으로 끝내는 코루틴 - 6~10강 정리
안녕하세요, 해을입니다🦖
코루틴 복습 차원에서 인프런 강의 중 ‘2시간으로 끝내는 코루틴’ 강의를 수강하게 되었는데요.
이번 글에서는 코루틴의 구성 요소와 원리를 다룬 6~10강내용을 정리해보겠습니다!
- 💡6강. Structued Concurrency
- 💡7강. CoroutineScope와 CoroutineContext
- 💡8강. suspending function
- 💡9강. 코루틴과 Continuation
- 💡10강. 코루틴과 Continuation
💡6강. Structued Concurrency
🥨 Structued Concurrency
부모 - 자식 관계의 코루틴이 한 몸 처럼 움직이는 것
fun main(): Unit = runBlocking {
launch {
delay(600L)
printWithThread("A")
}
launch {
delay(500L)
throw IllegalArgumentException("코루틴 실패!")
}
}
Exception in thread "main"
java.lang.IllegalArgumentException: 코루틴 실패
- 두 번째 코루틴에서 발생한 예외가
runBlocking으로 생성된 부모 코루틴에게 취소 신호를 전달 - 부모 코루틴은 해당 취소 신호를 받아 다른 자식 코루틴(첫 번째 코루틴)에게도 취소를 전파
🥨 정리
- 공식 문서 정의
- 수많은 코루틴이 유실되거나 누수되지 않도록 보장
- 코드 내의 에러가 유실되지 않고 적절히 보고될 수 있도록 보장
- 취소 및 예외
자식 코루틴에서 예외가 발생하면
→ Structured Concurrency에 의해 부모 코루틴이 취소되고,
→ 부모의 다른 자식 코루틴들도 함께 취소자식 코루틴에서 예외가 발생하지 않더라도 부모 코루틴이 취소되면 자식 코루틴들도 모두 취소
단,
CancellationException은 정상적인 취소로 간주되기 때문에 부모 코루틴으로 전파되지 않으며, 다른 자식 코루틴을 취소시키지도 않음
💡7강. CoroutineScope와 CoroutineContext
🥨 CoroutineScope
지금까지
runBlocking이 코루틴과 루틴의 세계를 이어주며CoroutineScope를 제공launch와async는CoroutineScope의 확장함수fun main(): Unit = runBlocking { val job1 = CoroutineScope(Dispatchers.Default).launch { delay(1_000L) printWithThread("Job 1") } }직접
CoroutineScope를 만들면runBlocking이 필요 없음main 함수를 일반 함수로 만들어 코루틴이 끌날 때까지 main 스레드를 대기시킬 수 있음
fun lec07Example2(): Unit = runBlocking { CoroutineScope(Dispatchers.Default).launch { delay(1_000L) printWithThread("Job 1") } Thread.sleep(1_500L) }main 함수 자체를 suspend 함수로 만들어
join()가능fun main(): Unit = runBlocking { val job = CoroutineScope(Dispatchers.Default).launch { delay(1_000L) printWithThread("Job 1") } job.join() }
💡CoroutineScope의 주요 역할은 CoroutineContext 라는 데이터를 보관하는 것
🥨 CoroutineCotext
- 코루틴과 관련된 여러가지 데이터를 갖고 있음
- 코루틴 이름
CoroutineExceptionHandler- 코루틴 자체(Job)
CoroutineDispatcher
- Map + Set을 합쳐놓은 형태
- Element(key - value)로 데이터 저장
- 같은 key의 데이터는 유일
- Element 추가/제거
+기호 : 각element를 합치거나 context에 추가 가능// + 기호를 이용한 Element 합성 CoroutineName("나만의 코루틴") + SupervisorJob() // context에 Element를 추가 coroutineContext + CoroutineName("나만의 코루틴")minusKey함수 :element제거coroutineContext.minusKey(CoroutineName.key)
🥨 총 정리
CoroutineScope- 코루틴이 생성되는 영역
CoroutineContext- 코루틴과 관련된 데이터를 보관
Structured Concurrency 기반

클래스 내부에서 독립적인 CoroutineScope를 관리
class AsyncLogic{ private val scope = CoroutineScope(Dispatchers.Default) fun doSomething(){ scope.launch { // 무언가 코루틴이 시작되어 작업! } } fun destroy(){ scope.cancel() } } val asyncLogic = AsyncLogic() asyncLogic.doSomething() asyncLogic.destory() // 필요 없어지면 모두 정리- 해당 클래스에서 사용하던 코루틴을 한 번에 종료시킬 수 있음
🥨 CoroutineDispatcher
- 코루틴을 어떤 스레드에서 실행할지 결정하는 역할
- 주요 디스패처 종류
- Dispatchers.Default
- 가장 기본적인 디스패처
- CPU 자원을 많이 쓸 때 권장
- 별다른 설정이 없으면 이 디스패처가 사용됨
- Dispatchers.IO
- I/O작업에 최적화된 디스패처
- Dispatchers.Main
- 보통 UI 컴포넌트를 조작하기 위한 디스패처
- 특정 의존성을 갖고 있어야 정상적으로 활용 가능
- Java의 스레드풀인 ExecutorService를 디스패처로 변환
asCoroutineDispatcher()이라는 확장함수를 이용해 ExecutorService를 디스패처로 전환 가능val threadPool = Executors.newSingleThreadExecutor() CoroutineScope(threadPool.asCoroutineDispatcher()).launch { printWithThread("새로운 코루틴") }
- Dispatchers.Default
💡8강. suspending function
🥨 suspending function
suspend지시어가 붙은 함수를 의미
suspend 함수 호출 가능
fun main(): Unit = runBlocking {
launch {
delay(100L)
}
}
suspend함수인delay()함수가 호출 가능한 이유
⇒
launch함수의 마지막 함수 타입이suspend lambda이기 때문
suspension point : 코루틴이 중지 되었다가 재개 될 수 있는 지점
fun main(): Unit = runBlocking {
launch {
a()
b()
}
launch {
c()
}
}
suspend fun a() {
printWithThread("A")
}
suspend fun b() {
printWithThread("B")
}
suspend fun c() {
printWithThread("C")
}
// 출력 결과
// [main @coroutine#2] A
// [main @coroutine#2] B
// [main @coroutine#3] C
- suspend 함수를 호출하더라도 반드시 중지되는 것은 아니기에 A와 B가 먼저 출력되고 나중에 C가 출력됨
여러 비동기 라이브러리를 사용하는데 활용 가능
fun main(): Unit = runBlocking {
val result1 = async {
call1()
}
val result2 = async {
call2(result1.await())
}
printWithThread(result2.await())
}
fun call1(): Int {
Thread.sleep(1_000L)
return 100
}
fun call2(num: Int): Int{
Thread.sleep(1_000L)
return num * 2
}
runBlocking입장에서result1과result2의 타입이Deferred이기에Deffered에 의존적인 코드가 되는 것이 아쉬움Deffered대신CompletableFuture또는Reactor와 같은 다른 비동기 라이브러리 코드를 써야할 수도 있음
fun main(): Unit = runBlocking {
val result1 = call1()
val result2 = call2(result1)
printWithThread(result2)
}
suspend fun call1(): Int {
return CoroutineScope(Dispatchers.Default).async {
Thread.sleep(1_000L)
100
}.await()
}
suspend fun call2(num: Int): Int{
return CompletableFuture.supplyAsync {
Thread.sleep(1_000L)
num * 2
}.await()
}
suspend fun으로 변경하여 이 안에서 어떤 비동기 라이브러리 구현체를 사용하건 해당 함수의 선택으로 남겨두는 것
🥨 함수 종류
coroutineScope
fun main(): Unit = runBlocking {
printWithThread("START")
printWithThread(calculateResult())
printWithThread("END")
}
suspend fun calculateResult(): Int = coroutineScope {
val num1 = async {
delay(1_000L)
10
}
val num2 = async {
delay(1_000L)
20
}
num1.await() + num2.await()
}
// 출력 결과
// [main @coroutine#1] START
// [main @coroutine#1] 30
// [main @coroutine#1] END
- 새로운 코루틴을 만들지만, 주어진 함수 블록이 바로 실행됨
- 만들어진 코루틴이 모두 완료된 이후, 반환
coroutineScope로 만든 코루틴은 이전 코루틴의 자식 코루틴이 됨
withContext
fun main(): Unit = runBlocking {
printWithThread("START")
printWithThread(calculateResult())
printWithThread("END")
}
suspend fun calculateResult(): Int = withContext(Dispatchers.Default) {
val num1 = async {
delay(1_000L)
10
}
val num2 = async {
delay(1_000L)
20
}
num1.await() + num2.await()
}
coroutineScope와 기본적으로 유사- 새로운 코루틴을 만들지만, 주어진 함수 블록이 바로 실행됨
- 만들어진 코루틴이 모두 완료된 이후, 반환
- context에 변화를 주는 기능이 추가적으로 존재 ⇒ Dispatcher를 바꿔 사용할 때 활용
withTimeout / withTimeoutOrNull
fun main(): Unit = runBlocking {
val result: Int = withTimeout(1_000L){
delay(1_500L)
10 + 20
}
printWithThread(result)
}
// 출력 결과
// Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
fun main(): Unit = runBlocking {
val result: Int? = withTimeoutOrNull(1_000L){
delay(1_500L)
10 + 20
}
printWithThread(result)
}
// 출력 결과
// [main @coroutine#1] null
coroutineScope와 기본적으로 유사- 단, 주어진 시간 안에 새로 생긴 코루틴이 완료되어야 함
withTimeout:TimeoutCancellationException예외 발생withTimeoutOrNull: null 반환
💡9강. 코루틴과 Continuation
🥨 suspending function
기본 예제 코드
class UserService {
private val userProfileRepository = UserProfileRepository()
private val userImageRepository = UserImageRepository()
suspend fun findUser(userId: Long): UserDto{
println("유저를 가져오겠습니다")
val profile = userProfileRepository.findProfile(userId)
println("이미지를 가져오겠습니다")
val image = userImageRepository.findImage(profile)
return UserDto(profile, image)
}
}
data class UserDto(
val profile: Profile,
val image: Image
)
class UserProfileRepository{
suspend fun findProfile(userId: Long): Profile {
delay(100L)
return Profile()
}
}
class Profile
class UserImageRepository {
suspend fun findImage(profile: Profile): Image{
delay(100L)
return Image()
}
}
class Image
suspend 지점 경계 나누기
class UserService {
private val userProfileRepository = UserProfileRepository()
private val userImageRepository = UserImageRepository()
suspend fun findUser(userId: Long): UserDto{
// state machine의 약자, 라벨을 기준으로 상태를 관리
val sm = object : Continuation{
var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
}
when(sm.label){
0 -> {
// 0단계 - 초기 시작
println("유저를 가져오겠습니다")
val profile = userProfileRepository.findProfile(userId)
}
1 -> {
// 1단계 - 1차 중단 후 재시작
println("이미지를 가져오겠습니다")
val image = userImageRepository.findImage(profile) // 에러 발생
}
2-> {
// 2단계 - 2차 중단 후 재시작
return UserDto(profile, image) // 에러 발생
}
}
}
}
에러 수정
class UserService {
private val userProfileRepository = UserProfileRepository()
private val userImageRepository = UserImageRepository()
suspend fun findUser(userId: Long): UserDto{
// state machine의 약자, 라벨을 기준으로 상태를 관리
val sm = object : Continuation{
var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
var profile: Profile? = null
var image: Image? = null
}
when(sm.label){
0 -> {
// 0단계 - 초기 시작
println("유저를 가져오겠습니다")
sm.label = 1
val profile = userProfileRepository.findProfile(userId)
sm.profile = profile
}
1 -> {
// 1단계 - 1차 중단 후 재시작
println("이미지를 가져오겠습니다")
sm.label = 2
val image = userImageRepository.findImage(sm.profile!!)
sm.image = image
}
2-> {
// 2단계 - 2차 중단 후 재시작
return UserDto(sm.profile!!, sm.image!!)
}
}
}
}
라벨 호출
// 라벨을 갖고 있을 인터페이스, 점점 더 많이 기능이 추가될 예정
interface Continuation {
suspend fun resumeWith(data: Any?)
}
class UserService {
private val userProfileRepository = UserProfileRepository()
private val userImageRepository = UserImageRepository()
suspend fun findUser(userId: Long, continuation: Continuation): UserDto{
// state machine의 약자, 라벨을 기준으로 상태를 관리
val sm = object : Continuation{
var userId = userId
var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
var profile: Profile? = null
var image: Image? = null
override suspend fun resumeWith(data: Any?) {
findUser(this.userId, this)
}
}
when(sm.label){
0 -> {
// 0단계 - 초기 시작
println("유저를 가져오겠습니다")
sm.label = 1
val profile = userProfileRepository.findProfile(userId, sm)
sm.profile = profile
}
1 -> {
// 1단계 - 1차 중단 후 재시작
println("이미지를 가져오겠습니다")
sm.label = 2
val image = userImageRepository.findImage(sm.profile!!, sm)
sm.image = image
}
2-> {
// 2단계 - 2차 중단 후 재시작
return UserDto(sm.profile!!, sm.image!!)
}
}
}
}
data class UserDto(
val profile: Profile,
val image: Image
)
class UserProfileRepository{
suspend fun findProfile(userId: Long, continuation: Continuation): Profile {
delay(100L)
return Profile()
}
}
class Profile
class UserImageRepository {
suspend fun findImage(profile: Profile, continuation: Continuation): Image{
delay(100L)
return Image()
}
}
class Image
최종코드
suspend fun main() {
val service = UserService()
println(service.findUser(1L, null))
}
// 라벨을 갖고 있을 인터페이스, 점점 더 많이 기능이 추가될 예정
interface Continuation {
suspend fun resumeWith(data: Any?)
}
class UserService {
private val userProfileRepository = UserProfileRepository()
private val userImageRepository = UserImageRepository()
private abstract class FindUserContinuation() : Continuation {
var label = 0 // 익명 클래스를 만들어 라벨을 갖게 만듦
var profile: Profile? = null
var image: Image? = null
}
suspend fun findUser(userId: Long, continuation: Continuation?): UserDto{
// state machine의 약자, 라벨을 기준으로 상태를 관리
val sm = continuation as? FindUserContinuation ?: object : FindUserContinuation(){
override suspend fun resumeWith(data: Any?) {
when (super.label){
0 -> {
profile = data as Profile
label = 1
}
1 -> {
image = data as Image
label = 2
}
}
findUser(userId, this)
}
}
when(sm.label){
0 -> {
// 0단계 - 초기 시작
println("프로필을 가져오겠습니다")
userProfileRepository.findProfile(userId, sm)
}
1 -> {
// 1단계 - 1차 중단 후 재시작
println("이미지를 가져오겠습니다")
userImageRepository.findImage(sm.profile!!, sm)
}
}
return UserDto(sm.profile!!, sm.image!!)
}
}
data class UserDto(
val profile: Profile,
val image: Image
)
class UserProfileRepository{
suspend fun findProfile(userId: Long, continuation: Continuation) {
delay(100L)
continuation.resumeWith(Profile())
}
}
class Profile
class UserImageRepository {
suspend fun findImage(profile: Profile, continuation: Continuation){
delay(100L)
continuation.resumeWith(Image())
}
}
class Image
Continuation을 통해 최초 호출인지 아니면 callback 호출인지 구분 가능Continuation의 구현 클래스에서 라벨과 전달할 데이터 등을 관리 가능- Continuation Passing Style(CPS)
💡코루틴은 내부적으로 Continuation을 매개변수마다 추가해서 callback처럼 사용하게끔 변경되는 것이라고 이해하면 충분
💡10강. 코루틴과 Continuation
🥨 코루틴 특징
- callback hell 해결
- Kotlin 언어 키워드가 아닌 라이브러리
⇒ 비동기 non-blocking이 필요하거나 동시성이 필요한 곳에 적극 활용
🥨 코틀린 활용
- Client : 비동기 UI 구성
- Server : 여러 API(I/O) 동시 호출
- Webflux와 같은 프레임워크에서 사용 가능
- 동시성 테스트에 적용 가능
.
이번 글에서는 코루틴 생성부터 취소와 예외처리까지 코루틴을 실제로 사용하기 위한 기본 방법들을 정리해봤습니다.
이번 강의 구간부터는 코루틴이 추상적인 개념이 아니라 실제 코드에서 어떻게 쓰이는지 조금씩 감이 잡히기 시작하더라구요!
다음 글에서는 코루틴의 구성 요소와 원리를 다룬 6~7강을 정리해보겠습니다.
끝!🦕
.
👍 참고