본문 바로가기
My Image
프로그래밍/kotiln

함수의 정의와 호출(3장)

by Lim-Ky 2023. 2. 5.
반응형

코틀린에서 컬렉션 만들기

fun main(arg:Array<String>){

    val set = hashSetOf(1, 7, 53)
    val list = arrayListOf(1, 7, 53)
    val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
    val strings = listOf("first", "second", "fourteenth")
    val numbers = setOf(1, 14, 2)
    
    println(set.javaClass)
    println(list.javaClass)
    println(map.javaClass)
    
    println(strings.last()) //마지막 원소
    println(numbers.max()) //최대값
}
  • 코틀린이 자신만의 컬렉션 기능을 제공하지 않는다. (즉, 기존 자바 컬렉션을 활용한다는 뜻)
  • 자바 컬렉션을 활용하기 때문에 자바와 호환성이 좋다. (코틀린 -> 자바, 자바 -> 코틀린 변환이 필요없기 때문)
  • 코틀린은 자바보다 더 많은 기능을 사용할 수 있다. (ex 마지막 원소 구하기, 최대값 구하기 등)

 

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

자바 컬렉션에는 디폴트 toString 구현이 들어가 있는데, 원하는 형식의 출력이 아닐 수 있다.

직접 toString을 구현해서 원하는 형식으로 출력해보도록 하자

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    println(joinToString(list, "; ", "(", ")")) //이름 붙이지 않은 인자
    println(joinToString(collection = list, separator = ";", prefix = "(", postfix = ")")) //이름 붙인 안자
    println(joinToString(list)) //전달하지 인자는 디폴트 인자로 처리
    println(joinToString(list, "; "))
}


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()
}
  • 함수 호출시 인자에 이름을 붙여서 가독성을 높일 수 있음
  • 인자로 전달하지 않은 값은 디폴트 파라미터 값으로 처리할 수 있음
  • 디폴트 파라미터 값을 선언함으로써 불필요한 오버로드를 상당 수 피할 수 있음

 

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

자바에서는 모든 코드를 클래스의 메서드로 작성해야함. 하지만 이로 인해서 한 클래스에 포함시키기 어려운 메서드나 코드들이 생기는 경우 불필요하게 util 성 클래스 등을 일부로 만들기도 함. 이런 문제점(?)을 해결하기 위해 코틀린은 함수를 직접 소스 파일의 최상위 수준, 모든 다른 클래스의 밖에 위치시킴으로써 불필요한 유틸리티 클래스를 없앨 수 있음

최상위 함수

package Strings

fun joinToString(...): String {...}
  • Strings 라는 패키지에 joinToString 함수를 위치시켜 최상위 함수로 사용 가능
  • 자바에서는 위 코드를 내부적으로 코틀린 소스 파일의 이름과 동일한 클래스를 만들고 그 클래스 내부에 해당 메서드를 위치시켜 정적 메서드로써 사용할 수 있도록 처리 함

최상위 프로퍼티

최상위 함수와 마찬가지로 프로퍼티를 최상위로 위치시킬 수 있음

var opCount = 0
const val UNIX_LINE_SEPERATOR = "\n"
//java 에서는 public static final String UNIX_LINE_SEPERATOR = "\n";
fun performOperation(){
    opCount++
}

fun main(args: Array<String>) {

    performOperation()
    performOperation()

    println("operation performed $opCount times")
    //operation performed 2 times
}
  • 최상위 프로퍼티 선언 및 값 수정, 출력에 대한 예제이다.
  • 자바에서 public static final 같이 사용하고 싶으면 코틀린에선 const val 를 선언하면 된다.

 

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

기존 코드와 코틀린 코드를 자연스럽게 통합하는 것은 코틀린의 중요한 핵심 목표 중 하나이다. 이런 기조 아래 기존 자바 프로젝트에 코틀린을 통합하는 경우 자바 코드를 코틀린으로 변환할 수 없거나 하는 상황에서 코틀린이 제공하는 확장 함수 및 확장 프로퍼티를 사용하면 자연스럽게 통합할 수 있다.

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

package strings
fun String.lastChar(): Char = this.get(this.length - 1)

