코딩하는 개굴이

[Kotlin IN ACTION] 함수 정의와 호출 본문

안드로이드/KOTLIN

[Kotlin IN ACTION] 함수 정의와 호출

개굴이모자 2021. 3. 29. 00:02
반응형

Kotlin IN ACTION 3강 : 함수 정의와 호출

본 내용은 Kotlin IN ACTION (드미트리 제메로프, 스베트라나 이사코바 지음 / 에이콘 출판사) 책을 기반으로 작성되었습니다.

Kotlin 의 Collection

코틀린에서 모든 컬렉션은 자바 컬렉션을 활용하고 있어 자바 코드와의 상호작용 및 호환이 용이하면서, 더 많은 기능들을 쓸 수 있는 장점을 지닌다.

println(set.javaClass) //javaClass는 자바 getClass에 해당한다.
//코틀린의 모든 컬렉션은 독자적인 컬렉션이 아닌 자바 컬렉션을 활용한다.
  • Collection : 여러 데이터를 모아놓은 하나의 단위로, 불변(immutable/read-only) 혹은 가변(mutable/read&write) 중 하나의 성질을 가진다.
    • immutable : 한번 정의된 후 변경이 불가하기 때문에 add 등의 메소드가 존재하지 않는다.
      • List, mapOf ..
    • mutable : 한번 정의된 후 변경이 가능하다.
      • ArrayList, hashMapOf ..

함수 호출

이름 붙인 인자

코틀린에서는 함수 호출 시 가독성을 높이기 위해 함수에 전달하는 인자에 이름을 명시할 수 있다.

//만일 자바에서 호출하는 각 부분에서 인자 각각의 의미가 어떤것인지 명시하기 위해서는 아래와 같이 주석을 이용해야 한다.
joinToString(collection, /*seperator*/ " ", /*prefix*/ "(", /*postfix*/")");
//코틀린에서는 전달하는 인자들의 일부, 혹은 전부에 대해 이름을 명시할 수 있다.
joinToString(collection, seperator = " ", prefix = "(", postfix = ")");

주의 사항

  • 호출 시 인자 중 하나라도 이름을 명시하고 나면 혼동을 막기 위해 뒤에 오는 모든 인자는 이름을 명시해야 한다.
    • 위의 경우 seperator의 이름을 명시 한 상태에서 prefix와 postfix의 이름을 명시해야한다.
  • 자바와 코틀린 코드를 혼용 시, 자바로 작성된 코드 함수를 호출할 때 이름 붙인 인자를 사용할 수 없다.
    • 코틀린 컴파일러에서 이름을 인식할 수 없다.

디폴트 파라미터 값

  • 자바에서는 특정 함수의 인자에 대해 Default 값을 지정하고 싶다면, 해당 인자를 받는 함수와 받지 않고 Default 값으로 처리하는 함수 총 2개로 오버로딩해야 한다.
    • 자바에서는 이 때문에, 일부 클래스가 오버로딩한 메소드가 많아진다는 문제가 발생할 수 있다.
  • 코틀린에서는 함수 선언 시, 파라미터에 직접 디폴트 값을 지정하여 오버로딩을 줄일 수 있다.
fun <T> joinToString(
    collection: Collection<T>,
    seperator: String = ", ", //디폴트 값으로 ", "을 지정
    prefix: String = "", //디폴트 값으로 ""을 지정
    postfix: String = "" //디폴트 값으로 ""을 지정
) : String

...

//일반 호출 문법 사용
joinToString(list)
joinToString(list, "&") //seperator 의 디폴트 값 대신 & 지정

//이름 붙은 인자 사용
joinToString(list, postfix = "]", prefix = "[") //순서 관계 없이 원하는 인자만 넘길 수 있다.
  • 함수 호출 시, 모든 인자를 쓸 수 있고 디폴트 값으로 지정된 일부 인자를 생략할 수 있다.
  • 일반 호출 문법을 사용 시 함수 선언 시와 같은 순서로 인자를 지정해야함
    • 일부 생략 시, 뒷 부분의 인자들도 생략된다. (seperator를 디폴트 값으로 사용하고 prefix 의 인자를 넘길 수 없다.)
  • 이름 붙은 인자 사용 시, 순서와 관계 없이 함수의 중간에 있는 인자를 생략할 수 있다.

