Search

RxJava - debounce

컴퓨터공부/안드로이드 2020. 7. 4. 15:17 Posted by 아는 개발자

 

debounce 는 특정 시간이 지난 후에 마지막으로 들어온 이벤트만 받을 수 있는 오퍼레이터다. 글로 구구 절절 설명하는 것보다는 아래 그림으로 이해하는게 쉬울 것 같은데 아래 그림에서 최종적으로 받는 이벤트가 1, 5, 6인데 모두 입력됐을 때랑 입력으로 들어올 때랑 배출 될 때 사이의 구간 길이가 동일하다. 아래 그림에서 길이는 시간을 의미하기 때문에 둘다 동일한 시간이 지난 후에 이벤트를 받는 것으로 보면 된다. 그리고 5 이벤트가 들어오기 전에 2, 3, 4가 있었는데 최종적으로 5만 이벤트로 받게 되는데 이건 특정 시간이 지나기 전에 다른 이벤트들이 들어와서 그렇다. 그래서 2, 3, 4 이벤트는 모두 무시하게 된다.

 

 

debounce 오퍼레이터는 텍스트 입력과 관계된 작업에서 아주 요긴하게 써먹을 수 있다. 대표적으로 요즘 검색창 같은데선 유저의 입력을 받고 예상되는 입력 결과를 보여주는 UX가 많은데 매번 탕핑 할 때마다 서버를 치는것이 부자연스럽고 부하도 크다. 대체적으로 1초간 아무런 입력이 없었을 때 서버를 치도록 규칙을 넣어 놓는데 이때 debounce를 사용하면 쉽게 구현할 수 있다.

 

이 포스트에서는 서버를 치는 것은 없고 타이핑 한 내용이 1초 뒤에 TextView에 업데이트 되는 것으로 debounce 사용 예제를 만들어봤다. text watcher를 등록해서 타이핑한 내용이 변할 때마다 textChange 오브젝트에 이벤트를 보내고 debounce를 1초 걸어서 1초를 기다린 다음 마지막으로 들어온 스트링 값에 대해서 화면에 표현하도록 구현했다.

 

val textChange: PublishSubject<String> = PublishSubject.create()

fr_rx_edit.addTextChangedListener(object: TextWatcher{
    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        textChange.onNext(s.toString())
    }
    override fun afterTextChanged(s: Editable?) {}
    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
})

textChange
    .debounce(1, TimeUnit.SECONDS)
    .observeOn(AndroidSchedulers.mainThread())
    .doOnNext {
        fr_rx_tv.text = it
    }
    .subscribe()

 

결과 아래 그림처럼 타이핑 후 1초를 기다린 다음에 화면에 업데이트 된다.

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

Hilt - Dagger를 이을 의존성 주입 라이브러리(1)  (0) 2020.07.08
RxJava - debounce  (0) 2020.07.04
RxJava - observeOn, subscribeOn  (0) 2020.06.28
RxJava - combineLatest  (0) 2020.06.28
status bar 영역 덮는 view 만들기  (0) 2020.06.24
Lottie 라이브러리  (0) 2020.06.24

Kotlin - Coroutine

컴퓨터공부/안드로이드 2020. 4. 15. 22:47 Posted by 아는 개발자

Kotlin의 Coroutine은 비동기 작업을 지원하는 "lightweight threads" 인 컴포넌트다. 이미 안드로이드에 있는 AsyncTask와 비슷한 역할을 수행하지만 Coroutine은 특별한 오버라이드 함 수 없이 간단하게 구현이 가능하고 깊게 들어가면 세부 동작 방식과 구현 철학은 다르다. 이번 포스트에서는 Kotlin의 Coroutine에 대해서 전반적인 소개와 사용 방법을 소개하려고 한다.

 

 

 

