본문 바로가기

Language

[Kotlin] 함수

 

 프로그래밍 분야에서 함수라는 말은 어떤 '입력'에 대해 '출력'을 반환하는 일련의 코드 조각을 나타내는데, 이것은 우리가 잘 알고 있는 수학적인 정의와도 유사하다. 메서드 또한 이러한 개념에서 함수이지만, 이것은 객체지향 문맥에서 주로 쓰이는 용어이다. 다시 말하자면, 수신객체에 보내는 '메시지'라고 하는 것이 올바르다. 이와 관련된 것은 'Message passing' 과 관련된 문서를 찾아보자.

 

이제 코틀린에서 말하는 함수 또는 메서드의 중요 개념들을 정리해 보도록 하겠다. (이하 코틀린 문맥에서 '함수'로 통일)

 

최상위(Top level) 함수

 각자 이해하기에 따라 다를 수 있지만 서두에 언급한 것과 같은 맥락에서, 함수라는 것은 어떠한 수신객체도 없다. 자바 또한 이러한 개념을 이용해 메서드를 작성할 수 있으며, 우리가 잘 아는 `static` 키워드를 붙인 메서드가 바로 그것이다. 다만 이것을 함수라고 부르기는 조금 어색한데, 그것은 수신객체 없이 호출할 수 있지만 어찌되었든 클래스 하위에 정의를 해야하기 때문일게다. 그렇기에 주로 '정적 메서드'라는 말을 많이 사용하는 것 같다.

 

코틀린은 제대로된 의미에서의 함수작성이 가능하다. 왜냐하면 함수를 특정 클래스에 속하지 않는 최상위로 선언할 수 있기 때문이다.

 

package com.asuraiv.kotlinpractice.function

fun printSomething(value: String): Unit = println(value)

 

 위와 같이 선언한 최상위 함수는 함수를 작성한 파일이름에 관계 없이 `com.asuraiv.kotlinpractice.function` 패키지에 속하게 된다. 따라서 해당 함수를 사용하려면 아래와 같은 `import` 문을 작성한다.

 

import com.asuraiv.kotlinpractice.function.printSomething

fun main() {
    printSomething("Hello Function!")
}

 

`import` 문을 눈여겨 보면 알 수 있듯, 클래스를 임포트한 것이 아닌 함수 자체를 임포트 했다. 이는 자바에서 `static` 키워드로 도배(?)된 `Utils` 클래스 만들지 않아도 재사용이 용이한 함수 집합 구성을 가능하게 한다.

 

식이 본문인 함수

 이 개념을 다루기에 앞서, 또는 표현식(expression)과 (statement)의 차이를 짚고 넘어가야 겠다. 이 둘의 차이는 우리가 작성한 코드가 평가( ) 될때의 관점에서 이해해야 한다. 아래의 설명이 비교적 정확하다.

 

은 값을 생성하며, 값이 있어야할 곳에는 어디든 쓸 수 있습니다. 다른 식의 하위 요소로써, 값을 만들어 내는 데 참여할 수 있습니다. 은 어떤 행동을 하는 것을 의미 합니다. 아무런 값을 생성하지 않습니다. 단순한 할당 문이나 if문, 반복문 등이 이에 해당됩니다.

                                                                                      - 자바스크립트를 말하다 <악셀 라우슈 마이어, 한빛미디어>

 

 다시 쉽게 말하면, 평가시에 값을 만들어 내는지 여부가 식과 문의 결정적인 차이라고 볼 수 있다. 이러한 맥락에서 `isTrue ? 1 : 0;`는 이고 `if(isTrue) { b = 1; } else { b = 0; }`는 이다.

 

코틀린에서는 `if then else` 구문, 'switch'의 코틀린 버전인 `when` 구문, 예외처리 구문인 `try catch` 구문을 각각 값을 만들어내는 식으로서 함수 본문으로 사용할 수 있다. 이때 가장 중요한 점은 반환을 나타내는 `return` 키워드를 생략 할 수 있다는 것이다. 아래의 예제를 살펴보면 직관적으로 이해할 수 있을 것이다.

 

// if then else의 경우
fun getInsurance(age: Int): Int = if(age > 65) 500 else 250

// when문의 경우
fun getCalorie(food: Any) =
        when(food) {
            is Meat -> 143.4
            is Kimchi -> 29
            is Rice -> 125
            else ->
                throw IllegalArgumentException("Can't find calorie")
        }

