관리 메뉴

막내의 막무가내 프로그래밍 & 일상

[안드로이드] 코틀린 범위지정함수(Scoping Functions) - let, run, with, apply, also - 본문

안드로이드/코틀린 & 아키텍처 & Recent

[안드로이드] 코틀린 범위지정함수(Scoping Functions) - let, run, with, apply, also -

막무가내막내 2021. 6. 28. 18:03
728x90

[코틀린의 특징]

코틀린은 젯브레인사에서 만든 언어로 JVM에서 동작하고 자바와 100% 호환된다는 특징을 갖고 있습니다.
이밖에도 Null Safety, 함수형프로그래밍, 확장함수, 코드의 간결함 등 다양한 이점이 있는 언어입니다. 자바를 업그레이드 시킨 언어라고 봐도 과언이 아닙니다.
이러한 이점 덕분에 안드로이드는 2017년 Kotlin을 공식 개발언어로 채택되었고 서버개발에도 코틀린을 사용하는 기업이 조금씩 증가하고 있습니다.

이번 포스팅에서는 다양한 코틀린 특징 중 범위지정함수(Scoping Functions)에 대해 포스팅을 해보려고 합니다.

범위지정함수를 다루기에 앞서 확장 함수고차 함수 대해 먼저 보는게 도움이 될 것 같아 살펴보고 가겠습니다.


[확장 함수(Extension Function)]

기존의 클래스에 함수를 추가 및 확장하는 것을 확장 함수라고 합니다.
코틀린은 확장 함수 기능을 사용하여 쉽게 기존 클래스에 함수를 추가할 수 있습니다.
사용 방법은 확장 함수를 추가할 클래스에 점(.)을 찍고 함수 이름을 작성하면 됩니다.
이 객체는 this로 접근할 수 있고 이러한 객체를 리시버 객체라고 합니다.
예제로 확인해보겠습니다.

[예제]

fun main() {
    val num = 6 
    val date = "2021-06-30" // 확장함수 사용 
    println(3.isEven())
    println(num.isEven())
    println(date.toTimestamp())
}

// Int 클래스에 짝수인지 판별하는 isEven() 함수를 확장 
fun Int.isEven() = this % 2 == 0
// 날짜를 타임스탬프로 변환하는 확장함수 
fun String.toTimestamp(): Long = SimpleDateFormat("yyyy-MM-dd", Locale.KOREA).parse(this).time

[결과]

false 
true 
1624978800000

 


[고차 함수(Higher-order function)]

코틀린에서는 함수의 인수로 함수를 전달하거나 함수를 반환할 수 있습니다. (함수형 프로그래밍 특성을 갖고있어 함수가 일급객체입니다.)
이렇게 다른 함수를 인수로 받거나 반환하는 함수를 고차 함수라고 합니다.
자바였으면 콜백을 구현할 때 인터페이스를 만들어야하는데 코틀린은 이를 고차 함수를 사용해 쉽게 구현할 수 있습니다.
예제로 확인해보겠습니다.

[예제]

fun main() {
    val x = 12
    val y = 6
    // 다양한 방법으로 콜백을 받을 수 있습니다
    add(x, y, { println(it) })
    add(x, y) { println(it) }
    add(x, y) { result -> println(result) }
    subtract(x, y, {// callback에 double값 전달
        it - 0.5
    })
}

// 인자 : 숫자, 숫자, 하나의 숫자를 인수로 하는 반환값이 없는 함수
// 코틀린에서 Unit 은 자바에서의 void 를 의미합니다
fun add(x: Int, y: Int, callback: (sum: Int) -> Unit) {
    callback(x + y)
}

// 인자 : 숫자, 숫자, 하나의 숫자를 인수로 하는 반환값이 있는 함수
fun subtract(x: Int, y: Int, callback: (sum: Int) -> Double) {
    val result: Double = callback(x - y)
    print("substract() result -> $result")
}


[결과]

18
18
18
substract() result -> 5.5

 


[범위지정함수(Scoping Functions)]

