devseop08 님의 블로그

[Basic] 2. 함수 정의와 호출 본문

Language/Kotlin

[Basic] 2. 함수 정의와 호출

devseop08 2025. 6. 1. 21:03
  • 3장에서는 코틀린이 함수 선언과 호출을 코틀린이 어떻게 개선했는지 살펴본다.
  • 추가로 확장 함수와 프로퍼티를 사용해 혼합 언어 프로젝트에서 코틀린의 이점을 모두 살릴 수 있는 방법을 살펴본다.

3.1 코틀린에서 컬렉션 만들기

  • 코틀린에서 컬렉션 만들기
val set = setOf(1, 7, 53)
val list = listOf(1,7, 53)
val map = (1 to "one", 7 to "seven", 53 to "fifty-three")
  • 코틀린은 표준 자바 컬렉션 클래스를 사용한다.
fun main(){
    val set = setOf(1, 7, 53)
    val list = listOf(1,7, 53)
    val map = (1 to "one", 7 to "seven", 53 to "fifty-three")

    println(set.javaClass)  // class.java.LinkedHashSet
    println(list.javaClass) // class.java.Array$ArrayList
    println(map.javaClass)  // class.java.LinkedHashMap
}
  • 코틀린 컬렉션은 자바 컬렉션과 똑같은 클래스이긴 하지만 코틀린에서는 자바보다 더 많은 기능을 쓸 수 있다.
fun main(){
    val strings = listOf("first", "second", "fourteenth")

    strings.last()
    // fourteenth

    println(strings.shuffled())
    // [fourteenth, second, first]

    val numbers = setOf(1, 14, 2)
    println(numbers.sum())
    // 17
}
  • 3장에서는 이런 기능이 어떻게 동작하는지 보여주고 자바 클래스에 없는 새로운 메서드들이 어디서 비롯됐는지 살펴본다.
  • 그 전에 먼저 함수 선언에 대한 새로운 개념을 살펴본다.

3.2 함수를 호출하기 쉽게 만들기

  • 주어진 리스트를 주어진 구분자, 접두사, 접미사를 붙여서 반환하는 함수 joinToString 초기 구현
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, "; ", "(", ")"))
    // (1; 2; 3)
}

3.2.1 이름 붙인 인자

  • joinToString 함수 호출 시의 가독성이 좋지 않다.
  • 함수 호출 시 인자로 전달된 각 문자열이 어떤 역할을 하는지 분명하지 않다.
joinToString(list, " ", " ", ".")
  • 코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부 또는 전부의 이름을 명시할 수 있다.
joinToString(collection, separator=" ", prefix=" ", postfix=".")
  • 전달하는 모든 인자의 이름을 지정하는 경우 심지어 인자 순서를 변경할 수도 있다.
joinToString(prefix=" ",separator=" ", postfix=".", collection)

3.2.2 디폴트 파라미터 값

  • 코틀린에서는 함수 선언에서 파라미터 기본값을 지정할 수 있으므로 중복되는 함수의 오버로드를 상당 수 피할 수 있다.
fun <T> joinToString(
    collection:Collection<T>,
    separator: String=", ",
    prefix: String="",
    postfix: String="",
): String
  • 이제 함수를 호출할 때 모든 인자를 쓸 수도 있고, 일부를 생략할 수도 있다.
fun main(){
    joinToString(list, ", ", "", "")
    joinToString(list)
    joinToString(list, "; ")
}
  • 일반 호출 문법을 사용할 때는 함수를 선언할 때와 같은 순서로 인자를 지정해야만 하며, 일부를 생략하고 싶을 때는 뒤쪽의 인자들을 생략할 수 있다.
  • 이름 붙은 인자를 사용하는 경우에는, 지정하고 싶은 인자에 이름을 붙여서 순서와 관계없이 지정할 수 있다.
  • 함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수를 선언하는 쪽에 인코딩로 어떤 클래스 안에 정의된 함수의 기본값을 바꾸고 그 클래스가 포함된 파일을 재컴파일하면 그 함수를 호출하는 코드 중에 값을 지정하지 않은 모든 인자는 자동으로 바뀐 기본값을 적용받는다.

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

  • 코틀린에서는 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시킬 수 있다.
/* strings 패키지의 join.kt 파일에 정의된 joinToString 함수*/
package strings

