Kotlin in Action 3장 - Extension

Tnalxmsk·2024년 1월 22일

Kotlin

목록 보기
4/8

코틀린은 표준 자바 컬렉션을 활용하여 자바 코드와의 상호작용이 수월하도록 하기 위해 자체 컬렉션을 제공하지 않습니다.

하지만 코틀린 컬렉션은 자바 컬렉션과 똑같은 컬렉션이지만 더 많은 기능이 존재합니다.

이것이 어떻게 가능한지 코틀린에서 제공하는 확장 기능을 이용해 joinToString 함수를 단계적으로 설계하며 알아보겠습니다.

joinToSring의 예시

코틀린이 지원하는 여러 기능을 사용하지 않은 코드입니다.

fun <T> joinToString(
    collection: Collection<T>,
    separator: String,
    prefix: Srting,
    postfix: String
) : String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    
    result.append(postfix)
    return result.toString()
    }
}

val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")" ))

-> (1; 2; 3)

이 함수는 제네릭합니다. 어떤 타입의 값을 원소로 하는 컬렉션이든 처리할 수 있습니다.

이 함수를 그대로 사용해도 되지만 코드가 번잡하다는 것을 알 수 있습니다.

함수 호출 마다 네 인자를 모두 전달하지 않을 수는 없을까요?

Named Arguments

Functions | Kotlin

위 joinToSring() 함수를 호출 하기 위해 인자를 작성해야 합니다. 그러나 인자의 수가 많은 경우 가독성이 떨어진다는 문제가 발생합니다.

이를 위해 코틀린은 Named Arguments(이름 붙은인인자) 기능을 제공합니다.

joinToSring(list, separator=";", prefix="(", postfix=")")

다음과 같이 함수 호출 시 함수에전달하는 인자의 이름을 명시하여 가독성을 높힐 수 있습니다. 주의할 점은 하나라도 인자를 명시하고 나면 그 뒤에 오는 모든 인자에 이름을 꼭 명시해야 합니다

Default Parameter Value


Functions | Kotlin

객체 지향 언어에서 제공하는 중요한 기능 중 하나는 메서드 오버로딩입니다. 메서드 오버로딩은 함수의 이름은 같지만 함수의 시그니쳐가 다른 여러 함수를 만들 수 있는 기능입니다. 그러나 오버로딩한 메서드가 너무 많아진다는 것도 문제가 될 수 있습니다.

코틀린에서는 디폴트 값을 지정하여 사용자에게 편의를 제공합니다.

이를 통해 joinToSring 함수를 개선해봅시다.


fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ", ",
    prefix: Srting = "",
    postfix: String = ""
) : String {
    ...
}

joinToString(list)       // separator, prefix, postfix 생략
joinToString(list, "; ") // separator를 ;로 지정. prefix, postfix 생략

함수의 Default Parameter 값은 함수를 호출하는 쪽이 아닌 선언 쪽에서 지정됩니다. 따라서 어떤 클래스 안에 정의된 함수의 디폴트 값을 바꾸고 그 클래스가 포함된 파일을 재컴파일하면 그 함수를 호출하는 코드 중에 값을 지정하지 않은 모든 인자는 자동으로 바뀐 디폴트 값을 적용받습니다.

또한 자바에서 함수는 어떤 클래스 안에 작성하였습니다. 그러나 코틀린은 그러지 않아도 됩니다. 코틀린에선 함수를 클래스 안에 선언할 필요가 없습니다.

최상위 함수와 프로퍼티

자바에선 모든 코드를 클래스의 메서드로 작성합니다. 그러나 실전에선 어느 클래스에 포함시키기 어려운 코드가 많이 생깁니다. 코틀린에선 이런 무의미한 클래스가 필요 없습니다. 대신 함수를 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 됩니다.

package strings

fun joinToSring(...): String { ... }

코틀린은 JVM 기반의 언어입니다. JVM은 클래스 안에 들어있는 코드만을 실행합니다. 그래서 컴파일러는 이 파일 strings를 컴파일 시 새로운 클래스를 정의해줍니다. 코틀린만 사용하는 어느 클래스가 생깁니다.

최상위 프로퍼티

함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있습니다.

