[코틀린 인 액션] 3장, 함수 정의와 호출

kindof

·

2023. 8. 15. 14:35

[코틀린 인 액션] 책을 읽으면서 이해한 내용을 정리합니다.

책의 설명을 기반으로 하되, Java와의 비교나 주관적인 생각들도 써보려고 하는데요. 코드를 작성하면서 궁금한 부분이나 다른 기본적인 내용들은 최대한 공식 문서를 참고해서 작성해보겠습니다.

 

이번 글에서는 코틀린의 함수 정의와 호출에 대해 정리해보려고 합니다.

 

설명하는 코드는 책에서 소개한 코드에 기반하여 설명을 위해 조금씩 각색 + 추가한 부분이 있을 수 있습니다.

 

 

1. 코틀린에서 컬렉션 만들기

코틀린은 자신만의 컬렉션 기능을 제공하지 않고 자바 컬렉션과 같은 클래스를 사용하되, 자바보다 더 많은 기능을 제공합니다.

val strings = listOf("a", "b", "c", "d")
println(strings.last()) // d
println(strings[2])     // c

val numbers = setOf(1, 10, 30)
println(numbers.min())  // 1

println(strings.javaClass)  // class java.util.Arrays$ArrayList
println(numbers.javaClass)  // class java.util.LinkedHashSet

 

그런데, 자바 컬렉션과 같은 클래스를 사용하면서 자바보다 더 많은 기능을 제공하는게 어떻게 가능할까요?

 

이에 대한 답을 아래에서 찾아보겠습니다.

 

 

2. 함수를 호출하기 쉽게 만들기

코틀린에서 함수를 호출하기 쉽게 만든다는 것은 어떤 뜻일까요? 자바와 어떤 부분이 다른지 예시 코드를 보면서 소개합니다.

 

아래 join.kt > joinToString() 메서드는 컬렉션의 각 원소를 ","로 구분하고 prefix, postfix를 붙여 리턴하는 메서드입니다.

 

[join.kt]

import java.lang.StringBuilder

fun <T> joinToString(
    collections: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) : String {
    val result = StringBuilder(prefix)

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

    result.append(postfix)
    return result.toString()
}

fun main() {
    val list = listOf(1, 2, 3, 4)
    println(joinToString(list, separator = ", ", prefix = "[", postfix = "]"))
}

// [1, 2, 3, 4]

자바와 다르게 편해진 부분 세 가지를 확인할 수 있습니다.

 

[1] 파라미터에 이름을 붙여 함수 호출 부분의 가독성을 높일 수 있다.

[2] 디폴트 파라미터를 사용하여 불필요한 메서드 오버로딩(Overloading)을 방지할 수 있다.

[3] Static Class 등을 생성하지 않고 함수를 직접 소스 파일의 최상위 수준, 다른 클래스의 밖에 위치시켜 사용할 수 있습니다.

 

위에 작성한 코드를 자바로 다시 작성하려고 시도하면 세 가지 장점이 명확히 와 닿을 수 있습니다.

 

 

 

3. 메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티

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

 

확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 됩니다.

 

클래스의 이름을 수신 객체 타입(Receiver Type)이라 부르며, 확장 함수가 호출되는 대상이 되는 값을 수신 객체(Receiver object)라고 합니다.

fun List<Int>.sum(): Int {
    var result = 0
    for (item in this) {
        result += item
    }
    return result
}

fun main() {
    val numbers = listOf(1, 2, 3, 4, 5)
    val sum = numbers.sum()
    println("Sum: $sum") // 출력: Sum: 15
}

위 코드에서 수신 객체 타입은 List<Int>, 수신 객체는 this가 되겠죠.

 

확장 함수를 사용하면 기존 클래스의 기능을 확장하거나, 새로운 유틸리티 함수를 추가하여 코드를 더 간결하고 가독성 있게 작성할 수 있습니다.

또한, 코틀린에서는 List 클래스에 우리가 새로운 메서드 자체를 추가할 수 있다는 말과 같게 되고 Java 클래스로 컴파일한 클래스가 파일이 있다면 그 클래스에 원하는 대로 확장을 추가할 수 있다는 뜻이 됩니다.

 

맨 처음에 던졌던 질문에 대한 답을 이제 조금 추측할 수 있겠습니다.