먼저 Coroutine은 하나의 task가 아니라, 여러 개 순서가 정해진 sub-tasks의 집합이다. Coroutine에서는 여러 개의 sub-task가 존재하는데 이들의 실행 순서는 보장된(guaranteed) 순서로 실행이 된다. 즉 코드 상에서는 얘네들이 sequential 하게 짜여져 있어도 코드를 어떻게 짜느냐에 따라서 실행 순서는 다를 수 있다. 이게 무슨 뚱딴지 같은 소리인가 싶을 수도 있지만 이건 asynchronous 작업을 좀더 유연하게 지원하기 위한 철학 정도로 이해하면 될 것 같다. 글로 보면 이해가 잘 되지 않을 수 있는데 예제 코드를 본다면 감이 어느정도 잡힐 것이다.

 

0. Quick Example

 

GlobalScope.launch {
    async { withContext(this.coroutineContext) { printLog("1") } }
    async { withContext(this.coroutineContext) { printLog("2") } }
    async { withContext(this.coroutineContext) { printLog("3") } }
}

 

안드로이드에서 Coroutine을 사용한 예제 코드다. 아직 Coroutine에 대해서 잘 모르더라도 느낌상으로는 로그를 찍는 함수 호출로 미뤄 볼 때 로그는 1 -> 2 -> 3 의 순서로 찍혀야 할 것 같다. 하지만 실제 출력 결과 매번 실행할 때마다 순서가 다르게 나온다.

 

2020-04-15 20:38:06.190 24595-24791/kwony.study D/CoroutineSample: 1
2020-04-15 20:38:06.191 24595-24790/kwony.study D/CoroutineSample: 3
2020-04-15 20:38:06.191 24595-24791/kwony.study D/CoroutineSample: 2

 

그런데 이번에는 로그 출력용 객체에 await 함수를 붙여주면

 

GlobalScope.launch {
    async { withContext(this.coroutineContext) { printLog("1") } }.await()
    async { withContext(this.coroutineContext) { printLog("2") } }.await()
    async { withContext(this.coroutineContext) { printLog("3") } }.await()
}

 

 이렇게 결과가 달라지고 값도 고정되게 나온다.

 

2020-04-15 20:40:12.687 25004-25078/kwony.study D/CoroutineSample: 1
2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 2
2020-04-15 20:40:12.689 25004-25078/kwony.study D/CoroutineSample: 3

 

Coroutine을 사용하면 작업의 순서를 요리조리 조정할 수 있다. 맛보기로 이정도면 좋을 것 같다.

 

1. Coroutine Builder 

 

Coroutine을 사용하려면 Coroutine 객체를 생성하는 작업을 먼저해야한다. Coroutine Builder는 Coroutine을 생성하기 위한 팩토리 함수다. Kotlin에서는 Coroutine을 생성하기위한 기본 빌더 함수를 제공한다.

 

runBlocking

 

현재 시작중인 thread를 block 시키고 Coroutine 작업을 시작하게 하는 Coroutine 빌더다. Main Thread에서 이 함수가 호출 됐으면 UI가 잠시 멈추고 coroutine 함수가 실행된다. 테스트를 위해 onCreate 함수에서 runBlocking Coroutine을 생성하고 3초간 딜레이를 주었다. 그 결과 화면이 초기화되는 시간이 3초간 딜레이되는 현상이 발생한다.

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    runBlocking {
        delay(3000)
    }

 

synchronous하게 처리해야하는 작업에 사용되는데 현재 실행중인 thread를 block시키는 문제가 꽤 골치 아프기 때문에 가능하면 쓰지 않을 것을 추천하는 문서도 있다. 정말로 필요한 경우가 아니면 가급적 사용하지 않는 것이 좋을 수도.

 

GlobalScope 

 

runBlocking 빌더와 달리 현재 실행중인 Thread를 block 시키지 않고 백그라운드에서 작업을 실행하는 Coroutine이다. 최상위 Coroutine을 생성 할 때 사용하는데 이 Coroutine은 애플리케이션과 동일한 생명주기를 갖게 된다. 이에 대한 자세한 내용은 CoroutineScope 에서 소개하는 것이 좋을 것 같다. 예제 코드를 한번 보자.

 

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    
    GlobalScope.launch {
        delay(3000)
        Log.d(coroutineSampleTag(), "World")
    }
    Log.d(coroutineSampleTag(), "Hello")
}

 

