Search

'코틀린'에 해당되는 글 2건

  1. 2021.06.05 kotlin lateinit, lazy by
  2. 2021.05.21 Kotlin - Coroutine

kotlin lateinit, lazy by

개발/안드로이드 2021. 6. 5. 14:59 Posted by 아는 개발자

자바에서 흔히 보게 되는 NullPointerException 문제를 예방하고자 코틀린에서는 변수 선언에서부터 Nullable 변수의 선언부터 엄격하게 관리한다. 변수를 선언 할 때도 Nullable인지 아닌지를 구분해야하고 Nullable인 경우에는 변수를 호출하는 코드에서 Nullsafe 지시자를 표시해야하며 그렇지 않으면 컴파일 단계에서 에러를 발생시킨다.

 

var name: String? = null 
name = "abcd"
name?.length() // name이 여전히 null 일 가능성이 존재하므로, null safe 접근만 허용된다

var name2: String = "abcd"
name2 = null // name2가 nullable하지 않으므로 이 코드는 컴파일 오류가 발생한다

 

 

그런데 코드 상에서는 null이 될 수도 있지만 실제 동작 중에는 null이 될 소지가 없는 경우가 있다. 아래 코드처럼 전역 변수인데 실행과 동시에 초기화를 시키는 경우가 이렇다. 논리적으로는 안전한 코드임에도 불구하고 값에 접근 할 때 null safe 지시자를 표시해야하는 불편함이 생긴다. 

 

class TestActivity: Activity() {
    var name: String? = null
    override fun onCreate() {
        name = "abcd"
        print("${name?.length}")
    }
}

 

lateinit 

 

lateinit을 사용하면 변수의 값을 지정하는 작업을 뒤로 미룰 수 있다. Nullable 하지 않은 변수를 선언하면서 Assign 하는 작업을 뒤로 미루고 싶을때는 lateinit 키워드를 사용하면 가능하면 된다. 아래 코드는 name 변수 앞에 lateinit 키워드를 두고 onCreate 콜백에서 값을 바로 지정했다. 선언 당시 Non-Null String으로 선언했기 때문에 호출할 때 

 

class TestActivity: Activity() {
    lateinit var name: String
    override fun onCreate() {
        name = "abcd"
        print("${name.length}")
    }
}

 

lateinit은 mutable 변수만 가능하기 때문에 var 키워드를 가진 변수에서만 사용이 가능하다. 실행 중에 값을 변경할 필요가 있는 경우 유용하다. 그리고 만약 값을 assign하지 않고 변수 값을 호출하는 경우에는 Kotlin 언어 상에서 에러를 발생시킨다.

 

by lazy 

 

by lazy 키워드는 lateinit과 비슷하게 값을 지정하는 작업을 미루는 작업인데 assign 되는 시점이 변수를 호출하는 시점이다. 아래 코드를 보면 name 변수 선언에 by lazy 키워드가 붙고 내부 브래킷에 "abcd" 코드가 있다. name변수가 호출되는 시점에 "abcd"로 assign 하겠다는 의미다. 실제 코드를 동작시켜보면 name 호출 시점에 by lazy 내부 로그가 먼저 호출되는 것을 볼 수 있다.

 

class Test {
    val name : String by lazy {
        println("this is name by lazy")
        "abcd"
    }

    init {
        println("I am here")
        println(name)
    }
}

// 실행 결과
I am here
this is name by lazy
abcd

 

by lazy는 immutable 변수에서만 적용이 가능해, val 키워드 변수에만 적용이 가능하다. 변수 값을 최초에만 설정하고 변경할 필요가 없는 경우 사용하면 유용하다. 변수 선언시에 값을 assign할 순 없지만 다른 변수들을 조합해 값을 설정하고 싶을 때 사용하면 유용하다.

728x90

Kotlin - Coroutine

개발/안드로이드 2021. 5. 21. 20:00 Posted by 아는 개발자

Coroutine을 공부할 때 당장 실행되는 코드를 짜려고 launch, async 함수부터 먼저 써보게 되는데(과거의 나) 이것보단 Coroutine을 이루는 구조가 무엇인지를 먼저 공부하고 유틸리티 함수를 사용하면 훨씬 이해하기가 쉽다.  Coroutine을 이루는 구조는 크게 CoroutineScope과 CoroutineContext다. 아래 그림으로 보면 CoroutineScope이 CoroutineContext를 포함하는 관계다.

 

 

1. CoroutineScope

 

CoroutineScope은 Coroutine이 활동할 수 있는 범위를 말한다. 예를 들어 Coroutine이 ViewModel의 생성주기 내에서만 동작하게 할 수 있고 Activity Lifecyle 생명주기를 따라서 동작하게 할 수 있는데 CoroutineScope은 Coroutine의 활동범위를 말한다. 이 속성을 잘 이용하면 Component의 생성주기에 맞춰 Coroutine 작업을 자동으로 취소할 수 있어서 유용하다. Kotlin에서는 안드로이드에서 사용할 수 있도록 몇가지 CoroutineScope을 미리 만들어뒀다. 

 

  • GlobalScope: 앱 프로세스의 생명주기를 따라감. 
  • MainScope: UI 관련 작업을 처리하는 용도.
  • ViewmodelScope: ViewModel의 생성주기를 따라감.
  • LifecycleScope: Activity, Fragment의 생명주기를 따라감. 생명주기별로 콜백이 다르다.
