이전 시간에서 setOf 함수를 사용해 집합을 만드는 방법을 사용한 적이 있었다.
val set = setOf(1, 7, 53)
비슷한 방법으로 리스트와 맵을 만들 수 있다.
val list = listOf(1, 7, 53)
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
여기서 map을 만들 때 사용한 to는 언어가 제공하는 특별한 키워드가 아니라 일반 함수이다.
여기서 만들어진 객체들의 클래스를 확인해보자
val set = setOf(1, 7, 53)
val list = listOf(1, 7, 53)
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
println(set.javaClass) // class java.util.LinkedHashSet
println(list.javaClass) // class java.util.Arrays$ArrayList
println(map.javaClass) // class java.util.LinkedHashMap
결과에서 알 수 있는 것처럼 코틀린은 표준 자바 컬렉션을 사용한다.
자바 컬렉션을 재활용했기 때문에 자바 컬렉션에 대한 지식을 활용할 수 있다.
하지만 자바와 달리 코틀린 컬렉션 인터페이스는 기본적으로 ‘읽기 전용’이다.
표준 자바 컬렉션을 활용하기 때문에 코틀린이 자바를 호출하고나 자바가 코틀린을 호출할 때 컬렉션을 변환할 필요가 없다.
같은 컬렉션을 사용하지만 코틀린은 자바의 라이브러리를 확장하여 사용하기 때문에 더 많은 기능을 쓸 수 있다.
예를 들어 마지막 원소 가져오기, 원소 섞기, 컬렉션의 합계 얻기 등을 할 수 있다.
fun main() {
val strings = listOf("first", "second", "fourteen")
println(strings.last())
// fourteen
println(strings.shuffled())
// [second, fourteen, first]
val numbers = setOf(1, 14, 2)
println(numbers.sum())
// 17
}
자바 컬렉션에는 디폴트 toString이 구현되어 있다.
fun main() {
val list = listOf(1, 2, 3)
println(list)
// [1, 2, 3]
}
디폴트 구현과 다른 형식으로 출력하려면 어떻게 해야할까?
자바에서는 구아바(Guava)나 아파치 커먼즈(apache commons)같은 서드파티 프로젝트를 추가해야 한다.
하지만 코틀린에는 이런 요구 사항을 처리할 수 있는 함수가 표준 라이브러리에 이미 들어있다.
코틀린 표준 라이브러리가 제공하는 기능을 사용하지 않고 해당 기능을 구현해보고 리펙터링 해 나가보자.
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()
}
fun main() {
val list = listOf(1, 2, 3)
println(joinToString(list, ";", "(", ")"))
}
joinToString 함수는 원소 사이에 구분자를 추가하고, stringBuilder의 맨 앞과 맨 뒤에 접두사와 접미사를 추가한다.
<>를 사용해 제네릭한 함수로 선언했기에 어떤 타입의 값을 원소로 하는 컬렉션이든 처리할 수 있다.
위에서 작성한 joinToString()은 호출할 때 가독성이 좋지 않다.
이런 문제는 불리언 플래그 값을 전달해야 하는 경우 흔히 발생한다.
코틀린에서 이를 해결하는 방법은 함수를 호출할 때 함수에 전달하는 인자에 이름을 명시할 수 있다.
모든 인자의 이름을 지정하는 경우 순서까지 바꿀 수 있다.
joinToString(collection, seperator = " ", prefix = " ", postfix = ".")
joinToString(postfix = ".", seperator = " ", collection = collection, prefix = " ")
자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 자주 발생한다.
하위 호환성, API 사용자 편의 제공 등의 이유들로 만들어지지만 중복이라는 결과는 같다.
코틀린에서는 함수 선언에서 파라미터의 기본값을 지정할 수 있으므로 이런 오버로드 중 상당수를 피할 수 있다.
기본값을 통해 joinToString()을 개선해보자.
fun <T> joinToString(
collection: Collection<T>,
separator: String = ",",
prefix : String = "",
postfix: String = ""
): String
이제 함수를 사용할 때 모든 인자를 사용할 수도 있고, 일부를 생략할 수도 있다.
fun main() {
val list = listOf(1, 2, 3)
println(joinToString(list, ",", "", ""))
println(joinToString(list))
println(joinToString(list, "; "))
}
// 1,2,3
// 1,2,3
// 1; 2; 3
함수에 디폴트 값을 설정하는 경우 함수를 호출하는 쪽이 아닌 함수를 선언하는 쪽에 인코딩된다.
기본값과 자바
자바에는 디폴드 파라미터 값이라는 개념이 없어 디폴트 파라미터를 선언한 코틀린 함수를 자바에서 호출하는 경우 모든 인자를 명시해야한다.
자바에서 좀 더 편하게 코틀린 함수를 호출하고 싶다면 @JvmOverloads 어노테이션을 함수에 추가할 수 있다.
이렇게 해면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로 부터 파라미터를 하나씩 생략한 오버로딩한 자바 메서드를 추가해준다.
각각의 오버로드 함수들은 시그니처에서 생략된 파라미터에 대해 코틀린 함수의 디폴트 파라미터 값을 사용한다.
@JvmOverloads
fun <T> joinToString(
collection: Collection<T>,
separator: String = ",",
prefix : String = "",
postfix: String = ""
): String { /*...*/ }
// 생성되는 자바 오버로드 함수
String joinToString(Collection<T> collection, String seperator, String prefix, String postfix);
String joinToString(Collection<T> collection, String seperator, String prefix);
String joinToString(Collection<T> collection, String seperator);
String joinToString(Collection<T> collection);
코틀린에서는 함수를 클래스 안에 선언할 필요가 없다.
객체지향 언어인 자바에서는 모든 코드를 클래스의 메서드로 작성해야 한다.
하지만 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다.
일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있고 중요한 객체는 하나뿐이지만 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지 않은 경우도 있다.
그 결과 특별한 상태나 인스턴스가 없는 클래스가 생겨난다.
이런 클래스는 다양한 정적 메서드를 모아두는 역할만 담당한다.
코틀린에선 이런 무의미한 클래스가 필요하지 않다.
함수를 직접 소스 파일의 최상위 수준 즉, 모든 클래스의 밖에 위치시키면 된다.
이렇게 선언한 함수는 패키지의 맴버 함수이므로 해당 패키지를 임포트 하는 것 만으로 호출해 사용할 수 있다.
클래스의 객체를 생성할 필요가 없어진다는 말이다.
joinToString() 함수를 strings라는 패키지에 넣고 join.kt라는 파일에 작성해보자.
package strings
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()
}
이 파일일 실행되는 원리를 알아보자.
JVM은 클래스 안에 들어있는 코드만을 실행할 수 있다.
따라서 이 함수를 실행할 때 새로운 클래스가 만들어져야 한다.
만약 자바등의 다른 JVM 언어에서 호출한다면 코틀린 컴파일러가 생성하는 클래스의 이름은 최상위 함수가 들어있던 코틀린 소스 파일의 이름으로 클래스가 만들어진다.
코틀린 파일의 모든 최상위 함수는 그 함수가 정의된 파일의 이름에 대응되는 클래스의 정적 메소드가 된다는 걸 알 수 있다.
따라서 자바에서는 다른 정적 메서드와 마찬가지로 쉽게 이 함수를 호출할 수 있다.
// 자바
import strings.JoinKt;
/*...*/
JoinKt.joinToString(list, ", ", "", "");
파일에 대응하는 클래스이 이름 변경하기
디폴트로 컴파일러가 만들어 주는 클래스의 이름은 파일이름 뒤에 KT라는 접미사를 붙인 것이다.
이 클래스의 이름을 바꾸고 싶다면 @file:JvmName(/…/) 어노테이션을 패키지 선언 전 파일의 최상위에 위치시킨다.
@file:JvmName("StringFunctions")
package strings
fun <T> joinToString( /*...*/ ): String { /*...*/ }
함수와 마찬가지로 프로퍼티도 파일 최사우이 수준에 놓을 수 있다.
어떤 연산을 수행한 횟수를 저장하는 var 프로퍼티를 만들 때 유용하다.
var opCount = 0
fun performOperation() {
opCount++
/*...*/
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}
이런 프로퍼티의 값은 정적 필드에 저장된다. 최상위 프로퍼티를 활용해 코드에서 상수를 정의할 수 있다.
val UNIX_LINE_SEPERATOR = "\n"
최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다.
(getter/setter) 이 상수를 자바 코드에게 자연스럽게 public static final 필드로 노출하고 싶다면 앞에 const 변경자(modifier)를 추가하면 된다.
const val UNIX_LINE_SEPERATOR = "\n"
// in Java
public static final String UNIX_LINE_SEPERATOR = "\n";
확장 함수를 사용하면 기존 자바 API를 재작성 하지 않고도 여러 기능을 사용할 수 있다.
확장 함수는 어떤 클래스의 맴버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 메서드다.
package strings
fun String.lastChar(): Char = this.get(this.length - 1)
// ㄴ수신객체 타입 ㄴ-------ㄴ수신 객체
// this는 수신 객체와 연결된다.
확장 함수를 사용하기 위해 해야 할 일은 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덕붙이는 것 뿐이다.
이런 클래스 이름을 “수신 객체 타입” 이라 부르며,
확장 함수 호출 시 호출하는 객체를 “수신 객체”라고 부른다.
이 함수를 호출하는 구문은 일반 함수를 호출하는 구문과 똑같다.
fun main() {
println("kotlin".lastChar())
}
이 구문에서 String이 수신 객체 타입이고 kotlin이 수신 객체다.
함수 확장을 통해 Groovy와 같은 JVM 언어로 작성된 클래스도 확장할 수 있고,
final로 상속을 할 수 없게 선언된 경우에도 문제가 되지 않는다.
자바 클래스로 컴파일된 클래스 파일이 있는 한 그 클래스에 원하는 대로 확장을 추가할 수 있다.
일반 메서드와 마찬가지로 확장 함수 본문의 this를 생략할 수 있다.
package strings
fun String.lastChar(): Char = get(length-1)
확장 함수 내부에서는 일반적인 인스턴스 메서드의 내부와 마찬가지로 씬 객체의 메서드나 프로퍼티를 바로 사용할 수 있다.
하지만 확장 함수가 캡슐화를 깨진 않는다.
확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 비공개(private) 맴버나 보호된(protected) 맴버를 사용할 수 없다.
확장 함수를 사용하려면 다른 클래스나 함수와 마찬가지로 해당 함수를 임포트해야만 한다.
코틀린에서는 클래스를 임포트할 때와 같은 구문으로 사용해 개별 함수를 임포트할 수 있다.
(*를 사용한 와일드카드 임포트도 가능)
import strings.lashChar
val c = "Kotiln".lastChar()
as 키워드를 통해 임포트한 클래스나 함수를 다른 이름으로 부를 수 있다.
import strings.lashChar as last
val c = "Kotiln".last()
확장 함수는 코틀린 문법상 반드시 짧은 이름을 써야 한다.
확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드다.
따라서 확장 함수를 호출해도 adapter 객체나 실행 시점 부가 비용이 들지 않는다.
이런 설계로 인해 자바에서 확장 함수를 사용하기도 편하다.
정적 메서드를 호출하면서 첫 번째 인자로 수신 객체를 넘기면 된다.
다른 최상위 함수와 마찬가지로 확장 함수가 들어있는 자바 클래스 이름도 확장 함수가 들어있는 파일 이름을 따라 결정된다.
따라서 확장 함수를 stringUtil.kt 파일에 정의 했다면 이렇게 호출할 수 있다.
char c = StringUtilKt.lastChar("Java")
joinToString() 함수를 확장 함수로 정의해보자
package strings
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()
}
import strings.joinToString
fun main() {
val list = listOf(1, 2, 4)
println(list.joinToString(separator = ";", prefix = "(", postfix = ")"))
}
원소로 이루어진 컬렉션에 대한 확장을 만들었다.
이제 joinToString()을 클래스의 맴버인 것처럼 호출할 수 있다.
확장 함수는 정적 메서드 호출에 대한 문법적인 편의일 뿐이기 때문에 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수 있다.
문자열의 컬렉션에 대해서만 호출하고 싶다면 제네릭 선언을 String으로 바꿔주면 된다.
package strings
fun Collection<String>.join(
separator: String = ",",
prefix: String = "",
postfix: String = ""
) = joinToString(separator, prefix, postfix)
import strings.join
fun main() {
println(listOf("one", "two", "eight").join())
}
코틀린의 메서드 오버라이드도 일반적인 객체지향의 메서드 오버라이드와 마찬가지다.
하지만 확장 함수는 오버라이드 할 수 없다.
View와 하위 클래스인 Button이 있는데, Button이 상위 클래스의 click 함수를 오버라이드하는 경우를 생각해보자.
이를 구현하려면 하위 클래스에서 click의 구현을 제공할 수 있도록 상위 클래스 코드에 open 변경자를 추가해야 한다.
open class View { //View를 open으로 표시하여 하위 클래스 생성을 허용한다.
open fun click() = println("View clicked") // click을 open으로 표시해서 구현을 오버라이드 할 수 있게 허용한다.
}
class Button: View { // Button은 View를 확장한다.
override fun click() = println("Button clicked") // Button은 click 구현을 오버라이드 한다.
}
하지만 확장 함수는 이런 식으로 동작하지 않는다. 확장 함수는 클래스의 일부가 아니기 때문이다.