최상위 함수와 프로퍼티

최상위 함수

자바에서는 여러 곳에서 비슷하게 중요한 역할을 하는 정적 메소드를 모아두는 역할을 담당하고, 특별한 상태나 인스턴스 메소드가 없는 클래스 (Utils 등)가 필요할 때가 있다.

코틀린에서는 이런 함수들을 위한 클래스를 따로 불필요하게 지정하지 않고, 함수를 소스 파일의 최상위 수준에 위치 시켜 이를 구현할 수 있다.

패키지의 멤버 함수로써 사용할 수 있게되고 다른 패키지에서 해당 함수를 사용 시, 해당 함수가 정의된 패키지를 선언해야 한다. 그러나, 이때 유틸리티 클래스가 불필요하게 추가되지 않는다.

  • 컴파일러는 JVM이 클래스 안에 들어있는 코드만을 실행할 수 있기 때문에 컴파일 시 해당 코틀린 소스 파일 이름에 해당하는 클래스를 생성 해 정적으로 생성 해 준다.

    • 예로, Sample.kt 파일에 joinToString 함수를 최상위 함수로 선언했다면 컴파일러는 아래와 같이 해당 클래스의 정적인 메소드로 생성한다.

      public class SampleKt {
        public static String joinToString(..) {...}
      }
  • 해당 코틀린 소스 파일 이름으로 클래스가 자동 생성되지 않게 하고 싶다면, @file:JvmName("YOUR_FILE_NAME")의 어노테이션을 파일의 맨 앞에 두어 이름을 바꿀 수 있다.

최상위 프로퍼티

프로퍼티 또한 최상위에 지정할 수 있다.

  • 최상위 프로퍼티는 정적 필드에 저장된다.
  • 최상위 프로퍼티를 활용해 코드에 상수를 추가할 수 있다.
    • 상수로 최상위 프로퍼티를 사용 시, const val SAMPLE_CONSTANTS = ""로 사용하여 프로퍼티를 public static final 필드로 컴파일 할 수 있게 해야한다.
    • 자바 코드로 변환 시 val SAMPLE_CONSTANTS = ""val 을 사용한다면 read-only 이기 때문에 getter 들이 생성된다. 이는, 상수처럼 보이지만 getter를 사용해서 접근해야 하는 것이기 때문에 부자연스럽다.

메소드를 다른 클래스에 추가

