개발 언어/Kotlin
Kotlin 함수 정의와 호출
나규태
2022. 1. 11. 10:01
이 글은 새로 배우면서 익히는 정보들을 기억하기 위한 글이며, 아래 내용들은 "kotlin IN ACTION" 책에서 발췌해온 내용과 개인적인 생각을 넣은 내용입니다.
이름 붙인 인자
코틀린에서는 함수를 호출할 때 파라미터 명을 지정할 수 있다.
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()
}
>>> println(joinToString(collection = collection, separator = ",", prefix = "(", postfix=")"))
위와 같이 파라미터 명을 지정할 수 있으며 파라미터 명을 하나라도 지정했을 경우 혼동을 막기 위해 모든 인자 값의 파라미터 명을 지정해줘야 한다.
- kotlin IN ACTION 107 ~ 108p -
디폴트 파라미터 값
자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있다. java.lang.Thread에 있는 8가지 생성자를 살펴보라.
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있으므로 이런 오버로드 중 상당수를 피할 수 있다.
fun <T> joinToString(
collection: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String
>>> println(joinToString(collection = collection))
위의 호출 예시처럼 컬렉션만 인자 값으로 넣어도 나머지 파라미터는 디폴트 값을 사용할 수 있다.
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다는 사실을 기억하라.
자바에는 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다.
그럴 때 @JvmOverloads 애노테이션을 함수에 추가할 수 있다. @JvmOverloads를 함수에 추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 하나씩 생략한 오버로딩한 자바 메서드를 추가해준다.
- kotlin IN ACTION 109 ~ 110p -
정적인 유틸리티 클래스 없애기: 최상위 함수
자바를 아는 사람은 객체지향 언어인 자바에서는 모든 코드를 클래스의 메서드로 작성해야 한다는 사살을 알고 있다. 보통 그런 구조는 잘 작동한다. 하지만 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다. 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다. 중요한 객체는 하나뿐이지만 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지는 않은 경우도 있다.
코틀린에서는 이런 무의미한 클래스가 필요 없다. 대신 함수를 직접 소스 파일 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다.
아래 예시는 join.kt에 최상위 메서드를 만든 예시이다.
package strings
fun joinToString(...) : String { ... }
자바로 변환하면 아래와 같다.
package strings;
public clas JoinKt {
public static String joinToString(...) { ... }
}
코틀린 join.kt파일을 컴파일한 결과와 같은 클래스를 자바 코드로 보여준다. 파일 명으로 클래스가 생성되는 것을 볼 수 있다.
최상위 함수가 포함된 클래스의 이름을 바꾸고 싶다면 @JvmName 애노테이션을 사용하면 파일 명을 변경할 수 있다.
@file:JvmName("StringFunctions")
- kotlin IN ACTION 111 ~ 113p -
최상위 프로퍼티
최상위 함수와 마찬가지로 클래스 밖에 프로퍼티를 추가할 수 있다.
상수를 추가하고 싶다면 const val str = ""과 같이 const를 추가하면 상수로 사용할 수 있다.
자바로 변환하면 public static final과 동일하다
- kotlin IN ACTION 113 ~ 114p -
메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
확장 함수는 소유하고 있는 API가 아니더라도 함수를 추가할 수 있는 기능이다.
그러면 기존 API를 재작성하지 않고도 편리한 기능을 사용할 수 있다.
fun String.lastChar() : Char = this.get(this.length - 1)
위와 같이 String 클래스에 lastChar라는 함수를 추가하여 마지막 자리의 문자를 얻는 기능을 추가하였다.
여기서 String을 수신 객체 타입이라고 하고 this를 수신 객체라고 한다.
이런 게 추가된 함수를 사용하면 아래와 같다.
>>> println("Kotlin".lastChar()) // n
String 클래스에 추가된 lastChar 함수를 바로 사용할 수 있다.
뿐만 아니라 수신 객체 멤버에 this 없이 접근할 수도 있다.
fun String.lastChar() : Char = get(length - 1)
단, 함장 함수는 클래스 내부에서만 사용할 수 있는 private, protected와 같은 멤버를 사용할 수 없다.
확장 함수를 사용하기 위해서는 다른 클래스나 함수와 마찬가지로 임포트 해야만 한다. 확장 함수를 정의하자마자 어디서든 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 자주 생길 수 있다.
import strings.lastChar
// import strings.* // * 를 사용해도 잘 된다.
val c = "Kotlin".lastChar()
한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 이름을 바꿔서 임포트 하면 이름 충돌을 막을 수 있다.
import strings.lastChar as last
val c = "Kotlin".last()
따라서 임포트 할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.
- kotlin IN ACTION 115 ~ 117p -
확장 함수로 유틸리티 함수 정의
이전에 만들었던 joinToString을 최종 버전으로 만들어보자.
fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수
separator: String, // 디폴트 값 지정
prefix: String, // 디폴트 값 지정
postfix: String): String { // 디폴트 값 지정
val result = StringBuilder(prefix)
for((index, element) in this.withIndex()) { // this는 수신 객체를 가리킨다. 여기서 T의 원소로 이뤄진 컬렉션이다.
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
확장 함수를 이용해서 Collection에서 바로 사용할 수 있게 하였다.
또한 구체적인 원소 타입을 지정할 수 있다.
fun Collection<String>.join(
separator: String,
prefix: String,
postfix: String) = joinToString(separator, prefix,postfix)
위의 두 예제 모두 다 아래와 같이 사용할 수 있다.
listOf(1, 2, 3).joinToString(separator = "; ", prefix = "(", postfix = ")") // (1; 2; 3)
listOf("one", "two", "three").joint(" ") // one two three
- kotlin IN ACTION 118 ~ 119p -
동적 디스패치와 정적 디스패치
실행 시점에 객체 타입에 따라 동적으로 호출될 대상 메서드를 결정하는 방식을 동적 디스패치(dynamic dispatch)라고 한다. 반면 컴파일 시점에 알려진 변수 타입에 따라 정해진 메소드를 호출하는 방식은 정적 디스패치(static dispatch)라고 부른다. 참고로 프로그래밍 언어 용어에서 '정적'이라는 말은 컴파일 시점을 의미하고, '동적'이라는 말은 실행 시점을 의미한다.
- kotlin IN ACTION 120p -
확장 함수는 오버라이드 할 수 없다.
일반적인 오버라이드부터 확인해보자
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
위의 예제를 보면 알 수 있듯이 View 타입 변수에 대해 click과 같은 일반 메서드를 호출했는데 click을 Button 클래스가 오버라이드 했다면 실제로 Button이 오버라이드 한 click이 호출된다.
하지만 확장 함수는 클래스 외부에 있는 것이기 때문에 오버라이드가 되지 않는다.
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!
view가 가리키는 객체의 실제 타입은 Button이지만, 이 경우 view의 타입이 View이기 때문에 무조건 View의 확장 함수가 호출된다.
일반 함수는 동적으로 결정되지만, 확장 함수는 정적으로 결정된다.
/* 노트 */
어떤 클래스를 확장한 함수와 그 클래스의 멤버 ㅂ함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선순위가 더 높다). 클래스의 API를 변경할 경우 항상 이를 염두에 둬야 한다. 여러분이 코드 소유권을 가진 클래스에 대한 확장 함수를 정의해서 사용하는 외부 클라이언트 프로젝트가 있다고 하자. 그 확장 함수와 이름과 시그니처가 같은 멤버 함수를 여러분의 클래스 내부에 추가하면 클라이언트 프로젝트를 재 컴파일하는 순간부터 그 클라이언트는 확장 함수가 아닌 새로 추가된 멤버 함수를 허용하게 된다.
- kotlin IN ACTION 120 ~ 122p -
메서드를 다른 클래스에 추가: 확장 프로퍼티
확장 함수와 동일하게 수신 객체를 추가해서 프로퍼티를 만들 수 있다.
val String.lastChar
get() = get(length - 1)
확장 프로퍼티는 뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야 한다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.
변경 가능한 프로퍼티는 var로 만들 수 있다.
var StringBuilder.lastChar
get() = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
자바에서 확장 프로퍼티를 사용하려면 게터나 세터를 명시적으로 호출해야 한다.
- kotlin IN ACTION 122 ~ 124p -
자바 컬렉션 API 확장
코틀린이 자바 라이브러리를 사용하여 더 많은 기능을 제공할 수 있었던 것은 바로 위에서 배운 확장 함수를 이용한 것이다.
- kotlin IN ACTION 124 ~ 125p -
가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
자바에서는 가변 인자를 받기 위해 타입 뒤에 ... 을 붙여서 배열 형태로 담아 준다. 코틀린도 같은 방식이지만 문법적으로 다르다.
코틀린에서는 파라미터 앞에 vararg 변경자를 붙인다.
fun <T> test(vararg values: T) {
val list = listOf(*values)
println(list)
}
test("1", "2", "3")
>>> [1, 2, 3]
이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 기술적으로는 스프레드 연산자(*)가 그런 작업을 해준다.
이 예제는 스프레드 연산자를 통하면 배열에 들어있는 값과 다른 여러 값을 함께 써서 함수를 호출할 수 있음을 보여준다. 이런 기능은 자바에서는 사용할 수 없다.
- kotlin IN ACTION 126p -
값의 쌍 다루기: 중위 호출과 구조 분해 선언
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
이 코드는 중위 호출이라는 특별한 방식으로 to라는 일반 메서드를 호출한 것이다. 중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣는다.(이때 객체, 메소드 이름, 유일한 인자 사이에는 공백이 들어가야 한다.)
1.to("one")
1 to "one"
위의 두 호출은 동일하다.
인자가 하나뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다. 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다.
infix fun Any.to(other: Any) = Pair(this, other)
Pair는 두 변수를 즉시 초기화할 수 있다. 이런 기능을 구조 분해 선언(destructuring declaration)이라고 부른다.
val (number, name) = 1 to "one"
-> 1 to "one"
-> Pair(1, "one")
-> val (number, name) = 1 to "one"
위와 같이 Pair가 동작한다.
to는 확장 함수이다. to를 사용하려면 타입과 관계없이 임의의 순서쌍을 만들 수 있다. 이는 to의 수신 객체가 제네릭하다는 뜻이다.
1 to "one", "one" to 1, list to list.size() 등의 호출이 모두 잘 동작한다.
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
- kotlin IN ACTION 127 ~ 129p -