본문으로 바로가기

코틀린의 함수

category Kotlin 코틀린 2019. 1. 4. 13:32
336x280(권장), 300x250(권장), 250x250, 200x200 크기의 광고 코드만 넣을 수 있습니다.

코틀린에서 컬렉션 만들기


코틀린에서 컬렉션을 만드는 방법은 정말 간단하다. 또한 비슷한 방법으로 맵과 리스트를 만들 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var set = hashSetOf(1,2,3)
var list = listOf("1""2""3")
var map = mapOf(1 to "one"2 to "two"3 to "three")
 
for(data in set) {
    print("$data ")
}
 
println()
 
for(data in list) {
    print("$data ")
}
 
println()
 
for((key, value) in map) {
    println("$key : $value")
}



위의 예제가 만든 객체는 자바의 클래스 또는 코틀린 클래스를 사용하는지 확인해보자 


1
2
3
4
5
6
7
8
9
10
11
var set = hashSetOf(1,2,3)
var list = listOf("1""2""3")
var map = mapOf(1 to "one"2 to "two"3 to "three")
 
println(set.javaClass)
println(list.javaClass)
println(map.javaClass)
 
//class java.util.HashSet
//class java.util.Arrays$ArrayList
//class java.util.LinkedHashMap



결과를 보면 자바 클래스를 사용하는 것을 알 수 있다. 코틀린은 표준 자바 컬렉션을 사용하면 자바 코드와 상호작용하기 쉽다. 자바 또는 코틀린에서 함수를 호출할 때 자바와 코틀린은 서로 변환할 필요가 없다.


코틀린의 컬렉션은 자바보다 더 많은 기능을 쓸 수 있다. 예를 들어 리스트의 마지막 원소를 가져오거나 컬렉션의 최댓값을 찾을 수 있다.


1
2
3
4
5
val strings = arrayListOf("ktko""theo""kkt")
println(strings.last())
 
val numbers = setOf(1,2,3)
println(numbers.max())



자바 컬렉션에서 디폴트 toString이 구현이 되어있다. 하지만 자바에서는 toString을 호출해야하지만 코틀린에서는 자동으로 toString을 호출해준다.


1
2
val list = listOf(1,2,3)
println(list) //[1,2,3]



디폴트 구현화 달리(1; 2; 3;) 처럼 형식을 바꿔서 호출하려면 자바에서는 구아바나 아파치 커먼즈 같은 서드 파티 프로젝트를 추가하거나 직접 관련 로직을 구현해야 하지만 코틀린에서는 표준 라이브러리를 제공한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun<T> joinToString(
    collection: Collection<T>,
    separator: String,
    prefix: String,
    postfix: String
) : String {
    var result = StringBuilder(prefix)
 
    for((index, value) in collection.withIndex()) {
        if(index > 0) result.append(separator)
        result.append(value)
    }
 
    result.append(postfix)
 
    return result.toString()
}



위 예제는 제네릭을 이용하여 직접 (1; 2; 3;) 포맷을 구현하기 위한 함수를 만들어 보았다.  하지만 선언 부분을 고민해봐야 한다. 덜 번잡하고, 함수를 호출할 때 인자를 모두 전달하지 않아도 잘 동작하는 방법에 대해서 고민해봐야 한다.


이름 붙인 인자 


해결하고자 하기 위한 첫 번째 발생 문제는 함수 호출 부분의 가독성이다. 함수 호출 인자가 무엇을 의미하는지 알기 힘들기 때문에 코틀린에서는 이름을 붙여 가독성을 높일 수 있다.


1
println(joinToString(list,separator = "; ", prefix = "(", postfix = ")"))



호출시 인자 중 어느 하나라도 이름을 붙인다면 나머지 인자들도 이름을 꼭 명시해야 한다. 이름 붙인 인자는 디폴트 파라미터 값과 함께 사용할 때 쓸모가 많다.


디폴트 파라미터 값


친절한 개발자라면 사용자를 위해 다양한 오버로딩된 메서드를 제공하지만 그만큼 설명을 반복해 달아야 한다. 그리고 생략된 오버로드 함수를 호출할 때 flow가 다를 수 있어서 모호할 수 있다.


