Kotlin

Kotlin - 함수 정의와 호출

엔꾸꾸 2020. 10. 25. 22:00

코틀린에서 컬렉션 만들기

코틀린에서는 다양한 컬렉션을 지원한다.

코틀린 고유의 컬렉션이 아닌 자바 표준 컬렉션을 사용한다.

 

다음은 코틀린에서 컬렉션을 사용하는 샘플 중 하나이다.

/**
 * setOf 외에도 아래와 같은 방법으로 다양한 컬렉션을 생성할 수 있다.
 * map을 생성할때는 to 를 사용하는데 이는 키워드가 아닌 일반 함수이다.
 * 코틀린은 코틀린의 컬렉션을 사용하지않고 표준 자바 컬렉션을 사용한다.
 * 이는 자바와 상호작업하게 훨씬 쉽게 때문이다.
 */
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")

/**
 * 컬렉션은 다양한 함수를 제공한다.
 * last() 함수는 가장 마지막 원소를 가져온다.
 * max() 는 최대 값을 가져온다.
 */
fun main(args: Array<String>) {
    val strings = listOf("first", "second", "fourteenth")
    strings.last()

    val numbers = setOf(1, 14, 2)
    numbers.max()
}

 

 

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

자바의 컬렉션에는 toString() 에 대한 기본 구현이 되어 있다.

이 출력 형식을 커스터마이징 하고 싶다면, Guava, Apache Commons 와 같은 서드파티 라이브러리를 추가해서 사용해야 한다.

하지만 코틀린에서는 이런 작업을 처리할 수 있는 표준 라이브러리 함수가 존재한다.

 

아래 예제는 표준 라이브러리를 사용하지 않고, toString 에 대한 커스터마이징 함수를 코틀린에서 구현한 코드이다.

 

import java.lang.StringBuilder

/**
 * 컬렉션의 toString() 을 커스터마이징 할 수 있는 함수
 * 하지만 가독성 부분에서 좋지가 않다.
 */
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()
}

 

이름이 있는 파라미터

위 함수를 사용할 경우 IDE의 도움을 받지만, 가독성이 좋지않다.

각 인자가 어떤 것을 의미하는지 알기 쉽지 않다. 만약 개발자의 실수로 같은 타입인 파라미터의 순서가 바뀐다거나 하는 일이 생기면 의도치않은 동작을 하게된다. 

이러한 이유때문에 코틀린에서는 함수에 전달하는 인자에 이름을 명시할 수 있는 기능을 제공한다.

사용시 유의점은 이름이 있는 파라미터를 명시했다면, 해당 파라미터 뒤에 오는 인자들은 모두 이름을 명시해서 사용해야 한다는 점이다.

fun main(args: Array<String>) {
    val list = listOf(1, 2, 3)
    // IDE 의 도움을 받을 수 있지만, 가독성 이 좋지않다.
    joinToString(list, "; ", "(", ")")

    // 코틀린에서는 위 문제를 다음과 같이 해결한다.
    joinToString(list, separator = " ", prefix = " ", postfix = ".")
}

 

기본 값이 존재하는 파라미터

어떤 함수의 파라미터의 값이 존재하지 않을경우, 기본 값이 필요한 경우, 자바에서는 if 조건절을 사용해서 일일히 설정해주거나, 혹은 메소드 오버로딩을 사용하여  이를 해결한다.

하지만 이는 파라미터의 기본값이 필요한 케이스가 많아질수록 오버로딩한 메소드가 많아지는 문제가 생긴다.

이러한 문제를 코틀린에서는 디폴트 파라미터 값 지정을 통해 해결한다.

함수의 디폴트 파라미터 값은 호출하는 쪽이 아닌, 선언하는 쪽에서 지정된다는 점에 유의해야 한다.

