Coroutine์ ์ค๋จ ๊ฐ๋ฅํ(suspendable) ๊ฒฝ๋ ์ฐ๋ ๋์ด๋ค.
์ผ๋ฐ ํจ์๋ ์์ํ๋ฉด ๋๊น์ง ์คํ๋์ด์ผ ํ์ง๋ง, Coroutine์ ์คํ ์ค ์ผ์ ์ค๋จ(suspend)ํ๋ค๊ฐ ๋์ค์ ์ฌ๊ฐํ ์ ์๋ค. OS ์ฐ๋ ๋๋ฅผ ์ง์ ์์ฑํ์ง ์๊ณ ๋ ๋น๋๊ธฐ ์์ ์ ํจ์จ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ค.
- ๊ฒฝ๋์ฑ: ์์ฒ ๊ฐ์ ์ฝ๋ฃจํด์ ์์ฑํด๋ ์ฐ๋ ๋๋ณด๋ค ๋ฉ๋ชจ๋ฆฌ์ ์ฑ๋ฅ ๋ถ๋ด์ด ์ ๋ค
- ์ค๋จ ๊ฐ๋ฅ:
suspendํจ์๋ฅผ ํตํด ์ฐ๋ ๋๋ฅผ ๋ธ๋กํนํ์ง ์๊ณ ์์ ์ ์ผ์ ์ค๋จํ ์ ์๋ค - ๊ตฌ์กฐํ๋ ๋์์ฑ:
CoroutineScope๋ก ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํ๋ฉฐ, Scope๊ฐ ์ข ๋ฃ๋๋ฉด ํ์ ์ฝ๋ฃจํด๋ ์๋์ผ๋ก ์ทจ์๋๋ค
๋น๋๊ธฐ ์์ ์ ์ฒ๋ฆฌํ ๋ ์ฐ๋ ๋๋ฅผ ์ง์ ๊ด๋ฆฌํ๊ฑฐ๋ ์ฝ๋ฐฑ ํจํด์ ์ฌ์ฉํ๋ฉด ๋ณต์ก๋๊ฐ ์ฆ๊ฐํ๊ณ ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง๋ค.
- ์ฐ๋ ๋(Thread): ์์ฑ ๋น์ฉ์ด ํฌ๊ณ ์ปจํ ์คํธ ์ค์์นญ ์ค๋ฒํค๋๊ฐ ๋ฐ์ํ๋ค
- ์ฝ๋ฐฑ(Callback): ์ค์ฒฉ์ด ๊น์ด์ง๋ฉด ์ฝ๋ฐฑ ์ง์ฅ(Callback Hell)์ด ๋ฐ์ํ๋ค
- RxJava/Future: ์ฒด์ด๋์ด ๋ณต์กํ๊ณ ๋ฌ๋ ์ปค๋ธ๊ฐ ๋๋ค
- ๊ฐ๋ ์ฑ: ๋๊ธฐ ์ฝ๋์ฒ๋ผ ์์ฑํ๋ฉด์ ๋น๋๊ธฐ๋ก ์คํ๋๋ค
- ํจ์จ์ฑ: ์ฐ๋ ๋๋ณด๋ค ํจ์ฌ ์ ์ ๋ฆฌ์์ค๋ก ์๋ง์ ๋์ ์์ ์ ์ฒ๋ฆฌํ๋ค
- ์์ ์ฑ: ๊ตฌ์กฐํ๋ ๋์์ฑ์ผ๋ก ๋ฉ๋ชจ๋ฆฌ ๋์์ ์์ ๋๋ฝ์ ๋ฐฉ์งํ๋ค
Coroutine์ CPS(Continuation-Passing Style) ๋ณํ์ ํตํด suspend ํจ์๋ฅผ ์ํ ๋จธ์ ์ผ๋ก ๋ณํํ๋ค. ์ปดํ์ผ ์์ ์ ํจ์๊ฐ ์ฌ๋ฌ ๋จ๊ณ๋ก ๋ถํ ๋๋ฉฐ, ๊ฐ ์ค๋จ ์ง์ ๋ง๋ค ์ํ๋ฅผ ์ ์ฅํ๊ณ ๋ณต์ํ๋ค.
- suspend ํจ์ ํธ์ถ: ํ์ฌ ์ํ๋ฅผ
Continuation๊ฐ์ฒด์ ์ ์ฅ - ์ค๋จ: ์ ์ด๊ถ์ ๋ฐํํ๊ณ ์ฐ๋ ๋๋ ๋ค๋ฅธ ์์ ์ํ
- ์ฌ๊ฐ: ์ ์ฅ๋
Continuation์์ ์ด์ด์ ์คํ
- Continuation: ์ค๋จ๋ ์ง์ ์ ์ํ์ ์ฌ๊ฐ ์ ๋ณด๋ฅผ ๋ด๋ ๊ฐ์ฒด
- Dispatcher: ์ฝ๋ฃจํด์ด ์คํ๋ ์ฐ๋ ๋๋ฅผ ๊ฒฐ์ (Main, IO, Default ๋ฑ)
- CoroutineScope: ์ฝ๋ฃจํด์ ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํ๋ ๋ฒ์
// ์ปดํ์ผ ์
suspend fun fetchData(): String {
delay(1000)
return "Data"
}
// ์ปดํ์ผ ํ (๊ฐ๋
์ )
fun fetchData(continuation: Continuation<String>): Any {
when (continuation.label) {
0 -> {
continuation.label = 1
return delay(1000, continuation) // COROUTINE_SUSPENDED ๋ฐํ
}
1 -> {
return "Data"
}
}
}์๋ช ์ฃผ๊ธฐ ๊ด๋ฆฌ๊ฐ ๋ถ๊ฐ๋ฅํ์ฌ ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ๋ฐ์ํ๋ค.
// โ ์๋ชป๋ ์
GlobalScope.launch {
// ์๋ฒ๊ฐ ์ข
๋ฃ๋์ด๋ ๊ณ์ ์คํ๋ ์ ์์
processBackgroundJob()
}
// โ
์ฌ๋ฐ๋ฅธ ์
class OrderService {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
fun processOrder(orderId: String) {
scope.launch {
// scope๊ฐ ์ทจ์๋๋ฉด ์๋์ผ๋ก ์ข
๋ฃ
processOrderInternal(orderId)
}
}
fun shutdown() {
scope.cancel() // ์๋น์ค ์ข
๋ฃ ์ ๋ชจ๋ ์ฝ๋ฃจํด ์ทจ์
}
}์ฐ๋ ๋ ํ์ด ๊ณ ๊ฐ๋์ด ๋ค๋ฅธ ์์ฒญ์ด ์ฒ๋ฆฌ๋์ง ์๋๋ค.
// โ ์๋ชป๋ ์
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: Long): User {
// Dispatchers.Default์์ ๋ธ๋กํน I/O ์คํ
val data = readFromDatabase(id) // ์ฐ๋ ๋ ํ ๊ณ ๊ฐ
return data
}
// โ
์ฌ๋ฐ๋ฅธ ์
@GetMapping("/users/{id}")
suspend fun getUser(@PathVariable id: Long): User = withContext(Dispatchers.IO) {
// I/O ์์
์ Dispatchers.IO์์ ์คํ
readFromDatabase(id)
}suspend ํจ์๋ ์ฝ๋ฃจํด ์ค์ฝํ๋ ๋ค๋ฅธ suspend ํจ์ ๋ด์์๋ง ํธ์ถ ๊ฐ๋ฅํ๋ค.
// โ ์๋ชป๋ ์
fun loadData() {
delay(1000) // ์ปดํ์ผ ์๋ฌ
}
// โ
์ฌ๋ฐ๋ฅธ ์
suspend fun loadData() {
delay(1000)
}
// ๋๋
fun loadData() {
CoroutineScope(Dispatchers.Main).launch {
delay(1000)
}
}- ์ ์ ํ Scope ์ฌ์ฉ (
viewModelScope,lifecycleScope๋ฑ) - I/O ์์
์
Dispatchers.IO์์ ์คํ - CPU ์ง์ฝ์ ์์
์
Dispatchers.Default์์ ์คํ - ๊ตฌ์กฐํ๋ ๋์์ฑ(Structured Concurrency) ์์น ์ค์
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L) // 1์ด ๋์ ์ผ์ ์ค๋จ (์ฐ๋ ๋ ๋ธ๋กํน ์๋)
println("World!")
}
println("Hello,")
}
// ๊ฒฐ๊ณผ
// Hello,
// World!์ฌ๋ฌ ๋น๋๊ธฐ ์์ ์ ๋์์ ์คํํ๊ณ ๋ชจ๋ ๊ฒฐ๊ณผ๋ฅผ ๊ธฐ๋ค๋ฆด ๋
suspend fun loadDashboard() = coroutineScope {
val user = async { fetchUser() }
val posts = async { fetchPosts() }
val notifications = async { fetchNotifications() }
Dashboard(
user = user.await(),
posts = posts.await(),
notifications = notifications.await()
)
}์ฝ๋ฃจํด์ suspend ์ง์ ์์ ์ทจ์๋ฅผ ํ์ธํ๊ณ ๋ฆฌ์์ค๋ฅผ ์ ๋ฆฌํ๋ค
// โ ์ผ๋ฐ ํจ์ - ์ทจ์ ๋ถ๊ฐ๋ฅ
fun processLargeFile(filePath: String): Int {
var totalBytes = 0
File(filePath).inputStream().use { input ->
val buffer = ByteArray(8192)
while (true) {
val bytesRead = input.read(buffer) // ๋ธ๋กํน - ์ฒญํฌ ์ฝ๋ ๋์ ๋ฉ์ถ ์ ์์
if (bytesRead == -1) break
totalBytes += bytesRead
// ์ทจ์ ๋ถ๊ฐ๋ฅ - ์ฐ๋ ๋ ๊ฐ์ ์ข
๋ฃ๋ง ๊ฐ๋ฅ
}
}
return totalBytes
}
// โ ์ฝ๋ฃจํด์ธ๋ฐ ์ทจ์ ์ง์ ์ด ์์
suspend fun processLargeFile(filePath: String): Int = withContext(Dispatchers.IO) {
var totalBytes = 0
File(filePath).inputStream().use { input ->
val buffer = ByteArray(8192)
while (true) {
val bytesRead = input.read(buffer) // ๋ธ๋กํน - ์ฌ์ ํ ์ทจ์ ๋ถ๊ฐ
if (bytesRead == -1) break
totalBytes += bytesRead
}
}
totalBytes
}
// โ
์ฝ๋ฃจํด - ์ทจ์ ๊ฐ๋ฅ
suspend fun processLargeFile(filePath: String): Int = withContext(Dispatchers.IO) {
var totalBytes = 0
File(filePath).inputStream().use { input ->
val buffer = ByteArray(8192)
while (true) {
ensureActive() // ์ฒญํฌ๋ง๋ค ์ทจ์ ํ์ธ - ์ทจ์๋๋ฉด CancellationException ๋ฐ์
val bytesRead = input.read(buffer)
if (bytesRead == -1) break
totalBytes += bytesRead
}
}
totalBytes
}
// ์ฌ์ฉ ์์
class FileService {
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
fun startProcessing(filePath: String): Job {
return scope.launch {
try {
val result = processLargeFile(filePath)
println("Processed ${result.size} lines")
} catch (e: CancellationException) {
println("Processing cancelled - resources cleaned up")
throw e // ์ทจ์ ์์ธ๋ ๋ฐ๋์ ์ฌ์ ํ
}
}
}
// 5์ด ํ ์๋ ์ทจ์ ์์
fun startWithTimeout(filePath: String) {
scope.launch {
val job = launch { processLargeFile(filePath) }
delay(5000)
job.cancel() // 5์ด ํ ์ทจ์ - ensureActive()์์ ๊ฐ์ง
}
}
}ํ์ ์ฝ๋ฃจํด์ ์์ธ๊ฐ ์์๋ก ์ ํ๋๋๋ก ์ฒ๋ฆฌ
// SupervisorJob: ํ๋์ ์์์ด ์คํจํด๋ ๋ค๋ฅธ ์์์ ๊ณ์ ์คํ
class NotificationService {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
suspend fun sendNotifications(userIds: List<String>) = coroutineScope {
userIds.map { userId ->
async {
try {
sendNotification(userId)
} catch (e: Exception) {
logger.error("Failed to send notification to $userId", e)
null // ์คํจํด๋ ๋ค๋ฅธ ์๋ฆผ์ ๊ณ์ ์ ์ก
}
}
}.awaitAll()
}
}๋น๋๊ธฐ ์คํ ํจํด์ ์กฐํฉ
// ๋
ผ๋ธ๋กํน + ๋๊ธฐ
runBlocking {
delay(100) // (1) ๋
ผ๋ธ๋กํน
test() // (2) ๋
ผ๋ธ๋กํน
println("Done") // (3) 1, 2๊ฐ ๋ชจ๋ ๋๋ ํ ์คํ
}
suspend fun test() { delay(200) }
// ๋
ผ๋ธ๋กํน + ๋น๋๊ธฐ
runBlocking {
val result1 = async { delay(100) } // (1) ๋น๋๊ธฐ ์์
val result2 = async { delay(200) } // (2) ๋น๋๊ธฐ ์์
println("Done") // (3) ๋จผ์ ์คํ
result1.await() // (4) 1์ด ๋๋ ๋๊น์ง ๋๊ธฐ
result2.await() // (5) 2๊ฐ ๋๋ ๋๊น์ง ๋๊ธฐ
}
// ๋ธ๋กํน + ๋น๋๊ธฐ
runBlocking {
val result = launch {
withContext(Dispatchers.Default) {
Thread.sleep(100) // ๋ธ๋กํนํ์ง๋ง ๋ณ๋ ์ฐ๋ ๋
}
}
println("Done") // (2) ๋จผ์ ์คํ ๊ฐ๋ฅ
result.join() // (3) result๊ฐ ๋๋ ๋๊น์ง ๋๊ธฐ
}// โ ์ฝ๋ฐฑ ๋ฐฉ์
fun fetchData(callback: (Result<Data>) -> Unit) {
api.getData(object : Callback {
override fun onSuccess(data: Data) {
callback(Result.success(data))
}
override fun onError(e: Exception) {
callback(Result.failure(e))
}
})
}
// โ
์ฝ๋ฃจํด ๋ฐฉ์
suspend fun fetchData(): Data {
return api.getData() // ๊ฐ๊ฒฐํ๊ณ ์ง๊ด์
}- Understanding Kotlin Coroutines - Coroutine ๋์ ์๋ฆฌ ์์ธ ์ค๋ช