▩ 목 차 ▩
1. 함수 정의와 호출
1-1. 코틀린에서 컬렉션 만들기
1-2. 함수를 호출하기 쉽게 만들기
1-2-1. 이름 붙인 인자
1-2-2. 디폴트 파라미터 값
1-2-3. 정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티
1-3. 메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
1-3-1. 임포트와 확장 함수
1-3-2. 자바에서 확장 함수 호출
1-3-3. 확장 함수로 유틸리티 함수 정의
1-3-4. 확장 함수는 오버라이드 할 수 없다.
1-3-5. 확장 프로퍼티
1-4. 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원
1-4-1. 자바 컬렉션 API 확장
1-4-2. 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
1-4-3. 값의 쌍 다루기: 중위 호출과 구조 분해 선언
1-5. 문자열과 정규식 다루기
1-5-1. 문자열 나누기
1-5-2. 정규식과 3중 따옴표로 묶은 문자열
1-6. 코드 다듬기: 로컬 함수와 확장
2. 정리
■ 1. 함수 정의와 호출 ■
이번에는 모든 프로그램에서 핵심이라 할 수 있는 함수 정의와 호출 기능을 코틀린이 어떻게 개선했는지 살펴본다. 추가로 자바 라이브러리를 코틀린 스타일로 적용하는 방법을 살펴본다.
■ 1-1. 코틀린에서 컬렉션 만들기
저번에는 setOf 함수를 이용해 색의 집합을 만들었지만, 이번에는 좀 더 단순하게 숫자로 이뤄진 집합을 만들어보자.
[EX]
package list3
val set = hashSetOf(1,7,53) //hashSet
val list = arrayListOf(1,7,53) //arrayList
val map = hashMapOf(1 to "0ne", 7 to "seven", 53 to "fifty-three") //hashMap
fun main(args : Array<String>){
println(set)
println(list)
println(map)
}
[1, 53, 7]
[1, 7, 53]
{1=0ne, 53=fifty-three, 7=seven}
여기서 만든 객체가 어떤 클래스에 속하는지 알기 위해서 이렇게 해보자.
fun main(args : Array<String>){
println(set.javaClass)
println(list.javaClass)
println(map.javaClass)
}
==> javaClass를 이용하면 어떤 클래스에 속하는지 알 수 있다. 즉 자바에서 getClass()에 해당한다. 이는 코틀린이 자체 컬렉션을 제공하지 않는 뜻이다. 이러한 점을 바탕으로 표준 자바 컬렉션을 활용하면 자바 코드와 상호작용하기가 훨씬 더 쉽다.
즉, 자바에서 코틀린 함수를 호출하거나 코틀린에서 자바 함수를 호출할 때 자바와 코틀린 컬렉션을 서로 변환할 필요가 없다.
정리하자면, 코틀린 컬렉션은 자바 컬렉션과 똑같은 클래스다. 하지만 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있다. [ 예를들어, 리스트의 마지막 원소를 가져오거나 수로 이뤄진 컬렉션에서 최댓값을 찾을 수 있다. ]
[EX]
val string = listOf("first", "second", "fourteenth")
val numbers = setOf(1, 14, 2)
println(string.last())
println(numbers.max())
fourteenth
14
이번 3장에서는 이런 기능이 어떻게 동작하는지 보여주고, 자바 클래스에 없는 메서드를 코틀린이 어디에 정의하는지 살펴본다.
■ 1-2. 함수를 호출하기 쉽게 만들기
위에서 여러 원소로 이뤄진 컬렉션을 만드는 방법을 배웠으므로 간단하게 모든 원소를 찍어보자. 원소를 찍는 과정에서 여러 중요한 개념이 나온다.
자바 컬렉션에는 디폴트 toString의 출력 형식은 고정돼 있고 우리에게 필요한 형식이 아닐 수 있다.
[EX]
val list = arrayListOf(1,7,53) //arrayList
println(list)
[1, 7, 53]
위와 같은 결과인 디폴트 구현과 달리 (1; 7; 23)처럼 원소 사이를 세미콜론으로 구분하고 괄호로 리스트를 둘러싸고 싶다면 어떻게 할까?
==> 코틀린에는 이런 요구 사항을 처리할 수 있는 함수가 표준 라이브러리에 들어있지만 지금은 직접 함수를 구현해보자.
[EX] - joinToString()함수의 초기 구현
package list3
import java.lang.StringBuilder
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(args : Array<String>){
val list = listOf(1,2,3)
println(joinToString(list,"; ","(",")"))
}
(1; 2; 3)
위의 함수는 제네릭하다. 즉, 어떤 타입의 값을 원소로 하는 컬렉션이든 처리할 수 있다.[ Collection<T>이기 때문에 ]
위의 함수를 호출하는 문장을 보면 복잡하다. 어떻게 하면 간단하게 할까 ? [ 매번 함수 호출을 할때 네 인자를 모두 전달하기 때문에 ]
==> 아래를 살펴보자.
■ 1-2-1. 이름 붙인 인자
위의 함수에서 해결하고 싶은 첫번째 문제점은 가독성이다.
==> 코틀린에서는 다음과 같이 해결할 수 있다. joinToStrng(collection, separator = " ", prefix = " ", postifx = "."
코틀린으로 작성한 함수를 호출할때는 함수에 전달하는 인자 중 일부(또는 전부)의 이름을 명시 할 수 있다. 호출 시 인자 중 어느 하나라도 이름을 명시하고 나면 혼동을 막기 위해 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야한다.
■ 1-2-2. 디폴트 파라미터 값
자바에서는 일부 클래스에서 오버로딩한 메서드가 너무 많아진다는 문제가 있다. 그리고 인 자 중 일부가 생략된 오버로드 함수를 호출 할 때 어떤 함수가 불릴지 모호한 경우가 생긴다.
==> 코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있으므로 이런 오버로드 문제를 피할 수 있다.
디폴트 값을 사용해 joinToString 함수를 개선해보자.
[ 대부분의 경우 아무 접두사나 접미사 없이 콤마로 원소를 구분한다. 따라서 그런 값을 디폴트로 지정하자. ]
[EX] - 디폴트 파라미터 값을 사용해 joinToString() 정의하기
fun <T> joinToString2(
collection: Collection<T>,
separator: String = ", ",
prefix: String = ", ",
postfix: String =""
) : String
fun main(args : Array<String>){
val list = listOf(1,2,3)
println(joinToString2(list, ",", "", ""))
println(joinToString2(list)) //separator, prefix, postfix 생략
println(joinToString2(list, "; ")) //separator를 "; "로 지정, prefix와 postfix todfir
}
1,2,3
1, 2, 3
1; 2; 3
위의 코드를 보게 되면 함수를 호출할 때 모든 인자를 쓸 수도 있고, 일부를 생략할 수도 있다.
일반 호출 문법을 사용하려면 함수를 선언할때와 같은 순서로 인자를 지정해야 한다. 그런 경우 일부를 생략하면 뒷부분의 인자들이 생략된다.
이름 붙인 인자를 사용하는 경우에는 인자 목록의 중간에 있는 인자를 생략하고 , 지정하고 싶은 인자를 이름을 붙여서 순서와 관계없이 지정할 수 있다.
[EX]
println(joinToString2(list, postfix=";", prefix="# "))
# 1, 2, 3;
다시 정리하자면, 함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다는 사실이다.
■ 1-2-3. 정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티
자바를 했던 사람들은 알 것이다. 객체지향 언어인 자바에서는 모든 코드를 클래스의 메서드로 작성해야 한다는 사실을...
이러한 구조들은 잘 잘동을 하지만, 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다.
==> 코틀린에서는 이런 무의미한 클래스가 필요 없다. 대신 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치 시키면 된다! 그러한 함수들은그 파일의 맨 앞에 정의된 패키지의 멤버 함수이므로 다른 패키지에서 그 함수를 사용하고 싶을때 그 함수가 정의된 패키지를 임포트해야만 한다. 하지만 임포트 시 클래스 이름이 들어갈 필요는 없다.
■ 1-3. 메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
기존 코드와 코틀린 코드를 자연스럽게 통합하는 것은 코틀린의 핵심 목표 중 하나다.
완전히 코틀린으로로만 이뤄진 프로젝트조차도 JDK나 안드로이드 프레임워크 또는 다른 서드파티 프레임워크 등의 자바 라이브러리를 기반으로 만들어진다.
또 코틀린을 기존 자바 프로젝트에 통합하는 경우에는 코틀린으로 직접 변환할 수 없거나 미처 변환하지 않은 기존 자바 코드를 처리할 수 있어야 한다.
이런 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러 편리한 기능을 어떻게 사용할까??
==> 확장 함수(extension function)이 해결해준다. 확장함수는 개념적으로 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다. [ 여기서 포인트는 클래스 밖에 선언된 것이다. ]
확장 함수를 보여주기 위해 어떤 문자열의 마지막 문자를 돌려주는 메서드를 추가해보자.
[EX]
package strings
fun String.lastChar() : Char = this.get(this.length -1)
확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다. 클래스 이름을 수신 객체 타입이라 부르며, 확장 함수가 호출되는 대상이 되는 값을 수신 객체 라고 부른다. 위의 예제에서는 String이수신 객체 타입 this가 수신 객체이다.
이 함수를 호출하는 구문은 다른 일반 클래스 멤버를 호출하는 구문과 똑같다.
println("Kotlin".lastChar())
위에서는 String이 수신 객체 타입이고, "Kotlin"이 수신 객체
쉽게 생각하면, String 클래스에 새로운 메서드를 추가하는 것과 같다. String 클래스가 우리가 직접 작성한 코드도 아니고 심지어 String 클래스의 소스코드를 소유한 것도 아니지만, 우리가 원하는 메소드를 String 클래스에 추가할 수 있다. 즉, Kotlin이 제공하는 클래스에 우리가 임의로 편하게 메소드를 추가하는 것이라고 생각하자~^^ 우리는 그 클래스 안에 함수를 정의하지 않고 밖에서 정의하지만 정상적으로 작동한다~^^
또한 일반 메서드와 마찬가지로 확장 함수 본문에서도 this를 생략할 수 있다.
package strings
fun String.lastChar() : Char = get(length -1)
또한 확장 함수 내부에서는 일반적인 메서드의 내부에서와 마찬가지로 수신 객체의 메서드나 프로퍼티를 바로 사용할 수 있다.
확장 함수가 캡슐화를 깨지는 않는다는 사실은 기억해야한다.
==> 클래스 안에서 정의한 메서드와 달리 확장 함수 안에서는 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없다. [ 당연하다. 클래스 밖에 정의했으니 인스턴스 변수를 사용할 수 없다. ]
이제부터는 클래스의 멤버 메서드와 확장 함수를 모두 메서드라고 부를 것이다.
예를 들어, "확장 함수 내부에서는 수신 객체의 모든 메서드를 호출 할 수 있다."라고 말하면 확장 함수 내부에서 수신 객체의 멤버 메서드와 확장 함수를 모두 호출할 수 있다는 뜻이다.
■ 1-3-1. 임포트와 확장 함수
확장 함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 사용할 수 있지는 않다.
확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 한다. [ 확장 함수를 정의하자마자 어디서든 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 자주 생길 수 있다. ]
코틀린에서는 클래스를 임포트 할 때와 동일한 구문을 사용해 개별 함수를 임포트 할 수 있다.
[EX] - 확장 함수 임포트
import strings.lastChar
import strings.*
import strings.lastChar as last // as 키워드를 사용하여 임포트한 클래스나 함수를 다른 이름으로 부를 수 있다.
as 키워드를 사용하면 임포트한 클래스나 함수를 다른 이름으로 부를 수 있다.
==> 한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 as 키워드로 이름를 바꿔서 임포트하면 이름 충돌을 막을 수 있다. 즉, 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.
■ 1-3-2. 자바에서 확장 함수 호출
내부적으로 확장 함수는 수신 객체를 첫번째 인자로 받는 정적 메서드다.
==> 그렇기에 확장함수를 호출해도 다른 어댑터 객체라던지 부가 비용이 발생하지 않는다.
그렇기에 자바에서 확장 함수를 사용하기도 편하다. 사용하는 방법은 아래와 같다.
[EX] - 확장함수를 String.kt 파일에 정의한 것을 자바에서 호출
/* 자바 */
char c = StringUtilKt.lastChar("Java");
■ 1-3-3. 확장 함수로 유틸리티 함수 정의
joinToString 함수의 최종 버전을 만들자.
[EX] - joinToString()를 확장으로 정의하기
package list3
import strings.lastChar
import strings.lastChar2
import java.lang.StringBuilder
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()
}
fun main(args : Array<String>){
val list = listOf(1,2,3)
val arraylist = arrayListOf(1,2,3)
println(list.joinToString(separator = "; ", prefix = "", postfix = "" ))
println(arraylist.joinToString(separator = "; ", prefix = "", postfix = "" ))
}
1; 2; 3
1; 2; 3
위 코드를 보자. 최종본이다.
Collection.joinToString() 이라는 코드는 모든 Collection 클래스로부터 joinTostring이라는 확장함수를 구현하겠다라는 뜻이다.
또한 모든 인자(프로퍼티)에 대해 디폴트 값(기본값)을 저장한다.
결과를 보게되면 모든 Collection(list, arraylist)에 대해 확장 함수를 사용할 수 있는 것을 알 수 있다.
확장 함수는 단지 정적 메서드(해당 클래스와 연결되어 있지만, 해당 클래스의 특정 인스턴스와 연결되어 있지 않음) 호출에 대한 문법적인 편의일 뿐이다.
==> 그래서 제네릭<>을 이용하여 문자열의 컬렉션에 대해서만 호출 할 수 있는 join 함수를 지정할 수 있다.
[EX] - 제네릭<>을 이용하여 문자열의 컬렉션에 대해서만 호출할 수 있는 join 함수
fun Collection<String>.join(
separator: String = ", ",
prefix: String = "",
postfix: String = ""
) = joinToString(separator,prefix,postfix)
fun main(args : Array<String>){
val list = listOf(1,2,3)
val list2 = listOf("one","tow","three")
println(list.join()) // 컴파일 오류 발생
println(list2.join())
}
■ 1-3-4. 확장 함수는 오버라이드 할 수 없다.
확장 함수는 오버라이드 할 수 없다.
View와 그 하위 클래스인 Button이 있는데, Button이 상위 클래스의 click 함수를 오버라이드 하는 경우를 생각해보자.
package list3
open class View{
open fun click() = println("View clicked")
}
class Button: View() { //Button은 View를 확장한다.
override fun click() = println("Button clicked")
}
fun main(args : Array<String>){
val view: View = Button() // "view"에 저장된 값의 실제 타입에 따라 호출할 메서드가 결정된다.
view.click()
}
Button clicked
Button 클래스가 click()메소드를 오버라이드를 정상적으로 했기 때문에 Button clicked가 출력된 것을 볼 수 있을 것이다.
확장 함수는 이런식으로 작동하지 않는다. 즉, 오버라딩이 되지 않는다. 확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언되기 때문이다.
[EX] - View와 Button 클래스에 대해 선언된 두 showOff 확장 함수
package list3
fun View.showOff() = println("I'm a view!")
fun Button.showOff() = println("I'm a button!")
fun main(args : Array<String>){
val view: View = Button()
view.showOff()
}
I'm a view!
위의 코드를 보자.
우선, Button 클래스는 View 클래스로부터 상속을 받는다. 이러한 상태에서 View 클래스와 Button 클래스의 확장 함수를 각각 showOff()라고 정해주었다. 그리고 view라는 변수를 선언해주었고 타입은 View 클래스 타입이고 Button()의 클래스의 성질을 받게 된다. 여기서 view에 대한 showOff()를 찍어 Button() 클래스의 함수 출력문인 I'm button! 가 출력 될 줄 알았으나, View 클랙스의 출력문인 I'm a view! 가 찍혔다.
==> 즉, 확장 함수는 오버라이드 할 수 없다. 객체의 실체 타입이 아닌 변수 타입에 선언된 함수로 호출된다.(오버라이딩 안된다는 말) 안되는 이유는 호출될 확장 함수를 정적으로 결정하기 때문이다.
■ 1-3-5. 확장 프로퍼티
확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있게 된다.
프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 적절한 방법이 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다.
하지만 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있어서 편한 경우가 있다.
앞의 예제 였던 lastChar라는 함수를 프로퍼티로 바꿔보자.
[EX] - 확장 프로퍼티 선언하기
val String.lastChar: Char
get() = get(length-1)
확장 함수의 경우와 마찬가지로 확장 프로퍼티도 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐이다.
뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의를 해야한다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.
[EX] - 변경 가능한 확장 프로퍼티 선언하기
package list3
import java.lang.StringBuilder
var StringBuilder.lastChar: Char
get() = get(length-1)
set(value: Char) {
this.setCharAt(length-1,value)
}
fun main(args : Array<String>){
val sb = StringBuilder("Kotlin?")
sb.lastChar = '!'
println(sb)
}
Kotlin!
확장 프로퍼티를 사용하는 방법은 멤버 프로퍼티를 사용하는 방법과 같다.
만약 자바에서 확장 프로퍼티를 사용하고 싶다면 StringUtilKt.getLastChar("Java")처럼 게터나 세터를 명시적으로 호출해야 한다.
■ 1-4. 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원
이번에는 컬렉션을 처리할 때 쓸 수 있는 코틀린 표준 라이브러리 함수 몇가지를 배워보자. 그 과정에서 코틀린 언어 특성을 설명한다.
- vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
- 중위 함수 호출 구문을 사용하면 인자가 하나뿐인 메서드를 간편하게 호출할 수 있다.
- 구조 분해 선언을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.
■ 1-4-1. 자바 컬렉션 API 확장
코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된 API를 제공한다고 했다.
앞의 예제에서 리스트의 마지막 원소를 가져오는 예제와 숫자로 이뤄진 컬렉션의 최댓값을 찾는 예제를 살펴봤다.
자바 라이브러리 클래스 인스턴스인 컬렉션에 대해 코틀린이 새로운 기능을 추가할 수 있을까?
==> last와 max는 모두 확장 함수였던 것이다!
last 함수는 List 클래스의 확장 함수다.
[ 실제 코틀린 라이브러리의 max는 Int를 포함하는 다양한 타입의 컬렉션에 대해 작동한다. ]
앞에서 컬렉션을 만들어내는 함수를 몇 가지 살펴보았는데, 그런 함수가 모두 가진 특징은 바로 인자의 개수가 그때그때 달라질 수 있다는 점이다. 이번에 파라미터 개수가 달라질 수 있는 함수를 정의하는 방법을 알아보자.
■ 1-4-2. 가변 인자 함수: 인자의 개수가 달라질 수 있는 함수 정의
리스트를 생성하는 함수를 호출할 때 원하는 만큼 많이 원소를 전달할 수 있다.
EX) val list = listOf(2, 3, 5, 7, 11)
라이브러리에서 이 함수의 정의를 보면 다음과 같다.
fun listOf<T>(vararg values: T):List<T> {...}
자바의 가변 길이 인자는 메서드를 호출 할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능이다.
코틀린의 가변 길이 인자도 자바와 비슷하다.
다만 문법이 다르다. 타입 뒤에 ...를 붙이는 대신 코틀린에서는 파라미터 앞에 vararg 변경자를 붙인다.
이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. [ 기술적으로는 스프레드 연산자가 그런 작업을 해준다. ]
실제로는 전달하려는 배열 앞에 *를 붙이기만 하면 된다.
[EX]
fun main(args : Array<String>){
val list = listOf("args: ",*args)
println(list)
}
위의 예제는 스프레드 연산자를 통하면 배열에 들어있는 값과 다른 여러 값을 함께 써서 함수를 호출할 수 있음을 보여준다. [ 자바에서는 사용 못함 ]
■ 1-4-3. 값의 쌍 다루기: 중위 호출과 구조 분해 선언
맵을 만들려면 mapOf 함수를 사용한다.
val map = map(1 to "one", 7 to "seven", 53 to "fifty-three")
여기서 to라는 단어는 코틀린 키워드가 아니다. 이 코드는 중위 호출[ 일종의 연산자 처럼 쓸 수 있는것 ] 이라는 특별한 방식으로 to라는 일반 메서드를 호출한 것이다.
중위 호출 시에는 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣는다.[ 이때 객체, 메서드 이름, 유일한 인자 사이에는 공백이 들어가야 한다. ] 다음 두 호출은 동일하다.
1.to("one") // "to" 메서드를 일반적인 방식으로 호출함
1 to "one" // "to" 메서드를 중위 호출 방식으로 호출함
인자가 하나 뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다.
함수(메서드)를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수(메서드) 선언 앞에 추가해야 한다.
다음은 to 함수의 정의를 간략하게 줄인 코드다.
infix fun Any.to(other:Any) = Pair(this,other)
이 to 함수는 Pair의 인스턴스를 반환한다. Pair는 코틀린 표준 라이브러리 클래스로, 그 이름대로 두 원소로 이뤄진 순서쌍을 표현한다.
Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.
val (number, name) = 1 to "one"
이런 기능을 구조 분해 선언 이라고 부른다.
Pair 인스턴스 외 다른 객체에도 구조 분해를 적용할 수 있다. 예를들어, key와 value라는 두 변수를 맵의 원소를 사용해 초기화 할 수 있다.
루프에서도 구조 분해 선언을 활용할 수 있다. 앞의 예제인 joinToString에서 본 withIndex를 구조 분해 선언과 조합하면 컬렉션 원소의 인덱스와 값을 따로 변수에 담을 수 있다.
for ((index, element) in collection.withIndex()) {
println("$index: $element")
}
to 함수는 확장함수다. to를 사용하면 타입과 관계없이 임의의 순서쌍을 만들 수 있다. 이는 to의 수신 객체가 제네릭하다는 뜻이다.
1 to "one", "one" to 1, list to list.size() 등의 호출이 모두 잘 작동한다.
mapOf 함수의 선언을 살펴보자.
fun <K, V> mapOf(vararg values: Pair<K, V> : Map<K, V>
listOf와 마찬가지로 mapOf에도 원하는 개수만큼 인자를 전달할 수 있다. 하지만 mapOf의 경우에는 각 인자가 키와 값으로 이뤄진 순서쌍이어야 한다.
■ 1-5. 문자열과 정규식 다루기
코틀린 문자열은 자바 문자열과 같다.
==> 코틀린 코드가 만들어낸 문자열을 아무 자바 메서드에 넘겨도 되며, 자바 코드에서 받은 문자열을 아무 코틀린 표준 라이브러리 함수에 전달해도 전혀 문제 없다.
코틀린은 다양한 확장 함수를 제공함으로써 표준 자바 문자열을 더 즐겁게 다루게 해준다. 알아보자.
■ 1-5-1. 문자열 나누기
자바에서 문자열을 나누기 위한 String의 split 메서드를 알고 있을 것이다. 자바에서는 split메소드로는 점(.)을 사용해 문자열을 분리할 수 없다.
==> "12.345-6.A".split(".")라는 호출의 결과가 [12, 345-6, A] 배열이라고 생각하는데 절대 아니다. 빈 배열을 반환하다. 왜냐하면 split의 구분 문자열은 실제로 정규식 이기 떄문이다. 따라서 마침표(.)는 모든 문자를 나타내는 정규식으로 해석된다.
코틀린에서는 split 확장 함수를 제공함으로써 앞의 자바 split(.나누기)의 혼동을 자재운다.
==> 따라서 코틀린에서는 split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.
[EX] - 코틀린에서 split의 확장함수 toRegex() 사용
fun main(args : Array<String>){
println("12.345-6.A".split("\\.|-".toRegex()))
}
[12, 345, 6, A]
또한 split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
[EX] - 코틀린에서 split의 확장함수 중 구분 문자열을 하나 이상 인자 받는 확장함수
fun main(args : Array<String>){
println("12.345-6.A".split(".","-"))
}
[12, 345, 6, A]
■ 1-5-2. 정규식과 3중 따옴표로 묶은 문자열
전체 경로명을 다렉토리, 파일 이름, 확장자로 구분을 해보자.
코틀린 표준 라이브러리에는 어떤 문자열에서 구분 문자열이 맨 나중에 나타난 곳(또는 처음)에 나타난 곳 뒤의 문자열을 반환하는 함수가 있다.
[EX] - String 확장 함수를 사용해 경로 파싱하기
package list3
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = fullName.substringAfterLast(".")
println("Dir: $directory, name: $fileName, ext: $extension")
}
fun main(args : Array<String>){
println(parsePath("/Users/yole/kotlin-book/chapter.adoc"))
}
Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
위의 결과를 보면 Dir에 디렉터리 경로가, name에 파일이름이, ext에 파일 확장자가 들어간 것을 알 수 있다.
코틀린에서는 위와 같이 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다.
■ 1-6. 코드 다듬기: 로컬 함수와 확장
많은 개발자들이 좋은 코드의 중요한 특징 중 하나가 중복이 없는 것이라 생각한다.
중복이 많은 경우 메서드 추출 리팩토링을 적용해서 긴 메서드 를 부분부분 나눠서 각 부분을 재활용 할 수 있다.
==> 이렇게 코드를 리팩토링하면 클래스 안에 작은 메서드가 많아지고 각 메서드 사이의 관계를 파악하기 힘들어서 코드를 이해하기 더 어려워 질 수 있다.
코틀린의 더 깔끔한 해법이 있다.
==> 코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩 시킬 수 있다. 그렇게 하게되면 문법적인 부가 비용을 들이지 않고도 깔끔하게 코드를 조직할 수 있다.
흔히 발생하는 코드 중복을 로컬 함수를 통해 어떻게 제거할 수 있는지 살펴보자.
[EX] - 사용자를 데이터베이스에 저장하는 함수에서 코드 중복을 보여주는 예제
package list3
import java.net.Inet4Address
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 java.lang.IllegalArgumentException(
"can't save user ${user.id}:empty address")
}
}
fun main(args : Array<String>){
saveUser(User(1,"",""))
}
Exception in thread "main" java.lang.IllegalArgumentException: can't save user 1:empty name
위에서는 코드 붕복이 그리 많지 않다. 하지만 클래스가 사용자의 필드를 검증 할 때 필요한 여러 경우를 하나씩 처리하는 메서드로 넘쳐나기를 바라지는 않을 것이다.
==> 이런 경우 검증 코드를 로컬 함수로 분리하면 중복을 없애는 동시에 코드 구조를 깔끔하게 유지할 수 있다.
[EX] - 사용자를 데이터베이스에 저장하는 함수에서 코드 중복을 줄이는 예제
fun saveUser2(user:User2){
fun validate(user: User2, value: String, fieldName: String){
if (value.isEmpty()){
throw IllegalArgumentException(
"can't save user ${user.id}:empty name")
}
}
validate(user, user.name, "Name")
validate(user, user.address, "Address")
}
위의 코드를 보면 검증 로직 중복은 사라졌고, 필요하면 User의 다른 필드에 대한 검증도 쉽게 추가할 수 이다.
하지만 User 객체를 로컬 함수에게 하나하나 전달해야 한다는 점은 아쉽다..
==> 로컴함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 이런 성질을 이용해 불필요한 User파라미터를 없애보자.
[EX] - 로컬 함수에서 바깥 함수의 파라미터 접근하기
fun saveUser3(user:User3){
fun validate(value: String, fieldName: String){
if(value.isEmpty()) {
throw IllegalArgumentException(
"can't save user ${user.id}: " +
"empth $fieldName"
)
}
}
validate(user.name, "Name")
validate(user.address, "Address")
}
위에 있는 예제를 더 개선하고 싶다면 검증 로직을 User 클래스를 확장한 함수로 만들 수도 있다.
[EX] - 검증 로직을 확장 함수로 추출하기
package list3
import java.lang.IllegalArgumentException
class User4(val id: Int, val name: String, val address: String)
fun User4.validateBeforeSave() {
fun validate(value: String, fieldName: String){
if(value.isEmpty()) {
throw IllegalArgumentException(
"can't save user ${id}: " +
"empth $fieldName"
)
}
}
validate(name, "Name")
validate(address, "Address")
}
fun saveUser4(user: User4){
user.validateBeforeSave()
}
fun main(args : Array<String>){
saveUser4(User4(1,"ss","aa"))
}
코드를 확장 함수로 뽑아내는 기법은 놀랄 만큼 유용하다. User는 라이브러리에 있는 클래스가 아니라 우리가 직접 만든 클래스 지만, 이 경우 검증 로직은 User를 사용하는 다른 곳에서는 쓰이지 않는 기능이기 때문에 User에 포함시키고 싶지는 않다. [ 함수안의 함수를 이용[로컬함수 ]
User를 간결하게 유지하면 생각해야 할 내용이 줄어들어서 더 쉽게 코드를 파악 가능하다.
한 객체만을 다루면서 객체의 비공개 데이터를 다룰 필요는 없는 함수는 위의 예제처럼 확장 함수로 만들면 객체.멤버처럼 수신 객체를 지정하지 않고도 공개된 멤버 프로퍼티나 메서드에 접근 가능하다.[ user.name 에서 name으로 접근이 가능한 것 처럼 ]
확장 함수를 로컬 함수로 정의할 수도 있다. 즉, User.validateBeforeSave를 saveUser 내부에 로컬 함수로 넣을 수 있다.
■ 2. 정리 ■
- 코틀린은 자체 컬렉션 클래스를 정의하지 않지만 자바 클래스를 확장해서 더 풍부한 API를 제공한다.
- 함수 파라미터의 디폴트 값을 정의하면 오버로딩한 함수를 정의할 필요성이 줄어든다. 이름 붙인 인자를 사용하면 함수의 인자가 많을때 함수 호출의 가독성을 더 향상 시킬 수 있다.
- 코틀린 파일에서 클래스 멤버가 아닌 최상위 함수와 프로퍼티를 직접 선언할 수 있다. 이를 활용하면 코드 구조를 더 유연하게 만들 수 있다.
- 확장 함수와 프로퍼티를 사용하면 외부 라이브러리에 정의된 클래스를 포함해 모든 클래스의 API를 그 클래스의 소스코드를 바꿀 필요 없이 확장할 수 있다. 확장 함수를 사용해도 실행 시점에 부가 비용이 들지 않는다.
- 중위 호출을 통해 인자가 하나 밖에 없는 메서드나 확장 함수를 더 깔끔한 구문으로 호출할 수 있다.
- 코틀린은 정규식과 일반 문자열을 처리할 때 유용한 다양한 문자열 처리 함수를 제공한다.
- 자바 문자열로 표현하려면 수많은 이스케이프가 필요한 문자열의 경우 3중 따옴표 문자열을 사용하면 더 깔끔하게 표현할 수 있다.
- 로컬 함수를 써서 코드를 더 깔끔하게 유지하면서 중복을 제거 할 수 있다.
'Kotlin > Kotlin in action' 카테고리의 다른 글
6장 코틀린 타입 시스템 (0) | 2022.10.04 |
---|---|
5장 람다로 프로그래밍 (1) | 2022.09.29 |
4장 클래스, 객체, 인터페이스 (1) | 2022.09.24 |
2장 코틀린 기초 (1) | 2022.09.22 |
1장 코틀린이란 무엇이며, 왜 필요한가? (1) | 2022.09.21 |