// 예외처리 구문의 경우
fun divide(n: Int, d: Int) =
        try {
            n / d
        } catch(e: ArithmeticException) {
            println("Can't divide by zero.")
            0
        }

 

확장 함수

코틀린에서는 언어 플랫폼에서 제공하는 기존 클래스에 아래처럼 함수를 추가할 수 있다.

 

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

 

여기서 `String` 은 수신객체의 타입이며, 함수 본문의 `this` 는 수신객체 자신이다. 따라서 이 코드는 자신의 `get` 메서드와 `length` 값을 이용하여 문자열의 마지막 문자를 가져온다.

 

이러한 개념을 응용하면 흔히 사용하게 되는 Collection 클래스에 유용한 유틸리티 함수를 추가 할 수 있다.

 

fun <T> Collection<T>.joinToStringWithDelimiter(delimiter: String): String {

    var result = StringBuilder()
    for((index, element) in this.withIndex()) {
        if(index > 0) {
            result.append(delimiter)
        }
        result.append(element)
    }
    return result.toString()
}

 

아래는 사용 예.

 

fun main() {
    val names = listOf("Mathew", "Mark", "John", "Luke")
    println(names.joinToStringWithDelimiter(","))
}

 

중위 호출

코틀린에서 맵 컨테이너를 생성 할 때, `mapOf` 라는 편리한 함수를 사용한다.

 

val calorieMap = mapOf("meat" to 143.4, "kimchi" to 29, "rice" to 125)

 

 다만 여기서 중요한 점은 `to` 가 코틀린의 키워드가 아니라는 것. 위의 코드는 중위 호출(infix call) 이라는 특별한 방식으로 `to` 라는 일반 메서드를 호출한 것이다.

 

중위 호출 시에는 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣는다. 각각 사이엔 공백이 들어간다.

 

"Kimchi".to(29) // 일반적인 방식
"Kimchi" to 29  // 중위 호출 방식

 

 이러한 중위 호출은, 인자가 하나뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에 사용 할 수 있다. 함수를 중위 호출에 사용하고 허용하고 싶으면 `infix` 변경자를 함수 선언 앞에 추가해야 한다.

 

infix fun Any.to(other: Any) = Pair(this, other)

 

가변 인자와 스프레드(spread) 연산자

 코틀린의 가변 인자(varargs)는 자바의 그것과 개념이 완전히 같다. 다만 타입 뒤에 `...` 를 붙이는 대신 `vararg` 변경자를 붙이는 것이 다르다.  리스트 컬렉션을 생성할때 자주 쓰는 `listOf` 함수의 정의를 보면 아래와 같이 되어 있다.

 

fun listOf<T>(vararg values: T): List<T> { ...생략...}

 

이미 배열에 있는 원소를 가변 인자로 넘길때는 배열을 명시적으로 풀어서 각 원소가 인자로 전달 되게 해야하는데, 기술적으로 스프레드(spread) 연산자가 그런 작업을 해준다. 전달하려는 배열 앞에 `*` 를 붙이면 된다.

 

fun main(args: Array<String) {
    val list = listOf("args: ", *args)
    println(list)
}

 

마치며

 아무리 새롭고 모던(modern)한 프로그래밍 언어라 할지라도, 그동안 사용해 왔던 언어들과 비슷한 면면들을 볼 수 있다. 이는 모든 프로그래밍 언어들이 가지고 있는 공통적인 컨셉의 존재 때문인데, 이를 잘 이해하고 있으면 가파른 학습 곡선을 그려 새로운 언어를 금방 익힐 수 있게 된다.

 

 사실 최상위 함수니, 스프레드 연산자니 하는 것들은 자바스크립트나 파이썬같은 언어들 또한 가지고 있는 개념들이지만, - 다른 언어들에 있는 것은 잘 모르겠다 - 어찌되었든 코틀린의 주된 비교대상은 자바이기 때문에, 자바와 명백히 다른 개념이나 문법들을 정리해 보았다. 그간 쭉 자바로만 코딩을 해왔던 개발자라면 해당 요소들이 참 신선하게 느껴질 것 같다. (물론 난 자바스크립트나 파이썬에 어느정도 발을 담가 봤기에 제외...? ㅎ)