fun joinToString(/* ... */):String {/* ... */}
  • 코틀린 파일의 최상위 수준으로 정의된 함수가 자바에서 호출되기 위해 컴파일 되는 형식
/* 자바 */
package strings;

public class JoinKt{    // join.kt라는 파일 이름에 해당하는 클래스
    public static String joinToString(/* ... */){/* ... */}
}
/* 자바 */
import strings.JoinKt;

/* ... */
JoinKt.joinToString(list, ", ", "","");
  • 디폴트로 컴파일러가 만들어주는 클래스의 이름은 파일 이름 뒤에 Kt라는 접두사를 붙인 것이다. 코틀린 최상위 함수가 들어있는 생성된 클래스의 이름을 바꾸고 싶다면, 파일 수준의
    @file:JvmName("...") 어노테이션을 추가한다.
@file:JvmName("StringFunctions")

package strings

fun JoinToString(/* ... */):String { /* ... */}
/* 자바 */
import strings.StringFunctions;

StringFunctions.joinToString(list, ", ", "", "");
최상위 프로퍼티: 함수와 마찬가지로 프로퍼티도 최상위 수준에 놓일 수 있다.
  • 어떤 데이터를 클래스 밖에 위치시켜야 하는 경우는 흔하지는 않지만, 그래도 가끔 유용할 때가 있다.
  • 어떤 연산을 수행한 횟수를 저장하는 var 프로퍼티를 만들 수 있다.
var opCount = 0 

fun performOperation() {
    opCount++
    //...
}

fun reportOperationCount() {
    println("Operation performed $opCount times")
}
  • 이런 프로퍼티의 값은 정적 필드에 저장된다.
  • 최상위 프로퍼티를 활용해 코드에서 상수를 정의할 수 있다.
val UNIX_LINE_SEPERATOR = "\n" // val 선언: 상수
  • 디폴트로 최상위 프로퍼티도 다른 모든 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다.
  • 이 상수를 자연스럽게 public static final 필드로 노출하고 싶다면 const 변경자를 추가하면 된다.
const val UNIX_LINE_SEPERATOR = "\n"
/* 자바 */
public static final String UNIX_LINE_SEPERATOR = "\n"

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

  • 확장 함수는 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 선언된 함수이다.
package strings

fun String.lastChar() : Char = this.get(this.length - 1)
// String : 함수가 확장할 클래스의 이름, 수신 객체 타입
// this : 확장 함수 호출 시 호출하는 대상 값, 수신 객체 
fun main(){
    println("Kotlin".lastChar())
}
  • 자바 클래스로 컴파일된 클래스 파일이 있는 한 그 클래스에 원하는 대로 확장을 추가할 수 있다. 이렇게 되면 기존 자바 API를 재작성하지 않아도 편리한 기능을 사용할 수 있게 된다.
  • 확장 함수에서 일반 메서드 본문에서 this를 사용할 때와 마찬가지로 수신 객체 this도 생략 가능하다.

3.3.1 임포트와 확장 함수

  • 확장 함수를 정의했다고 해도 자동으로 프로젝트 안의 모든 소스코드에서 그 함수를 쓸 수 있지는 않다.
  • 확장 함수를 쓰려면 다른 클래스나 함수와 마찬가지로 해당 함수를 임포트해야만 한다.
  • 이는 이름 충돌을 막기 위함이다.
import strings.lastChar

val c = "Kotlin".lastChar()
import strings.*

val c = "Kotlin".lastChar()
import strings.lastChar as last

val c = "Kotlin".last()

3.3.2 자바에서 확장 함수 호출

  • 내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드다. 따라서 확장 함수를 호출해도 다른 어댑터 객체나 실행 시점 부가 비용이 들지 않는다.
/* 자바 */
char c = StringUtilKt.lastChar("Java"); // static 메서드 

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

  • 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 키워드는 수신 객체,
        if(index > 0) result.append(separator) // 즉 T 타입의 요소로 이루어진 
        result.append(element)                 // 컬렉션을 가리킨다. 
    }

    result.append(postfix)
    return result.toString()
}

fun main(){
    val list: List<Int> = listOf(1,2,3)
    println(
        list.joinToString(
            separator = "; ",
            prefix = "(",
            postfix = ")"
        )
    )
    // (1; 2; 3)
}

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

  • 코틀린의 메서드 오버라이드도 일반적인 객체지향의 메서드 오버라이드와 마찬가지다.
