JSON-OBJECT Software Engineering Blog

Professional Senior Backend Engineer. Specializing in high volume traffic and distributed processing with Kotlin and Spring Boot as core technologies.

View on GitHub
4 July 2022

Kotlin, 코루틴 사용법 정리

by Taehyeong Lee

개요

build.gradle.kts

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
    runtimeOnly("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.6.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.6.0")
}

runBlocking

fun main() {

    // [1] 현재 쓰레드에서 코루틴을 실행할 수 있는 상태로 전환
    runBlocking {

        // 블록 내에서는 여전히 현재 쓰레드 사용
        // [2] 0개 이상의 코루틴 블록 작성
    }

    // [3] 앞의 runBlocking의 코루틴을 포함한 블록 내 모든 로직이 종료된 후 현재 쓰레드 재개
}
fun doSomeCoroutine() = runBlocking {

    // 0개 이상의 코루틴 블록 작성
}

launch

runBlocking {

    // [1] 현재 쓰레드를 그대로 사용하는 코루틴 블록 작성
    launch {
        // 실행 로직 작성
    }

    // [2] IO 요청에 최적화된 새로운 쓰레드를 사용하는 코루틴 블록 작성
    launch(Dispatchers.IO) {
        // DefaultDispatcher-worker-{thread-number} 이름의 쓰레드로 실행
        // 실행 로직 작성
    }

    // [3] CPU 연산에 최적화된 새로운 쓰레드를 사용하는 코루틴 블록 작성
    launch(Dispatchers.Default) {
        // DefaultDispatcher-worker-{thread-number} 이름의 쓰레드로 실행
        // 실행 로직 작성
    }

    // [4] 응용하면 특정 로직을 서로 다른 쓰레드에서 병렬 실행이 가능
    (1..1000).forEach {
        launch(Dispatchers.IO) {
            // DefaultDispatcher-worker-{thread-number} 이름의 쓰레드로 실행
            doSomething(it)
        }
    }

    // [5] 각 launch 블록의 실행 순서는 랜덤하게 결정
}

suspend

// fun 앞에 suspend 키워드를 추가
suspend fun doSomething(number: Int): String {

    // [1] 로직 1
    // [2] 100ms 걸리는 IO 로직 2, 이 시점에 놀게 되는 현재 쓰레드를 다른 suspend 함수가 가져갈 수 있다.
    // [3] 로직 3, 이 것을 실행하는 쓰레드는 다른 쓰레드가 될 확률이 높다.

    return "something"
}

// 호출한 쓰레드의 MDC 필드를 보존하려면 withContext(MDCContext()) 추가
suspend fun doSomething(number: Int): String = withContext(MDCContext()) {
    ...
    return@withContext "foobar"
}
// 현재 쓰레드만 doSomething()을 위해 일한다.
runBlocking {
    doSomething(100)
}

// 여러 IO 쓰레드가 doSomething()를 위해 일한다.
runBlocking(Dispatchers.IO) {
    doSomething(100)
}

// 여러 Default 쓰레드가 doSomething()를 위해 일한다.
runBlocking(Dispatchers.Default) {
    doSomething(100)
}

Spring MVC에서 suspend 함수 호출

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.slf4j.MDCContext
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
class FooController(
    private val fooService: FooService
) {
    @GetMapping("/foos")
    fun getFoo(): ResponseEntity<Foo> {

        return runBlocking(Dispatchers.IO + MDCContext()) {
            // @Service 스프링 빈이자 suspend 함수인 FooService#getFoo() 호출
            val result = async { fooService.getFoo({id}) }
            ResponseEntity.ok(result.await())
        }
    }
}

@Scheduled에서 suspend 함수 호출

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.slf4j.MDCContext
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock
import org.springframework.context.annotation.Profile
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Profile("batch")
@Component
class BatchScheduledService(
    private val fooService: FooService
) {
    @Scheduled(cron = "0 0 * * * *")
    @SchedulerLock(name = "batchUpdateFoos", lockAtLeastFor = "30s", lockAtMostFor = "1h")
    fun batchUpdateFoos() {

        runBlocking(Dispatchers.IO + MDCContext()) {

            val result = async { fooService.getFoo({id}) }
            result.await()
        }
    }
}

참고 글

tags: Kotlin - Spring Boot