확장 함수

  • 확장 함수 : 어떤 클래스의 멤버 메소드인 것 처럼 호출 가능하지만, 실제로는 그 클래스의 밖에서 선언된 함수

    • 자바와 코틀린을 혼용 시, 기존 자바코드 API 를 재 작성하지 않고 코틀린으로 특정 함수를 확장할 수 있다.
  • 추가하려는 함수 이름 앞에 확장할 클래스의 이름을 덧붙인다.
    fun String.lastChar() : Char = this.get(this.length -1)

    • String 클래스에 새로운 메소드인 lastChar를 추가하는 것으로 볼 수 있다.

    • 확장할 클래스 이름 : 수신 객체 타입 (receiver type) -> String

    • 확장 함수가 호출되는 대상이 되는 객체 : 수신 객체 (receiver object) -> this (String 객체)

    • 호출 시에는 일반 함수와 동일하게 호출한다.

    • 확장 함수는 캡슐화는 깨지 않는다.

      • 클래스 안에서 정의된 메소드가 아니기 때문에 확장 함수 안에서는 수신 객체의 메소드나 프로퍼티를 바로 사용할 수는 있지만 private 나 protected 된 멤버를 사용할 수 없다.
      • 확장 함수는 내부에서 클래스의 멤버 메소드와 다른 확장 함수들을 호출할 수 있다.
    • 확장함수를 사용하려면 해당 함수를 다른 클래스나 함수처럼 임포트 해야 한다.

      • lastChar 만 사용 시, import strings.lastChar를, string 전체도 임포트 시 확장함수들도 같이 임포트된다.
      • as 키워드로 같은 이름을 가진 확장함수의 충돌을 방지할 수 있다.
        • import strings.lastChar as last 처럼 다른 이름으로 사용 가능.
    • 자바 코드에서 코틀린 코드의 확장 함수 호출 시, 해당 확장함수의 파일 이름과 수신 객체를 고려하여 아래와 같이 호출한다.

      • lastChar 확장함수의 위치를 SampleKt라고 가정한다.
        char c = SampleKt.lastChar("Java");
    • 더 구체적인 타입을 수신 객체 타입으로 지정 가능

        fun Collection<String>.join(
            seperator: String, 
            prefix: String, 
            postfix: String
        ) = joinToString(seperator, prefix, postfix)
      
        ...
      
        println(listOf("one", "two", "three").join(" ", "(", ")")
        //결과 : (one two three)
        //다른 int 형식의 list 등에서 호출 시도 시, type mismatch 가 발생한다.
    • 확장함수는 클래스 밖에 선언되고 정적으로 결정되기 때문에 오버라이딩이 불가하다.

확장 프로퍼티

일반 프로퍼티와 사용하는 방식은 동일하나, 수신 객체 클래스가 추가된다.

    val String.lastChar: Char 
        get() = get(length -1)

    val StringBuilder.lastChar: Char 
        get() = get(length -1)
        set(value: Char) {
            this.setCharAt(length -1, value)
        }

...

println("Kotlin".lastChar) //n

val sb = StringBuilder("Kotlin?")
sb.lastChar = "!"
println(sb) //"Kotlin!"

컬렉션 처리

자바 컬렉션 API 확장

  • 자바 라이브러리 클래스의 인스턴스인 컬렉션에 대해서 코틀린은 원하는 확장 함수를 추가할 수 있고, 코틀린 표준 라이브러리는 수많은 확장 함수를 포함한다.

가변 인자 함수

가변 인자 함수는 인자의 개수가 달라질 수 있는 함수를 의미한다. 이런 함수에 가변 길이 인자가 들어가게 되는데, 자바와 코틀린 각각의 구현을 알아보자.

코틀린의 가변 길이 인자는 자바의 가변 길이 인자와 비슷하다.

  • 가변 길이 인자 (varargs) : 메소드를 호출 시, 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 해당 값들을 넣어주는 기능이다. (원하는 개수만큼 값을 인자로 넘길 수 있음)
    • 자바에서는 type 뒤에 ...을 붙여 구현한다.
    • 코틀린에서는 vararg를 파라미터 앞에 붙여준다.
  • 코틀린에서의 가변 길이 인자 구현
    • 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야한다.
      • 스프레드(spread) 연산자를 사용한다.
fun listOf<T>(vararg values: T): List<T> { ... }

var list = listOf("args : ", *args) //전달하려는 배열 앞에 *를 붙여 펼쳐준다.

값의 쌍 다루기

중위 호출과 구조 분해 선언

맵 즉, key-value의 형태를 가지는 데이터 타입을 만들기 위해서는 mapOf 함수를 사용하며 아래와 같다.

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

일반 메소드를 호출하는 방식 중, 1.to("one") 처럼 일반적인 방식으로 호출 할 수도 있지만

위 코드는 중위 호출이라는 방식으로 to 라는 일반 메소드를 호출하였다.

  • 중위 호출(infix call) : 중위 호출 시에는 수신 객체(1)와 메소드 인자(one) 사이에 메소드 이름을 넣는다.
  • 중위 호출 선언 : 중위 호출을 선언 시, infix 변경자를 함수 선언 앞에 추가해야 한다.
    infix fun Any.to(other: Any) = Pair(this, other)

이 to 일반 메소드는 코틀린 표준 라이브러리 클래스로 두 원소로 이뤄진 순서쌍을 의미하는 Pair의 인스턴스를 반환하고 있다.

이때, val (number, name) = 1 to "one" 이렇게 Pair의 내용으로 number와 name 두 변수를 즉시 초기화 할 수도 있는데,
해당 기능을 구조 분해 선언(destructuring declaration) 이라고 한다.

문자열과 정규식 다루기

코틀린의 문자열은 자바 문자열과 완전히 동일하여, 변환이 필요없고 자바 문자열을 감싸는 별도의 래퍼도 존재하지 않는다. 그러나, 문자열을 나누는 등의 기능을 하는 확장 함수들에 대해서는 차이점들이 존재하는데 특히 정규식에 대한 것이 그러하다.

  • split 메소드
    • split 메소드는 특정 구분자를 이용해 문자열을 분리할 수 있는 기능을 제공
    • 자바에서는 점(.)을 이용해 문자열을 분리할 수 없음
      • 점을 구분자로 사용 시, 자바에서 실제로는 해당 "."을 정규식으로 해석하기 때문
    • 코틀린에서는 string 타입으로 구분자를 받을 수도 있지만, 정규식을 전달하기 위해 Regex 타입 자체도 받는다.
      • "12.345.6.7".split("\\.".toRegex()) 로 사용 가능하다.
        • . 을 정규식에서 문자 그대로 해석하기 위해 . 가 되는데 이때 쓰이는 \ 문자 자체를 쓰기 위해 escape 인 \를 하나 더 붙이게 된다. 따라서, . 하나에 대한 정규식은 \.가 된다.
  • 3중 따옴표 : 삼중 따옴표 안에서는 어떠한 문자도 escape 할 필요가 없다.
    • 장점
      • escape 를 쓰지 않는다.
        • 위의 "12.345.6.7".split("\\.".toRegex()) 에서의 . 의 표현도 삼중 따옴표 안에서는 """."""로 쓰일 수 있다.
      • 줄 바꿈을 표현하는 문자들도 그대로 들어갈 수 있다.
          val sampleString = """((()))
                                 o  o 
                                  (   3
                                  =    """

로컬 함수

코드를 작성 시 중요하게 여기는 부분 중 하나는 DRY(Don't Repeat Yourself)인데, 자바에서는 해당 부분을 온전히 지키기에 쉽지 않은 점들이 있다.

  • 자바에서의 DRY 원칙
    • 중복되는 코드들이 있을 경우, 해당 부분들을 메소드로 추출하여 클래스 안에 작은 메소드드 들이 많아지게 되고, 해당 메소드 사이의 관계 파악이 쉽지 않을 수 있다.
    • inner class 안에 추출한 메소드들을 모을 수 있지만, 불필요한 준비 코드들이 늘어나게 된다.
  • 코틀린에서의 DRY 원칙 지키기
    • 로컬 함수 : 함수에서 중복되는 코드들을 추출한 함수를 원래 함수의 내부에 중첩 시킨다.
      • 자신이 속한 바깥 함수의 파라미터와 변수들을 사용할 수 있다.
      • 로컬 함수 안에 로컬 함수를 넣어 중첩 시킬 수도 있지만, 중첩된 함수의 깊이가 깊어질수록 가독성이 떨어지기 때문에 한 단계만 함수를 중첩시키는 것을 권장한다.
class People(val id: Int, val name: String, val address: String)
fun People.validateBeforeSave() { //People의 확장함수인 validateBeforeSave
    fun validate(value: String, fieldName: String) { //확장함수 내부의 로컬 함수
        if (value.isEmpty()) {
            throw IllegalArgumentException("ERROR")
        }
    }
    //로컬함수로 validate 체크.
    validate(name, "Name") //이때, 바깥함수인 validateBeforeSave의 People.name을 사용
    validate(address,"Address")
}

fun saveUser(ppl: People) { //타 함수에서 People객체의 확장함수를 부른다.
    ppl.validateBeforeSave()
    ...
}

참고 링크

반응형
Comments