open class View {
    public fun click() = println("View clicked")
}

class Button: View(){
    override fun click() = println("Button clicked")
}

fun main(){
    val view: View = Button()
    view.click() // Button clicked
}
  • 하지만 확장은 이런 식으로 작동하지 않는다.
  • 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의할 수 있다.
  • 하지만 실제 호출될 함수는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 컴파일 시점의 타입에 의해 결정되지, 실행 시간에 그 변수에 저장된 객체의 타입에 의해 결정되지 않는다.
fun View.showOff() = println("View")
fun Button.showOff() = println("button")

fun main(){
    val view: View = Button()
    view.showOff() // View 출력
}
  • 확장 함수를 첫 번째 인자가 수신 객체인 정적 자바 메서드로 컴파일한다는 사실을 기억하라
  • 자바도 마찬가지 방식으로 호출할 때 static 함수를 결정한다.
class Demo {
    public static void main(String[] args){
        View view = new Button()
        ExtensionKt.showOff(view) // View 출력
    }
}
  • 어떤 클래스를 확장한 함수와 그 클래스의 멤버 함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다.
  • 멤버 함수의 우선 순위가 더 높다.

3.3.5 확장 프로퍼티

  • 확장 프로퍼티는 실제로 아무 상태도 가질 수 없기 때문에 커스텀 접근자를 정의해야 한다.
val String.lastChar: Char
    get() = get(length - 1)
  • StringBuilder의 맨 마지막 문자를 변경할 수 있으므로 StringBuilder에 대해 같은 프로퍼티를 정의한다면, 이를 var로 선언할 수 있다.
var StringBuilder.lastChar: Char
    get() = get(length -1)
    set(value: Char){
        this.setCharAt(length - 1, value)
    }
  • 확장 프로퍼티 사용 방법은 멤버 프로퍼티 사용 방법과 동일하다.
fun main(){
    val sb = StringBuilder("Kotlin?")

    println(sb.lastChar) // getter 호출, "?" 반환

    sb.lastChar = "!" // setter 호출

    println(sb)  //  Kotlin!
}

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

  • 함수 선언 시 파라미터에 vararg 키워드를 사용하면 호출 시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.
  • 중위 함수 호출 구문을 사용하면 인자가 하나뿐인 메서드를 간편하게 호출할 수 있다.
  • 구조 분해 선언을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.

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

  • 파라미터 개수가 달라질 수 있는 함수를 정의하는 법을 알아보자
  • 코틀린에서는 자바처럼 타입 뒤에 ...을 붙이는 대신 파라미터 앞에 키워드 vararg 변경자를 붙인다.
fun <T> listOf(vararg values: T): List<T> {...}
  • 코틀린에서는 배열에 들어있는 원소를 가변 길이 인자로 넘길 때 스프레드 연산자(*)를 이용해 배열을 풀어서 넘겨야 한다.
fun main(args: Array<String>){
    val list = listOf("args: ", *args)
    println(list)
}

3.4.3 쌍(튜플) 다루기: 중위 호출과 구조 분해 선언

val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
1.to("one") // to 함수를 일반적인 방식으로 호출
1 to "one"  // to 함수를 중위 호출
  • 인자가 하나뿐인 확장 함수에만 중위 호출을 사용할 수 있다.
  • 함수를 중위 호출에 사용하게 허용하고 싶으면 infix 변경자를 함수 선언 앞에 추가
infix fun Any.to(other: Any) = Pair(this, other)
  • 구조 분해 선언
val (number, name) = 1 to "one"
  • 루프에서도 구조 분해 선언을 활용할 수 있다.
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>

3.5 문자열과 정규식 다루기

  • 코틀린 문자열은 자바 문자열과 똑같다. 코틀린 코드가 만들어낸 문자열을 아무 자바 메서드에 넘겨도 되며, 자바 코드에서 받은 문자열을 아무 코틀린 표준 라이브러리 함수에 전달해도 문제없다.
  • 특별한 변환도 필요없고 자바 문자열을 감싸는 별도의 래퍼 객체도 생기지 않는다.
  • 코틀린은 다양한 확장 함수를 제공함으로써 표준 자바 문자열을 더 즐겁게 다루게 해준다.