그런데, 자바 컬렉션과 같은 클래스를 사용하면서 자바보다 더 많은 기능을 제공하는게 어떻게 가능할까요?

정답은 확장 함수였습니다.

 

 

4. 컬렉션 처리

컬렉션 처리를 할 때 코틀린 언어의 세 가지 특성이 있습니다.

 

[1] vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.

[2] 중위 함수 호출 구문을 사용하면 인자가 하나뿐인 메서드를 간편하게 호출할 수 있다.

[3] 구조 분해 선언을 사용하면 복합적인 값을 분해하여 여러 변수에 나눠 담을 수 있다.

 

무슨 말인지 이해하려면 역시 코드를 보는 게 빠를 것 같습니다.

 

[1] vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.

fun sum(vararg numbers: Int): Int {
    var result = 0
    for (num in numbers) {
        result += num
    }
    return result
}

fun main() {
    val sum1 = sum(1, 2, 3) // 호출 시 인자 개수가 다른 경우
    val sum2 = sum(10, 20, 30, 40, 50)
    
    println("Sum 1: $sum1") // 출력: Sum 1: 6
    println("Sum 2: $sum2") // 출력: Sum 2: 150
}

 

[2] 중위 함수 호출 구문을 사용하면 인자가 하나뿐인 메서드를 간편하게 호출할 수 있다.

data class Point(val x: Int, val y: Int)

infix fun Point.isSameAs(other: Point): Boolean {
    return this.x == other.x && this.y == other.y
}

fun main() {
    val point1 = Point(1, 2)
    val point2 = Point(1, 2)
    val point3 = Point(3, 4)
    
    println(point1 isSameAs point2) // 중위 호출 구문 사용, 출력: true
    println(point1.isSameAs(point3)) // 일반 함수 호출, 출력: false
}

 

[3] 구조 분해 선언을 사용하면 복합적인 값을 분해하여 여러 변수에 나눠 담을 수 있다.

data class Person(val name: String, val age: Int)

fun main() {
    val person = Person("Alice", 30)
    
    val (name, age) = person // 구조 분해 선언을 통한 값 분해
    
    println("Name: $name, Age: $age") // 출력: Name: Alice, Age: 30
}

 

 

 

5. 로컬 함수와 확장

코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있습니다. 그렇게하면 문법적인 부가 비용을 들이지 않고도 깔끔하게 코드를 조작할 수 있습니다.

 

아래는 사용자를 DB에 저장하는 함수입니다. 이 때 DB에 사용자 객체를 저장하기 전에는 각 필드를 검증해야 합니다.

import java.lang.IllegalArgumentException

class User (val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Name")
    }
    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user ${user.id}: empty Address")
    }
    
    // Save user...
}

위 코드에는 유저의 이름과 주소값에 대한 검증이 중복됩니다.

 

이 코드를 로컬 함수의 확장을 통해 리팩토링 해보겠습니다.

import java.lang.IllegalArgumentException

class User (val id: Int, val name: String, val address: String)

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user $id: empty $fieldName")
        }
    }

    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave()

    // Save user...
}

훨씬 깔끔한 구조가 되었다는 것을 볼 수 있는데요. 앞에서 소개한 확장 함수를 잘 이해했다면 위 코드가 어떻게 동작하는 지 이해하실 수 있으리라 생각합니다.

 

 

 

6. 정리 / Reference

이번 글에서는 코틀린이 어떻게 자체 컬렉션 클래스를 정의하지 않고도 자바 클래스를 확장할 수 있는지 알아봤습니다.

 

그리고 코틀린은 자바보다 편해진 함수 호출 방식, 확장 함수 등을 통해 구조적으로 더 깔끔해진 코드 관리가 가능해졌다는 것도 알 수 있었습니다.

 

다음 글에서는 코틀린의 클래스에 대해 정리해보겠습니다.

 

감사합니다.

 

 

Kotlin in Action | 드미트리 제메로프 - 교보문고

Kotlin in Action | 코틀린이 안드로이드 공식 언어가 되면서 관심이 커졌다. 이 책은 코틀린 언어를 개발한 젯브레인의 코틀린 컴파일러 개발자들이 직접 쓴 일종의 공식 서적이라 할 수 있다. 코틀

product.kyobobook.co.kr