GlobalScope 으로 만들어진 Coroutine 에서는 3초간 딜레이를 주고 World를 출력하고 생성 다음에는 Hello를 출력하는 코드다. 실행 결과는 아래와 같다. Hello가 먼저 출력되고 Coroutine 내부의 World 문자열은 3초뒤에 출력되는 것을 확인 할 수 있다.

 

2020-04-15 21:21:45.224 30001-30001/kwony.study D/CoroutineSample: Hello
2020-04-15 21:21:48.234 30001-30069/kwony.study D/CoroutineSample: World

 

CoroutineScope(context: CoroutineContext)

 

Coroutine이 동작할 context를 지정해서 Coroutine을 만드는 함수다. 저기 안에 인자에 특정 CoroutineContext를 넣을 수 있는데, 이에 대해서 자세하게 설명하는 것보다는 어떤 Thread에서 동작할 것인지 지정하는 것 정도로만 보면 좋을 것 같다. 대표적으로 Dispatchers.Main과 Dispatchers.IO가 있다.

 

CoroutineScope(Dispatchers.Main).launch {}
CoroutineScope(Dispatchers.IO).launch {}

 

Dispatchers.Main으로 넣은 경우는 해당 Coroutine 작업이 MainThread 즉 UI를 관장하는 Thread에서 돌아가도록 선언한 것이고 Dispatchers.IO인 경우에는 Blocking IO 용 공유 쓰레드 풀에서 동작하도록 지정한 것이다. RxJava에서 subscribeOn 함수에 넣은 값들과 유사한 개념이다. 이 Builder 함수들도 GlobalScope 처럼 진행중인 Thread를 block 하지 않고 동작한다.

 

2. async { }

 

Coroutine 초기 예제에서도 확인 했던 것 처럼 Coroutine 작업들은 비동기적으로 처리하는 것이 가능하다. 비동기로 처리하려는 Coroutine들은 async { } 로 작업을 선언하면 된다.

 

CoroutineScope(Dispatchers.IO).launch {
    printLog("Coroutine Start")
    val deferred1 = async {
        delay(3000)
        printLog("1")
        return@async 1
    }
    val deferred2 = async {
        delay(3000)
        printLog("2")
        return@async 2
    }
    val deferred3 = async {
        delay(3000)
        printLog("3")
        return@async 3
    }
    printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
}

 

예제에서 확장해서 async Coroutine에 딜레이 3초와 리턴값을 넣고 마지막에 각 Coroutine 결과의 총합을 출력하는 코드를 추가했다.

 

2020-04-15 22:18:29.090 3877-3990/? D/CoroutineSample: Coroutine Start
2020-04-15 22:18:32.108 3877-3996/kwony.study D/CoroutineSample: 2
2020-04-15 22:18:32.109 3877-3993/kwony.study D/CoroutineSample: 3
2020-04-15 22:18:32.112 3877-3991/kwony.study D/CoroutineSample: 1
2020-04-15 22:18:32.125 3877-3996/kwony.study D/CoroutineSample: total sum: 6

 

Coroutine 의 로그 출력 순서는 실행할 때마다 변경되는 반면 마지막에 Coroutine 반환 값의 합을 출력하는 부분은 항상 마지막에 출력되는데 이는 각 async 객체에 await() 함수를 사용해서 리턴값을 반환했기 때문이다. await() 함수는 async 함수가 모두 종료될 때 까지 구문 실행을 기다리도록 하는 함수다. 합을 출력하는 부분은 세개의 Coroutine의 연산결과를 모두 기다려야하기 때문에 제일 마지막에 실행 될 수 밖에 없다.

 