3.5.1 문자열 나누기

  • 코틀린에서는 자바의 split 대신에 여러 가지 다른 조합의 파라미터를 받는 split 확장 함수를 제공함으로써 혼동을 야기하는 메서드를 감춘다.
  • 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받는다.
  • 따라서 코틀린에서는 split 함수에 전달하는 값의 타입에 따라 정규식이나 일반 텍스트 중 어느 것으로 문자열을 분리하는지 쉽게 알 수 있다.
fun main(){
    println("12.345-6.A".split("\\.|-".toRegex())) // 정규식을 명시적으로 만든다.
}
  • 정규식을 처리하는 API는 표준 자바 라이브러리 API와 비슷하지만 좀 더 코틀린답게 변경됐다.
  • 코틀린에서는 toRegex 확장 함수를 사용해 문자열을 정규식으로 변환한다.
  • 코틀린에서 split 확장 함수를 오버로딩한 버전 중에는 구분 문자열을 하나 이상 인자로 받는 함수가 있다.
fun main(){
    println("12.345-6.A".split(".", "-")) // 여러 구분 문자열을 지정한다. 
}

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

  • String 확장 함수를 사용해 경로 파싱하기
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(){
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}
  • 코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다. 정규식은 강력하기는 하지만 나중에 알아보기 힘든 경우가 많다
  • 정규식이 필요할 때는 코틀린 라이브러리를 사용하면 도움이 된다.
