Link
Today
Total
10-16 22:05
Archives
관리 메뉴

초보개발자 긍.응.성

(코틀린 인 액션) 3장 함수 정의와 호출 본문

책 정리/코틀린 인 액션

(코틀린 인 액션) 3장 함수 정의와 호출

긍.응.성 2022. 7. 13. 01:25
반응형

3장 함수 정의와 호출

💡 다루는 내용

  • 컬렉션, 문자열, 정규식을 다루기 위한 함수
  • 이름 붙인 인자, 디폴트 파라미터 값, 중위 호출 문법 사용
  • 확장 함수와 확장 프로퍼티를 사용해 자바 라이브러리 적용
  • 최상위 및 로컬 함수와 프로퍼티를 사용해 코드 구조화

3.1 코틀린에서 컬렉션 만들기

val set = hashSetOf(1, 7 ,53)
println(set.javaClass) // class java.util.HashSet

val list = arrayListOf(1, 7, 53)
println(list.javaClass) // class java.util.ArrayList

val map = hashMap(1 to "one", 7 to "seven", 53 to "fifty-three")
println(map.javaClass) // class java.util.HashMap

코틀린은 자체 컬렉션을 제공하지 않는다. 자바 개발자는 기존의 자바 컬렉션을 활용할 수 있으며 자바 코드와도 호환 가능하다. 하지만 코틀린에서는 확장 함수를 통해 자바보다 더 많은 기능을 제공한다.

3.2 함수를 호출하기 쉽게 만들기

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()
}

위 함수는 컬렉션의 원소를 원하는 접두사(prefix), 구분자(separator), 접미사(postfix)로 조합하여 출력하는 함수이다. 이 예시 함수를 통해 추가적인 코틀린의 기능들을 알아보자.

이름 붙인 인자

joinToString(collection, " ", " ", ".")

함수 호출 부분에서 각 인자가 무엇을 뜻하는지 모르기 때문에 가독성이 좋지 않다. 코틀린은 함수에 전달하는 인자에 이름을 명시할 수 있다.

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

디폴트 파라미터 값

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

함수 선언에서 파라미터의 디폴트 값을 지정할 수 있다. 디폴트 값을 선언하였다면 디폴트 값을 선언한 필드에 대하여 함수를 호출할 때 인자를 넘겨주지 않아도 사용이 가능하다.

>>> joinToString(list, ", ", "", "")
1, 2, 3
>>> joinToString(list)
1, 2, 3
>>> joinToString(list, "; ")
1; 2; 3
>>> joinToString(list, prefix = "# ", postfix = ";") // 이름붙인 인자와 조합
# 1, 2, 3;
  • 자바에는 디폴트 파라미터 값이라는 개념이 없기 때문에, 자바에서 코틀린 함수를 사용하는 경우 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다.
@JvmOverloads
fun <T> joinToString(...): String { ... }
  • @JvmOverloads 를 함수에 추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 오버 로딩한 자바 메서드를 추가해준다.
public static String joinToString $default(Collection var0, String var1, String var2, String var3, int var4, Object var5) {
    if ((var4 & 2) != 0) {
    var1 = ", ";
}

    if ((var4 & 4) != 0) {
    var2 = "";
}

    if ((var4 & 8) != 0) {
    var3 = "";
}

    return joinToString(var0, var1, var2, var3);
}

@JvmOverloads
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator, @NotNull String prefix) {
    return joinToString$default(collection, separator, prefix, (String)null, 8, (Object)null);
}

@JvmOverloads
@NotNull
public static final String joinToString(@NotNull Collection collection, @NotNull String separator) {
    return joinToString$default(collection, separator, (String)null, (String)null, 12, (Object)null);
}

@JvmOverloads
@NotNull
public static final String joinToString(@NotNull Collection collection) {
    return joinToString$default(collection, (String)null, (String)null, (String)null, 14, (Object)null);
}
  • 실제 @JvmOverload를 사용했을 때 생성되는 자바 바이트 코드를 확인해보면 메서드 명$default 메서드가 생성된다.
  • 해당 메서드는 null로 값이 들어온 경우 디폴트 값으로 값을 치환해주는 역할을 한다.

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

자바에선 유틸리티 함수나 상수를 담는 클래스를 반드시 구현해야 했다(ex. CollectionUtils). 하지만 코틀린은 패키지 최상위 함수와 프로퍼티를 정의할 수 있다.

최상위 함수

JVM은 클래스 안에 들어있는 코드만을 실행할 수 있다. 하지만 코틀린에서는 최상위 함수라는 개념이 있다. 코틀린 컴파일러는 최상위 함수가 존재하는 파일을 컴파일할 때 새로운 클래스를 정의해주어 JVM에서 동작하도록 한다.

@file:JvmName("StringFunctions")
package strings
fun <T> joinToString(...): String { ... }

코틀린 컴파일러가 생성하는 클래스 이름은 최상위 함수가 들어있는 코틀린 소스 파일의 이름과 대응한다. 원하는 클래스 이름으로 지정하고 싶다면 @JvmName애노테이션을 통해 지정가능하다.

최상위 프로퍼티

val opCount = 0
fun performOperation() {
    opCount++
}

프로퍼티도 파일의 최상위 수준에 선언할 수 있다. 이를 최상위 프로퍼티라고 한다. 이런 프로퍼티의 값은 정적 필드에 저장된다.

최상위 프로퍼티를 상수처럼 사용하고 싶다면 const 키워드를 통해 자바의 public static final필드로 컴파일하게 할 수 있다.

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