이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의할 수 있지만,
실제 호출될 함수는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 컴파일 시점의 타입에 의해 결정되지 실행 시간에 그 변수에 저장된 객체의 타입에 의해 결정되지는 않는다.
다음 예제는 View와 Button 클래스에 대해 선언된 두 showOff 확장 함수를 보여준다.
View 타입의 변수에 대해 showOff를 호출하면 그 변수 안에 들어있는 값이 실제로는 Button 타입이었다해도 View 타입에 해당하는 showOff() 확장 함수가 호출된다.
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
fun main() {
val view: View = Button()
view.showOff()
}
//I'm a view! 출력
확장 함수를 첫 번째 인자가 수신 객체인 정적 자바 메서드로 컴파일 한다는 사실을 기억한다면 동작을 이해할 때 도움이 될 것이다.
코틀린은 호출할 확장 함수를 정적으로 결정한다.
어떤 클래스를 확장한 함수와 그 클래스의 맴버 함수의 이름고 시그니처가 같다면 확장 함수가 아니라 맴버 함수가 호출된다.
확장 함수와 마찬가지로 확장 프로퍼티를 사용하면 함수가 아니라 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다.
프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 방법이 없기 때문에 확장 프로퍼티는 아무 상태도 가질 수 없다.
따라서 확장 프로퍼티는 커스터 접근자를 정의해야 한다.
접근자를 호출할 때 함수 문법 대신 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어 편한 경우가 많다.
앞에서 정의한 lastChar() 함수를 프로퍼티로 바꿔서 “myText.lastChar”라고 쓸 수 있게 해보자
val String.lastChar: Char get() = get(length - 1)
확장 프로퍼티도 프로퍼티에 수신 객체 클래스를 추가한다.
뒷바침하는 필드가 없어 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의를 해야 한다.
초기화 코드 또한 계산 한 값을 저장할 필드가 없기 때문에 초기화 코드를 쓸 수 없다.
var StringBuilder.lastChar: Char
get () = get(length - 1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
확장 프로퍼티를 사용하는 방법은 맴버 프로퍼티를 사용하는 방법과 같다.
fun main() {
val sb =StringBuilder("Kotiln?")
println(sb.lastChar)
sb.lastChar = '!'
println(sb)
}
자바에서 확장 프로퍼티를 사용하고 싶다면 항상 게터나 세터를 명시적으로 호출해야 한다.
// getter
StringUtilKt.getLastChar(”Java”)
// setter
StringUtilKt.setLastChar(sb, ”!”)
컬렉션 관련 표준 라이브러리 함수에 대해 설명하면서 다음의 코틀린 언어 특성을 공부한다.
코틀린에 추가된 컬렉션 관련 API들이 어떻게 작동하며,
어떻게 자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해 코틀린이 새로운 기능을 추가할 수 있었을까?
답은 확장 함수로 정의되어 코틀린 파일에서 디폴트로 임포트되기 때문이다.
fun <T> List<T>.last(): T { /*마지막 원소를 반환함*/ }
fun Collection<Int>.max(): Int { /*컬렉션의 최댓값을 찾음*/ }
코틀린에서 컬렉션을 만드는 함수는 인자의 개수가 그때그때 다를 수 있다는 특징이 있다.
(리스트를 만든다던지…)
파라미터 개수가 달라질 수 있는 함수를 정의하는 방법을 알아보자.
리스트를 생성하는 함수를 호출할 때 원하는 만큼 원소를 전달할 수 있다.
val list = listOf(1, 2, 3, 4, 5)
이 함수가 어떻게 만들어졌는지 함수의 시그니처를 봐보자.
fun listOf<T>(vararg values: T): List<T> { /*구현*/ }
이 함수는 호출할 때 원하는 개수만큼 여러 값을 인자로 넘기면 배열에 그 값들을 넣어주는 언어 기능인 가변 길이 인자(varargs)를 사용한다.
자바와의 차이점은 자바는 타입뒤에 … 을 붙이지만, 코틀린은 파라미터 앞에 vararg 변경자를 붙인다는 것이다.
이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때는 자바와 좀 다르다.
자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어 배열의 각 원소가 인자로 전달되게 해야한다.
이런 기능을 스프레드(spread) 연산자라고 한다.
스프레드 연산은 배열 앞에 *를 붙이기만 하면 된다.
fun main(args: Array<String>) {
val list = listOf("args: ", *args)
println(list)
}
val map = mapOf(1 to "one", 2 to "two", 3 to "three")
코틀린에서 맵을 생성하는 간단한 코드다.
여기서 to라는 단어는 코틀린 키워드가 아니다.
이 코드는 중위 호출(infix call)이라는 특별한 방식으로 to 라는 일반 메서드를 호출한 것이다.
중위 호출 시에는 수신 객체 뒤에 메서드 이름을 위치시키고 그 뒤에 유일한 메서드 인자를 넣는다.
(각각 공백으로 구분)
다음의 두 구문은 동일한 구문이다.
1.to("one")
1 to "one"
인자가 하나뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에만 중위 호출을 사용할 수 있다.
함수에서 중위 호출을 사용하려면 함수 선언시 infix 변경자를 함수 선언 앞에 추가해야 한다.
다음은 to 함수의 정의를 간략하게 줄인 코드다.
infix fun Any.to(other: Any) = Pair(this, other)
이 함수는 Pair의 인스턴스를 반환한다.
Pair는 표준 코틀린 라이브러리 클래스로 이름 그대로 두 원소로 이뤄진 순서쌍을 표현한다.
위에 예시는 간략하게 줄인 코드라 적혀있지 않지만 실제로 to 함수는 제네릭 함수다.
Pair의 내용을 갖고 두 변수를 즉시 초기화 할 수도 있다.
val (number, name) = 1 to "one"
이런 기능을 구조 분해 선언(destruction declaration)이라고 한다.

구조 분해는 순서쌍에만 한정되지 않는다.
예를 들어 key와 value라는 두 변수를 map의 항목을 사용해 초기화 할 수 있다.
루프에서도 구조 분해 선언을 활용할 수 있다.
위에서 작성한 joinToString() 함수에서 그 사용을 볼 수 있다.
for((index, element) in collection.withIndex()){
println("$index: $element")
}
to 함수는 확장 함수다.
to를 사용하면 타입과 상관없이 임의의 순서 쌍을 만들 수 있다.
이는 to 함수가 제네릭 함수로 선언되었다는 뜻이다.
mapOf 함수의 시그니처를 살펴보자
fun <K, V> mapOf(vararg values: Pair<K, V>): Map<K, V>
mapOf도 임의의 개수를 인자로 받는 가변 인자 함수다.
하지만 이 경우에는 각 인자가 키와 값으로 이뤄진 순서쌍이어야 한다.
코틀린의 문자열은 자바의 문자열과 똑같다.
하지만 코틀린은 다양한 확장 함수를 제공함으로써 표준 자바 문자열을 더 즐겁게 다루게 해준다.
두 API의 차이를 알아보기 위해 문자열을 구분 문자열에 따라 나누는 작업을 코틀린에서 어떻게 처리하는지 알아보자.
자바에서 split 함수는 불편한 점이 있다.
바로 점( . )을 사용해 문자열을 분리할 수 없다는 점이다.
split은 정규식(regex)를 구분 문자열로 받아 그 정규식에 따라 문자열을 나누기 때문이다.
정규식에서 점( . )은 모든 문자를 의미한다.
코틀린에서는 자바의 split 대신 여러 가지 다른 조합의 파라미터를 받는 split 확장 함수를 제공함으로써 혼동을 야기하는 메서드를 감춘다.
정규식을 파라미터로 사용하고 싶을 때는 Regex 타입임을 명시해야 한다.
fun main() {
println("12.345-6.A".split("\\.|-".toRegex()))
//[12, 345, 6, A]
}
코틀린에서 3중 따옴표를 사용하면 백슬래시( \ )를 포함한 어떤 문자도 이스케이프 할 필요가 없다.
즉 마침표를 이스케이프 하려면 “\\.”이 아니라 "\.”만 써주면 된다.
val regex = """(.+)/(.+)\.(.+) """.toRegex()

3중 따옴표는 이스케이프를 피하기는 것 뿐만 아니라 줄 바꿈을 포함한 아무 문자열을 넣는데 사용할 수 있다.
val kotlinLogo = """
| //
| //
|/ \
""".trimIndent()
fun main(){
println(kotlinLogo)
}
// | //
// | //
// |/ \
trimIndent를 호출하면 문자열의 모든 줄에서 가장 짧은 공통 들여쓰기를 찾아 각 줄의 첫 부분에서 제거하고 공백만으로 이뤄진 첫 줄과 마지막 줄을 제거해준다.
파일의 줄 끝을 표시하는 문자는 운영체제 마다 다르다.(CRLF, CR, LF) 하지만 코틀린에선 사용한 운영체제와 관련 없이 처리 할 수 있다.
3중 따옴표 문자열 안에 문자열 템플릿을 사용할 수도 있다.
하지만 $나 유니코드 이스케이프를 사용하고 싶을 때는 내포 식을 사용해야 한다.
val think = """Hmm \uD83E\uDD14""" ( X )
val think = """Hmm ${"\uD83E\uDD14"}""" ( O )
코틀린에서는 함수에서 추출한 함수를 원래의 함수 내부에 내포시킬 수 있다.
이를 통해 문법적인 부가 비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
흔히 발생할 수 있는 코드 중복을 로컬 함수를 통해 제거할 수 있는지 알아보자.
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")
}
}
fun main() {
saveUser(user = User(1, "", ""))
}
// java.lang.IllegalArgumentException: Can't save user 1: empty Name
이 코드에선 필드를 검증하는 코드가 중복된다.
이 중복을 로컬 함수를 사용해 해결해보자
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")
}
이렇게 하면 코드의 중복은 사라지고 User의 다른 필드에 대한 검증을 추가 하기도 쉬워진다.
이 예제를 더 개선하고 싶다면 User 클래스를 확장한 함수로 검증 로직을 만들 수도 있다.
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()
}