(kotlin) 004. Working with Functions

Working with Functions

코틀린은 Java처럼 클래스없이 아무것도 할 수 없는 언어가 아니다.

이렇게 클래스없이 함수만 존재하는 케이스를 Standalone funtion(단독 함수) 라고 칭한다.

코틀린은 이 단독 함수마저 재사용이 가능하다.

단독 함수는 클래스에 속하지 않기떄문에 임의로 정적 메서드로 작성할 필요가 없고, Top-Level이나, Package-Level에 작성해서 사용할 수도 있다.

그 외에도 Java는 지원하지 않는 기본 파라미터와 구조 분해 등의 기능을 이용해 함수를 좀 더 다채롭게 사용할 수 있다.

하나씩 살펴보도록 하자.

Creating Functions

코틀린에서 함수를 생성하는 방법을 알아보고, Java와는 어떤 차이가 있는지 살펴보도록 하자.

KISS Functions : Keep It Simple, Stupid

코틀린은 함수를 정의할때 최대한 단순하게 하는 것을 권장한다.

유닛 메서드처럼, 작은 함수들은 최대한 단순하게 그리고 별도의 노이즈나 실수가 없도록 작성해야 한다.

아래 예제 코드를 보자.

1
2
3
4
fun greet(): String {
return "Hello, Kotlin!"
}
println(greet())

예제 코드에서 보듯이 코틀린은 함수를 function을 뜻하는 키워드인 fun을 사용해서 정의한다.

그 다음으로 함수의 이름, 함수명이 나오고 그 이후로 파라미터 목록이 나온다.

물론 위의 예제 코드처럼 불필요한 경우 별도의 파라미터 형태를 작성하지 않아도 된다.

그 다음에 반환할 타입, 마지막으로 구현체인 블록이 나온다.

정리하자면 아래와 같이 볼 수 있다.

1
2
3
fun functionName(List of parameter): returnType {
// Body
}

Java라면 더 줄일 게 없겠지만, KISS 원칙에 의거하여 하나씩 간소화해보도록 하자.

만약 함수가 매우 짧은 Single-expression function(단일 표현 함수)* 라면 함수의 바디를 둘러산 {} 대신 =를 사용할 수가 있다.

좀 더 정확히 말하자면 함수가 단일 식을 반환하는 경우 를 단일 표현 함수로 정의하고 이때 =를 쓸 수 있다는 뜻이다.

예제 코드에서 제거하면 아래와 같다.

1
2
fun greet(): String = "Hello, Kotlin!"
println(greet())

자 여기서 더 줄여나가보자.

Return type and Type inference

1
2
fun greet(): String = "Hello, Kotlin!"
println(greet())

위의 예제 코드는 문법 구조상 String 타입을 반환하는 것이 명시되어 있다.

아래처럼 반환 타입 부분을 생략하면 어떻게 될까?

1
2
fun greet() = "Hello, Kotlin!"
println(greet())

코틀린은 반환 타입이 없더라도, 컴파일 타임에서 타입 추론을 통해 반환 타입을 지정해줄 수 있으므로 정상 동작하게 된다.

1
2
fun greet(): Int = "Hello, Kotlin!"
println(greet())

따라서 위와 같이 작성할 경우, 컴파일 오류가 발생하게 된다.

다만 =로 단일 표현 함수로 작성된 경우에 사용할 수 있으므로, 외부에서 호출되는 API를 작성한 것이거나 복잡성이 있는 경우 반환 타입을 지정해주는 것이 좋다.

Every functions are expressions

이전 포스팅에서 언급했듯 코틀린은 명령문보다는 표현식을 더 좋아하는 언어이다.

따라서, 함수는 명령문보다는 표현식으로 취급되는 것이 좋다.

만약 아무것도 반환하지 않는 함수는 어떻게 표현식으로 쓸 수 있을까?

Java의 void에 대응하는 이 타입을 코틀린은 Unit 타입으로 처리한다.

반환할 게 없는 경우 Unit으을 사용하는 방식이다.

아래 예제 코드를 보자.

1
2
fun greet() = println("Hello, Kotlin!")
val result: String = greet()

println()은 Unit을 반환하기때문에 String 타입과 맞지않아 위의 코드를 컴파일 오류가 발생하게 된다.

아래와 같이 코드를 고쳐 보자.

1
2
3
fun greet(): Unit = println("Hello, Kotlin!")
val result: Unit = greet()
println("result is $result")

이렇게 Unit 타입을 이용해 반환값이 없더라도, 표현식으로 사용할 수 있다.

Define parameters

기존 구현된 함수의 로직을 변경하게 되면 파라미터의 변경도 발생할 여지가 있다.

이러한 경우를 체크하고 대응하기 위해 코틀린은 파라미터도 타입을 명시해줄 수 있다.

아래 예제 코드를 보자.

