Kotlin에서 코루틴을 사용할 때 가장 기본적이면서도 중요한 두 가지 개념이 있습니다. 바로 runBlocking과 CoroutineScope입니다. 이 글에서는 이 두 가지의 차이점과 각각의 사용 방법에 대해 알아보겠습니다.

runBlocking

runBlocking은 현재 스레드를 차단(block)하고, 블록 내의 모든 코루틴이 완료될 때까지 기다립니다. 주로 테스트나 간단한 예제에서 사용되며, 메인 함수에서 코루틴을 동기적으로 실행할 때 유용합니다.

특징

  • 차단(blocking): runBlocking 블록 내의 모든 코드가 완료될 때까지 현재 스레드를 차단합니다.
  • 동기적(synchronous): 블록 내의 코드가 완료될 때까지 다음 코드로 진행하지 않습니다.
  • 주로 테스트용: 주로 테스트 코드나 예제 코드에서 사용됩니다.
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    // "World!"가 출력될 때까지 runBlocking은 메인 스레드를 차단합니다.
}

CoroutineScope

CoroutineScope는 비차단(non-blocking) 방식으로 코루틴을 실행할 수 있는 컨텍스트를 제공합니다. CoroutineScope는 일반적으로 애플리케이션 내에서 코루틴을 관리하고, 특정 라이프사이클(예: ViewModel, Activity)에 따라 코루틴의 생명주기를 제어하는 데 사용됩니다.

특징

  • 비차단(non-blocking): CoroutineScope 내의 코루틴은 비차단 방식으로 실행되며, 스코프의 컨텍스트에 따라 코루틴을 관리합니다.
  • 유연성: 다양한 코루틴 빌더(launch, async 등)를 사용하여 비동기 작업을 처리할 수 있습니다.
  • 라이프사이클 관리: 스코프는 특정 객체(ViewModel, Activity 등)의 라이프사이클에 따라 코루틴을 취소하거나 정리할 수 있습니다.
import kotlinx.coroutines.*

fun main() {
    val scope = CoroutineScope(Dispatchers.Default)

    scope.launch {
        delay(1000L)
        println("World!")
    }

    println("Hello,")
    Thread.sleep(2000L) // 메인 스레드가 종료되지 않도록 잠시 대기
}

주요 차이점

차단 여부

  • runBlocking: 현재 스레드를 차단합니다. 블록 내의 모든 작업이 완료될 때까지 기다립니다.
  • CoroutineScope: 비차단 방식으로 코루틴을 실행합니다. 코루틴은 백그라운드에서 실행되며, 스레드를 차단하지 않습니다.

사용 목적

  • runBlocking: 주로 테스트나 간단한 콘솔 애플리케이션에서 동기적으로 코루틴을 실행할 때 사용됩니다.
  • CoroutineScope: 실제 애플리케이션에서 비동기 작업을 처리하고, 객체의 라이프사이클에 따라 코루틴을 관리할 때 사용됩니다.

라이프사이클 관리

  • runBlocking: 블록이 끝날 때까지 모든 코루틴이 완료되기를 기다립니다.
  • CoroutineScope: 스코프가 종료되면 해당 스코프 내의 모든 코루틴이 취소됩니다. 예를 들어, Android에서 ViewModel의 viewModelScope는 ViewModel이 클린업될 때 자동으로 취소됩니다.

결론

  • runBlocking: 주로 동기적 실행이 필요한 간단한 테스트나 예제에서 사용됩니다. 현재 스레드를 차단하므로 실제 애플리케이션에서는 잘 사용되지 않습니다.
  • CoroutineScope: 비차단 방식으로 코루틴을 실행하며, 실제 애플리케이션에서 비동기 작업을 처리하고, 라이프사이클 관리가 필요한 경우에 사용됩니다.

실제 애플리케이션 개발에서는 CoroutineScope를 사용하여 비동기 작업을 관리하는 것이 좋습니다. 이를 통해 비차단 방식으로 코루틴을 실행하고, 애플리케이션의 다른 부분과 자연스럽게 통합할 수 있습니다.

'Kotlin' 카테고리의 다른 글

JDK의 다양한 종류와 그 역사적 배경  (0) 2024.05.26
Kotlin 기본 개념 정리  (0) 2024.05.26

JDK의 다양한 종류와 그 역사적 배경

Kotlin 프로젝트를 IntelliJ에서 설정할 때 OpenJDK, Amazon Corretto, Oracle JDK 등 여러 종류의 JDK 옵션이 보이는 이유가 궁금하신 적이 있으신가요? 왜 이렇게 다양한 JDK가 존재하는지, 그 배경을 이해하면 좀 더 명확한 선택을 할 수 있습니다.