또 주목할 만한 점은 모든 Coroutine에 3초씩 딜레이를 주었는데 각 Coroutine의 로그 출력 시간은 3초씩 차이가 나지 않는다. 이는 각각의 Coroutine이 Sequential하게 동작하지 않고 Parallel하게 동작했기 때문이다. Coroutine의 로그 출력 순서가 바뀌는 이유도 Parallel 하게 동작했기 때문이다.

 

3. suspend fun method() = coroutineScope { }

 

Kotlin 함수 선언 앞에 suspend와 coroutineScope 으로 body를 만들어주면 Coroutine 에서 실행가능한 함수로 선언 해줄 수 있다. 아래 코드는 앞서 예제로 소개한 코드랑 동일한 로직으로 동작하며 출력되는 결과도 동일하다 (Coroutine 로그 출력 순서만 빼면)

 

private suspend fun coroutineMsg(msg: String, ret: Int): Int = coroutineScope {
    delay(3000)
    printLog(msg)
    return@coroutineScope ret
}

CoroutineScope(Dispatchers.IO).launch {
    printLog("Coroutine Start")
    val deferred1 = async { coroutineMsg("1", 1) }
    val deferred2 = async { coroutineMsg("2", 2) }
    val deferred3 = async { coroutineMsg("3", 3) }
    printLog("total sum: " + (deferred1.await() + deferred2.await() + deferred3.await()))
}
2020-04-15 22:42:07.182 7959-8043/kwony.study D/CoroutineSample: Coroutine Start
2020-04-15 22:42:10.197 7959-8045/kwony.study D/CoroutineSample: 1
2020-04-15 22:42:10.197 7959-8048/kwony.study D/CoroutineSample: 3
2020-04-15 22:42:10.198 7959-8043/kwony.study D/CoroutineSample: 2
2020-04-15 22:42:10.217 7959-8044/kwony.study D/CoroutineSample: total sum: 6

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

Navigator - Getting Started  (0) 2020.04.20
안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12
FragmentManagers Android  (0) 2020.04.06

Kotlin에서 제공하는 apply 범위 함수를 이용해서 클래스 내부 속성 값을 간결하게 선언할 수 있지만 DSL(Domain Specific Language) 언어인 점을 응용하면 여러 클래스를 중첩한 클래스의 속성값에 대해서 더욱 간결하게 값을 설정 할 수 있다. 얼마나 간결한지 글로 길게 설명하는 것 보다는 간단한 예시로 보는게 좋을 것 같다.

 

Kotlin의 Builder 패턴을 사용하면 아래와 같이 선언된 data 클래스들을

 

data class Group(
    val name: String,
    val company: Company,
    val members: List<Member>
)

data class Company(
    var name: String = ""
)

data class Member(
    val name: String,
    val alias: String,
    val year: Int
)

 

이렇게 선언 하는 것이 가능하다.

 

val redVelvet = group {
    name { "레드벨벳" }
    company {
        name { "SM Entertainment" }
    }
    members {
        member {
            name { "슬기" }
            alias { "곰슬기" }
            year { 1994 }
        }

        member {
            name { "아이린" }
            alias { "얼굴 천재" }
            year { 1991 }
        }

        member {
            name { "웬디" }
            alias { "천사" }
            year { 1994 }
        }
    }
}

 

이렇게 간결하게 코드를 만들기 위해선 각각의 클래스에 대해서 Builder 클래스와 lambda 함수를 사용한 내부에 셋팅 함수를 선언해야한다. Member 함수부터 구현 방법을 살펴보자.

 

1. MemberBuilder

 

class MemberBuilder {
    private var name: String = ""
    private var alias: String = ""
    private var year: Int = 0

    fun name(lambda: () -> String) {
        name(lambda)
    }

    fun alias(lambda: () -> String) {
        alias(lambda)
    }

    fun year(lambda: () -> Int) {
        year(lambda)
    }

    fun build() = Member(name, alias, year)
}

 