@JvmOverloads
fun <T> joinToStringWithDefaultParameter(
        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 main2() {
    val list = listOf(1, 2, 3)
    joinToStringWithDefaultParameter(list, "", "", "")

    joinToStringWithDefaultParameter(list, separator = ":")
}
@JvmOverloads 애노테이션을 사용하면, 코틀린에서 정의한 함수를 메소드 오버로딩 형태로 코드를 생성해 준다.
이로 인해 자바에서 코틀린 함수를 편하게 호출할 수 있다.

 

 

최상위 함수와 프로퍼티

자바에서는 Util 성 메소드들을 작성하기 위해 ~~Utils 와 같은 네이밍을 가지는 클래스들을 생성하고 그 클래스들 내부에 모아둔다.

코틀린에서는 최상위 레벨에 함수를 정의할 수 있기 때문에 그런 무의미한 클래스가 필요 없다.

JVM은 클래스 내에 존재하는 코드만 실행할 수 있기 때문에 코틀린 컴파일러가 컴파일시, 최상위 레벨의 함수들은 임의의 클래스를 생성해 준다는 것을 유의해야 한다.

만약 최상위 함수가 포함되는 클래스 명을 바꾸고 싶다면, @JvmName 애노테이션을 사용해서 지정할 수 있다.

프로퍼티도 함수와 동일하게 최상위 수준에 놓을 수 있으며, const 키워드를 사용하면, 자바의 public static final ~ 과 같은 상수를 만들 수 있다.

const val HELLO = "HELLO"
public static final String HELLO = "HELLO";

 

 

확장 함수와 확장 프로퍼티

확장 함수는 어떤 클래스의 메소드 처럼 호출이 가능하지만, 해당 클래스 밖에 선언된 함수이다.

확장 함수 선언시 추가하려는 함수 앞에 그 함수가 확장할 클래스명을 정의해야 한다.

클래스 명을 수신 객체 타입 (receiver type) 이라고 하며, 확장 함수가 호출되는 대상이 되는 값을 수신 객체 (receiver object) 라고 한다.

확장 함수가 캡슐화를 깨지는 않으며, private, protected 멤버를 사용할 수 는 없다.

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

fun main(args: Array<String>) {
    println("Hello".lastChar())
}
위 예제 코드는 String 이 수신객체 타입이고, "Hello" 가 수신 객체가 된다.

 

임포트와 확장 함수

확장 함수를 정의해도, 해당 함수를 사용하기 위해 임포트를 해서 사용해야 한다.

하지만 이름이 충돌하는 경우 이를 회피하는 방법이 필요하다.

as 키워드를 사용하여, 임포트한 클래스나 함수를 다른 이름으로 부를수 있는데(alias), 이를 활용해서 중복을 피해야 한다.

import lastChar as last

fun main(args: Array<String>) {
    "Hello".last()
}

 

자바에서의 확장 함수 호출

내부적으로 확장 함수는 수신객체를 첫 번째 인자로 받는 정적 메소드이다.

확장 함수를 호출하더라도 다른 어뎁터 객체를 사용하거나, 실행 시점에 부가 비용이 들지 않는다.

 

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

확장 함수는 클래스의 일부가 아닌, 클래스 외부에 선언된다.

Util 클래스를 편하게 호출하기 위한 syntax sugar 일 뿐이다.

객체지향 메소드 오버라이드를 생각해 본다면 쉽게 이해할 수 있다.

 

확장 함수 사용시 주의점

확장 함수와 클래스의 멤버 함수의 시그니쳐가 동일하다면, 확장 함수가 아닌 멤버 함수가 우선 순위를 더 높게 가지게 된다는 것을 주의해야 한다.

 

확장 프로퍼티

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

하지만 상태를 저장할 수는 없다는 점을 유의 해야한다.

import java.lang.StringBuilder

/**
 * 확장 프로퍼티는 상태를 가질 수 없다.
 * 
 */
val String.lastChar: Char
    get() = get(length - 1)

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

 

 

코틀린에서 컬렉션 처리하기

컬렉션 처리시 사용 가능한 코틀린 표준 라이브러리 함수 중 몇가지를 살펴본다.

 

코틀린 언어 특성 중 3가지

1. vararg 키워드를 사용하면 호출시 인자 개수가 달라질 수 있는 함수를 정의할 수 있다.

2. 중위 (infix) 함수 호출 구문을 사용하면 인자가 하나 뿐인 메소드를 편리하게 호출할 수 있다.

3. 구조 분해 선언을 사용하면 복합적인 값을 분해해서 여러 변수에 나눠 담을 수 있다.

 

자바 컬렉션 API 확장

코틀린 컬렉션은 자바와 동일한 클래스를 사용하지만 더 확장된 기능을 제공한다.

last(), max() 와 같은 함수들은 모두 확장 함수 이다.

코틀린 표준 라이브러리는 수 많은 확장 함수들을 제공한다.

 

가변 인자 함수

리스트를 생성하는 함수 호출시 원하는 만큼 원소를 전달할 수 있다.

listOf() 함수가 정의된 것을 살펴보면 vararg 키워드를 사용했다.

val list = listOf(2, 3, 5, 7, 11)
fun listOf<T> (vararg values: T) List<T> {...}

자바와 다른 점은, 배열을 가변길이 인자로 넘길 때 자바는 그대로 넘기면 되지만, 코틀린에서는 명시적으로 풀어서 넘겨 주어야한다.

 

/**
 * 자바에서는 배열을 가변길이 인자로 넘길때 배열을 그대로 넘기면 되지만, 코틀린에서는 스프레드 연산자를 사용해야한다.
 * 단지 배열 앞에 *를 붙이기만 하면 된다.
 */
fun main(args: Array<String>) {
    val list = listOf("args", *args)
    println(list)
}

 

중위 호출과 구조 분해 선언

코틀린에서 맵 컬렉션을 생성할 때 mapOf 함수를 사용한다.

아래의 예제 코드를 한번 살펴보자.

fun main() {
    val map = mapOf(1 to "Hello", 2 to "Kotlin")
    println(map)
}

 

위 코드에서 to 는 중위 호출 (infix call) 이라는 방식으로 일반 메소드를 호출한 것이다.

중위 호출 시 수신 객체와 유일한 메소드 인자 사이에 메소드 명을 넣는다.

함수를 중위 호출이 가능하게 하려면 infix 변경자를 함수 선언 앞에 추가하면 된다.

 

/**
 * 맵을 생성할 때 to 는 중위 호출을 한 예이다.
* 중위 호출을 가능하게 하려면, 함수 선언 앞에 infix 변경자를 선언해야 한다.
*/
infix fun Any.to(other: Any) = Pair(this, other)

fun main(args: Array<String>) {
    // number, name 을 1 to "one" 의 결과로 초기화 한다. 이런 방식을 구조분해선언 이라고 한다.
    val (number, name) = 1 to "one"
}

 

 

문자열과 정규식 다루기

코틀린과 자바의 문자열은 동일하다.

코틀린은 다양한 확장 함수를 제공함으로 써 표준 자바 문자열을 편하게 다를 수 있게 한다.

 

문자열 나누기

자바의 split 메소드로는 점(.) 을 사용해 문자열을 분리할 수 없다.

split 메소드의 구분 문자는 실제로는 정규식 이기 때문에 점(.) 은 모든 문자를 나타내는 정규식으로 취급 된다.

코틀린에서는 자바의 split 대신 여러 조합 파라미터를 받는 split 확장 함수를 제공해서 혼동을 없앤다.

만약 정규식을 사용하고 싶다면, Regex 타입 값을 받는 함수를 사용하면 된다.

/**
 * 코틀린에서는 자바의 split 대신 여러 조합 파라메터를 받는 split 함수를 제공함으로써 혼동을 없앤다.
 */
fun main(args: Array<String>) {
    // 정규식을 전달하는것을 명시적으로 표시
    println("12.345-6.A".split("\\.|-".toRegex()))

    // 여러 구분 문자열을 지정
    println("12.345-6.A".split(".", "-"))
}

 

 

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

코틀린에서는 삼중 따옴표 (""") 를 제공한다.

삼중 따옴표를 사용하면, 역슬래시 를 사용해여 문자열 escape 를 할 필요가 없다.

다음 예제는 파일 전체 경로를 디렉터리 / 파일명 / 확장자로 구분하는 함수이다.

/**
 * 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(args: Array<String>) {
    parsePath("/Users/yole/kotlin-book/chapter.adoc")
}
/**
 * 정규식을 사용한 경로 파싱
 */
fun parsePathUseRegEx(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")
    }
}

fun main(args: Array<String>) {
    parsePathUseRegEx("/Users/yole/kotlin-book/chapter.adoc")
}

 

여러 줄 3중 따옴표 문자열

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

코틀린에서 줄바꿈을 표현하는 문자열에서 많이 사용한다.

/**
 * 3중 따옴표 문자열을 이용한 ascii art
 */
fun main(args: Array<String>) {
    val kotlinLogo = """|  //
                       .| //
                       .|/ \""".trimMargin(".")
    println(kotlinLogo)
}

 

 

로컬 함수와 확장

좋은 코드를 작성하는 원칙중 DRY (Don't Repat Yourself) 원칙이 있다.

자바 코드 작성시 DRY 원칙을 지키기는 쉽지 않다.

코틀린에서는 함수에서 추출한 함수를 원 함수 내부에 중첩시킬 수 있다.

 

아래 예제 코드는 DB에 Persist 하기 이전, 간단한 벨리데이션을 수행하는 코드이다.

import java.lang.IllegalArgumentException

class User(val id: Int, val name: String, val address: String)

/**
 * 필드 검증 로직이 중복된다.
 */
fun saveUser(user: User) {
    if (user.name.isEmpty()) {
        throw IllegalArgumentException()
    }

    if (user.address.isEmpty()) {
        throw IllegalArgumentException()
    }

    // DB SAVE
}

 

위 코드는 벨리데이션을 수행하는 필드가 empty 하다면, 예외를 반복시키는 코드가 중복된다.

이를 로컬 함수를 이용해서 중복을 제거해보자.

로컬 함수는 자신이 속한 외부 함수의 모든 파라미터와 변수에 접근이 가능하다.

fun saveUserWithLocalFunction(user: User) {
    fun validate(value: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("${user.id}...")
        }
    }
    validate(user.name)
    validate(user.address)

    // DB SAVE
}

 

위 로직을 확장 함수를 사용하여 리팩토링 하면 다음과 같다.

/**
 * 필드 검증 로직을 확장 함수로 User 에 등록한다.
 */
fun User.validateBeforeSave() {
    fun validate(value: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("$id...")
        }
    }
    validate(name)
    validate(address)
}

fun saveUserUseExtendFunction(user: User) {
    user.validateBeforeSave()
    // DB SAVE
}

 

정리

- 코틀린은 자바 표준 컬렉션을 사용하되 확장 함수를 이용하여 더 많은 기능을 제공한다.

- 함수 파라미터의 디폴트 값을 정의하면 오버로딩한 함수를 정의할 필요가 줄어든다.

- 이름이 있는 파라미터를 사용하여 가독성을 향상시킬 수 있다.

- 최상위 함수와 프로퍼티를 직접 선언하여 이를 활용하면 코드 구조를 유연하게 만들 수 있다.

- 확장 함수와 프로퍼티를 사용하여 모든 클래스의 API 를 확장할 수 있다.

- 확장 함수를 호출하더라도 추가비용이 들지 않는다.

- 중위 호출을 통해 인자가 1개 뿐인 메소드나 확장함수를 깔끔한 구문으로 호출할 수 있다.

- 정규식과 문자열 처리시 다양한 문자열 처리 함수를 제공한다.

- 이스케이프가 필요한 문자열은 3중 따옴표기능을 활용하자.

- 로컬 함수를 사용해서 코드를 깔끔하게, 중복을 제거할 수 있다.