Kotlin - 함수 정의와 호출

2021. 4. 24. 23:16 Kotlin/Kotlin

 

오늘은 코틀린의 함수 정의와 호출에 대해 다루어 본다.

 

컬렉션 객체 만들기
fun createHashSet() = hashSetOf(1, 7, 53)

fun createArrayList() = arrayListOf(1, 7, 53)

fun createHashMap() = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")

 

기본적으로 코틀린은 컬렉션을 만드는 함수를 기본 라이브러리에 내장이 되어 있다. 또한 마지막에 hashMap을 만드는 함수 안에 "to"라는 키워드가 있는데, 사실 키워드가 아니고 일반 함수이다. 이점은 뒤에서 자세히 설명한다. 또한 생성하는 컬렉션 객체는 코틀린만의 컬렉션 객체가 아니고, 자바의 컬렉션 객체를 생성한다. 하지만 자바에서 제공하고 있는 기능보다 더 유용한 기능들을 제공한다.

 

fun collectionOperate() {
    val list = listOf("one", "two", "three")
    //리스트의 마지막 원소를 가져온다.
    println(list.last())
    
    val numbers = setOf(1, 14, 2)
    //숫자로 이루어진 컬렉션(Set)에서 가장 큰 값을 가져온다.
    println(numbers.max())
}

 

디폴트 파라미터 값
fun <T> defaultParamValue(collection: Collection<T>, separator: String = ", ", prefix: String = "", postfix: String = ""): String {
    val result = StringBuilder(prefix)
    for ((index, elem) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(elem)
    }
    result.append(postfix)
    return result.toString()
}

fun main() {
    println(defaultParamValue(list))
}

->
1, 2, 3

 

만약 이름있는 파라미터를 사용하면, 함수 호출시 파라미터 순서를 지정할 필요가 없어진다.

 

println(defaultParamValue(list, postfix = "#", prefix = ";"))

 

이러면, 순서와 상관없이 파라미터를 지정할 수 있고, seperate라는 파라미터도 디폴트 값이 적용된다.

 

최상위 함수와 프로퍼티
fun topLevelFunction() {
    println("top level function")
}

 

코틀린에서 클래스 밖에 함수를 정의하면 마치 자바의 static method와 같이 작동한다. 하지만 자바와는 다르게 클래스명을 임포트 할 필요 없이 함수명을 바로 임포트해서 사용할 수 있다. 어떻게 이렇게 실행 될 수 있을까? 이는 바로 JVM이 컴파일할때 새로운 클래스를 정의해주기 때문이다.(file이름을 기반으로 클래스 생성) 실제로 자바에서 해당 함수를 가져다 쓰려면 컴파일러가 만들어준 클래스명을 명시해서 사용해야 한다. ex) JoinKt.joinToString(..)

 

함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 기본적으로 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다. 겉으론 상수처럼 보이지만, 실제로는 게터를 이용해야한다니 조금 이상하다. 그렇게 때문에 더 자연스럽게 자바처럼 public static final 같은 구문을 넣어주는 것이 자연스럽기 때문에 최상위 프로퍼티는 보통 아래와 같이 정의한다.

//Java : public static final String UNIX_LINE_SEPARATOR = "\n"
const val UNIX_LINE_SEPARATOR = "\n"

 

확장 함수와 확장 프로퍼티

기존 자바 API를 이용해 확장함수 및 확장 프로퍼티를 정의하는 것이 가능하다. 그 말은 우리가 지금까지 사용했던 setOf(), arrayListOf()등을 호출할 수 있었던 이유다. 다음 예제를 보자.

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

fun main() {
    println("Kotlin".lastChar())
}

 

함수명 앞에 "String." 을 붙여서 확장할 클래스명을 넣어준다. 이것을 수신객체타입이라 부르고, 함수 내용에 "this" 키워드로 수신객체 타입을 참조할 수 있다. 이 this 키워드로 참조하는 객체를 수신객체(위에서 "Kotlin"이라는 문자열이 수신객체가 된다.)라고 부른다. 여기서 기억할 것은 확장 함수가 캡슐화를 깨지않는다는 것이다. 그 이유는 수신 객체의 private 메서드 같은 것은 접근할 수 없기 때문이다. 여기서 만약, 다른 패키지의 많은 확장함수를 임포트해서 사용하는데 함수의 시그니쳐 등이 모두 동일하면 충돌하는 현상이 발생되는데 이런 경우는 아래와 같이 "as" 키워드로 확장함수의 별칭을 지어줄 수 있다.

import com.example.kotlin.ch_3.lastChar as last

fun main() {
    println("Kotlin".last())
}

 

확장함수로 유틸리티 클래스 만들기

