[Kotlin in Action] 3장. 함수 정의와 호출

2021. 10. 9. 17:15Kotlin

반응형

예전부터 같이 공부했던 분들과 주말에 스터디를 하고 있다. 벌써 스터디를 한지 2년 정도 된 것 같다. 스터디 주제는 다양했는데, 

React-Native, 자료구조, 임베디드 OS 구축, 안드로이드 등등 하나의 주제가 끝나면 다음에 해보고 싶은 주제를 정해서 하는 식이다. 

이번에는 Spring Boot 를 공부해보기로 했는데, 그 전에 2개월 정도 kotlin 기초를 다지고 가려 한다! 그래서 Kotlin in Action 이라는 책을 보면서 매주 공부한 내용을 블로그에 기록한다. 

 

목차

     


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

    이름 붙인 인자 (Named arguments)

    코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자(parameter) 중 일부 또는 모든 파라미터명을 명시할 수 있다. 이를 통해 함수 호출의 가독성을 높인다.

    fun joinToString(
    		collection: Collection<String>,
    		separator: String,
    		prefix: String,
    		postfix: String
    ) : String
    
    // named arguments 사용하여 함수 호출.
    joinToString(collection, separator = "-", prefix = "", postfix = "")

    이는 코틀린의 특징으로, 자바로 작성한 코드를 호출할 때는 named arguments 를 사용할 수 없다.

     

    디폴트 파라미터 값 (Default argument)

    자바에서는 일부 클래스에서 overloading한 메소드가 너무 많아진다는 문제가 있다. 코틀린에서는 함수 선언에서

    파라미터의 디폴트 값을 지정할 수 있다.

    fun joinToString(
    		collection: Collection<String>,
    		separator: String = ",",
    		prefix: String = "",
    		postfix: String = ""
    ) : String
    
    joinToString(collection)

     

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

     


    Function Scope

    코틀린에서는 다양한 위치에 함수가 존재할 수 있다.

    최상위 함수와 최상위 프로퍼티 (Top level function/property)

    자바에서 함수는 반드시 class 내에 존재한다. 그러나, 코틀린에서는 class 없이도 함수를 작성할 수 있다!

    이를 최상위 함수라고 한다. 함수 외에 프로퍼티 역시 클래스 밖에 정의할 수 있다.

    // Join.kt 라는 파일이라고 가정. 
    package strings
    
    // 최상위 프로퍼티 
    var count = 0
    
    // 최상위 함수 
    fun joinToString(...) : String { ... }

    이 함수가 어떻게 실행될 수 있을까? JVM이 클래스 안에 들어있는 코드만을 실행할 수 있기 때문에 컴파일러는 이 파일을 컴파일할 때 새로운 클래스를 정의해준다.

    위의 join.kt 파일을 컴파일하면 아래와 같이 변환된다. 아래에서 보듯이, 코틀린 컴파일러가 생성하는 클래스의 이름은 코틀린 소스 파일의 이름과 대응한다.

    💡 만약 파일에 대응하는 클래스의 이름을 변경하고 싶다면, @JvmName("원하는 이름") 어노테이션을 사용하면 된다!

    package strings;
    
    public class JoinKt {
    	public static String joinToString(...) { ... }
    }

     

    로컬 함수 (Local function)

    함수 안에서 또 다른 함수를 정의해 사용할 수 있다. 이를 local function 이라고 부른다.

    로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다.

    ❓책에서는 로컬함수를 사용해 중복된 코드를 줄일 수 있다고 하는데, 아래와 같은 궁금증이 있다. 이는 별도로 공부 후 포스팅해볼 예정.

    • 꼭 로컬함수를 써야만 줄일 수 있나? 밖에 선언해도 가능한 것 아닌가? 즉, 어떤 이점이 있는지.
    • 로컬 함수 사용 시 발생하는 부가 비용? 

     

    멤버 함수 (Member functions)

    가장 일반적인 형태의 함수이다. class 또는 object 내부에 존재하는 함수이다.

     

     


    확장 함수와 확장 프로퍼티 (Extension function/property)

    Kotlin provides the ability to extend a class with new functionality without having to inherit from the class or use design patterns such as Decorator. This is done via special declarations called extensions.
    - 공식문서

    확장 함수

    확장 함수란, 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만 그 클래스의 밖에 선언된 함수다.

    아래 확장 함수에서, String을 receiver type 이라고 하며, "kotlin" 을 receiver object 라고 부른다.

    // 문자열의 마지막 문자를 돌려주는 확장함수
    fun String.lastChar(): Char {
        // 여기서 this 는 String 자체이다 
        return this.get(this.length - 1)
    }
    
    println("kotlin".lasChar()) // n 출력

    확장 함수 내부에서는 일반적인 인스턴스 메소드의 내부에서와 마찬가지로 receiver object 의 메소드나 프로퍼티를 바로 사용할 수 있다. 하지만 클래스 내부에서만 사용할 수 있는 private/protected 멤버는 사용할 수 없다. 즉, 확장 함수가 캡슐화를 깨지는 않는다!

     

    확장 프로퍼티

    확장 프로퍼티의 경우, backup field 가 없어서 기본 getter 구현을 제공할 수 없으므로 최소한 getter 는 꼭 정의를 해야 한다.

    val String.lastChar: Char
        get() = get(length - 1)
    
    
    var StringBuilder.lastChar: Char
        get() = get(length - 1)
        set(value: Char) {
            this.setChartAt(length - 1, value)
        }
    
    
    println("kotlin".lastChar) // n 출력
    
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!' // setter 호출 
    println(sb) // Kotlin! 출력

     

    자바 컬렉션 API 확장

    아래와 같은 코틀린 컬렉션이 있다. 이는 코틀린 자체 라이브러리일까? No. 그렇지 않다! 아래와 같이 컬렉션의 class 를 출력해보면 자바에서 제공되는 컬렉션임을 알 수있다.

    val set = hashSetOf(1, 7, 53)
    val list = arrayListOf(1, 7, 53)
    
    println(set.javaClass) // class java.util.HashSet 출력
    println(list.javaClass) // class java.util.ArrayList 출력

     

    그런데, 코틀린 컬렉션은 자바와 같은 클래스를 사용하지만 더 확장된, 다양한 API 들을 제공한다. 아래의 예시와 같은 last(), max() 등.

    자바 클래스인데 어떻게 코틀린에서만 이런 API 들을 제공할까? 정답은 확장 함수이다. 그렇다! last(), max() 는 모두 확장 함수인 것.

    코틀린 표준 라이브러리는 수많은 확장 함수를 포함한다.

    val set = hashSetOf(1, 7, 53)
    val list = arrayListOf(1, 7, 53)
    
    println(list.last()) // 53 출력
    println(set.max()) // 53 출력

    _Strings.kt 내의 last() 함수

     


    컬렉션 처리

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

    리스트를 생성하는 함수를 호출할 때 원하는 만큼 원소를 전달할 수 있다. 이게 어떻게 가능할까?

    val list = listOf(1,2,3,4)
    val list2 = listOf(1,2,3,4,5,6)

    varargs(가변 길이 인자)를 사용하면 가능하다. 가변 길이 인자는 메서드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 컴파일러가 배열에 그 값들을 넣어주는 기능이다.

    위 예시의 listOf 구현체를 보면 아래와 같이 varargs 를 사용한 것을 볼 수 있다.

    listOf 구현체

     

    코틀린에서는 가변 길이 인자로 배열을 넘길 때, 배열을 풀어서 배열의 각 원소가 전달되게 해야 한다. 이를 가능하게 해주는 것이 spread operator(연산자) 이다. 배열앞에 * 를 붙이면 된다. 아래 예시를 보자.

    val subArray = arrayOf(1, 2, 3)
    // 전달할 배열 변수명 앞에 * 를 붙이면 배열의 각 원소가 인자로 전달되게 된다. 
    val list = asList(-1, 0, *subArray, 4)
    
    println(list) // [1, 2, 1, 2, 3] 출력

     

    Infix notation (중위 표기법)

    infix 키워드가 붙은 함수는 infix 표기법을 사용해 호출할 수도 있다. 단, Infix 함수는 아래의 조건을 만족해야 한다.

    • 멤버 함수이거나 확장 함수여야 한다.
    • 단 1개의 파라미터 인자를 갖고 있어야 한다.
    • 파라미터는 varargs 가 아니여야 하고, 디폴트 값이 없어야 한다.

    그럼 여기서 infix 표기법이 무엇이냐. 아래의 예시를 보자.

    // 실제로 코틀린 라이브러리에 존재하는 infix 함수 
    infix fun Any.to(other: Any) = Pair(this, other)
    
    // 위의 to() 메소드를 일반적인 방식으로 호출하는 경우
    1.to("one")
    
    // infix 표기법으로 호출
    1 to "one"

     

    위의 예시에서 본 to() 함수는 map을 생성할 때 보았을 것이다. 즉, 이때 사용했던 to 는 코틀린 키워드가 아니라 일반 메소드였다는 것!

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

     

    위에서 본 Pair 의 두 변수를 즉시 초기화 할 수 있다. 이를 destructuring declaration 이라고 부른다.

    val (key, value) = Pair(1, "one)
    
    // 또는
    
    val (key, value) = 1 to "one"

     


    문자열과 정규식 다루기

    문자열 함수에서의 정규식 구분

    자바 개발자라면 자바에서의 split 메소드를 잘 알고 있을 것이다. 그렇다면 자바에서 "12.345-6.A".split(".") 의 결과값이 무엇일까? [12, 345-6, A] 라고 생각하겠지만, 땡! 틀렸다. 정답은 [] 다. 즉, 빈 배열을 반환한다.

    이유는, 자바 split 의 구분 문자열은 실제로는 정규식이기 때문이다.

     

    코틀린에서는 자바의 split 대신에 여러 가지 다른 조합의 파라미터를 받는 split 확장 함수를 제공함으로써 혼동을 없앴다. 즉, 코틀린에서는 정규식을 파라미터로 받는 함수는 String 이 아닌 Regex 타입의 값을 받는다.

     

    val string = "12.345-6.A"
    
    string.split(".") // 구분 파라미터 : String 값 그대로
    string.split(Regex(".")) // 구분 파라미터 : 정규식

     

    3중 따옴표 

    문자열을 표시할 때 val string = """blah blah""" 와 같이 3중 따옴표를 사용할 수 있다. 장점은 아래와 같다.

    • 3중 따옴표 문자열에서는 역슬래시(\)를 포함한 어떤 문자도 escape 할 필요가 없다.
    • 들여쓰기나 줄 바꿈을 포함한 모든 문자가 들어간다.
    // 역슬래시 등의 문자가 포함된 일반 문자열 예시 
    val string = "\\s\\n\\."
    println(string) // \s\n\. 출력 => 즉, 역슬래시 자체를 사용하기 위해서는 escape 필요
    
    val string3 = """\s\n\."""
    println(string3) // \s\n\. 출력 => 3중 따옴표에서는 escape 필요없음.

     


    간단 요약

    • 코틀린은 자체 컬렉션 클래스를 정의하지 않지만 자바 클래스를 확장해서 더 다양한 API를 제공
    • 함수 파라미터의 디폴트 값을 정의하면 오버로딩한 함수를 정의할 필요성이 줄어든다.
    • 이름 붙인 인자(named arguments)를 사용하여 함수 호출의 가독성을 향상시킬 수 있다.
    • 코틀린 파일에서 클래스 멤버가 아닌 최상위 함수와 프로퍼티를 직접 선언할 수 있다.
    • 확장 함수와 프로퍼티를 사용하면 그 클래스의 소스코드를 바꿀 필요 없이 확장할 수 있으며, 실행 시점에 부가 비용이 들지 않는다.
    • 코틀린은 정규식과 일반 문자열을 처리할 때 다양한 문자열 처리 함수를 제공한다.
    • 수많은 escape가 필요한 문자열의 경우 3중 따옴표를 사용하면 escape 없이 표현할 수 있다.
    반응형