008. 좀 더 호출하기 쉬운 함수 작성

좀 더 호출하기 쉬운 함수 작성

먼저 만들어진 Collections 객체를 출력해보도록 하자.

Java의 Collections를 사용하기 때문에 기본적인 toString()을 사용할 순 있지만, 출력 형식이 고정되어 있다.

참고 AbstractCollection#toString
Returns a string representation of this collection.
The string representation consists of a list of the collection’s elements in the order they are returned by its iterator, enclosed in square brackets (“[]”).
Adjacent elements are separated by the characters “, “ (comma and space). Elements are converted to strings as by String.valueOf(Object).

1
2
3
>>> val list = listOf(1, 2, 3)
>>> println(list) Invokes toString()
[1, 2, 3]

위의 출력 결과를 [1, 2, 3]에서 (1; 2; 3)으로 교체한다고 가정해보자.

Java에서 위의 문제를 해결하기 위핸 별도의 라이브러리를 사용하거나 기존 Collections의 로직을 새로 작성해야 한다.

코틀린은 이러한 작업을 표준 라이브러리에서 처리할 수 있도록 기본 제공해주고 있다.

이번 포스팅에서 jsonToString 이라는 함수를 구현해나가면서 위의 문제를 해결해보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()
}

위의 코드에 작성된 jsonToString함수는 파라미터로 넘겨받은 Collection의 요소들을 뽑아내어, StringBuilder사이에 구분 기호 및 접두사와 접미사를 추가하여 원하는 출력 포맷을 만들 수 있도록 처리해준다.

이 코드를 이용해 아래와 같이 실행한다면 문제를 해결할 수 있을 것이다.

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

위의 로직은 정상적으로 동작하지만, 호출할 때마다 4개의 파라미터를 넘겨줘야한다.

이를 좀 더 단순하게 만들어보도록 하자.

이름이 지정된 가변인자

첫 번재 문제는 각 파라미터가 어떤 값을 받을 것인지 알 수 없기때문에 가독성이 떨어진다는 것이다.

1
joinToString(collection, " ", " ", ".")

2~4번째 파라미터의 경우 어떤 문자열이든 넘길 수 있을까?

만약 넘길 수 있다면 안정성은 어떻게 보장받을 수 있을까?

호출하고자하는 함수의 명세를 살펴보지않으면 파악하기 어려울 수 있다. 그냥 IntelliJ 쓰면 된다

이를 해결하기 위해 호출 예제에서 아래와 같은 주석을 추가하면 어떨까?

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

얼핏 문제는 해결된 것처럼 보일 수 있겠지만, 코틀린은 좀 더 우아하게 처리해줄 수 있다.

아래 코드를 보자.

1
joinToString(collection, separator = " ", prefix = " ", postfix = ".")

코틀린으로 작성된 함수를 호출할 때 파라미터에 이름을 아예 지정해서 넘길 수 있다.

참고 코틀린으로 작성되어야만 쓸 수 있다. Java에서 작성된 메소드에는 적용할 수 없다.

다만, 헷갈릴 여지를 남기지 않기 위해서 하나의 파라미터에 이름을 지정하면 모든 파라미터에 이름을 지정해야 한다.

이를 Named arguments라고 한다.

파라미터 기본값

Java가 안고 있는 문제점 중 하나는 일부 클래스에서 많은 메소드를 오버로딩해서 쓰고 있다는 점이다.

제일 대표적인 예시로 Thread 클래스를 들 수 있다.

이 클래스는 생성자가 무려 8개 존재한다.

참고 Thread Documents(Java SE 8)

이러한 구조는 API를 사용하려는 개발자에게 편의를 제공하기 위한 조치이긴 하지만, 파라미터명과 타입이 반복되며, 동일한 명세 또한 반복해야 한다.

심지어 결과물이 같은데도 말이다. (여기서는 Thread 객체가 될 것이다)

그와 동시에, 일부 파라미터를 생략하는 경우 어떤 결과가 벌어지는 지 명확하지 않다는 문제도 있다.