기존 자바 API 혹은 코틀린 클래스를 수신객체로 확장함수를 사용해 유틸클래스를 만들면 유용하다.

fun <T> Collection<T>.joinToString(seperate: String): String {
    val result = StringBuilder()
    for ((i, elem) in this.withIndex()) {
        if (i > 0) result.append(seperate)
        result.append(elem)
    }
    return result.toString()
}

fun main() {
    val list = listOf(1,2,3)
    println(list.joinToString(","))
}

 

이렇게 제네릭 타입으로 확장함수를 만들수 있고, 특정 타입으로 제한하여 확장함수를 만들 수 있다.

fun main() {
    val list = listOf(1,2,3)
    println(list.join2(","))
}

fun Collection<String>.join2(seperate: String): String {
    val result = StringBuilder()
    for ((i, elem) in this.withIndex()) {
        if (i > 0) result.append(seperate)
        result.append(elem)
    }
    return result.toString()
}

 

만약 String을 타입으로 받는 확장함수를 만들었는데, Int를 요소로 가지는 리스트에 대해 확장함수를 호출하면 컴파일 에러가 발생한다. 마지막으로 "확장함수는 함수 오버라이드가 불가능하다." 또한 확장함수와 그 수신객체의 함수의 시그니쳐가 동일하면 수신객체의 멤버 함수를 우선시 한다.

 

상속, 인터페이스 구현관계의 확장함수

아래와 같은 예제가 있다고 가정하자. 코틀린에서 클래스는 final 클래스와 같으므로, 그냥 상속이 안된다. 그렇기 때문에 class 앞에 open 키워드를 넣어준다. 이는 함수 레벨도 동일하다.

open class View {
    open fun click() = println("View clicked")
}

class Button: View() {
    override fun click() {
        println("Button clicked")
    }
}

fun main() {
    val view: View = Button()
    view.click()
}

->
Button clicked

 

위 예제는 상속을 예제로 코드를 작성하였는데, 결과로는 Button의 오버라이드된 메서드의 결과가 출력된다. 그렇다면 아래 예제는 어떨까?

fun View.showOff() = println("I'm a view !")
fun Button.showOff() = println("I'm a button !")

fun main() {
    val view: View = Button()
    view.showOff()
}

->
I'm a view !

 

위 결과는 상속과는 다르게 동작한다. 실제 변수에 담긴 타입은 Button()이지만, 결과는 View의 확장함수로 작동한다.

 

가변 인자 함수
fun <T> listOf(vararg values: T): List<T> {...}

fun main(args: Array<String>) {
    listOf("abc", *args)
}

 

위 listOf는 "vararg"라는 키워드로 가변인자를 받을 수 있는 파라미터를 정의했다. 자바에서는 "..."으로 배열인 가변인자를 받을 수 있지만, 코틀린은 vararg라는 키워드를 이용한다. 그리고 또 다른 자바와의 차이점은 배열자체를 인자로 넘기는 것이 아니고, 배열 변수 앞에 "*"를 붙여서 명시적으로 배열을 풀어서 넣는 것이다. 기술적으로는 스프레드 연산자가 그런 작업을 해준다. 그리고 또하나의 차이점은 자바와는 달리 배열 혹은 인자를 나열하는 것이 아니라, 인자+배열로 여러 값을 함께 인자로 넘길 수 있다.

 

값의 쌍을 다루기

이전에  Map 오브젝트를 만드는 예제에서 "to"라는 키워드를 사용한 적이 있었다. 여기서 "to"는 키워드가 아니고, 함수라고 이야기를 했는데, 이 함수를 바로 중위함수라는 특별한 방식으로 동작하는 함수이다.

fun pair() {
    val pair = 1 to "one"
    println(pair)
}

fun main() {
    pair()
}

->
(1, one)

 

여기서 to, 중위함수는 파라미터가 유일한 함수를 이렇게 호출할 수 있게 되는 것이다. 사실은 "1.to("one")"과 같은 구문이며, 실제로는 to는 파라미터가 하나로 유일한 함수인 것이다. 이것을 실제로 예제로 구현해보면 아래와 같다.

infix fun Int.pair(v: String): Pair<Int, String> {
    return Pair(this, v)
}

fun main() {
    1 pair "one"
}

 

함수의 정의를 보면, Int의 확장함수로 선언했으며 실제로 파라미터는 유일하게 하나만 받고 있다. 이러한 함수를 중위호출에 사용하려면 "infix"라는 키워드를 선언하면 된다.

 

구조 분해 선언
fun main() {
    val (left, right) = Pair(1, "one")
    val list = listOf(1, 2, 3)
    for ((index, value) in list.withIndex()) {}
}

 