>>> println("Kotiln".lastChar())
//n
  • 확장 함수를 만들기 위해선 함수 이름 앞에 확장할 클래스 명을 명시(위 예제에선 String : '수신 객체 타입')
  • 확장 함수가 호출되는 대상이 되는 값을 '수신 객체'라고 부름
  • Stirng 이라는 클래스에 lastChar() 메서드를 추가함으로써 String 클래스 내부의 메서드 처럼 사용할 수 있음
  • 확장 함수 본문에도 this를 사용할수도 있고 생략할 수도 있음
  • 수신 객체의 메서드나 프로퍼티를 바로 사용할 수 있음
  • 확장 함수는 캡슐화를 깨지 않는다. 즉, 확장 함수 안에서 클래스 내부에서만 사용할 수 있는 비공개 멤버나 메서드를 확장 함수라고 하더라도 사용할 순 없다.
  • 호출하는 쪽에서는 확장 함수와 멤버 메서드를 구분할 필요도 없으며, 구분할 필요성도 없다.

 

임포트와 확장 함수

확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해야만 한다. 확장 함수를 임포트 없이 사용한다면 동일한 이름의 확장 함수와 충돌할 수도 있기 때문에 임포트로 어떤 확장함수인지 명시해 주어야 한다.

import strings.lastChar // 명시적으로 사용
import strings.* // * 사용 가능
import strings.lastChar as last // as 키워드를 사용 가능

 

자바에서 확장 함수 호출

단지 정적 메소드를 호출하면서 첫 번째 인자로 수신 객체를 넘기기만 하면 된다. 다른 최상위 함수와 마찬가지로 확장 함수가 들어있는 자바 클래스 이름도 확장 함수가 들어있는 파일 이름에 따라 결정된다. 따라서 확장 함수를 StringUtil.kt 파일에 정의했다면 다음과 같이 호출할 수 있다.

char c = StringUtilKt.lastChar("java");

 

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

이전에 선언했던 함수를 확장함수로 변환하고 유틸리티 함수로 사용해보자

fun main(args: Array<String>) {
    val list = arrayListOf(1, 2, 3)
    println(list.joinToString(" "))
    // 1 2 3
}

fun <T> Collection<T>.joinToString(
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
): String {
    val result = StringBuilder(prefix)

    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }

    result.append(postfix)
    return result.toString()
}
  • joinToString 함수를 collection<T> 확장 클래스, this 수신객체 정의하여 확장 함수로 적용하였다.
  • 컬렉션이 쓰이는 곳에서 바로 내부 메서드 처럼 joinToString 호출 할 수 있다.

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

확장 함수는 클래스의 일부가 아니다. 확장 함수는 클래스 밖에 선언된다. 이름과 파라미터가 완전히 같은 확장 함수를 기반 클래스와 하위 클래스에 대해 정의해도 실제로는 확장 함수를 호출할 때 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장함수가 호출될지 결정될지, 그 변수에 저장된 객체의 동적인 타입에 의해 확장 함수가 결정되지 않는다.

 

확장 프로퍼티(필드/접근자 메서드)

확장 프로퍼티를 사용하면 기존 클래스 객체에 대한 프로퍼티 형식의 구문으로 사용할 수 있는 API를 추가할 수 있다. 

fun main(args: Array<String>) {
    println("Kotlin".lastChar)
    //n
    val sb = StringBuilder("Kotlin?")
    sb.lastChar = '!'
    println(sb)
    //Kotlin!
}

var StringBuilder.lastChar: Char
    get() = get(length - 1)
    set(value: Char) {
        this.setCharAt(length - 1, value)
    }
  • getter, setter 를 확장하여 선언
  • 자바에서 사용할 땐 getter, setter 명시적으로 호출해야 함.

 

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

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

코틀린도 비슷하다. 다만 타입 뒤에 ... 을 붙이는 대신 vararg 변경자를 붙이면 된다.

이미 배열에 들어있는 원소를 가변 길이 인자로 넘길 때도 자바에서는 배열을 그냥 넘기면 되지만 코틀린에서는 배열을 명시적으로 풀어서 배열의 각 원소가 인자로 전달되게 해야 한다. 기술적으로는 스프레드(spread) 연산자가 그런 작업을 해준다.

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

fun main(args:Array<String>){
    val list = listOf("args: ", *args)
}

 

문자열과 정규식 다루기

코틀린은 혼동이 야기될 수 있는 일부 메서드에 대해 더 명확한 코틀린 확장 함수를 제공함으로써 프로그래머의 실수를 줄여준다.

예를 들어 자바 split 메서드의 구분 문자열은 실제로는 정규표현식이기 때문에 단순히 split(".") 으로 처리 할 수 없다. 왜냐면 마침표(.)는 모든 문자을 나타내는 정규식으로 해석되기 때문이다. 