코틀린 표준 라이브러리는 객체의 Context내에서 코드의 블록을 실행하는 것이 유일한 목적인 몇 가지 함수를 가지고 있습니다. 람다 식이 제공되는 객체에서 이러한 함수를 호출하면 임시 스코프가 형성됩니다.
이 범위 에서는 객체의 이름 없이 객체에 엑세스할 수 있습니다.
좀 더 이해하기 쉽게 설명하면 특정 범위(Scope) 블록({ }) 내에서 특정 객체를 this, it을 통해 접근 및 조작할 수 있고 이를 통해서 코드를 간결하게 만들 수 있다는 장점을 가진 함수입니다.
이러한 기능을 가진 함수를 범위지정함수(Scoping Functions)라고 하며 let, run, with, apply, also 총 다섯가지가 있습니다.

출처 : https://medium.com/@fatihcoskun/kotlin-scoping-functions-apply-vs-with-let-also-run-816e4efb75f5
출처 : https://kotlinlang.org/docs/scope-functions.html#function-selection

위 사진들은 다섯가지 범위지정함수에 대해 정리된 표와 사용목적들입니다. (고차함수와 확장함수 개념이 적용되어 있어서 앞서 살펴보았습니다.)
사진을 보면 뭔가 비슷하게 생겼는데 조금씩 다른게 느껴지시나요?
범위지정함수는 서로 유사한점이 많아 어느 상황에서 어떤 함수를 쓰는게 적절한지 햇갈리는 경우가 많습니다.
그래서 이번 포스팅에서 이 다섯가지 범위지정함수에 대해 알아보려고 합니다.

 

 

1. let

 fun<T, R> T.let(block : (T) -> R) : R

let 함수는 자기 자신을 인수로 전달하고 수행된 결과 즉 블록의 마지막 값을 반환합니다. 인수로 전달한 객체는 it으로 참조합니다. 주로 ?. 연산자와 함께 사용하여 null이 아닐때만 실행하는 코드로 자주 사용합니다. ( if(example != null) 대체가능)

[예제 1]

fun main() {
    var company: String? = "아이티"
    company?.let { // ?. 는 null 이 아닌 경우 실행하겠다는 의미입니다 
        println("첫번째 let -> $it")
    }
    company = null
    company?.let { println("두번째 let -> $it") }
}


[결과 1]

첫번째 let -> 아이티

"두번째 let" 처럼 company가 null인 경우 다음 블록이 실행되지 않습니다.


[예제 2]

fun main() {
    val str = "1234"
    val result = str.let {
        it + "aaaa" //블록의 마지막 결과값을 반환한다.
    }
    println("str 값 -> $str")
    println("result 값 -> $result")
}


[결과 2]

str 값 -> 1234
result 값 -> 1234aaaa

블록의 마지막 결과값이 result에 반환됩니다.


[안드로이드 예제]

@BindingAdapter("htmlText")
fun TextView.setHtmlText(html: String?) {
    text = html?.let { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_COMPACT) }
}

null safe 용도로 많이 사용하고 있습니다.

 

 

2. run

fun<R> run(block : () -> R ) : R //익명함수처럼 사용하는 방법 
fun<T, R> T.run(block : T.() -> R ) : R

run 함수는 익명 함수처럼 사용하는 방법과 객체에서 호출하는 방법을 모두 제공합니다. 앞선 let과 마찬가지로 블록의 마지막 결과를 반환합니다. 블록안에 선언된 변수는 모두 임시로 사용되는 변수입니다. 그래서 복잡한 계산이나 임시변수가 많이 필요할때 유용합니다.

[예제 1]

fun main() {
    val result = run { 
        val name = "홍길동" 
        val hobby = "프로그래밍" 
        val age = 27 
        "이름 : $name, 취미 : $hobby, 프로그래밍 : $age" 
    } 
    print(result) //print(name) 컴파일에러 }
}

익명함수처럼 사용한 예제입니다.
복잡한 계산을 위해 여러 임시 변수가 필요할 때 유용하게 사용할 수 있습니다. run 함수 내부에서 선언되는 변수들은 블록 외부에 노출되지 않으므로 변수 선언 영역을 확실하게 분리가 가능하기 때문입니다.

[결과 1]

이름 : 홍길동, 취미 : 프로그래밍, 프로그래밍 : 27



[예제 2]

fun main() {
    val str = "abCdEfG"
    val result1 = str.run {
        // receiver 로 암시적 전달
        this.toLowerCase() //this == str
    }
    val result2 = str.run {
        toLowerCase()// this 생략가능, 마지막 코드 블록 수행결과 반환
    }
    println("result1 -> $result1")
    println("result2 -> $result2")
    println("str -> $str")
}