MemberBuilder 클래스 내부에는 Member 데이터 클래스와 동일하나 name, alias, year 를 변수로 가지는데 보면 셋팅하는 함수들의 인자가 lambda로 선언되어 있고 바로 내부 변수를 초기화해준다는 점만 다르다. lambda가 포함된 함수는 아까 레드벨벳 초기화 코드에서 확인 할 수 있듯이 간단히 primitive 인자값을 전달하는 방식 만으로도 선언이 가능하다. build() 함수는 현재까지 초기화된 정보로 Member 클래스를 만드는 작업이다. 다른 곳에서 호출 받게 된다.

 

2. MemberListBuilder

 

class MemberListBuilder {
    private val employeeList = mutableListOf<Member>()

    fun member(lambda: MemberBuilder.() -> Unit) =
        employeeList.add(MemberBuilder().apply(lambda).build())

    fun build() = employeeList
}

 

Group의 데이터 클래스에 Member가 리스트 형태로 선언돼있기 때문에 Member의 개수는 1개 이상이 될 수 있다. 그래서 복수의 Member에 대해서 처리할 수 있는 MemberListBuilder가 이 부분을 담당한다. 레드벨벳 초기화 코드에서 member { ... } 로 호출한 부분은 바로 이 MemberListBuilder 클래스의 내부 함수를 호출한 것이다. 함수 내부를 보면 받아온 정보를 가지고 바로 MemberBuilder() 클래스 내부의 build() 함수를 통해 멤버를 생성하고 내부 배열 변수(employeeList)에 추가한다. build() 함수에서는 가지고 있는 배열 정보를 리턴한다.

 

3. CompanyBuilder

 

class CompanyBuilder {
    private var name = ""

    fun name(lambda: () -> String) {
        this.name = lambda()
    }

    fun build() = Company(name)
}

 

CompanyBuilder는 말단 노드라 MemberBuilder랑 생긴게 거의 비슷하다. 굳이 다시 한번 설명하지 않아도 될 것 같다. lambda 인자로 값을 받고 build() 에서 현재 클래스 값을 전달한다.

 

4. GroupBuilder 

 

class GroupBuilder {
    private var name = ""
    private var company = Company("")
    private val employees = mutableListOf<Member>()

    fun name(lambda: () -> String) {
        name = lambda()
    }

    fun company(lambda: CompanyBuilder.() -> Unit) {
        company = CompanyBuilder().apply(lambda).build()
    }

    fun members(lambda: MemberListBuilder.() -> Unit) =
        employees.addAll(MemberListBuilder().apply(lambda).build())

    fun build() = Group(name, company, employees)
}

 

GroupBuilder 클래스에는 지금까지 만들어왔던 builder를 포함하고 있다. name 함수에서는 Group의 이름을 정하고, company 함수에서는 CompanyBuilder를 통해서 company 속성값 정보를 셋팅한다. members 함수에서도 마찬가지로 MemberListBuilder 클래스를 통해 현재 입력된 모든 멤버의 정보를 입력한다. GroupBuilder() 또한 build() 함수를 호출해서 현재 Group 클래스를 최종적으로 반환한다. 

 

GroupBuilder 클래스의 build() 함수를 효출하는 부분은 따로 함수를 만들어야하는데 이렇게 만들면 된다.

 

fun group(lambda: GroupBuilder.() -> Unit): Group {
    return GroupBuilder().apply(lambda).build()
}

 

선언부에서 알 수 있듯이 가장 먼저 호출한 함수는 group {..} 이었다.

 

전체 코드는 다음과 같다.

 

Builder 패턴 구현 부분 코드

 

데이터 초기화 부분 코드

 

 

'컴퓨터공부 > 안드로이드' 카테고리의 다른 글

안드로이드 그림자(Shadow) 효과 넣기  (0) 2020.04.18
Kotlin - Coroutine  (0) 2020.04.15
Kotlin으로 깔끔한 Builder를 만들어보자  (0) 2020.04.14
Exoplayer2 사용하기  (0) 2020.04.12
FragmentManagers Android  (0) 2020.04.06
ViewModelProviders.of deprecated  (0) 2020.04.06

RxJava: defer, fromCallable