Pair과 같이 값과 쌍으로 이루어진 객체의 값을 분해해서 받을 수 있는데, 이것을 구조 분해 선언이라고 한다.

 

문자열 나누기

자바의 split과 비슷하게 사용하면 되지만, 조금더 확장된 함수를 제공한다. 아래와 같이 문자열 하나 혹은 문자열 여러개를 넘겨 여러 구분 문자열로 나눌수도 있고, 혹은 직접 정규식을 생성해서 split의 구분문자 정규식을 넘길 수도 있다.

fun main() {
    val str = "12.345-6.A"

    println(str.split(".", "-"))
    println(str.split("\\.|-".toRegex()))
}

 

Stirng의 substring 함수
fun main() {
    val path = "/Users/levi/kotlin/chapter3.kt"
    val directory = path.substringBeforeLast("/")
    val fullName = path.substringAfterLast("/")
    val fileName = fullName.substringBefore(".")
    val extension = fullName.substringAfter(".")
    println("Dir : $directory, FullName : $fullName, FileName : $fileName, Extension : $extension")
}

->
Dir : /Users/levi/kotlin, FullName : chapter3.kt, FileName : chapter3, Extension : kt

 

로컬 함수와 확장

코틀린에서는 중복된 로직을 깔끔하게 리팩토링할 수 있는 해법이 있다. 코틀린에서는 함수에서 추출한 함수를 원 ㅎ마수 내부에 중첩시킬 수 있고, 그렇게 하면 문법적인 부가 비용을 들이지 않고 깔끔하게 코드를 조작할 수 있다. 바로 예시를 살펴보자.

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        if (user.name.isEmpty()) {
            throw IllegalArgumentException("empty user name")
        }
        
        if (user.address.isEmpty()) {
            throw IllegalArgumentException("empty user address")
        }
        
        // user를 데이터베이스에 저장하는 로직.
    }
}

 

위와 같은 코드가 있다. 해당 코드에서는 검즈하는 필드 값만 다를뿐 사실 같은 중복된 검증 로직(문자열이 비어있는지)을 가지고 있다. 이러한 중복된 코드 내용은 로컬 함수로 분리하면서 제거할 수 있다.

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        fun validate(user: User, value: String, fieldName: String) {
            if (value.isEmpty()) {
                throw IllegalArgumentException("empty ${user.id} $fieldName")
            }
        }
        
        validate(user, user.name, "name")
        validate(user, user.address, "address")

        // user를 데이터베이스에 저장하는 로직.
    }
}

 

위 예제는 중복된 검증 로직을 로컬 함수로 분리함으로써 중복을 제거하였다. 하지만, 뭔가 User 객체를 넘기는 것이 보기 싫다. 조금 더 개선하자면, 로컬 함수는 자신을 감싸고 있는 함수의 변수를 캡쳐링해서 가져갈 수있는 클로저로 동작하기 때문에 굳이 User 객체를 인자로 넘길 필요가 없이 아래처럼 리팩토링이 가능하다.

class User(val id: Long, val name: String, val address: String) {
    fun saveUser(user: User) {
        fun validate(value: String, fieldName: String) {
            if (value.isEmpty()) {
                throw IllegalArgumentException("empty ${user.id} $fieldName")
            }
        }

        validate(user.name, "name")
        validate(user.address, "address")

        // user를 데이터베이스에 저장하는 로직.
    }
}

 

여기서 멈추지 않고, 조금더 깔끔하게 확장함수를 활용해 리팩토링해보자.

class User(val id: Long, val name: String, val address: String) {
    private fun User.validate() {
        fun validateParam(value: String, fieldName: String) {
            fun validate(value: String, fieldName: String) {
                if (value.isEmpty()) {
                    throw IllegalArgumentException("empty ${this.id} $fieldName")
                }
            }            
        }
        validateParam(this.name, "name")
        validateParam(this.address, "address")
    }
    fun saveUser(user: User) {
        user.validate()
        // user를 데이터베이스에 저장하는 로직.
    }
}

 

클래스 내부에 확장함수를 정의해서 saveUser 함수 내부는 훨씬더 군더더기 없는 코드로 리팩토링 되었다. 클래스 내부에 적용한 확장함수는 해당 클래스 내부에서만 사용가능하다는 것을 유의하자(private 키워드를 붙이지 않더라도 해당 클래스 내부에서만 사용가능하다.)

 

여기까지 코틀린의 함수에 대한 내용은 간단히 다루어보았다. 사실 위 내용들보다 많은 내용이 있지만, 다음 포스팅들에서 다루어보지 못한 내용들을 더 많이 다루어볼 것이다.



출처: https://coding-start.tistory.com/395?category=894862 [코딩스타트]