var buttonClickCount = 0

fun addButtonClickCount {
    buttonCount++
}

버튼의 클릭 횟수를 저장하는 프로퍼티를 만들 수 있습니다.

이러한 프로퍼티의 값은 정적 필드에 저장됩니다.

const 변경자를 추가해 프로퍼티를 public static final 필드로 컴파일하게 만들 수 있습니다.

const val MAX_COUNT = 100

이제 joinToString 함수를 더욱 개선해봅시다.

확장 함수, 확장 프로퍼티

확장 함수


Extensions | Kotlin

코틀린은 Java와의 상호 운용성을 고려하여 만들어졌습니다. 기존 코드와 코틀린 코드를 자연스럽게 통합하는 것은 코틀린의 핵심 목표 중 하나였습니다. 다양한 프로젝트, 프레임워크는 자바 라이브러리를 기반으로 만들어집니다. 코틀린은 기존 자바로 이루어진 프로젝트를 통합하는 경우 직접 변환할 수 없거나 미처 변환하지 않은 기존 자바 코드를 처리할 수 있어야 했습니다. 이러한 기존 자바 API를 재작성하지 않고 코틀린이 제공하는 여러 편리한 기능을 사용할 수 있는 방법이 존재합니다. 바로 확장 함수가 그런 역할을 합니다.

확장 함수는 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수입니다.

확장 함수를 만들려면 추가하려는 함수 이름 앞에 함수가 확장한 클래스의 이름을 작성하면 됩니다. 클래스 이름을 수신 객체 타입이라 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)를 수신 객체라고 부릅니다.

위 사진의 예시에서 MutableList<Int>가 수신 객체 타입이 되며, this라는 지시어가 수신 객체가 됩니다.

다른 예로

println("kotlin extension function".lastChar())

여기선 String이 수신 객체 타입이며, “kotlin extension function”이 수신 객체가 됩니다.

확장 함수는 클래스의 일반 메서드 본문에서 this를 사용할 때와 마찬가지로 본문에서 this를 사용할 수 있으며 생략할 수 있습니다.

확장 함수 내부에서 일반적인 인스턴스 메서드의 내부에서와 마찬가지로 수신 객체의 메서드나 프로퍼티를 바로 사용할 수 있습니다. 하지만 확장 함수가 캡슐화를 깨지는 않습니다.

한 가지 유의할 점은 private 멤버나 protected 멤버를 사용할 수는 없다는 것입니다.

확장 함수를 사용한 joinToString()

이제 joinToString() 함수를 확장 함수 기능을 이용하여 코틀린스럽게 만들어봅시다.

// 확장 함수로 만든 joinToString
fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: Srting = "",
    postfix: String = ""
) : String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)

    result.append(postfix)
    return result.toString()
    }
}
val list = listOf(1, 2, 3)
println(list.joinToString(separator="; "))
-> (1; 2; 3)

println(list.joinToString(separator=" "))
-> (1 2 3)

확장 함수는 정적 메서드와 같은 특징을 가지므로, 하위 클래스에서 오버라이드할 수는 없습니다.

확장 함수는 클래스의 일부가 아닙니다. 확장 함수는 클래스 밖에 선언됩니다.

만약 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수에 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않습니다.

확장 프로퍼티

Extensions | Kotlin

확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 구현할 수 있다. 프로퍼티라는 이름으로 불리지만 상태를 저장할 방법이 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다. 그러나 프로퍼티 문법으로 더 짧은 코드 작성이 가능하다.

val String.lastChar: Char
    get() = get(length - 1)

확장 프로퍼티도 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐.

Backing Field가 없어 최소한 getter는 꼳 정의를 해야 한다.

확장 프로퍼티를 사용하는 방법은 멤버 프로퍼티를 사용하는 방법과 같다.

println("extension properties".lastChar)

마치며

코틀린의 확장 기능과 default argument 등을 이용하여 joinToString 함수를 단계적으로 개선하는 과정을 진행했습니다.
코틀린의 이러한 기능을 이용하면 불필요하게 반복되는 함수를 줄일 수 있으며 재사용성이 용이한 코드를 작성하는데 큰 도움이 됩니다.

참고

0개의 댓글