JDK의 종류

1. OpenJDK

OpenJDK는 오픈 소스 자바 개발 키트로, 대부분의 Java 개발자들에게 기본 옵션입니다. 자유롭게 사용할 수 있으며 다양한 플랫폼을 지원합니다.

2. Amazon Corretto

Amazon Corretto는 아마존에서 제공하는 OpenJDK 배포판입니다. 성능 최적화와 보안 패치가 잘 되어 있으며, AWS 환경과의 호환성이 뛰어납니다.

3. Oracle JDK

Oracle JDK는 Oracle에서 제공하는 공식 JDK입니다. 상업적 지원과 추가 기능을 제공하지만, 상용 라이선스가 필요합니다.

4. AdoptOpenJDK

AdoptOpenJDK는 다양한 플랫폼에 대해 빌드된 OpenJDK입니다. 다양한 버전과 설정을 제공하여 개발 환경에 맞추기 쉽습니다.

JDK가 나뉘어진 역사적 배경

Sun Microsystems와 Oracle

Java는 원래 Sun Microsystems에서 개발되었습니다. Sun이 Oracle에 인수된 후, Oracle은 Java의 상용화와 라이선스 변경을 시도했습니다. 이에 따라 OpenJDK라는 오픈 소스 프로젝트가 더 강조되었고, Oracle JDK와는 별도로 유지 관리되기 시작했습니다.

오픈 소스 커뮤니티의 성장

Java 커뮤니티는 OpenJDK를 중심으로 활성화되었고, 다양한 기업들이 이 프로젝트에 기여하게 되었습니다. 각 기업은 자신들의 요구에 맞는 최적화와 기능을 추가한 JDK 배포판을 만들기 시작했습니다.

기업의 요구사항

각 기업은 자신들의 클라우드 환경, 배포 환경, 보안 정책 등에 맞추어 최적화된 JDK를 원했습니다. 예를 들어, Amazon Corretto는 AWS 환경에 최적화된 JDK이고, Azul Zulu는 다양한 플랫폼을 지원하는 JDK입니다.

오픈 소스와 상용화의 균형

Oracle JDK는 상업적 지원을 제공하면서 일부 기능을 추가로 제공하지만, 이에 대한 비용이 발생합니다. 반면, OpenJDK 기반 배포판들은 무료로 사용 가능하고, 각기 다른 벤더가 지원을 제공합니다.

기술적 차별화

성능, 보안 패치 주기, 호환성 등 기술적인 차별화를 제공하여 특정 환경에서 더 나은 성능을 발휘하거나 특정 요구사항을 충족시킬 수 있습니다.

결론

다양한 JDK 배포판은 각각의 개발 환경과 요구 사항에 맞춰 선택할 수 있는 다양한 옵션을 제공합니다. 각 JDK는 기본적으로 동일한 Java 표준을 따르지만, 벤더별 최적화와 추가 기능이 다를 수 있습니다. 자신의 프로젝트와 환경에 맞는 JDK를 선택하면 최적의 개발 경험을 누릴 수 있습니다.

'Kotlin' 카테고리의 다른 글

Kotlin Coroutine: runBlocking과 CoroutineScope의 차이점  (0) 2024.05.26
Kotlin 기본 개념 정리  (0) 2024.05.26

해당 게시글은 인프런의 코틀린 문법 총 정리 강의를 들으면서 작성 되었습니다.

해당 강의 중 맨 마지막 섹션인 코루틴 섹션은 내용을 추가로 조사하여 작성 하였음을 알립니다.

컴파일 상수

const val num = 20

fun main() {
    println("Hello, World")
}

main() 함수 상단부에 작성된 const valmain() 함수보다 우선해서 컴파일되어 성능에 유리한 이점을 가져올 수 있다.

문자열에서 첫 번째 문자 다루기

fun main() {
    val name = "abc"

    println("${name[0].uppercase()}")
}

name[0]와 같이 문자열 접근시 문자열 변수에서 첫 번째 문자를 접근할 수 있다.

min, max

초기 버전의 코틀린에서는 자바 코드의 Math.max 함수를 사용하였지만, 최근에는 코틀린 코드로 작성된 min, max 함수를 사용하면 된다.

import kotlin.math.max
import kotlin.math.min

fun main() {
    val i = 10
    val j = 20

    println(max(i, j))
    println(min(i, j))
}

import 구문에 kotlin.math.*가 포함돼 있음을 체크하자.

Random

import kotlin.random.Random

fun main() {
    val randomNumber = Random.nextInt(0, 100) // 0 ~ 99
    val randomDouble = Random.nextDouble(0.0, 1.0) // 0.0 ~ 0.9
}