코틀린에서는 파라미터의 디폴트 값을 지정할 수 있으므로 오버헤드를 줄일 수 있다. 위의 joinToString을 새로 정의하면 아래와 같다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun<T> joinToString(
    collection: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) : String {
    var result = StringBuilder(prefix)
 
    for((index, value) in collection.withIndex()) {
        if(index > 0) result.append(separator)
        result.append(value)
    }
 
    result.append(postfix)
 
    return result.toString()
}



위의 함수를 아래와 같이 실행하면 몇몇개의 파라미터를 생략하여 실행한 것을 알 수 있다. 또한 이름 붙은 인자를 사용 하는 경우에는 지정하고 싶은 인자를 이름에 붙여서 순서와 관계없이 지정할 수 있다.


1
2
3
4
5
println(joinToString(list, ", """"")) //1, 2, 3
println(joinToString(list)) //1, 2, 3
println(joinToString(list, "; ")) //1; 2; 3;
 
println(joinToString(list, postfix = ">", prefix = "<")) //<1, 2, 3>



자바에서 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다. 자바에서 좀 더 편하게 코틀린 함수를 호출하고 싶을 때 @JvmOverloads 어노테이션을 함수에 추가할 수 있다. 이 어노테이션을 이용하면 코틀린 컴파일러가 자동으로 모든 파라미터를 포함하는 오버로딩한 메서드를 추가해준다.


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


자바에서는 클래스에 포함시키기 어려운 코드가 있으며, 정적 메서드를 모아두는 역할을 담당하는 클래스가 생겨난다. 하지만 코틀린에서는 이런 무의미한 클래스는 필요 없다. 대신 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다. 그런 함수들은 여전히 그 파일의 맨 앞에 정의된 패키지의 멤버 함수이므로 다른 패키지에서 그 함수를 사용하고 싶을 때는 그 함수가 정의된 패키지를 임포트해야만 한다.


strings라는 패키지를 선언하여 패키지 안에 join.kt 파일을 아래와 같이 작성한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package strings
 
fun<T> joinToString(
    collection: Collection<T>,
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) : String {
    var result = StringBuilder(prefix)
 
    for((index, value) in collection.withIndex()) {
        if(index > 0) result.append(separator)
        result.append(value)
    }
 
    result.append(postfix)
 
    return result.toString()
}



이 함수는 JVM이 클래스 안에 들어있는 코드만을 실행할 수 있기 때문에 컴파일러는 이 파일을 컴파일 할 때 새로운 클래스를 정의한다. 

이 함수를 사용하는 다른 JVM언어에서 호출되었을 때 어떻게 컴파일되는지 알아야 위와 같은 최상위 함수를 사용할 수 있다.

join.kt를 컴파일한 결과와 같은 클래스를 자바코드로 작성해보면 아래와 같다.


1
2
3
4
5
package strings;
 
public class JoinKt {
    public static String joinToString(...) {.....} 
}




코틀린 컴파일러가 생성하는 클래스의 최상위 함수가 들어있던 코틀린 소스 파일의 이름과 대응한다. 코틀린 파일의 모든 최상위 함수는 이 클래스의 정적인 메서드가 된다. 따라서 자바에서는 joinToString를 호출하기 쉽다.


1
2
3
import strings.JoinKt
 
JoinKt.joinToString(list, ", """ ,"");




파일에 대응하는 클래스의 이름 변경하기를 쓸까 말까


최상위 프로퍼티


함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 어떤 데이터를 클래스 밖에 위치시켜야 하는 경우는 흔하지는 않지만, 가끔 유용할 때가 있다. 


아래 예제는 top라는 패키지에 변수와 함수를 선언하였다.


1
2
3
4
5
6
7
8
9
10
11
12
13
package top
 
var opCount = 0
 
fun performOperation() {
 
    opCount++
}
 
fun reportOperCnt() {
 
    println(opCount)
}



최상위 프로퍼티를 사용하기 위해 메인 함수에서 import를 한 후에 정적 프로퍼티를 사용할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
import top.*
 
fun main(args: Array<String>) {
    println(opCount)
 
    performOperation()
    performOperation()
    performOperation()
 
    reportOperCnt()
}



또한 아래와 같이 상수를 추가할 수 있다. 기본적으로 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다. 겉으론 상수처럼 보이는데 실제로는 게터를 사용해야 한다면 자연스럽지 못하다. 더 자연스럽게 사용하려면 사웃를 public static final 필드로 컴파일 해야한다. const 변경자를 추가하면 프로퍼티를 public static final 필드로 컴파일하게 만들 수 있다. 주의해야 할 점은 원시 타입과 String 타입의 프로퍼티만 const로 지정할 수 있다.


1
2
3
const val ENGLISH_NAME = "Theo"
 
public static final String THEO_NAME = "고경태"



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


코틀린을 기존 자바 프로젝트에 통합하는 경우에는 코틀린으로 직접 변환할 수 없거나 미처 변환하지 않은 기존 자바 코드를 처리할 수 있어야 한다. 이런 기존 자바 API를 재작성하지 않고도 코틀린이 제공하는 여러 편리한 기능을 사용하려면 코틀린의 확장 함수를 사용해야 한다.


확장 함수는 어떤 클래스의 메소드인 것처럼 호출할 수 있지만 그 클래스 밖에 선언된 함수다. 확장 함수의  예제로 어떤 문자열의 마지막문자를 돌려주는 메서드를 추가하자면 아래와 같다.


1
2
3
package strings
 
fun String.lastChar(): Char = this.get(this.length - 1)



확장 함수를 만드려면 추가하려는 함수 이름 앞에 그 함수가 확장할 클래스의 이름을 덧붙이기만 하면 된다. 클래스 이름을 수신 객체 타입이라고 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체 라고 부른다.


fun String.lastChar(): Char = this.get(this.length - 1) <-- String이 수신 객체, this는 수신 객체가 된다.

쉽게 말해서 수신 객체 타입은 확징이 정의될 클래스의 타입이며, 수신 객체는 그 클래스에 속한 인스턴스 객체이다.


위의 예제를 실행하기 위해서는 아래와 같다. 아래 예제에서는 String이 수신 객체 타입이고, ktko가 수신 객체다.


1
println("ktko".lastChar())



임포트와 확장 함수


확장 함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 사용할 수 있지는 않다. 확장 함수를 사용하기 위해서는 그 클래스를 임포트 해야만 한다.


확장 함수를 정의하자마자 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 발생한다. 코틀린에서는 클래스를 임포트할 때와 동일한 구믄을 사용해 개별 함수를 임포트 할 수 있다.


아래에 strings 패키지 안에 lasgChar 확장 함수가 있다면 


1
2
3
package strings
 
fun String.lastChar(): Char = this.get(this.length - 1)



아래와 같은 방법으로 사용가능하다. 이름이 같은 함수를 가져와 사용해야 하는 경우 함수 이름을 as 키워드를 사용하여 이름을 변경하여 사용할 수 있다.  코틀린 문법상 확장 함수는 반드시 짧은 이름을 써야 한다. 따라서 임포트할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.



1
2
3
4
5
6
7
8
import strings.*
println("Theo".lastChar())
 
import strings.lastChar
println("Theo".lastChar())
 
import strings.lastChar as last
println("Theo".last())



자바에서 확장 함수 호출


자바에서는 확장 함수를 사용하기 편하다. 확장 함수를 Str.kt 파일에 정의했다면 자바에서는 Str.lastChar("Theo")와 같이 호출할 수 있다.


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


joinToString 함수를 유틸리티 함수로 변경해보자

joinToString함수는 컬렉션에 대한 확장 함수가 되고, 모든 인자에 대한 디폴트 값을 지정한다. 이제 joinToString을 마치 멤버인 것처럼 호출할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun<T> Collection<T>.joinToString( //Collection<T>에 대한 확장 함수를 선언한다.
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
) : String {
    var result = StringBuilder(prefix)
 
    for((index, value) in this.withIndex()) {
        if(index > 0) result.append(separator)
        result.append(value)
    }
 
    result.append(postfix)
 
    return result.toString()
}



1
2
3
var list = listOf(123);
 
println(list.joinToString(separator = "; ", prefix = "(", postfix =  ")"))



확장 함수는 단지 정적 메서드 호출에 대한 문법적인 편의일 뿐이다. 그래서 클래스가 아닌 더 구체적인 타입을 수신 객체 타입으로 지정할 수도 있다. 그래서 문자열의 컬렉션에 대해서만 호출할 수 있는 join 함수를 정의하고 싶다면 아래와 같이 수정할 수 있다.


1
2
3
4
5
6
7
fun Collection<String>.join(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
= joinToString(separator, prefix, postfix)
 
println(listOf("1""2""3").join(" "))



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


코틀린의 메서드 오버러이드도 자바의 오버라이드와 같다. 하지만 코틀린의 확장 함수는 오버라이드 할 수 없다. 아래 일반적인 클래스를 상속받아서 오버라이드하는 일반적인 코드 예제이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
package classes
 
open class View {
    open fun click() = println("View Click")
}
 
class Button: View() {
    override  fun click() = println("Button Click")
}
 
//메인에서 호출
var view: View = Button()
view.click()



확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언된다. 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정되지, 그 변수에 저장된 객체의 종적인 타입에 의해 확장 함수가 결정되지 않는다.


위에 말이 너무 복잡하여 아래 예제를 보면 쉽게 이해할 수 있다. 아래 showOff함수는 클래스 밖에서 선언된 함수들이다. 때문에 다형성을 이용한 객체 선언을 하여 메서드를 실행하였을 때 변수가 가리키는 객체의 실제 타입에 따라 확장 함수가 호출된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main(args: Array<String>) {
 
    var view: View = Button()
    view.click() //Button Click
    view.showOff() //I' am a view!
 
    var button: Button = Button()
    button.click() //Buton Click
    button.showOff() //I' am a Button!
 
}
 
fun View.showOff() = println("I' am a view!")
fun Button.showOff() = println("I' am a Button!")




1
2
3
4
/** 자바 **/
 
View view = new Button();
ExtensionKt.showOff(view); //I' am a View!



확장 프로퍼티


확장 프로퍼티를 사용하면 기존 클래스에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다. 프로퍼티라는 이름으로 불리기는 하지만 상태를 저장할 방법이 없기 때문에 실제로 확장 프로퍼티는 아무 상태도 가질 수 없다. 하지만 프로퍼티 문법으로 더 짧게 코드를 작성할 수 있다.


위 코드에서 lastChar이라는 String 함수를 정의 했는데, 함수를 프로퍼티로 바꾸면 아래와 같다.

확장 함수의 경우와 마찬가지로 확장 프로퍼티도 일반적인 프로퍼티와 같은데, 단지 수신 객체 클래스가 추가됐을 뿐이다. 뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로, 최소한 게터는 꼭 정의를 해야한다.


1
2
3
4
5
fun String.lastChar(): Char = this.get(this.length - 1)
 
//확장 프로퍼티 
val String.lastChar: Char
    get() = get(length-1)



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


코틀린 언어 특성을 설명하면


1. vararg 키워드를 이용하면 호출 시 인자 개수가 달라지는 함수를 정의할 수 있다.

2. 중위 함수 호출 구문을 사용하면 인자가 하나뿐인 메서드를 간편하게 호출할 수 있다.

3. 구조 분해 선언을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.


자바 컬렉션 API 확장


저번 포스팅에서 list의 마지막 원소를 가져오거나, 컬렉션의 최대값을 가져오는 예제가 있다. 이런 코드들은 자바 라이브러리 클래스에서 존재하지 않고, 코틀린에서 새로운 기능을 추가한 확장 함수였다.


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


리스트를 생성하는 함수를 호출할 때 원하는 만큼 원소를 전달할 수 있다.


1
2
3
4
var list = listOf(235711)
 
//라이브러리 함수 정의
fun listOf<T>(vararg values: T): List<T> {...}



자바의 가변 길이 인자(varargs)는 자바에서 원하는 만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값을 넣어주는 기능이다. 코틀린의 가변 길이 인자도 자바와 비슷하다. 다만 문법이 조금 다르다. 자바에서 ...를 붙이는 대신, 코틀린에서는 파라미터 앞에 vararg 변경자를 붙인다.


이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 코틀린과 자바 구문이 다르다. 자바에서는 배열을 그냥 넘기면 되지만, 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 기술적으로 스프레드연산자가 그런 작업을 해준다.


1
2
3
4
var array = arrayOf("1""2""3")
var list = listOf("0"*array)
 
println(list)



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


맵을 만들 때 mapOf 함수를 사용하는데 아래와 같다.


1
val map = mapOf(1 to "one"2 to "two"3 to "three")




여기서 사용된 to는 코틀린 키워드가 아니라 중위 호출 이라는 특별한 방식으로 to라는 일반 메서드를 호출한 것이다.

중위 호출 시에는 수신 객체와 유일한 메서드 인자 사이에 메서드 이름을 넣는다(객체, 메서드 이름, 유일한 인자 사이에는 공백이 들어가야 한다. 다음 두 호출은 동일하다.


1
2
3
4
11.to("one")
2 to ("two")
    
val map = mapOf(1.to("one"), 2.to("two"), 3 to "three")



인자가 하나뿐인 일반 메서드나, 인자가 하나 뿐인 확장 함수에 중위 호출을 사용할 수 있다. 함수를 중위 호출을 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가해야 한다. 다음은 to 함수의 정의를 간략하게 줄인 코드이다.


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



이 to 함수는 Pair의 인스턴스르 ㄹ반환한다. Pair은 코틀린 표준 라이브러리 클래스로, 그 이름대로 두 원소로 이뤄진 순서쌍을 표현한다.

Pair의 내용으로 두 변수를 즉시 초기화할 수 있다.


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



이런 기능을 구조 분해 선언이라고 한다.


문자열과 정규식 다루기


코틀린 문자열은 자바 문자열과 같다. 코틀린 코드가 만들어낸 문자열을 아무 자바 메서드에 넘겨도 되며 자바 코드에서 받은 문자열을 아무 코틀린 표준 라이브러리에 전달해도 문제가 없다. 코틀린은 다양한 확장 함수를 제공함으로써 표준 자바 문자열을 더 즐겁게 다루게 해준다. 또한 혼동이 야기될 수 있는 일부 메서드에 대해 더 명확한 코틀린 확장 함수를 제공함으로써 프로그래머의 실수를 줄여준다.


문자열 나누기 


코틀린에서는 자바의 split 대신에 여러 가지 다른 조합의 파라미터를 받는 split 확장 함수를 제공한다. 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 갑을 받는다. 따라서 코틀린에서는 split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.


1
2
val str = "12.345-6.A"
println(str.split("\\.|-".toRegex())) //[12, 345, 6, A]



또한 split 확장 함수를 오버로딩한 버전 중에서는 구분 문자열을 하나 이상 인자로 받는 함수를 사용할 수 있다.


1
2
val str = "12.345-6.A"
println(str.split(".""-")) //[12, 345, 6, A]



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


코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다. 정규식은 강력하기는 하지만 알아보기 힘든 경우 많다.

정규식을 이용하기 위해 코틀린 라이브러리를 사용하면 더 편하다. 3중 따옴표 문자열에서는 역슬래시(\)를 포함한 어느 문자도 이스케이프할 필요가 없다. 


3중 따옴표 문자열은 문자열 이스케이프를 피하기 위해서만 사용되지는 않는다. 3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열이나 그대로 들어간다. 또한 3중 따옴표 문자열안에 문자열 템플릿을 사용할 수 있다.


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


코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다 그렇게 하면 문법적인 부가 비용을 들이지 않고도 깔끔하게 코드를 조작할 수 있다.


아래 예제에서는 사용자를 저장하기 위해 User 객체를 전달받는다. 중복된 코드는 없지만 User 프로퍼티가 많아진다면 문제가 발생한다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun main(args: Array<String>) {
    saveUser(User(1""""))
}
 
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 because of name")
    }
 
    if (user.address.isEmpty()) {
        throw IllegalArgumentException("Can't save user because of address")
    }
}



아래와 같이 변경하여 검증 로직 중복이 사라졌고, 다른 필드에 대한 검증도 쉽게 추가할 수 있다. 


1
2
3
4
5
6
7
8
9
10
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")
}



하지만 User 객체를 로컬 함수에게 하나하나 전달해야 한다는 점은 아쉽다. 다행이지만 사실은 전혀 그럴 필요가 없다. 로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 이런 성질을 이용해 불필요한 User 파라미터를 없앨 수 있다.


1
2
3
4
5
6
7
8
9
10
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.validateBeforeSave 함수를 saveUser 내부의 로컬 함수로 넣을 수 있다. 중첩된 함수의 깊이가 깊어지면 가독성이 떨어지므로 한 단계만 함수를 중첩시키는 것이 좋다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
un 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()
    //user를 데이터베이스에 저장한다.
}