코틀린에서 컬렉션 만들기
코틀린은 자체적인 컬렉션 클래스를 제공하진 않으나 자바보다 더 풍부한 API를 지원합니다.
val set = hashSetOf(1, 7, 53) // class java.util.HashSet
val list = arrayListOf(1, 7, 53) // class java.util.ArrayList
val map = hashMapOf(1 to "one", 7 to "seven", 53 to "fifty-three") // class java.util.HashMap
함수를 호출하기 쉽게 만들기
Named Parameter
이름 붙인 파라미터를 사용하면 함수의 파라미터가 많을 때 함수 호출의 가독성을 향상시킬 수 있습니다.
joinToString(collection, separator = " ", prefix = " ", postfix = ".")
이때 파라미터 중 일부에만 이름을 명시할 수도 있지만, 혼동을 막기 위해 어느 하나의 파라미터라도 이름을 명시했다면 나머지 파라미터도 이름을 명시하는 것을 추천합니다.
Default Parameter
파라미터의 디폴트 값을 정의해서 불필요한 메소드 오버로딩을 줄일 수도 있습니다.
fun <T> joinToString(
collection: Collection<T>,
seperator: String = ", ",
prefix: String = "",
postfix: String = ""
): String
디폴트 파라미터를 사용하면 파라미터 중 일부를 생략하고 Named Parameter를 써서 순서와 관계없이 사용할 수 있습니다.
joinToString(list, postfix = ";", prefix = "# ")
최상위 함수
자바에서는 모든 메소드가 클래스 안에 있어야 합니다. 그래서 정적 메소드를 모아두는 역할만을 담당하는, 상태나 인스턴스 메소드는 없는 클래스가 생겨나게 됩니다.
하지만 코틀린에서는 함수를 클래스 밖, 즉 최상위에 위치시킬 수 있습니다. 이를 활용하면 코드 구조를 더 유연하게 만들 수 있다는 장점이 있습니다.
다른 패키지에서 그 함수를 사용할 땐 해당 패키지를 임포트하면 됩니다.
// join.kt
package strings
fun joinToString(...): String { ... }
다음과 같은 파일이 자바로 컴파일되면 아래와 같이 변하게 됩니다
package strings;
public class JoinKt {
public static String joinToString(...) { ... }
}
코틀린 컴파일러가 생성하는 클래스의 이름은 해당 최상위 함수의 파일 이름과 대응됩니다. 따라서 자바에서 해당 코틀린 함수를 호출하려면 JoinKt.joinToString(...)
으로 사용하면 됩니다.
만약 클래스 이름을 바꾸고 싶다면 @JvmName 어노테이션을 추가하면 됩니다.
최상위 프로퍼티
프로퍼티 역시 함수처럼 최상위에 위치시킬 수 있습니다.
최상위 프로퍼티도 다른 프로퍼티와 마찬가지로 접근자 메소드(getter
/setter
)를 통해 자바 코드에 노출됩니다.
메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
확장 함수를 사용하면 외부 라이브러리에 정의된 클래스의 소스 코드를 바꿀 필요 없이 클래스를 확장할 수 있습니다.
또한 어떤 클래스의 멤버 메소드처럼 호출할 수 있지만 사실은 그 클래스 밖에 선언된 함수입니다.
확장 함수를 선언하려면 함수 이름 앞에 그 함수가 확장하려는 클래스 이름을 덧붙이면 됩니다.
fun String.lastChar(): Char = this.get(this.length - 1)
이렇게 선언된 확장 함수를 호출할 때는 클래스의 일반적인 함수를 호출하는 것과 동일하게 호출할 수 있습니다.
확장 함수 내부에서는 일반적인 인스턴스 메소드처럼 수신 객체(확장하려는 클래스)의 메소드나 프로퍼티를 바로 사용할 수 있습니다. 다만 private이나 protected 멤버는 사용할 수 없다.
임포트와 확장 함수
확장 함수를 사용하려면 그 함수를 임포트해야 합니다. 그렇지 않으면 같은 이름의 확장 함수가 둘 이상 있어서 충돌이 생길 수 있습니다.
import strings.lastChar as last
val c = "Kotlin".last()
as 키워드로 임포트한 함수나 클래스를 다른 이름으로 호출할 수도 있습니다.
자바에서 확장 함수 호출
char c = StringUtilKt.lastChar("Java");
확장 함수를 StringUtil.kt 파일에 정의했다면 다음과 같이 호출할 수 있습니다.
확장 함수는 오버라이드할 수 없다
확장 함수는 클래스의 일부가 아니고 클래스 밖에 선언되기에, 다른 일반 메소드를 오버라이드 하듯이 하위 클래스에 완전히 같은 확장 함수를 정의하더라도 사용할 수 없습니다.
Why? 코틀린은 호출되는 확장 함수를 정적으로 결정하기 때문. (정적 바인딩)
어떤 클래스의 멤버 함수 이름, 시그니처와 완전히 같은 확장 함수가 있다면, 멤버 함수가 호출된다. 즉, 멤버 함수의 우선 순위가 더 높다.
확장 프로퍼티
확장 프로퍼티는 확장 함수와 마찬가지로, 직접 수정할 수 없는 클래스에 변수를 추가하고 싶을 때 쓸 수 있습니다.
확장 프로퍼티를 사용하는 방법은 멤버 프로퍼티를 사용하는 방법과 같습니다.
val String.lastChar: Char get() = get(length - 1)
자바에서 확장 프로퍼티를 사용하려면 항상 StringUtilKt.getLastChar(”Java”)
처럼 게터나 세터를 명시적으로 호출해야 합니다.
컬렉션 처리
코틀린은 자체적인 컬렉션을 지원하지 않고 자바 라이브러리 클래스의 인스턴스인 컬렉션을 사용합니다.
그럼에도 코틀린이 컬렉션에 대해 새로운 기능을 추가할 수 있었던 이유는, 그러한 기능을 확장 함수로 구현했기 떄문입니다.
가변 인자 함수
var list = listOf(2, 3, 5, 7, 11)
리스트를 생성할 때에는 다음과 같이 원하는 만큼 많이 원소를 정의할 수 있습니다. 이 listOf
함수의 정의를 보면 다음과 같습니다. fun listOf<T>(vararg values: T) : List<T> { ... }
자바는 가변 길이 인자를 정의할 때 타입 뒤에 ...를 붙이지만, 코틀린은 파라미터 앞에 vararg를 붙입니다.
자바에서는 배열에 이미 들어 있는 요소를 가변 길이 인자로 넘길 때, 배열을 그냥 넘기면 되지만,
코틀린은 배열을 명시적으로 풀어서 각 원소가 인자로 전달되게 해야 하므로 스프레드 연산자를 사용해야 합니다. (최신 버전에서는 배열 앞에 *를 붙이기만 해도 됨)
fun showAll(vararg s: String) {
println(s.joinToString())
}
fun main() {
val names = arrayOf("Navi", "Chan", "Heli", "Hash", "Tassadar")
showAll(*names)
}
하지만 만약 vararg 파라미터가 제네릭 타입일 경우, *을 생략해도 인자로 받아들일 수 있습니다.
값의 쌍 다루기: 중위 호출과 구조 분해 선언
코틀린은 다음과 같이 맵을 정의할 수 있습니다.
val map = mapOf(1 to "one", 7 to "seven", 53 to "fifty-three")
마치 코틀린이 맵에 대해 제공하는 특별한 문법처럼 보이지만, 실제론 일반적인 함수를 더 간결한 구문으로 호출하는 것 뿐입니다.
여기서 to라는 키워드는 코틀린 키워드가 아니라 중위 호출이라는 방식으로 일반 메소드를 호출한 것일 뿐입니다.
중위 호출을 사용하면 인자가 하나 밖에 없는 메소드를 더 깔끔한 구문으로 호출할 수 있게 됩니다.
다음 두 호출은 동일합니다.
1.to("one") // 일반적인 방식으로 to를 호출하여 Pair<Int, String> 정의
1 to "one" // 중위 호출 방식으로 to를 호출하여 Pair<Int, String> 정의
파라미터가 하나뿐인 일반 메소드나 확장 함수에 중위 호출을 사용할 수 있습니다.
만약 중위 호출로 함수를 호출하고 싶다면, infix 변경자를 함수 선언 앞에 추가하면 됩니다.
infix fun Any.to(other: Any) = Pair(this, other)
1.to("one")
1 to "one"
문자열과 정규식 다루기
코틀린 문자열은 자바 문자열과 같습니다. 특별한 변환도 필요 없고 별도 래퍼도 생기지 않습니다.
하지만 코틀린은 확장 함수를 제공함으로써 자바 문자열을 더 손쉽게 다룰 수 있게 해줍니다.
문자열 나누기
자바의 split 메소드는 많은 혼동을 야기합니다.
자바 split()의 파라미터는 정규식으로 되어 있고, 정규식에서 .은 임의의 문자열을 의미하기에, .을 파라미터로 넣으면 split()이 작동하지 않습니다. (그래서 .을 구분자로 쓰려면 “[.]”을 파라미터로 넣어야한다.)
코틀린은 그 대신 다른 확장 함수를 제공합니다. 정규식을 파라미터로 받는 함수는 String이 아닌 Regex 타입의 값을 받기에 혼동을 줄여주고 있습니다.
여러 줄 3중 따옴표 문자열
val kotlinLogo = """| //
.|//
.|/ \"""
fun main(args: Array<String>) {
println(kotlinLogo.trimMargin("."))
}
/*
--- 출력 ---
| //
|//
|/ \
*/
자바 문자열로 표현하려고 보면, 이케이프가 필요한 문자열이 정말 많이 있습니다. 하지만 코틀린은 3중 따옴표 문자열을 사용해 더 깔끔히 표현할 수 있습니다.
3중 따옴표 문자열에는 아무 문자열이나 이스케이프 없이 그대로 들어간다는 특징이 있습니다.
코드 다듬기: 로컬 함수와 확장
코틀린은 함수에서 추출한 코드 블럭을 원 함수 내부에 중첩시킬 수 있습니다. 이 방식으로 흔히 발생하는 코드 중복을 제거할 수 있게 됩니다.
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
}
이런 검증 코드를 로컬 함수로 분리하면 중복을 없애는 동시에 코드 구조를 깔끔하게 유지할 수 있게 됩니다. 필요하면 다른 필드에 대한 검증도 쉽게할 수 있습니다.
하지만 여전히 굳이 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
}
로컬 함수는 자신이 속한 바깥 함수의 모든 파라미터와 변수를 사용할 수 있기에, 불필요한 User 파라미터를 제거할 수 있습니다.
물론 이 검증 로직을 확장 함수로 만들 수도 있습니다.
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 자체에 포함시켜 외부에 공개하고 싶지는 않습니다.
그럴 때 이렇게 검증 로직을 확장 함수로 빼면 User를 간결하게 유지시킬 수 있다는 장점이 지닐 수 있습니다.
또한 확장 함수를 로컬 함수로 정의할 수도 있습니다. 즉, User.validateBeforeSave
를 saveUser 내부에 로컬 함수로 정의할 수도 있다.
☕️ Networking
기술 직군의 기술적인 교류, 커리어 이야기, 직군 무관 네트워킹 모두 환영합니다!
위클리 아카데미 오픈 채팅방(비밀번호: 9323)
kakaotalk: https://open.kakao.com/o/gyvuT5Yd