Scanner

값을 입력받는 Scanner 클래스이다.

import java.util.Scanner

fun main() {
    val reader = Scanner(System.`in`)
    reader.nextInt()
}

코틀린에서 in 키워드는 예약어라서 자동으로 백틱(`)이 붙게된다.

if, when 제어문

코틀린에서는 3항 연사자가 따로 없다. if 또는 when 제어문을 사용하면 된다.

fun main() {
    val i = 5

    // when으로 변경 가능
    // 3항 연산자를 따로 사용하지 않는다.
    val result = if (i > 10) {
        "10 보다 크다"
    } else if (i > 5) {
        "5 보다 크다"
    } else {
        "!!!"
    }

    val result2 = when {
        i > 10 -> "10 보다 크다"
        i > 5 -> "5 보다 크다"
        else -> "!!!"
    }

    println(result)
    println(result2)
}

두개의 코드 블록 모두 동일한 코드이다. 마지막 줄의 코드가 리턴되기 때문에 각각 result, result2 변수에 값이 담기게 된다.

for, forEach

리스트의 각 아이템들을 출력시키는 다양한 방법이다. 정통적으로 사용되는 for (var i = 0; i < items.length; i++)과 같은 형태는 지양한다.

더 직관적이면서 다양한 문법들이 지원되기 때문이다. 그럼에도 불구하고 정통적인 반복문 형태의 포맷을 찾는다면 예시의 맨 마지막 형태가 정통적인 형태와 가장 유사하다.

fun main() {
    val items = listOf(1, 2, 3, 4, 5)

    for (item in items) {
        print(item)
    }

    items.forEach {
        println(it)
    }

    items.forEach { item ->
        println(item)
    }

    items.forEach(::println)

    println()

    for (i in 0..(items.size - 1)) {
        print(items[i])
    }
}

List

fun main() {
    val items = listOf(1, 2, 3, 4, 5) // 변경안됨

    val mItems = mutableListOf(1, 2, 3, 4, 5)
    mItems.add(6)
    mItems.remove(3)
}

Array

fun main() {
    val items = arrayOf(1, 2, 3) // 배열은 실질적으로 잘 사용하지 않고, List를 사용하면 된다.

    println(items.size)
    items[0] = 2

    try {
        val item = items[4]
    } catch (e: Exception) {
        println(e.message)
    }
}

배열(Array)은 실질적으로 잘 사용하지 않고, List를 사용하면 된다.

배열과 리스트의 차이

  • 배열은 고정된 크기를 가지며, 인덱스를 통해 각 요소에 직접 접근하고 변경할 수 있습니다. 배열의 크기는 생성 시에 결정되며 이후에는 변경할 수 없습니다.
  • 성능의 관점에서 배열은 메모리에서 연속된 블록을 사용하기 때문에 성능이 약간 더 좋을 수 있습니다. 그러나 이러한 차이는 대부분의 애플리케이션에서 큰 영향을 미치지 않습니다.
  • 리스트는 더 많은 기능과 유연성을 제공하지만, 성능 측면에서 배열보다 약간 느릴 수 있습니다. 특히, MutableList는 내부적으로 배열을 사용하기 때문에 성능 차이가 크지 않습니다.
  • 대부분의 경우 리스트가 더 사용하기 편리하고(map, filter, reduce), Kotlin의 컬렉션 API와 더 잘 통합됩니다.

Null Safety

코틀린에서 nullable 타입을 표현하기 위해서는 String?과 같이 물음표를 붙여준다.

다만, Nullable 타입은 명시된 타입을 바로 할당할 수 없기 때문에 ?.let { it } 구문을 사용해 코드를 작성하는 것이 관례이다.

그 밖에 Null이 아님을 강제하는 !! 문법을 사용할 수 있지만, 이는 개발자에 실수가 될 수 있기 때문에 지양하도록 하자.

fun main() {
    var nameNullable: String? = null
    var name: String = ""

    nameNullable?.let { // null이 아니라면, 블록을 실행하자.
        name = it
    }
}

함수 작성하기

파일에 작성된 함수들을 Top-Level 함수라 한다. 어느 파일에서나 사용가능하다.

아래의 sum, sum2 함수들을 호출하는 문법들은 모두 유효한 문법들이다.

fun main() {
    println(sum(1, 2))
    println(sum2(a = 10, b = 20))
    println(sum2(b = 30, a = 10))
}

// Top-Level 함수, 어느 파일에서나 사용가능하다.
fun sum(a: Int, b: Int): Int {
    return a + b
}

fun sum2(a: Int, b: Int, c: Int = 0) = a + b + c

클래스, Data 클래스

fun main() {
    val jhon = Person("Jhon Smith", 28, "private name")
    val jhon2 = Person("Jhon Smith", 28, "private name")

    println(jhon) // Person@65b54208
    println(jhon2) // Person@1be6fc3
    println(jhon2 == jhon2) // false

    val jhon3 = DataPerson("a", 1)
    val jhon4 = DataPerson("a", 1)
    println(jhon3 == jhon4) // `data class`는 주소 참조가 아닌 값 참조를 하기 때문에 `true`가 된다.

    jhon.some()
    println(jhon.hobby) // 취미: 농구
}

class Person(
    private val name: String,
    private val age: Int,
    private val name2: String,
) {
    var hobby = "축구"
        private set // 외부에서 set이 불가능해 짐
        get() = "취미: $field"

    init {
        // 초기화 구문, 객체 생성시 실행된다.
        println("My name is $name and I am $age")
    }

    fun some() {
        hobby = "농구"
    }
}

data class DataPerson(
    val name: String,
    val age: Int,
)

상속을 위한 open 그리고 interface

  • 일반 클래스를 상속 가능하게 하려면 open 키워드를 붙여야 한다.
  • 추상 클래스에서 작성된 메소드 또한 오버라이드가 가능하게 하려면 open 키워드를 붙여야 한다.
interface Drawable {
    fun draw()
}

abstract class Animal {
    open fun move() {
        println("move")
    }
}

class Dog: Animal(), Drawable {
    override fun move() {
        println("살금")
    }

    override fun draw() {
        TODO("Not yet implemented")
    }
}

class Cat: Animal(), Drawable {
    override fun draw() {
        TODO("Not yet implemented")
    }
}

타입 체크 is

위의 작성된 예시에서 타입을 체크 하려면 다음과 같이 작성해 볼 수 있다.

fun main() {
    val dog: Animal = Dog()
    val cat = Cat()

    if (dog is Dog) {
        println("멍멍이")
    }

    cat as Animal // type casting

    if (dog is Animal) {
        println("Animal")
    }
}

Generic 클래스 생성

fun main() {
    val box = Box(10)
    val box2 = Box("asdf")

    println(box.value)
    println(box2.value)
}

class Box<T>(val value: T)

고차함수

fun main() {
    myFunc(10) { // Lambda 식
        println("Hello World")
    }
}

// 콜백함수(고차함수)
fun myFunc(a: Int, callback: () -> Unit) {
    println("함수 시작")
    callback()
    println("함수 끝")
}

코루틴(coroutine)

코루틴?

  • 코루틴은 비동기 프로그래밍을 쉽게 할 수 있도록 도와주는 프로그래밍 구조이다.
  • 쉽게 말해, 코루틴은 멈췄다가 나중에 다시 시작할 수 있는 함수이다.
  • 코루틴을 사용하면 코드가 멈추지 않고 다른 작업을 할 수 있다.

코루틴으로 가능한 것들

  1. 메인 스레드가 멈추지 않음: 네트워크 요청이나 파일 읽기 같은 시간이 오래 걸리는 작업을 하면서도 메인 스레드가 멈추지 않아서 UI가 끊기지 않는다.
  2. 간편한 비동기 코드 작성: 콜백이나 복잡한 스레드 관리를 할 필요 없이, 동기 코드처럼 읽기 쉬운 비동기 코드를 작성할 수 있다.
  3. 효율적인 자원 사용: 필요할 때만 코드를 실행하므로, 자원을 효율적으로 사용할 수 있다.

코루틴의 작동 방식

  1. 시작: suspend 함수로 표시된 코루틴 블록을 시작한다.
  2. 멈춤: suspend 키워드가 있는 부분에서 멈춘다. 예를 들어, 네트워크 요청을 기다리는 동안 다른 작업을 한다.
  3. 재개: 요청이 완료되면 멈췄던 지점부터 다시 시작한다.
import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        val result = fetchData()
        println("Data: $result")
    }
    println("Waiting for data...")
    Thread.sleep(2000) // 메인 스레드가 종료되지 않도록 잠시 대기
}

suspend fun fetchData(): String {
    delay(1000) // 네트워크 요청 시뮬레이션
    return "Hello, Coroutine!"
}

이 예제에서는 fetchData 함수가 suspend로 표시되어 있고, 이 함수는 delay를 통해 잠시 멈췄다가 1초 후 다시 실행된다. 메인 스레드는 멈추지 않고 "Waiting for data..."를 출력하고, 이후 fetchData의 결과가 출력된다.

+ Recent posts