기록하는 습관

[Kotlin In Action]3장 - 함수 정의와 호출 본문

스터디/Kotlin In Action

[Kotlin In Action]3장 - 함수 정의와 호출

로그뉴 2023. 2. 6. 22:53

 

 

** 이 글은 Kotlin In Action을 읽고 정리한 글입니다. **

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

  • 코틀린은 자체 컬렉션을 제공하지 않는다.
    • 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기 훨씬 더 쉽기 때문.
val set = hashSetOf(1, 7, 53)
val list = arrayListOf(1, 7, 53)
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
  • 코틀린 컬렉션은 자바 컬렉션과 똑같은 클래스다.
  • 하지만 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있다.
    • ex) last(), max()
val strings = listOf("first", "second", "fourteeth")
println(strings.last()) // fourteeth
 
val numbers = setOf(1, 14, 2)
println(numbers.max()) // 14

 

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

  • 자바 컬렉션에는 디폴트 toString 구현이 들어있다. 하지만 이 toString의 출력 형식은 고정돼 있다.
    • 커스텀을 위해서는 자바에서는 구아바나 아파치 커먼즈 같은 서드파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야 한다.
    • 코틀린에서는 함수가 표준 라이브러리에 이미 들어있다.
val list = listOf(1, 2, 3)
println(list) // [1, 2, 3]

 

이번에는 함수를 직접 구현해보자.

fun <T> joinToString(
        collection: Collection<T>,
        separator: String,
        prefix: String,
        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)
  • 개선하고 싶은 부분
    • 매번 네 인자를 모두 전달하지 않도록 해보자.

 

2-1. 이름 붙인 인자

  • 코틀린에서는 함수에 전달하는 인자 중 일부 또는 전부의 이름을 명시할 수 있다.
  • 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.
joinToString(collection, separator=" ", prefix=" ", postfix=".")

 

2-2. 디폴트 파라미터 값

  • 자바에서는 일부 클래스에서 오버로딩한 메서드가 많아진다는 문제가 있다.
  • 코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있으므로 이런 오버로드 중 상당수를 피할 수 있다.
  • 일반 호출 문법을 사용하려면 함수를 선언할 때와 같은 순서로 인자를 지정해야 한다.
  • 이름 붙은 인자를 사용하는 경우에는 인자 목록의 중간에 있는 인자를 생략하고, 지정하고 싶은 인자를 이름에 붙여서 순서와 관계없이 지정할 수 있다.
  • 디폴트 파라미터의 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다.
fun <T> Collection<T>.joinToString(
        separator: String = ", ",      // 디폴트 값이 지정된 파라미터들
        prefix: String = "",
        postfix: String = ""
): String

 

2-3. 정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티

  • 다양한 정적 메서드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메서드는 없는 클래스가 생겨난다.
    • ex) JDK의 Collections 클래스, Util

 

최상위 함수

  • 코틀린에서는 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시킬 수 있다.
/* join.kt */
package strings
fun joinToString(...): String{ ... }
  • JVM이 클래스 안에 들어있는 코드만을 실행할 수 있기 때문에 컴파일러는 아래 파일을 컴파일 할 때 새로운 클래스를 정의해준다.
/* java */
package strings;

public class JoinKt {
	public static String joinToString(...) { ... } 
}
  • 클래스의 이름은 최상위 함수가 들어있던 코틀린 소스 파일 이름과 대응한다.
  • 이름을 변경하고 싶으면 @JvmName 어노테이션을 사용하면 된다.

 

최상위 프로퍼티

  • 함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다.
  • 다른 모든 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다. (getter, setter 접근 가능)
/* kotlin */
const val UNIX_LINE_SEPARATOR = "\n"


/* java */
public static final String UNIX_LINE_SEPARATOR = "\n";

 

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

확장함수

  • 어떤 클래스와 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수이다.
  • 확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이면 된다.
    • 수신 객체 타입: 클래스 이름
    • 수신 객체: 확장함수가 호출되는 대상이 되는 값
fun String.lastChar(): Char = this.get(this.length - 1)