객체에서 호출한 예제입니다.
코드블록으로 객체 자신이 리시버 객체로 전달되며 this를 생략할 수 있습니다. 이를 통해 수신 객체의 반복적인 접근을 막아 코드의 간결성에 큰 도움을 줍니다.
run 도 ?. 안전한(null safe) 호출을 사용할 수 있습니다.

[결과 2]

result1 -> abcdefg
result2 -> abcdefg
str -> abCdEfG

블록의 마지막 결과값을 result에 반환합니다.


[안드로이드 예제]

private fun provideOkHttpClient(
        interceptor: AppInterceptor
    ): OkHttpClient = OkHttpClient.Builder()
        .run { //this 생략가능
            addInterceptor(interceptor)
            build() //빌더패턴 build()한 결과값 반환
        }
binding.vpViewpager.run { //this(binding.vpViewpager) 생략가능
    adapter = pagerAdapter
    val dpValue = 54
    val d = resources.displayMetrics.density
    val margin = (dpValue * d).toInt()
    clipToPadding = false
    setPadding(margin, 0, margin, 0)
    pageMargin = margin / 2
}
// run을 안쓸 경우 binding.vpViewPager.adapter = ?, binding.vpViewPager.clipToPadding = ? 
// 으로 하나하나 다 세팅을 해줘야하는 번거로움이 있습니다.

마지막 블록값을 리턴값으로 넘겨줄때와 반복적인 초기화 작업을 간편하게 하기 위해 주로 사용합니다.

3. with

fun<T, R> with(receiver : T, block T.() -> R) : R

with 함수는 인수로 객체를 받고 블록에 리시버 객체로 전달합니다. 그리고 블록에서 수행된 마지막 결과를 반환합니다. 리시버 객체로 전달된 객체는 this로 접근할 수 있으며 생략도 가능합니다. with()를 사용시 주의할 점은 null safe 호출이 불가능하므로 인자가 절대로 null 이 아닌게 확실할 경우에만 사용해야 합니다.


[예시]

fun main() {
    val person = Person("홍길동", 27, "서울")
    with(person) {
        println(this.name)
        println(age) //this 생략가능
        println(city)
    }
    println()
    
    val result1 = with(person) {
        println("Result With") // 블록 마지막 결과값 println() 함수의 리턴타입인 Unit 반환, Unit은 자바로 치면 void와 같습니다.
    }
    println()
    
    val result2 = with(person) {
        "RESULT" // 블록 마지막 결과값 String 반환 
    }
    println("result1 -> $result1")
    println("result2 -> $result2")
}
data class Person(val name: String, val age: Int, val city: String)

블록의 마지막 결과값이 반환되고 코드 블록으로 객체 자신이 리시버 객체로 전달되고 this를 생략할 수 있습니다.

[결과]

홍길동
27
서울

Result With

result1 -> kotlin.Unit
result2 -> RESULT


[안드로이드 예제]

class AppInterceptor : Interceptor {
        @Throws(IOException::class)
        override fun intercept(chain: Interceptor.Chain)
                : Response = with(chain) {
            val newRequest = request().newBuilder()
                .addHeader("X-Naver-Client-Id", "33chRuAiqlSn5hn8tIme")
                .addHeader("X-Naver-Client-Secret", "fyfwt9PCUN")
                .build()

            proceed(newRequest)
        }
    }
private val viewModel: HomeViewModel by viewModel()
        with(viewModel) {
            goSearch.observe(this@HomeFragment, Observer {
                val direction: NavDirections =
                    HomeFragmentDirections.actionBottomNav1ToSearchFragment()
                findNavController().navigate(direction)
            })

            goAlarm.observe(this@HomeFragment, Observer {
                val direction: NavDirections =
                    HomeFragmentDirections.actionBottomNav1ToAlarmFragment()
                findNavController().navigate(direction)
            })
        }

run과 마찬가지로 초기화 작업 및 마지막 블록 결과값을 리턴할 때 주로 사용합니다. 단 with 인자값이 null이 절대로 올 일이 없는 경우에만 사용합니다.

4. apply

fun<T> T.apply(block : T.() -> Unit ) : T

apply() 함수는 블록에 객체 자신이 리시버 객체로 전달되고 이 객체가 반환됩니다. 객체의 상태를 변화시키고 그 객체를 다시 반환할 때 주로 사용합니다.


[예제]

