코틀린[Kotlin] #04_함수 정의와 호출
안녕하세요. 문범우입니다.
이번 포스팅에서는 코틀린에서의 함수 정의와 함수 호출을 중심으로 알아보도록 하겠습니다.
관련된 코드의 내용은 아래 주소에서 확인할 수 있습니다.
https://github.com/doorBW/kotlin-study
1. 컬렉션
코틀린에서 알아보고자 했던 함수에 대해 확인하기 이전에 컬렉션을 만드는 방법부터 확인해보고 넘어가자.
아래와 같이 컬렉션을 만들고, 만들어진 컬렉션 객체가 어떤 클래스에 속하는지 함께 확인해보자.
1 2 3 4 5 6 7 8 9 10 11 | fun main(){ 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 = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three") println(map.javaClass) // class java.util.HashMap } | cs |
우선 코틀린에서는 hashSetOf, arrayListOf, hashMapOf 등의 함수로 집합이나 리스트 등을 만들 수 있다.
이때, hashMapOf에서 사용된 to에 대해서는 추후 다루도록 한다.
객체에 대해서 어떤 클래스에 속하는지 확인한 결과 기존 자바 컬렉션을 나타내고 있다.
이는 코틀린에서 자체적인 컬렉션을 제공하지 않는다는 의미와 같으며, 자바에서의 컬렉션 객체와 동일한 객체임을 알 수 있다.
하지만 코틀린의 컬렉션에서는 자바의 컬렉션 보다 많은 기능을 제공한다.
예를 들어 리스트의 마지막 원소를 가져오는 last() 함수나 최대 값을 반환하는 max() 등의 함수가 있다.
우리는 앞으로 코틀린에서 자바 클래스에 없는 메소드를 어디서 정의하는지, 그리고 어떻게 동작하는지에 대해서 함께 살펴볼 것이다.
2. 함수 선언
우선 함수를 선언하는 내용부터 살펴보자.
이를 위해, 리스트를 괄호로 감싸고 각 원소를 세미콜론으로 구분하여 반환하는 함수를 다음과 같이 만들어보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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(){ val list = listOf(1,2,3,4) println(joinToString(list, ";", "(", ")")) // (1;2;3;4) } | cs |
위에서 선언한 joinToString 함수는 4개의 인자를 받는다.
컬렉션과 각 원소 사이에 추가할 separator 그리고 prefix와 postfix. 선언한 함수를 확인하기 위해 1,2,3,4 총 4개의 원소를 가지는 리스트를 만들어 함수의 작동을 확인해보니 의도와 동일하게 (1;2;3;4) 가 출력됨을 확인할 수 있다.
이제 위에서 선언한 joinToString 함수를 기반으로 선언에 대해 추가적인 내용을 알아보도록 하자.
2-1. 인자에 이름 붙이기
함수를 호출할 때, 선언시 사용했던 인자의 이름을 활용할 수 있다. 실제로 위 코드의 20번라인처럼 함수를 호출 할 때에는 어떠한 것이 separator이고, 무엇이 prefix인지, 무엇이 postfix인지 혼동이 될 수 있다.
이를 해결하기 위해 다음과 같이 함수 호출시에 인자의 이름을 활용한다.
1 2 3 4 5 | fun main(){ val list = listOf(1,2,3,4) println(joinToString(list, separator = ";", prefix = "(", postfix = ")")) // (1;2;3;4) } | cs |
2-2. 디폴트 값 지정하기
코틀린에서는 함수 선언 시점에 특정 인자에 대한 디폴트 값을 지정할 수 있다.
이를 통해 디폴트 값을 가지는 인자 때문에 함수를 오버로딩(overloading)하는 경우를 방지할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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(){ val list = listOf(1,2,3,4) println(joinToString(list, separator = ";", prefix = "(", postfix = ")")) // (1;2;3;4) println(joinToString(list)) // 1, 2, 3, 4 println(joinToString(list, separator = ";")) // 1;2;3;4 } | cs |
2-3. 최상위 함수
자바에서는 모든 메소드가 클래스 내부에 작성되어야 한다. 하지만 현업에서는 특정 클래스에 포함시키기 어려운 함수들이 존재하고 일반적으로는 Util의 성격을 가지는 함수들이 그러하다.
이 때문에 ~Util.java 와 같은 형태를 가지는 클래스들이 존재한다.
하지만 코틀린에서는 함수를 직접 소스파일의 최상위 수준, 다른 모든 클래스의 바깥에 위치시키면 된다. 최상위에 선언된 함수들에 대해서는 그 함수가 정의된 패키지를 임포트하여 사용될 수 있다.
앞에서 만든 joinToString 함수를 strings 패키지안에 join.kt라는 코틀린 파일로 넣고 자바에서 호출해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import ch3.strings.JoinKt; import java.util.ArrayList; public class Test { public static void main(String[] args){ final ArrayList list = new ArrayList<String>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); System.out.println(JoinKt.joinToString(list, ", ", "[", "]")); // [1, 2, 3, 4] } } | cs |
위의 코드와 같이 자바에서 코틀린 파일을 import 하여 사용하였다.
실제로 코틀린 파일의 이름은 join.kt이지만 이는 컴파일러가 자동으로 JoinKt라는 class로 컴파일하여 내부에 있는 최상위 함수를 사용할 수 있게 한다.
이때 만약 클래스 이름을 바꾸고 싶다면, @JvmName 어노테이션을 다음과 같이 활용하면 된다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @file:JvmName("StringFunctions") package ch3.strings 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(){ val list = listOf(1,2,3,4) println(joinToString(list, separator = ";", prefix = "(", postfix = ")")) // (1;2;3;4) println(joinToString(list)) // 1, 2, 3, 4 println(joinToString(list, separator = ";")) // 1;2;3;4 } | cs |
이렇게 되면 기존의 JoinKt라는 이름으로 컴파일 되지 않고, 우리가 지정한 StringFunctions라는 이름의 클래스로 컴파일 되는 것을 확인할 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // import ch3.strings.JoinKt; import ch3.strings.StringFunctions; import java.util.ArrayList; public class Test { public static void main(String[] args){ final ArrayList list = new ArrayList<String>(); list.add("1"); list.add("2"); list.add("3"); list.add("4"); // System.out.println(JoinKt.joinToString(list, ", ", "[", "]")); System.out.println(StringFunctions.joinToString(list, ", ", "[", "]")); // [1, 2, 3, 4] } } | cs |
3. 확장 함수와 확장 프로퍼티
우선 확장 함수의 개념은 단순하다. 확장 함수(Extension function)은 어떤 클래스의 멤버 메소드인 것 처럼 호출할 수 있지만 클래스의 내부에서가 아닌, 밖에서 선언된 함수이다.
아래와 같이 문자열의 마지막 문자를 반환하는 확장 함수를 만들어보자.
1 | fun String.lastChar() : Char = this.get(this.length - 1) | cs |
위와 같이 확장 함수를 만들 때에는, 함수의 이름 앞에 해당 함수가 확장할 클래스의 이름을 붙여주면 된다. 위의 lastChar() 함수는 String 클래스를 확장하고 있다.
이때, 클래스 이름을 수신 객체 타입(receiver type)이라고 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)을 수신 객체(receiver object)라고 부른다.
위의 lastChar() 함수의 경우, String이 수신 객체 타입이고 수신 객체는 this로 받고 있다.
물론 이러한 확장 함수를 호출하는 구문은 다른 일반 클래스 멤버를 호출하는 방법과 동일하다.
1 2 3 4 5 6 | fun String.lastChar() : Char = this.get(this.length - 1) fun main(){ println("Kotlin".lastChar()) // n } | cs |
위의 예제에서의 수신 객체 타입은 String이고 수신 객체는 "Kotlin" 이다.
확장 함수도 일반 메소드에서와 같이 this를 생략할 수 있다.
일반 메서드와 다른 점은, 확장 함수에서는 클래스 내부에서만 사용할 수 있는 private, protected 멤버를 사용할 수 없다. 이로 인해 확장 함수가 기존 클래스의 캡슐화는 깨지 않는다.
3-1. 확장 함수 임포트
확장 함수를 정의 했다고 해서 모든 범위에서 그 함수를 사용할 수 있는 것은 아니다.
확장 함수를 사용하기 위해서는 그 함수를 임포트해야 한다. 물론 일반적인 클래스를 임포트할 때와 동일한 구문을 사용하여 개별 함수를 임포트할 수 있다.
또한 아래와 같이 as 키워드를 사용하여 다른 이름으로 호출할 수 있다.
1 2 3 | import strings.lastChar as last val c = "Kotlin".last() | cs |
3-2. 자바에서 확장 함수 호출
내부적으로 확장 함수는, 수신 객체를 첫 번째 인자로 받는 정적 메소드이다.
때문에 자바에서 코틀린의 확장함수를 호출하기 위해서는 정적 메소드를 호출하면서 첫 번째 인자로 수신 객체를 넘기기만 하면 된다. 예를 들어, 확장 함수를 StringUtil.kt 파일에 정의 했다면 다음과 같이 자바에서 호출할 수 있다.
1 | char c = StringUtilKt.lastChar("Kotlin"); | cs |
3-3. 확장 함수의 오버라이드
우선 결론부터 이야기하면, 코틀린에서의 확장 함수는 오버라이드 되지 않는다.
이를 확인 하기 위해 아래와 같이 View 클래스와 Button 클래스를 선언하고, click() 함수를 Button에서 오버라이드 해보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 | open class View{ open fun click() = println("View clicked!") } class Button: View() { override fun click() = println("Button clicked!") } fun main(){ val view: View = Button() view.click() // Button clicked! } | cs |
위와 같이 view에 저장된 값의 실제 타입이 Button이기 때문에 오버라이드 된 Button의 click함수가 실행된다.
하지만 확장 함수는 클래스의 일부가 아니다.
실제로 확장 함수를 호출할 때 수신객체로 지정한 변수의 정적 타입에 의해서 어떤 확장 함수가 호출될지 결정된다. 즉, 그 변수에 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않는다.
확장 함수를 첫 번째 인자가 수신 객체인 정적 자바 메소드로 컴파일 한다는 사실을 생각해두어야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | open class View{ open fun click() = println("View clicked!") } class Button: View() { override fun click() = println("Button clicked!") } fun View.showOff() = println("View showOff") fun Button.showOff() = println("Button showOff") fun main(){ val view: View = Button() view.click() // Button clicked! view.showOff() // View showOff } | cs |
3-4. 확장 프로퍼티
확장 함수의 경우와 마찬가지로 확장 프로퍼티도 일반적인 프로퍼티와 같다. 단지 수신 객체 클래스가 추가되었을 뿐이다.
하지만 기본 게터 구현을 제공할 수 없기 때문에 최소한 게터는 꼭 정의를 해주어야 한다.
1 2 3 4 5 6 7 | val String.lastChar: Char get() = this.get(this.length - 1) fun main() { println("Kotlin".lastChar) // n } | cs |
4. 컬렉션 처리
마지막으로는 컬렉션에 대해 처리할 수 있는 몇가지 코틀린 표준 라이브러리 함수를 알아보자.
4-1. 가변 길이 인자
1 2 3 4 5 6 7 8 9 10 | fun test(vararg values: String){ for(s in values) println(s) } fun main() { test("1", "2", "3") // 1 // 2 // 3 } | cs |
4-2. 중위 호출
처음에, 맵을 만들었던 코드를 생각해보자.
1 | val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three") | cs |
여기서 사용된 to는 코틀린의 키워드가 아니다.
이 코드는 중위 호출(infix call)이라는 특별한 방식으로 to라는 일반 메소드를 호출한 것이다.
즉, 아래 두줄의 코드는 의미가 같다.
1 2 | 1.to("one") // 일반적인 방식의 메소드 호출 1 to "one" // 중위 호출 방식으로 메소드 호출 | cs |
중위 호출 시에는 수신 객체와 유일한 메소드 인자 사이에 메소드 이름을 넣으면 된다.
인자가 하나뿐인 일반 메소드나 인자가 하나뿐인 확장 함수에 대해서 중위 호출을 사용할 수 있다. 인자가 하나뿐인 함수에 대해 중위 호출 사용을 허용시키려면 함수 선언 앞에 infix 변경자를 추가하면 된다.
참고 서적 및 링크
* [Kotlin in Action] - 드미트리 제메로프/스베트라나 이사코바 지음, 오현석 옮김, 에이콘 출판사