1
2
fun greet(name: String): String = "Hello, $name!"
println(greet("Kotlin"))

위의 코드에서 greet() 함수는 name: String을 통해 String 타입의 name을 파라미터로 요구함을 알 수 있다.

또 하나 코틀린의 특징으로는 파라미터를 전부 immutable로 간주하기때문에 값을 변경하려는 시도는 컴파일 오류를 유발한다.

참고 Effective Java 3/E 기준 Item 17에 관련 내용이 나온다. 해당 Item의 제목은 Minimize mutability

Functions with block body

함수가 복잡하여 단일 표현 함수로 쓰기 힘든 경우, 함수의 선언부와 구현체를 분리하기 위해 {}을 사용한다.

{}를 사용하여 선언부와 구현체를 분리하는 경우, 명시적으로 반환 타입을 지정해주어야 하며 지정하지 않을시 Unit으로 간주된다.

주어진 배열에서 가장 작은 수를 찾는 함수를 작성해보자.

1
2
3
4
5
6
7
8
9
fun min(numbers: IntArray): Int {
var result = Int.MAX_VALUE
numbers.forEach {
result = if (it < result) it else result
}
return result
}

println(min(intArrayOf(1, 10, 2, 9, 3, 8, 4, 7, 5, 6)))

min 함수를 파라미터로 IntArray 타입의 numbers를 받아 최솟값을 찾은 뒤 반환한다.

{}를 사용해서 구현체를 작성한 뒤 return 처리를 하지 않으면 코틀린은 이를 람다표현식이나 익명 함수로 간주한다.

이에 대해선 좀 더 나중에 알아보도록 하자.

참고 반대로 {}를 사용하지 않고 =를 사용하는 경우 return 처리를 하면 컴파일 오류가 발생하게 된다.

Default and Named Arguments

Java의 경우 기본 인자를 처리할 수 없지만, 코틀린은 기본 인자를 사용할 수 있다.

이 기본 인자에 대해서 알아보자.

Evolving Functions with Default Arguments

위의 예제 중 하나를 가져와보자.

1
2
fun greet(name: String): String = "Hello, $name!"
println(greet("Kotlin"))

위의 코드에서 Hello라는 문자열은 변경이 불가능하게 리터럴로 작성되어 있다.

함수의 구현체를 변경하기 위해 오버로딩을 사용하는 방법도 있지만, 코드의 중복이 발생하게 된다.

먼저 유연성을 부여해보자.

1
2
fun greet(message: String, name: String): String = "$message, $name!"
println(greet("Hello", "Kotlin"))

의도하고자 하는 바는 처리되었지만, 기존 Hello를 출력하는 경우에도 파라미터를 추가해야하는 경우가 생겼다.

기본 인자를 이용해 기존 기능의 보전하면서도 유연성을 부여하려면 아래와 같이 처리하면 된다.

1
2
3
fun greet(name: String, message: String = "Hello"): String = "$message, $name!"
println(greet("Kotlin", "Hello"))
println(greet("Kotlin"))

namemessage의 위치를 바꾼 것을 알 수 있는데, 이는 파라미터를 참조하여 기본값을 생성할때 name에 파라미터를 주입하기 위해서다.

따라서, 기본 인자를 적용한 파라미터의 경우 위치를 가장 마지막에 두어야 한다.

Improve Readability with Named Arguments

코드를 작성할때, 로직 이상으로 중요한 것이 가독성일 것이다.

아래 예시를 보자.

1
createDevice("iPhone", 13, 4, 128)

전달인자만 보면 어떤 값이 어떻게 쓰여서 Person을 만들어내는지 알 수 없다.

이를 파악하려면 해당 함수의 선언부를 봐야하는 불편함이 있다.

1
2
3
fun createDevice(name: String, version: Int = 1, ram: Int, storage: Int) {
println("$name $version $ram $storage")
}

선언부를 보면 각각의 파라미터가 이름, 나이, 키, 몸무게로 쓰인다는 것을 알 수 있다.

굳이 선언부까지 들어가지않아도, 가독성을 높이기 위해 코틀린은 Named Arguments(명시적 인자) 를 제공한다.

명시적 인자를 사용하면 아래와 같이 메서드를 호출할 수 있다.

1
createPerson(name = "iPhone", version = 13, ram = 4, storage = 128)

참고 명시적 인자를 사용하면, 파라미터의 순서를 꼭 맞춰서 호출할 필요가 없다.

vararg and Spread

이번엔 varag(다중 인자)Spread(스프레드) 연산자에 대해서 알아보자.

다중인자란 함수가 한 번에 여러 개의 인자를 받을때 타입 안정성을 제공해주는 기능이며, 스프레드 연산자는 콜렉션의 값을 개별 요소로 분해하거나 나열할 때 유용하다.

Variable Number of Arguments