fun main() {
    val person = Person("에이스", 20, "이스트블루")
    val result = person.apply {
        this.name = "흰수염"
        age = 75 // this 생략가능
        city = "신세계 스핑크스"
    }
    println("person -> $person")
    println("result -> $result")
}

data class Person(var name: String, var age: Int, var city: String)


[결과]

person -> Person(name=흰수염, age=75, city=신세계 스핑크스)
result -> Person(name=흰수염, age=75, city=신세계 스핑크스)

apply 함수를 사용할 시 person객체(리시버)가 자신의 상태를 변화하고 전달 받은 수신객체인 즉 자기 자신을 리턴하기 때문에 result 도 person과 같은 값이 나오는 것을 볼 수 있습니다.


[안드로이드 예제]

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding: ItemBoardBinding = DataBindingUtil.inflate(
            LayoutInflater.from(parent.context),
            R.layout.item_board,
            parent,
            false
        )
        return ViewHolder(binding).apply {
            binding.root.setOnClickListener { view ->
                val position = bindingAdapterPosition.takeIf { it != RecyclerView.NO_POSITION }
                    ?: return@setOnClickListener
                itemClick(items[position])
            }
        }
    }

안드로이드에서는 객체를 생성함과 동시에 값을 세팅 후 바로 사용하기 위해 자주 사용합니다. 예를들어 리사이클러뷰 어댑터에서 뷰홀더를 생성할때 사용하고 있습니다.


5. also

fun<T> T.also(block: (T) -> Unit): T


also 함수는 수신 객체 지정 람다에 매개변수 T 로 코드 블록 내에 명시적으로 전달되며 코드 블록 내에 전달된 수신객체를 그대로 다시 반환 합니다.
수신 객체 람다가 전달된 수신 객체를 전혀 사용 하지 않거나 수신 객체의 속성을 변경하지 않고 사용하는 경우 also 를 사용합니다. 또한, 객체의 데이터 유효성을 확인하거나, 디버그, 로깅 등의 부가적인 목적으로 사용할 때에 적합합니다.
also 는 apply 와 마찬가지로 수신 객체를 반환 하므로 블록 함수가 다른 값을 반환 해야하는 경우에는 also 를 사용할수 없습니다.

[예제]

fun main() {
    val name = "HongGillDong"
    val fixedName = name.also {
        it.toUpperCase()
    }
    println("name -> $name")
    println("fixedName -> $fixedName")
    
    val person = Person(name = "홍길동", age = 27, city = "서울")
    val result = person.also {
        it.name = "백종원"
        it.age = 50
        it.city = "부산"
    }
    println("person -> $person")
    println("result -> $result")
    
    val numbers = mutableListOf("one", "two", "three")
    numbers.also { println("numbers: $it") }.add("four")
    println("numbers -> $numbers")
}

data class Person(var name: String, var age: Int, var city: String)


[결과]

name -> HongGillDong
fixedName -> HongGillDong
person -> Person(name=백종원, age=50, city=부산)
result -> Person(name=백종원, age=50, city=부산)
numbers: [one, two, three]
numbers -> [one, two, three, four]


[안드로이드 예제]

@Database(entities = [GardenPlanting::class, Plant::class], version = 1, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun gardenPlantingDao(): GardenPlantingDao
    abstract fun plantDao(): PlantDao

    companion object {

        // For Singleton instantiation
        @Volatile private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }


저는 평소 also는 사용하지 않고 있습니다. 구글 SunFlower 프로젝트에서는 위와 같이 Room 데이터베이스의 인스턴스를 생성하는 경우 사용하고 있습니다.



범위지정함수를 상황에 맞게 잘 사용하면 클린한 코드에 큰 도움이 될 것 같다고 느꼈습니다.


댓글과 공감은 큰 힘이 됩니다. 감사합니다. !!!






참고 : https://medium.com/@limgyumin/%EC%BD%94%ED%8B%80%EB%A6%B0-%EC%9D%98-apply-with-let-also-run-%EC%9D%80-%EC%96%B8%EC%A0%9C-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94%EA%B0%80-4a517292df29

 

코틀린 의 apply, with, let, also, run 은 언제 사용하는가?

원문 : “Kotlin Scoping Functions apply vs. with, let, also, and run”

medium.com

https://kotlinlang.org/docs/scope-functions.html

 

Scope functions | Kotlin

 

kotlinlang.org

728x90
Comments