// 수신 객체 타입: String
// 수신 객체: this
  • 어떤 면에서 이는 String 클래스에 새로운 메서드를 추가하는 것과 같다.
  • 그루비언어와 같은 다른 JVM 언어로 작성된 클래스도 확장 가능하다.
  • 일반 메서드와 마찬가지로 확장 함수 본문에도 this를 사용할 수 있고, 생략할 수 있다.
  • 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없다.

 

3-1.  임포트와 확장 함수

  • 확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트 해야 한다.
  • as 키워드를 사용하면 임포트한 클래스나 함수를 다른 이름으로 부를 수 있다.

 

3-2. 자바에서 확장 함수 호출

  • 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드다.
  • 확장 함수를 StringUtil.kt 파일에 정의했다면 자바에서 아래와 같이 호출 가능하다.
/* java */
char c = StringUtilKt.lastChar("Java");

 

 

3-3. 확장 함수로 유틸리티 함수 정의

fun <T> Collection<T>.joinToString(
        separator: String = ", ",
        prefix: String = "",
        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()
}
  • Collection<T>에 대한 확장함수 선언.
  • joinToString을 마치 클래스의 멤버인 것처럼 호출할 수 있다.

 

fun Collection<String>.join(
        separator: String = ", ",
        prefix: String = "",
        postfix: String = ""
) = joinToString(separator, prefix, postfix)
  • 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다.

 

3-4. 확장 함수는 오버라이드 할 수 없다.

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

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

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

>>> val view: View = Button()
>>> view.showOff() // I'm a view!
  • 확장 함수는 클래스의 일부가 아니다. 클래스 밖에 선언된다.
  • 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수에 저장된 동적인 타입에 의해 확장 함수가 결정되지 않는다.
  • 확장함수는 정적으로 결정된다!
  • 어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다. (멤버 함수의 우선순위가 더 높다)

 

3-5. 확장 프로퍼티

val String.lastChar: Char
    get() = get(length - 1)
  • 확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다.
var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

 

  • 자바에서 확장 프로퍼티를 사용하고 싶다면 StringUtilKt.getLastChar(”Java”) 처럼 항상 게터나 세터를 명시적으로 호출해야 한다. 

 

4. 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원

4-1. 자바 컬렉션 API 확장

  • 어떻게 자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린이 새로운 기능을 추가할 수 있을까?
    • last와 max는 모두 확장 함수 였다. (last는 List 클래스의 확장 함수다.)

 

4-2. 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의

  • 가변 길이 인자는 메서드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
  • 파라미터 앞에 vararg 변경자를 붙인다.
  • 자바에서는 배열을 그냥 넘기지만, 코틀린에서는 배열을 명시적으로 풀어 배열의 각 원소가 전달되게 해야 한다.
    • 스프레드 연산자(*)가 이런 역할을 해준다.

 

4-3. 값의 쌍 다루기: 중위 호출과 구조 분해 선언

중위 호출

  • 인자가 하나 뿐인 일반 메서드나 인자가 하나 뿐인 확장 함수에 중위 호출을 사용할 수 있다.
  • 함수를 중위 호출에 사용할 수 있게 하고 싶다면 infix 변경자를 함수 선언 앞에 추가한다.
1 to "one" // 중위 호출

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

 

구조 분해

val (number, name) = 1 to "one"
  • Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있다.

 

5. 문자열과 정규식 다루기

5-1. 문자열 나누기

  • 코틀린 문자열은 자바 문자열과 같다.
  • 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받는다.
    • 코틀린 정규식 문법은 자바와 똑같다.
println("12.345-6.A".split("\\.|-".toRegex()))

 

5-2. 정규식과 3중 따옴표로 묶은 문자열

val regex = """(.+)/(.+)\.(.+)""".toRegex()

 

5-3. 여러 줄 3중 따옴표 문자열

val kotlinLogo = """| //
                   .|//
                   .|/ \"""

 

6. 코드 다듬기: 로컬 함수와 확장

  • 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다.
  • 로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다.
  • 확장 함수를 로컬 함수로 정의할 수 있다.
// 기존
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 to the database
}


// 로컬 함수를 사용해 코드 중복 줄이기
class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {

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

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

    // Save user to the database
}


// 검증 로직을 확장 함수로 추출하기
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")

    // Save user to the database
}

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

 

Comments