Kotlin-In-Action | #3. 함수 정의와 호출

보람·2022년 4월 26일
0

Kotlin-In-Action

목록 보기
4/12

3장에서는 함수 정의와 호출기능을 코틀린에서 어떻게 개선했는지에 대해 다뤘다.

코틀린의 컬렉션

  • hashSetOf() instead of HashSet()
  • arrayListOf(1, 7, 10) instead of ArrayList()
  • hashMapOf(1 to "one", 7 to "seven") instead of HashMap()
    • 위에 있는 to는 특별한 키워드가 아닌 일반함수임

여기서 알수있는 점은?

  • 코틀린 컬렉션은 자체 컬렉션이 아닌 자바 컬렉션임

하지만 자바보다 더 많은 기능 사용이 가능하다.

  • 컬렉션에서 last(), max() 함수 지원

호출하기 쉬운 함수 만들기

아래와 같은 함수가 존재한다.

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()
}

아래와 같은 결과값을 갖는다.

val list = listOf(1, 2, 3)
println(joinToString(list, "; ", "(", ")"))

>> (1; 2; 3)

위 함수는 어떤 타입의 값을 원소로 하는 컬렉션이든 처리할 수 있으며 문법이 자바와 비슷하다. 잘 작동하지만 더 줄일 수 있는 방법이 있을 것 같다!

이름 붙인 인자

  • 가독성 해결

네가지의 파라미터를 다 기억하려면 너무 힘들다.
자바에서는 아래와 같은 형식으로 가독성을 해결하지만

joinToString(collection, /* separator */ " ", /* prefix */ * *, /* postfix */ ".");

코틀린에서는 이런식으로 해결한다.

joinToString(list, separator = " ", prefix = " ", postfix = ".")

그냥 보기에도 코틀린에서 지원하는 이름 붙인 인자를 사용한 코드가 더 깔끔하다!

디폴트 파라미터 값

  • 자바 일부 클래스는 오버로딩한 메서드가 너무 많아지는 문제가 발생(중복 발생)한다.
  • 코틀린에서는 디폴트 값이 선언 가능하여 이런 오버로딩의 상당수를 개선할 수 있다.
  • 자바에서 코틀린 함수를 자주 호출한다면? @JvmOverloads를 붙이면 하나의 코틀린 함수에서 오버로딩이 필요한 함수를 알아서 생성해줌

아래 코드를 보자

fun <T> Collection<T>.joinToString(
        separator: String = ", ", // ", "라는 기본값 지정
        prefix: String = "",      //""라는 기본값 지정
        postfix: String = ""      //""라는 기본값 지정
): String {//구현 코드}

사용 예시

joinToString(list, ", ", "", "") //기존과 동일
>> 1, 2, 3

joinToString(list) // separator, prefix, postfix 생략 -> 기본값 사용
>> 1, 2, 3

joinToString(list, "; ") // prefix, postfix 생략
>> 1; 2; 3

//아래와 같이 지정하고 싶은 인자를 이름을 붙여서 순서와 관계없이 지정 가능
joinToString(list, postfix=";", prefix="# ")
>> # 1, 2, 3;

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

  • 자바 : 함수가 클래스 내부에 존재해야 함
  • 코틀린 : 함수를 직접 소스 파일 최상위 수준, 클래스 밖에 위치 가능
//join.kt
package strings
fun joinToString(...): String {...}

//자바라면 JointKt라는 클래스를 생성해준다.
//클래스명을 바꾸기를 원한다면 @JvmName 사용
  • 코틀린 컴파일러가 해당 파일명을 기반으로하여 클래스를 생성해준다.
  • 최상위 프로퍼티 (값)
    • const val UNIX_LINE_SEPARATOR="\n"
      instead of
      public static final String UNIX_LINE_SEPARATOR="\n"

확장 함수와 확장 프로퍼티

어떤 클래스의 멤버 메서드인 것처럼 호출가능하지만 해당 클래스 외부에서 선언된 함수 혹은 프로퍼티

package strings
//String : 수신객체타입 - 확장이 정의될 클래스의 타입
//this : 수신객체 - 그 클래스에 속한 인스턴스 객체 
fun String.lastChar(): Char = this.get(this.length - 1)

위 코드를 통해 알수 있는 점은?

  • 해당 클래스 내부에서 정의한 값에 접근 가능(but private, protected X => 캡슐화 보존)
  • 내가 정의한 클래스가 아니여도 원하는 함수 구현 가능

확장함수를 사용하기 위해선? import 필수

  • import strings.lastChar
  • import strings.*
  • import strings.lastChar as last
    import 시 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법!!

확장함수는 오버라이드 불가능

  • 확장함수는 정적으로 결정되기 때문에 상위 클래스의 확장함수가 호출된다.
  • 호출 우선순위 : 멤버함수 > 확장함수

확장 프로퍼티

  • 아무 상태도 가질 수 없으나 더 짧은 코드 작성 가능
  • 기본 게터 구현을 제공할 수 없어 최소한의 게터는 꼭 정의해야 함
    • 프로퍼티의 값을 저장할 수 있는 필드인 '백본필드'가 존재하지 않음
val String.lastChar: Char   //확장 프로퍼티
    get() = get(length - 1)
var 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!

컬렉션 처리

코틀린의 컬렉션에는 여러가지 라이브러리를 제공한다.

vararg : 인자의 개수가 달라질 수 있는

val list = listOf(2,3,5,7,11)

listOf는 개발자가 원하는대로 값을 추가할 수 있다.

fun listOf<T>(vararg values:T) : List<T> {....}

중위 호출 일반함수 1 to "one"

//infix로 중위 호출에 사용하는 함수라는 것을 표시
infix fun Any.to(other:Any) = Pair(this, other)

구조분해선언 Pair(1, "one")

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

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

  • 좋은 코드의 중요한 특징중 하나 : 중복이 없는 것(DRY원칙)
  • 자바에서는 긴 함수를 부분부분 나누는 추출 리팩토링을 적용할 수 있지만 이는 클래스 내부에 작은 메서드가 많아지고 각 메서드사이의 관계를 파악하기 힘들어진다.
  • 코틀린에서는 함수 내부에 중첩함수를 둘 수 있다.
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")
    }

    // Save user to the database
}

위 예제는 유효성체크하는 코드가 값마다 들어가있다.
2개만 있어서 많이 길지는 않지만 값이 더 늘어난다면 마음아픈 코드가 될 것이다.

로컬 함수 사용하여 코드 중복 줄이기

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")

    // Save user to the database
}

중복 코드가 많이 사라졌다.
하지만 validate함수의 user를 호출하는 부분이 신경쓰인다.

로컬함수는 바깥 함수 파라미터에 접근할 수 있지

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")

    // Save user to the database
}

validate()함수에서 user를 넘기는 부분까지 사라져서 중복이 더 줄었다!

확장함수까지 사용하면? 더 개선 가능

fun User.validateBeforeSave() {
    fun validate(value: String, fieldName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
               "Can't save user $id: empty $fieldName")
            // User의 프로퍼티를 직접 사용 가능
            //private과 protected value를 제외하고 확장 함수에서 수신객체 멤버변수 사용 가능
        }
    }

    validate(name, "Name") //중복되는 user 파라미터 삭제  
    validate(address, "Address")
}

fun saveUser(user: User) {
    user.validateBeforeSave() //확장 함수 호출
}

//saveUser(User(1, "", ""))
  • 이런 중첩함수는 가독성을 위해 1번까지만 추가하는 것을 권장
profile
백엔드 개발자

0개의 댓글