MainScope().launch {}

GlobalScope.launch {}

 

CoroutineScope 인터페이스를 구현해서 커스텀한 CoroutineScope을 만들수도 있긴 한데 공식 문서에서 이 방법은 추천하진 않고 있다.

 

2. CoroutineContext 

 

CoroutineContext는 Coroutine을 이루는 정보다. Coroutine 이름, Job, Dispatcher, ExceptionHandler 가 이에 해당한다. Dispatcher는 Coroutine이 실행될 쓰레드 풀을 의미한다. 대표적으로 Main, IO 쓰레드 풀이 있어서 UI 작업의 경우에는 Main, 디스크 작업에는 IO 쓰레드를 사용하도록 지정 할 수 있다. Executors 라이브러리를 이용해 커스텀으로 만든 쓰레드 풀에도 지정이 가능하다. 아래 코드는 우선순위가 높은 쓰레드 풀에서 동작하는 CoroutineScope을 만든 예다.

 

val customExecutor: Executor = Executors.newCachedThreadPool { r ->
    Thread(r, "CustomThread").apply {
        priority = Thread.MIN_PRIORITY
    }
}
val customDispatcher = object : CoroutineDispatcher() {
    override fun dispatch(context: CoroutineContext, block: Runnable) {
        customExecutor.execute(block)
    }
}

CoroutineScope(customDispatcher).launch {

}

 

ExceptionHandler는 Coroutine 내의 코드 실행중 발생하는 Exception을 처리할 수 있는 Handler다. 현재 Scope 별로 Exception Handler를 다르게 둘 수 있기 때문에 이것도 잘 써먹으면 유용하다. 

 

val handler = CoroutineExceptionHandler { context, th->
    println("$context ${th.toString()} ")
}

GlobalScope.launch(handler) {
    val async1 = async(){ 1 }
}

 

3. Utility 함수 

 

3.1 launch

 

launch 함수는 CoroutineScope내에서 실행되며, 현재 쓰레드를 막지 않고(blocking) 동작할 수 있는 새로운 Coroutine Job을 생성한다. 병렬로 수행되기 때문에 여러가지 작업을 동시에 수행할 때 쓰면 좋다. 아래 코드는 GlobalScope 내에서 두개의 Coroutine Job을 생성한 코드다. 앞에 코드에 300ms 의 딜레이를 줬다. 그 결과 scope2가 먼저 프린트 되고, scope1은 그 이후에 프린트 된다.

 

GlobalScope.launch {
    launch {
        delay(300)
        println("scope1")
    }
    launch {
        println("scope2")
    }
}

2021-05-21 16:14:17.677 I/System.out: scope2
2021-05-21 16:14:17.978 I/System.out: scope1

 

Job 내부 함수인 join() 은 동시성을 제어할 수 있는 함수다. 현재 Coroutine의 실행이 종료되지 않을 때까지 다음 코드를 실행하지 않는다. 순서를 관리할 때는 이 함수를 쓰면 된다. 그리고 cancel() 처럼 취소할 수 있는 함수도 있다. 이건 실제로 사용하다 보면 어떻게 써야하는지 감이 온다.

 

3.2 async 

 

async 함수는 launch 와 거의 동일하고 결과 값을 받을 수 있다는 점이 추가 됐다. 아래 코드의 두 async Job은 각각 1, 2를 리턴하는 CoroutineScope이다. async 내부 await() 함수는 여기서 실행된 결과 값을 받아오게 된다. 여기서 주의깊게 볼 부분은 각각에 delay를 300ms, 100ms 씩 줬는데도 start 로그로부터 결과 값까지 걸린 시간은 둘의 합인 400ms가 아니라 가장 긴 delay인 300ms라는 점이다. 두 Job을 병렬로 처리했기 때문에 가장 delay를 오래 잡는 Job의 시간만큼 소요된다.

 

GlobalScope.launch {
    println("start")
    val a = async {
        delay(300)
        1
    }
    val b = async {
        delay(100)
        2
    }

    println("a + b = ${a.await() + b.await()}")
}

2021-05-21 16:22:19.885 I/System.out: start
2021-05-21 16:22:20.190 I/System.out: a + b = 3

 

3.3 withContext 

 

동일한 CoroutineScope 내에서도 종종 Coroutine Context를 바꿔야 할 일이 생긴다. 예를 들면 I/O 작업을 수행 중에도 중간중간 화면 UI를 업데이트 해야하는 경우 Dispatcher를 바꿔 코드에 적용하는 쓰레드를 변경해야한다. 이럴때 쓰면 유용한 함수가 withContext다. 아래 코드는 I/O 스레드 풀에서 "abc", "def"라는 문자열을 받아오고 두 문자를 합해서 text라는 변수를 만들었다. UI 에 적용하려면 withContext를 이용해 임시로 Main함수로 바꿔주어 UI 컴포넌트에 접근 할 수 있다. 참고로 withContext는 내부적으로 async{}.await()로 구현돼 있어 내부 코드가 모두 실행된 다음에 다음 코드로 넘어가게된다. 

 

CoroutineScope(Dispatchers.IO).launch {
    val a = async {
        "abc"
    }
    val b = async {
        "def"
    }
    
    val text = a.await() + b.await()
    
    withContext(Dispatchers.Main) {
        textView.text = text
    }
}
728x90