컴퓨터공부/안드로이드 2020. 2. 15. 16:16 Posted by 아는 개발자

1. defer


Observable 클래스내에 포함된 defer() 함수는 관찰하고 있는 대상의 값을 구독한 이후 시점부터 볼 때 사용한다. 즉 subscribe 함수가 불린 시점부터 대상의 값을 관찰한다. 좀 더 이해를 쉽게 하고자 Person이라는 클래스를 만들어봤다.

class Person {
    var name: String = "None"

    fun observableName(): Observable<String> 
            = Observable.just(name)

    fun observableDeferName(): Observable<String>
            = Observable.defer { Observable.just(name) }
}


Person 클래스에는 수정이 가능한 name 변수와 name을 Observable로 변환해주는 observableName() 함수, 그리고 코드는 거의 비슷한데 앞에 defer 함수가 붙어있는 observableDeferName() 함수가 있다. 이 두 함수의 차이를 알아보고자 onCreate 함수에서 Person 클래스에서 선언한 함수값을 이용해 다음과 같이 코드를 짜봤다. 두 함수를 통해 Observable 객체를 만든 후 person의 name 변수 값을 수정한 다음 Observable 객체에서 구독받은 값을 출력하는 간단한 예제다.

val person = Person()
val observableName = person.observableName()
val observableDeferName = person.observableDeferName()

person.name = "selfish developer"

observableName.subscribe {
    Log.d(TAG, "observableName: " + it)
}

observableDeferName.subscribe {
    Log.d(TAG, "observableDeferName: " + it)
}

실행 결과 출력되는 로그는 다음과 같다.



observableDeferName은 subscribe된 시점부터 값을 보기 때문에 수정한 person 객체의 name 값이 갱신되었고 observableName은 생성 시점의 값을 받아오기 때문에 초기 값으로 세팅된 값을 가져온다. 


그런데 아래 코드는 똑같은 값을 출력한다.

person.name = "selfish developer"
person.observableName().subscribe {
    Log.d(TAG, "person.observableName(): " + it)
}

person.observableDeferName().subscribe {
    Log.d(TAG, "person.observableDeferName(): " + it)
}


그 이유는 관찰하고 있는 두 객체의 생성 시점이 모두 name 변수값이 업데이트 된 이후기 때문이다. 관찰용 객체를 함수로 선언하지 않고 Person 클래스 내의 변수로 바꾸면 앞서 보인 예시와 동일하게 서로 다른 값을 출력하게 된다.

class Person {
    var name: String = "None"

    val observableName 
            = Observable.just(name)
    val observableDeferName 
            = Observable.defer { Observable.just(name) }
}

person.name = "selfish developer"

observableName.subscribe {
    Log.d(TAG, "observableName: " + it)
}

observableDeferName.subscribe {
    Log.d(TAG, "observableDeferName: " + it)
}


2. fromCallable


Observable 클래스의 형제격인 Maybe, Flowable, Single 클래스에서는 fromCallable 함수가 defer와 같은 역할을 한다. 테스트를 해보고자 Observable과 동일하게 코드를 짜봤다.

class Person {
var name: String = "None"

fun singleName(): Single<String>
        = Single.just(name)

fun singleCallableName(): Single<String>
        = Single.fromCallable { name }
}

val singleName = person.singleName()
val singleCallableName = person.singleCallableName()

person.name = "selfish developer"

singleName.subscribe { it ->
    Log.d(TAG, "singleName: " + it)
}

singleCallableName.subscribe { it ->
    Log.d(TAG, "singleCallableName: " + it)
}

실행 결과 defer와 동일하게 fromCallable이 붙은 함수는 구독한 시점 이후에 갱신된 값을 읽어온다.




3. 총평


실행 결과는 신기하기도 하지만 실제로 사용할때는 꽤 실수가 잦을 것 같은 기능인 것 같다. 가능하면 매번 새로운 Observable 객체를 생성하는 함수를 따로 변수로 만들어두지 않고 바로 구독하게 해 만들어서 갱신 타이밍 이슈를 피하는게 좋지 않을까 싶다.