최솟값을 찾는 예제를 재활용해보자.

1
2
3
4
5
6
7
8
9
fun min(numbers: IntArray): Int {
var result = Int.MAX_VALUE
numbers.forEach {
result = if (it < result) it else result
}
return result
}

println(min(intArrayOf(1, 10, 2, 9, 3, 8, 4, 7, 5, 6)))

min() 함수는 IntArray 타입을 인자로 요구하기때문에 배열을 만들어서 넘겨주어야 한다.

아래와 같이 다중인자 기능을 사용하면 좀 더 유연하게 만들 수 있다.

1
2
3
4
5
6
7
8
9
fun min(vararg numbers: Int): Int {
var result = Int.MAX_VALUE
numbers.forEach {
result = if (it < result) it else result
}
return result
}

println(min(1, 10, 2, 9, 3, 8, 4, 7, 5, 6))

단, vararg 키워드는 하나의 파라미터에만 적용할 수 있다.

아래와 같이 처리하면 여러 파라미터를 가진 함수에도 적용할 수 있다.

1
2
3
4
fun greet(message: String, vararg names: String) {
println("$message, ${names.joinToString("! ")}")
}
println(greet("Hello", "Kotlin", "Java", "CPP"))

위와 같이 작성하고 실행하면 아래의 출력 결과가 나온다.

1
Hello, Kotlin! Java! CPP!

vararg가 적용된 파라미터를 꼭 제일 뒤에 둘 필요는 없지만, 제일 뒤에 두지 않으면 무조건 명시적 인자를 통해서 호출되도록 강제된다.

아래 두 가지 항목은 vararg에 대한 권장사항이다.

  1. vararg는 제일 뒤에 두어서, 함수 호출시 무조건적인 명시적 인자를 사용을 강제하지말자.
  2. 마지막 파라미터가 람다표현식일 경우, 람다표현식 바로 전에 위치시킨다.

2번 항목에 대해서는 람다를 다룰 때 좀 더 상세히 다루도록 하겠다.

Spread Operator

파라미터 타입이 다중 인자로 작성되어있는 경우, 여러 개의 값을 넘길 수는 있지만

배열이나 리스트를 넘길 수는 없다.

이럴때 스프레드 연산자를 통해 처리할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
fun min(vararg numbers: Int): Int {
var result = Int.MAX_VALUE
numbers.forEach {
result = if (it < result) it else result
}
return result
}

println(min(1, 10, 2, 9, 3, 8, 4, 7, 5, 6)) // OK

val numbers = intArrayOf(1, 10, 2, 9, 3, 8, 4, 7, 5, 6)
println(min(numbers)) // Error
println(min(*numbers)) // Error

위와 같이 인자 앞에 *를 붙이는 것만으로 배열의 값들을 추출해서 vararg 파라미터로 전달할 수 있다.

Destructuring

객체지향에서 structuring(구조화) 란 다른 값을 가진 변수들로 객체를 생성하는 것을 말한다.

참고 구조화는 structuring 혹은 construction로 표현한다.

destructuring(구조분해) 는 반대로, 이미 존재하는 객체로부터 값을 추출해 변수로 넣는 작업을 뜻한다.

구조분해는 반복되는 코드와 로직상의 노이즈를 제거하는 데 용이하게 쓰인다.

Javascript에도 구조분해 개념은 존재하지만 Javascript의 경우 프로퍼티의 이름을 기준으로 하며, 코틀린의 경우 프로퍼티의 위치를 기준으로 한다.

예제를 통해 구조분해에 대해서 이해해보자.

코틀린은 2-tuple을 구현하는 Pair, 3-tuple을 구현하는 Triple 객체가 있다.

아래 예제는 Triple을 사용한 코드이다.

1
2
3
4
5
6
7
8
9
fun getTeamRocket() = Triple("Rosa", "Roy", "Na-ong")

val result = getTeamRocket()
val first = result.first
val second = result.second
val third = result.third

println("The Team Rocket consists of $first, $second and $third")
// The Team Rocket consists of Rosa, Roy and Na-ong

getTeamRocket()을 호출하여 얻은 Triple을 출력하는 코드임을 알 수 있다.

이 코드를 구조분해를 사용해서 구현하면 아래와 같다.

1
2
val (first, second, third) = getTeamRocket()
println("The Team Rocket consists of $first, $second and $third")

이런 방식이 가능한 이유는 Triple 객체가 구조분해를 대응하기 위한 특별한 함수를 가지고 있기 때문이다.

이를 componentN 함수라하는데, 나중에 자세히 다루도록 하자.

구조분해를 사용하면, 사용하지 않는 변수를 언더스코어 기호인 _ 를 사용하여 스킵처리할 수 있다.

1
2
3
val (_, _, third) = getTeamRocket()
println("The Team Rocket consists of $third")
// The Team Rocket consists of Na-ong