fun pasePathRegex(path:String){
    val regex = """(.+)/(.+)\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path)
    if(matchResult != null){
        val (directory, filename, extension) = matchResult.destructed
        // 구조 분해해
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}

fun main(){
    parsePathRegex("/Users/yole/kotlin-book/chapter.adoc")
}
  • 3중 따옴표 문자열에서는 백슬래시를 포함한 어떤 문자도 이스케이프할 필요가 없다.
  • 일반 문자열을 사용해 정규식을 작성하는 경우에는 마침표 기호를 이스케이프하려면 \\. 라고 써야하지만, 3중 따옴표 문자열에서는 \.라고 쓰면 된다.
  • 예제에서 쓴 정규식은 슬래시와 마침표를 기준으로 경로를 세 그룹으로 분리한다.
  • 패턴 . 는 임의의 문자와 매치될 수 있다. 따라서 첫 번째 그룹인 (.+)는 마지막 슬래시를 제외한 모든 슬래시도 들어간다.
  • 비슷한 이유로 두 번째 그룹에도 마지막 마침표 전까지 모든 문자가 들어간다.
  • 세 번째 그룹에는 나머지 모든 문자가 들어간다.

3.5.3 여러줄 3중 따옴표 문자열

  • 3중 따옴표 문자열을 문자열 이스케이프를 피하기 위해서만 사용하지는 않는다.
  • 3중 따옴표 문자열에는 줄 바꿈을 포함해 아무 문자열이나 그대로 들어간다.
  • 3중 따옴표를 쓰면 줄바꿈이 들어있는 텍스트를 쉽게 프로그램에 포함시킬 수 있다.
val kotlinLogo = 
"""
| //
| //
|/ \
""".trimIndent()

fun main(){
    println(kotlinLogo)
}
  • 파일에서 줄 끝을 표현하기 위해 운영체제마다 서로 다른 문자를 사용하는데, 코틀린에서는 각 운영체제에서 줄 끝을 표현하는 문자(window: CRLF, linux & mac : LF) 모두를 줄 끝으로 취급한다. CRLF, CR, LF를 모두 줄 끝으로 취급한다.
  • 여러 줄 문자열에는 줄바꿈이 들어가지만 줄 바꿈을 \n과 같은 특수 문자를 사용해 넣을 수는 없다.
  • 일반 문자열에서는 공백 문자, 특수 문자에 대한 이스케이프가 필요할 시 해당 공백 문자, 특수문자 앞에 백슬래시()를 추가해줘야 했지만, 3중 따옴표 문자열에서는 따로 이스케이프가 필요없다.
  • "C:\\\ Users\\\yole\\\kotlin-book"이라고 쓴 윈도우 파일 경로를 3중 따옴표 문자열로 쓰면 """C\\Users\\yole\\kotlin-book"""이다.
  • 3중 따옴표 문자열 안에 문자열 템플릿을 사용할 수도 있으나 3중 따옴표 문자열 안에서는 이스크케이프가 불가하기 때문에 문자열 내용에서 $나 유니코드 이스케이프를 유효하게 사용하기 위해서는 내포식을 사용해야 한다.
  • """ Hmm ${"\uD83E\uDD14"} """ 처럼 써야만 한다.
  • 3중 따옴표를 이용한 여러 줄 문자열이 요긴한 분야로는 테스트가 있다.
  • 테스트 결과가 여러 줄의 텍스트 출력일 때 유용하다. 복잡하게 이스케이프를 쓰거나 파일에서 텍스트를 불러올 필요가 없다.

3.6 코드 깔끔하게 다듬기: 로컬 함수와 확장

  • 코틀린에서는 함수에서 추출한 함수를 원래의 함수의 내부에 내포시킬 수 있다.
  • 코드 중복을 보여주는 예제
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(1, "", ""))
}
  • 로컬 함수를 사용해 코드 중복 줄이기
class User(val id: Int, val name: String, val address: String) 

fun saveUser(user: User)
{
    fun validate(user: User, value: String, fieldName: String)
    {
        if(value.isEmpty())
        {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: $fieldName"
            )
        }
    }

    validate(user, user.name, "Name")
    validate(user, user.address, "Address")
}

fun main()
{
    saveUser(User(1, "", ""))
}
  • 로컬 함수에서 자신이 속한 바깥 함수의 파라미터에 접근할 수 있다.
class User(val id: Int, val name: String, address: String)

fun saveUser(user: User)
{
    fun validate(value: String, fieldName: String)
    {
        if(value.isEmpty())
        {
            throw IllegalArgumentException(
                "Can't save user ${user.id}: $fieldName" // 바깥 함수 파라미터에                                                              // 접근한다.
            )
        }
    }

    validate(user.name, "Name")
    validate(user.address, "Address")
}

fun main()
{
    saveUser(User(1, "", ""))
}
  • 로컬 함수를 수신 객체의 확장 함수로 만들 수도 있다.
class User(val id: Int, val name: String, address: String)

fun User.validateBeforeSave(){ // 로컬 함수를 수신 객체의 확장 함수로

    fun validate(value: String, fieldName: String)
    {
        if(value.isEmpty())
        {
            throw IllegalArgumentException(
                "Can't save user ${id}: $fieldName" // 수신 객체 지정하지 않고도
            )                                       // 공개된 멤버 프로퍼티 접근 
        }
    }

    validate(name, "Name")
    validate(address, "Address")
}

fun saveUser(user: User)
{
    user.validateBeforeSave()
}

fun main()
{
    saveUser(User(1, "", ""))
}
  • 로컬 함수를 확장하면, 수신 객체를 지정하지 않고도 로컬 함수 내에서 공개된 멤버 프로퍼티나 메서드에 접근할 수 있게 된다.

요약

  • 코틀린은 자체 컬렉션 클래스를 정의하지 않지만, 자바 클래스를 확장해서 더 풍부한 API를 제공한다.
  • 함수 파라미터의 기본값을 정의하면, 오버로딩한 함수를 정의할 필요성이 줄어든다. 이름 붙인 인자를 사용하면, 함수의 인자가 많을 때 함수 호출의 가독성을 더 향상시킬 수 있다.
  • 코틀린 파일에서 클래스 멤버가 아닌 최상위 함수와 프로퍼티를 직접 선언할 수 있다. 이를 통해 코드 구조를 더 유연하게 만들 수 있다.
  • 확장 함수와 프로퍼티를 사용하면, 외부 라이브러리에 정의된 클래스를 포함해 모든 클래스의 API를 그 클래스의 소스코드를 바꿀 필요없이 확장할 수 있다.
  • 중위 호출을 통해 인자가 하나밖에 없는 메서드나 확장 함수를 더 깔끔한 구문으로 호출할 수 있다.
  • 코틀린은 정규식과 일반 문자열을 처리할 때 유용한 다양한 문자열 처리 함수를 제공한다.
  • 자바 문자열로 표현하려면 수많은 이스케이프가 필요한 문자열의 경우 3중 따옴표 문자열을 사용하면 더 깔끔하게 표현할 수 있다.
  • 로컬 함수를 써서 코드를 더 깔끔하게 유지하면서 중복을 제거할 수 있다.