코틀린 apply, also, let, run, with

컴퓨터공부/안드로이드 2020. 2. 9. 14:21 Posted by 아는 개발자


자바에 비해 코틀린이 가지는 가장 큰 장점은 코드를 간결하게 작성 할 수 있는 것이라고 생각하는데 모든 객체에 기본적으로 제공하는 범위함수인 apply, also, let, run, with 들이 이 이점을 살리는데 큰 도움이 된다.


이 함수의 차이점에 대해서 설명한 글은 코틀린 공식 문서도 있고 다른 개발 블로그에도 무수히 많지만, 범위 함수에서 강조하는 수신객체와 람다식과 관련된 내용은 문서를 읽는 것 보다는 직접 코드를 짜면서 체험해 볼 때 이해하기가 쉽다. 이번 포스트에서는 apply, also, let, run, with를 언제 사용해야하는지에 대해서 수신객체에 관련된 내용을 제외하고 사용이 필요한 경우만 간략하게 소개해보려고 한다.


1. apply 


apply는 객체의 property 값을 적용할 때 사용한다. 어떤 객체를 선언할 때 생성자만으로 값을 세팅할 수 없다면 apply를 통해서 값을 따로 붙여서 연속적으로 값을 세팅할 수 있다. 아래의 두 코드는 모두 동일한 결과를 가지는데 apply 함수를 사용한 경우가 더 명시적이다.


val adam = Person("Adam").apply { 
    age = 20     
    city = "London"
}

val adam = Person("Adam")
adam.age = 20
adam.city = "London"


2. also 


also는 속성 변경을 허용하지 않고 로그를 출력하거나 값의 유효성을 검증하고 싶을 때 사용한다. 아래 코드처럼 also 문 앞에 있는 코드는 property를 바꿀 수 없다. 나도 모르게 저지르는 실수를 사전에 차단하고 싶을 때 사용하면 유용하다.


val numbers = mutableListOf("one", "two", "three")
numbers
    .also { println("The list elements before adding new one: $it") }
    .add("four")


3. let 


객체가 null이 아닌 코드를 실행하는 경우 사용한다. Null pointer 에러로 크래쉬가 나는걸 막을 때 꽤나 유용한 범위함수다. 주로 많이 사용하는 코드다. Java의 Optional.ofNullable 이렇게 길게 쓸 필요가 없어 간결해서 좋다.


val str: String? = "Hello"   
//processNonNullString(str)       // compilation error: str can be null
val length = str?.let { 
    println("let() called on $it")        
    processNonNullString(it)      // OK: 'it' is not null inside '?.let { }'
    it.length
}


4. run 


객체에 포함된 함수를 실행하고 그 결과를 반환할 때 사용한다. 아래 코드를 보면 리스트 타입인 numbers에 포함된 add 함수를 run 내부에서 실행하고 있고 마지막 구문에 e로 끝나는 문자열의 개수를 countEndsWithE 변수에 넣어주고 있다. 실행 결과는 주석으로 처리된 부분을 읽어보면 된다.


나는 이 함수는 자주 사용하지는 않는다. 굳이 함수를 먼저 실행한 다음에 리턴타입이 필요할 일도 없어서. apply랑 비슷한것 같기는 한데 그만한 유용성은 못찾겠다.


val numbers = mutableListOf("one", "two", "three")
val countEndsWithE = numbers.run { 
    add("four")
    add("five")
    count { it.endsWith("e") }
}
println("numbers: " + numbers)
println("There are $countEndsWithE elements that end with e.")
// numbers: [one, two, three, four, five]
// There are 3 elements that end with e.


5. with 


null이 될 수 없는 객체의 값을 출력할 때 사용한다. also랑 거의 차이가 없어서 나는 잘 사용하지 않는다. 


val numbers = mutableListOf("one", "two", "three")
with(numbers) {
    println("'with' is called with argument $this")
    println("It contains $size elements")
}