위와 같은 문제점을 극복할 수 있는 방법으로 코틀린은 함수 선언시 파라미터의 기본값을 지정할 수 있도록 제공한다.

다시 jsonToString 함수를 가져와서 개선해보도록 하자.

여기서 추가할 조건은 대부분의 Collections 요소들이 접두사와 접미사 없이 쉼표로 구분되도록 하는 것이다.

1
2
3
4
5
6
fun <T> joinToString(
collection: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String

위와 같이 파라미터의 기본값을 지정하고나면 아래와 같이 사용할 수 있다.

1
2
3
4
5
6
>>> joinToString(list, ", ", "", "")
1, 2, 3
>>> joinToString(list)
1, 2, 3
>>> joinToString(list, "; ")
1; 2; 3

기존과 같이 모든 파라미터를 넘겨 호출할 수도 있고, 생략을 하더라도 동일한 출력값이 나오는 것을 확인할 수 있다.

이때 파라미터의 순서는 함수의 선언과 동일해야 하며, 뒤에서부터만 생략이 가능하다.

다만 위에서 살펴본 Named arguments를 활용하는 경우 원하는 파라미터만 원하는 순서대로 지정할 수 있다.

1
2
>>> joinToString(list, suffix = ";", prefix = "# ")
# 1, 2, 3;

최상위 함수

Java는 객체지향적인 언어로, 모든 코드는 클래스의 메소드 형태로 작성된다.

하지만 그런 지향점과는 다르게, 거의 모든 대규모 프로젝트는 단일 클래스 하나로 동작되지 않는다.

때때로 하나의 작업을 하기 위해 두 개의 클래스가 같이 동작하는 경우도 있고, 기본 객체에 특정 작업이 인스턴스화되어 추가되는 경우도 있다.

코틀린에서는 모든 클래스의 외부에 존재하는 최상위 수준에 직접 함수를 작성할 수 있다.

따라서 아무 클래스에서나 패키지만 추가하면 접근하여 호출할 수 있다.

이번엔 jsonToString함수를 최상위에 배치해보도록 하자.

package명은 strings로 문자열 관련 패키지에 직접 산입한 후 join.kt라는 파일로 저장한다.

1
2
package strings
fun joinToString(...): String { ... }

만약 Java로 짠다면 아래와 같은 코드가 작성될 것이다.

1
2
3
4
5
package strings;

public class JoinKt {
public static String joinToString(...) { ... }
}

코틀린 컴파일러가 join.kt를 컴파일하면 위의 클래스명과 동일한 결과물이 나오는 것을 확인할 수 있다.

만약 Java에서 해당 파일을 참조한다면 아래와 같이 사용할 수 있다.

1
2
3
import strings.JoinKt;
...
JoinKt.joinToString(list, ", ", "", "");

최상위 프로퍼티

함수들처럼 프로퍼티 또한 최상위에 배치할 수 있다. 클래스 외부에 데이터를 개별적으로 젖아하는 것은 가끔씩 유용할 때가 있다.

예를 들어 var 속성에 특정 작업이 몇 번 수행되었는 지를 기록하는 경우 아래와 같이 작성할 수 있다.

1
2
3
4
5
6
7
8
9
var opCount = 0

fun performOperation() {
opCount++
// ...
}
fun reportOperationCount() {
println("Operation performed $opCount times")
}

opCount의 값은 정적 필드에 저장될 것이며, 이 특성을 사용해 공통적으로 사용할 만한 상수를 정의할 수 있다.

1
val UNIX_LINE_SEPARATOR = "\n"

기본적으로 최상위에 배치며 프로퍼티들은 다른 일반적인 프로퍼티들과 마찬가지로 Get/Set 등을 통해 Java에서 접근할 수 있다.

1
const val UNIX_LINE_SEPARATOR = "\n"

Java 코드에는 아래와 같이 작성한 것과 동일한 값이 제공된다.

1
public static final String  UNIX_LINE_SEPARATOR = "\n";