코틀린에서는 spilt 대신에 다른 여러가지 조합의 파라미터를 받는 split 확장함수를 제공하고, 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입으로 값을 받을 수 있도록 하며 정규식을 명시적으로 표현하여 혼동을 줄일 수 있다. 또 하나 이상의 인자로 구분문자를 처리할 수도 있다.

fun main(args:Array<String>){

    println("12.345-6.A".split("\\\\.|-".toRegex())) // 정규식을 명시적으로 만든다.
    //[12.345, 6.A]
    println("12.345-6.A".split(".","-")) //구분 문자열을 하나 이상 인자로 받는 함수가 있다.
    //[12, 345, 6, A]
}

 

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

코틀린의 확장함수를 통해 손쉽게 디렉토리, 파일명, 확장자를 추출할 수 있다.

fun main(args:Array<String>){
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
    //Dir: /Users/yole/kotlin-book, name: chapter, ext: adoc
}

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")
}
  • path에서 처음부터 마지막 슬래시 직전까지의 부분 문자열은 파일이 들어있는 디렉터리경로다.
  • path에서 마지막 마침표 다음부터 끝까지의 부분 문자열은 파일 확장자다.
  • 코틀린에서는 정규식을 사용하지 않고도 문자열을 쉽게 파싱할 수 있다.
  • 정규식은 강력하기는 하지만 나중에 알아보기 힘든 경우가 많다.

정규식을 만약 써야하면 아래와 같이 3중 따옴표로 처리할 수 있다.

fun parsePath(path: String) {
    val regex = """(.+)/(.+)\\.(.+)""".toRegex()
    val matchResult = regex.matchEntire(path)
    if (matchResult != null) {
        val (directory, filename, extension) = matchResult.destructured
        println("Dir: $directory, name: $filename, ext: $extension")
    }
}
  • 정규표현식을 3중 따옴표로 구현했다.
  • 3중 따옴표 문자열에서는 역슬래시(\)를 포함한 어떤 문자도 이스케이프할 필요가 없음
  • 일반 문자열을 사용하는 경우 마침표 기호를 이스케이프 하려면 \\. 해야하지만, 3중따옴표 안에서는 \. 만 하면 된다.
  • regex 정규식을 만들고, 그 정규식을 인자로 받은 path에 매칭시킨다.
  • 매칭에 성공하면 그룹별로 분해한 결과를 의미하는 destructured 프로퍼티를 각 변수에 대입한다.

3중 따옴표 문자열을 문자열 이스케이프를 피하기 위해서만 사용하지 않는다.

3중 따옴표 문자열에는 줄 바꿈을 표현하는 아무 문자열 표현이 가능하면 중 바꿈이 있는 텍스트를 쉽게 문자열로 만들 수 있다.

    val kotlinLogo = """|  //
                       .| //
                       .|/\"""
    println(kotlinLogo.trimMargin("."))
   //결과
    |  //
    | //
    |/\
  • 들여쓰기의 끝부분을 특별한 문자열로 표시하고, trimMargin 을 사용해 그 문자열과 그 직전의 공백을 제거한다. 여기서는 마침표(.)
  • \ 를 문자열에 넣고 싶으면 3중 따옴표를 이용하면 된다. (ex : """C:\\User\\LImky""")

 

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

코틀린에서는 로컬 함수를 통해 중복된 코드를 제거할 수 있다.

아래 중복된 코드이다.

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
}
  • 검증 로직이 여러번 중복 처리됨을 알 수 있다

검증 로직을 로컬 함수로 빼서 처리해보도록 하자.

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
}
  • 검증 로직을 로컬함수로 뺐고, 해당 로컬함수를 saveUser에서 호출할 수 있다.

하지만, User 객체를 로컬 함수에게 하나하나 전달해야 한다는 점은 아쉽다. 로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있다. 이런 성질을 이용해 불필요한 User 파라미터를 없앨 수 있다. 다시 개선해보자.

fun saveUser(user: User) {
    fun validate(value: String, fieldName: String) { // user 파라미터를 중복 사용하지 않는다. 
        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
}
  • 로컬 함수에서 바깥 함수의 모든 파라미터와 변수를 사용함으로써 코드가 더 깔끔해졌다.

마지막으로 확장 함수로 변환하여 코드를 더 개선해보자

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

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

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

    // Save user to the database
}
  • 함수 앞에 User. 클래스를 선언하여 User 클래스의 객체에서 vaildateBeforeSave() 함수를 확장해서 사용할 수 있도록 하였다.
  • saveUser 함수에서 별도로 vaildateBeforeSave() 선언을 할 필요없이 바로 User 클래스 확장함수를 통해 데이터 값을 검증할 수 있다.
반응형

댓글