fun String.lastChar(): Char = this.get(this.length - 1)
println("Kotlin".lastChar()) // n
  • 어떤 클래스의 멤버 메서드인 것 처럼 호출할 수 있지만, 그 클래스의 밖에 선언된 함수.
  • 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이면 된다
  • 수신 객체 타입(receiver type): 확장이 정의될 클래스의 타입
  • 수신 객체(receiver object): 그 클래스에 속한 인스턴스 객체
  • 위의 예시에선 String 이 수신 객체 타입이며, 확장 함수 구현부의 this가 수신 객체이다.
fun String.lastChar(): Char = get(length - 1)

확장 함수 내부에서는 일반적인 인스턴스 메서드의 내부와 마찬가지로 수신 객체의 메서드나 프로퍼티를 바로 사용할 수 있다. (this를 생략 가능하다)

하지만 클래스 안에서 정의한 메서드와 달리 확장 함수 안에서는 클래스 내부에서만 사용 가능한 private, protected 멤버는 사용할 수 없다.

임포트와 확장 함수

import strings.lastChar
val c = "Kotlin".lastChar()

---
import strings.lastChar as last
val c = "Kotlin".last()
  • 정의한 확장 함수를 사용하기 위해선 임포트해야만 한다.
  • as 키워드를 사용하면 임포트한 클래스나 함수를 다른 이름으로 부를 수 있다.
  • 중복되는 이름의 클래스나 함수를 임포트할 시 충돌이 있을 수 있는데 이럴땐 두가지 충돌 해결 방법이 있다.
    1. 전체 이름(FQN)을 사용한다.
    2. as 로 이름을 변경하여 충돌을 회피한다.

코틀린에서는 확장 함수는 짧은 이름을 쓰는 것을 권장하므로 후자(as)의 충돌 해결의 방법을 권장한다.

자바에서 확장 함수 호출

char c = StringUtilKt.lastChar("Java")
  • 내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드이다.
  • 그렇기에 확장 함수 호출에 대하여 추가 비용이 들지 않는다

확장 함수로 유틸리티 함수 정의

fun <T> Collection<T>.joinToString(...): 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()
}
  • 앞서 정의했던 joinToString 메서드를 수신 객체 타입 Collection<T> 을 갖는 확장 함수로 선언한다.
  • for문에서 collection 파라미터 대신 this 접근자로 수신 객체를 가리킨다

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

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

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

val view: View = Button()
view.click() // Button clicked
  • open 키워드는 상속을 허용하기 위해 사용한다.
  • open 키워드 없이 상속할 경우 final 취급되기에 에러가 발생된다.
  • 멤버 함수에 대해선 동적으로 오버라이드 한 함수가 호출된다.
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

확장 함수는 정적 타입에 의해 함수 호출을 결정한다. 따라서 오버라이드 할 수 없다.

만약 같은 시그니처를 가진 멤버 함수가 있다면, 반드시 확장 함수가 아닌 멤버 함수가 호출된다.

확장 프로퍼티

val String.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }

확장 프로퍼티는 프로퍼티 형식 구문으로 선언하여 사용할 수 있다. 프로퍼티라 불리지만 기존 클래스에 필드를 추가할 수 없으므로 실제 상태를 가지진 않는다.

프로퍼티 문법이 더 짧은 코드를 만들 수 있어서 사용할 때 더 편한 경우가 있다.

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

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

자바 컬렉션 API 확장

코틀린은 확장 함수를 통해 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공할 수 있다.

가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의

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

코틀린은 vararg 키워드를 통해 가변 인자를 지원한다.

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

* 스프레드(spread) 연산자를 통해 배열의 원소를 펼쳐서 가변 인자로 사용할 수 있다.

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

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

val (number, name) = 1 to "one"

중위 호출(infix call)은 은 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣어 사용한다. 인자가 하나뿐인 메서드나 확장 함수에서만 중위 호출을 사용할 수 있다.

메서드에 중위 호출을 허용하고 싶다면 infix 변경자를 함수 선언 앞에 추가해야 한다.

위의 to 함수는 두 원소를 통해 Pair의 인스턴스를 반환한다.

1 to “one”으로 number와 name 변수를 초기화하는 방법을 구조 분해 선언(destructing declaration)이라고 한다.

3.5 문자열과 정규식 다루기

코틀린에서 제공하는 String 클래스의 확장 함수들에 대한 소개이다.

문자열 나누기

println("12.345-6.A".split("\\.|-".toRegex()) // [12, 345, 6, A]

println("12.345-6.A".split(".", "-")) // [12, 345, 6, A]
  • 정규식을 파라미터로 받을 때 문자열이 아닌 Regex 타입의 값으로 받아 처리하는 API 제공
  • 구분 문자열을 하나 이상 인자로 받는 API 제공

정규식과 3중 따옴표로 묶은 문자열

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

코틀린에선 3중 따옴표 문자열을 쓸 수 있다. 3중 따옴표 문자열에선 이스케이프 문자를 사용할 필요가 없으며, 공백과 줄 바꿈이 모두 인식된다.

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

클래스에 기능이 많아질수록 중복되는 코드가 발생할 수 있다. 하지만 이를 추가적인 메서드로 리팩토링하면 클래스 안에 작은 메서드들이 많아지고 각 메서드 사이 관계를 파악하기 힘들어지게 된다.

코틀린은 로컬 함수를 통해 이를 해결한다.

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

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

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

    // save ...
} 
  • saveUser 함수 내 validate라는 로컬 함수를 선언하였다.
  • 중복을 없애며 saveUser 메서드 내 선언되어 코드 구조를 깔끔하게 유지할 수 있다.
  • 로컬 함수 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다.
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")
}
  • 응집도를 위해 User 메서드 내부에 검증 로직을 혼재하고 싶지 않다면 확장 함수로 만들 수 있다.
반응형
Comments