Kotlin in Action 2/e 서적을 읽으면서 정리하고자 쓴 글입니다.

코틀린에서 컬렉션 만들기
코틀린은 다음과 같이 컬렉션을 생성하는 함수를 제공합니다.
또한 코틀린은 표준 자바 컬렉션 클래스를 사용하기 때문에 자바 코드와 상호작용하기가 훨씬 더 쉽습니다.
즉, 자바 → 코틀린 또는 코틀린 → 자바 함수를 호출할 때 컬렉션을 서로 변환할 필요가 없습니다.
fun main() {
val set = setOf(1, 7, 10)
val list = listOf(1, 10, 100)
val map = mapOf(1 to "one", 7 to "seven", 10 to "ten") // ex) key=1, value="one"
println(set.javaClass) // class java.util.LinkedHashSet
println(list.javaClass) // class java.util.Arrays$ArrayList
println(map.javaClass) // class java.util.LinkedHashMap
}
1) 이름 붙인 인자
아래 함수를 보면 인자로 전달하는 값이 어떤 역할을 하는지 구분하기 어렵습니다.
joinToString(collection, " ", " ", =".")
코틀린은 이를 해결하기 위해 인자의 이름을 명시할 수 있는 기능을 제공합니다.
심지어 인자 순서를 변경할 수도 있습니다.
joinToString(
prefix = " ",
postfic = ".",
separator = " ",
collection = collection
)
2) 디폴트 파라미터 값
자바의 경우 여러 파라미터를 받기 위해서는 `overloaing`한 메서드가 많아진다는 문제를 가지고 있습니다.
그러나 코틀린은 함수 선언 시 파라미터의 디폴트 값을 지정할 수 있습니다.
fun <T> joinToString(
collection: Collection<T>,
separator: String = ",",
prefix: String = "",
postfix: String = ""
): String { ... }
fun main() {
val list = listOf("A", "B", "C")
joinToString(list) // 이외 다른 파라미터를 받지 않아도 디폴트 파라미터 사용
}
3) 정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티
코틀린은 특정 클래스에 속하지 않는 공통 로직을 처리하기 위해 자바처럼 무의미한 클래스를 만들 필요가 없습니다.
최상위 수준에 함수와 프로퍼티를 직접 선언합니다.
| 구분 | 설명 | 비고 |
| 최상위 함수 | 클래스 밖에 최상위에 선언된 함수 | 자바의 `static` 메서드로 컴파일 |
| 최상위 프로퍼티 | 클래스 밖 최상위에 선언된 변수 | 자바의 `static` 필드로 컴파일 |
| `const` 상수 | `const val`로 선언하며, 컴파일 타임 상수 | 원시 타입과 `String`만 사용 가능 |
파일의 최상위 수준에서 선언한다면 다음과 같습니다.
package com.example.utils
// 최상위 프로퍼티: 상수 선언
const val UNIX_LINE_SEPARATOR = "\n"
// 최상위 프로퍼티: 일반 변수 (게터가 생성됨)
var opCount = 0
// 최상위 함수: 유틸리티 함수
fun joinToString(items: List<String>): String {
opCount++
return items.joinToString(separator = UNIX_LINE_SEPARATOR)
}
각각 코틀린 및 자바에서 사용 예시입니다.
// Kotlin
fun main() {
val list = listOf("Kotlin", "Java")
println(joinToString(list)) // 클래스 이름 없이 직접 호출합니다.
}
// Java
class Main {
public static void main(String[] args) {
List<String> list = List.of("Kotlin", "Java");
String result = StringUtilsKt.joinToString(list);
}
}
코틀린은 다음과 같은 특징을 가집니다.
- 가독성 향상: 불필요한 클래스 선언(ex: `class StringUtils { private ...() }`)가 사라져 코드가 훨씬 간결해집니다.
- 컴파일 구조: 코틀린 파일은 내부적으로 자바 클래스로 변환되므로 자바와의 상호 운용성이 완벽하게 유지됩니다.
- 상수 선언의 적절성: 실행 시점에 결정되는 값은 `val`을 사용하고, 컴파일 시점 완전히 고정되는 값은 `const val`을 사용하여 인라인 최적화를 유도해야 합니다.
메서드를 다른 클래스에 추가: 확장 함수와 프로퍼티 🌟
코틀린의 확장은 기존 크래스의 소스코드를 수정하지 않고도 새로운 메서드나 프로퍼티를 추가할 수 있게 해주는 매우 강력한 기능입니다. 이는 자바의 외부 라이브러리나 표준 API를 코틀린 스타일로 확장할 때 핵심적인 역할을 합니다.
1) 확장 함수 (Extension Functions)
확장 함수는 어떤 클래스의 멤버 메서드인 것처럼 호출할 수 있지만 클래스 밖에 선언된 함수를 말합니다.
// String: 수신 객체 타입 (Receiver Type)
// this: 수신 객체 (Receiver Object)
fun String.lastChar(): Char = this.get(this.length - 1)
println("Kotlin".lastChar()) // n (String 클래스를 건들지 않고 확장할 수 있다.)
- 수신 객체 타입: 확장이 정의될 클래스의 이름입니다.
- 수신 객체: 확장 함수가 호출되는 실제 객체 인스턴스입니다. 함수 내부에서 `this` 키워드를 통해 접근할 수 있으며, `this`를 생략하고 멤버에 접근하는 것도 가능합니다.
확장 함수는 클래스 내부에서 선언된 것이 아니므로, 클래스의 `private`이나 `protected` 멤버에는 접근할 수 없습니다. 또한 확장 함수를 사용하기 위해 반드시 `import`를 해야합니다. 이름이 충돌할 경우 as 키워드를 사용하여 이름을 변경할 수 있습니다.
import strings.lastChar as last // last로 이름 변경
val c = "Kotlin".last()
2) 자바에서 확장 함수 호출
내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메서드로 컴파일됩니다.
- 비용 없음: 실행 시점에 부가적인 객체 생성이나 오버헤드가 발생하지 않습니다.
- 자바에서 호출: 자바에서는 해당 확장 함수가 포함된 파일 이름 뒤 `Kt`가 붙은 클래스를 통해 정적 메서드로 호출합니다.
// Java 코드
char c = StringUtilsKt.lastChar("Java");
3) 확장 함수로 유틸리티 함수 정의
fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수 선언
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String {
val result = StringBuilder(prefix)
// this는 수신 객체. 여기서는 T 타입의 원소로 이루어진 Collection
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(element)
}
result.append(postfix)
return result.toString()
}
fun main(args: Array<String>) {
val list = arrayListOf(1, 2, 3)
println(list.joinToString(" "))
}
원소로 이뤄진 컬렉션에 대한 확장 함수를 만들고, 모든 인자에 대한 디폴트 값을 지정하였습니다. `joinToString`을 마치 클래스의 멤버인 것처럼 호출할 수 있습니다.
4) 확장 함수는 오버라이드할 수 없습니다.
확장 함수는 클래스의 멤버가 아니며, 정적(Static) 메서드와 동일한 방식으로 동작합니다.
따라서 일반적인 객체지향의 다형성이 적용되지 않습니다.
| 구분 | 멤버 메서드 (`Override` 가능) | 확장 함수 (`Override` 불가) |
| 결정 시점 | 실행 시점 (Runtime) | 컴파일 시점 (Compile-time) |
| 호출 기준 | 객체의 실제 인스턴스 타입 | 변수에 선언된 타입 |
확장 함수는 변수의 정적 타입(컴파일 시점의 타입)에 의해 호출될 함수가 결정됩니다.
객체의 실제 동적 타입(런타임 타입)에 의해 결정되지 않음을 유의해야 합니다.
5) 확장 프로퍼티
기존 클래스에 프로퍼티 문법(점 표기법)으로 접근할 수 있는 기능을 추가할 수 있습니다.
val String.lastChar: Char
get() = get(length - 1) // backing field가 없으므로 게터 정의 필수
확장 프로퍼티는 실제로 필드를 가질 수 없으므로, 상태를 저장할 방법이 없습니다. 즉, 초기화 코드(`init { ... }`)를 쓸 수 없으며 오직 `getter`나 `setter`만 정의할 수 있습니다.
컬렉션 처리: 가변 인자, 중위 함수 호출, 구조 분해 선언
코틀린은 자바의 컬렉션 API를 그대로 사용하면서도, 확장 함수를 통해 훨씬 편리한 사용성을 제공합니다.
1) 가변 인자 함수: `varage` 키워드
메서드를 호출할 때 인자의 개수를 자유롭게 조절할 수 있다면 `vararg` 변경자를 사용할 수 있습니다.
- 선언 방식: 자바의 타입 뒤 `...` 대신, 코틀린은 파라미터 앞에 `vararg`를 붙입니다.
- 스프레드 연산자 (`*`): 이미 존재하는 배열을 가변 인자로 넘길 때는 배열 앞에 별표(`*`)를 붙여야 합니다. 이를 통해 배열의 각 원소가 인자로 하나씩 펼쳐져서 전달됩니다.
// args = {"two", "eight"}
fun main(args: Array<String>) {
val list = listOf("one", "two", "eight") // 가변 인자를 받는 listOf 함수
val combinedList = listOf("one", *args) // 배열 앞에 *를 붙여 스프레드(펼침) 처리
println(list) // ["one", "two", "eight"]
println(combinedList) // ["one", "two", "eight"]
}
2) 중위 호출(Infix call) 및 구조 분해 선언
인자가 하나뿐인 메서드는 `infix` 변경자를 붙여 중위 호출 방식으로 사용할 수 있습니다. 수신 객체와 인자 사이에 메서드 이름을 넣어 가독성을 높입니다.
class Person(val name: String) {
// infix 키워드를 붙여 중위 함수로 정의
infix fun likes(other: Person) {
println("${this.name}은(는) ${other.name}을(를) 좋아합니다.")
}
}
fun main() {
val alice = Person("Alice")
val bob = Person("Bob")
// 일반적인 호출 방식
alice.likes(bob)
// 중위 호출 방식 (마치 문장처럼 읽힘)
alice likes bob
}
`to` 함수는 두 원소로 이뤄어진 순서쌍 `Pair` 객체를 반환합니다. 코틀린은 `Pair`와 같은 객체의 내용을 여러 변수에 나누어 담는 구조 분해 선언을 지원합니다.
1.to("one") // 일반적인 호출 방식
1 to "one" // 중위 호출 방식 (객체 이름 인자 순서로 공백을 두어 작성)
// Pair의 내용을 즉시 두 변수에 나누어 담습니다.
val (number, name) = 1 to "one"
println(number) // 1
println(name) // "one"
`to` 함수는 확장 함수로 `to`를 사용하면 타입과 관계없이 임의의 순서쌍을 만들 수 있습니다. 이는 `to`의 수신 객체가 제네릭하기 때문입니다.
코드 다듬기: 로컬 함수와 확장
많은 개발자들은 좋은 코드의 중요한 특징 중 하나로 중복이 없는 코드를 꼽으며, 이를 DRY(Don't Repeat Yourself) 원칙이라 부릅니다. 하지만 자바에서 이 원칙을 지키기란 쉽지 않습니다. 보통 긴 메서드를 분리하기 위해 메서드 추출 리팩토링을 적용하지만, 그 결과 클래스 안에 작은 메서드가 늘어나면서 코드 흐름을 파악하기 어려워지는 문제가 생길 수 있습니다. 리팩토링을 진행하여 추출한 메서드를 별도 내부 클래스에 넣으면 코드를 깔금하게 조작할 수 있지만, 그에 따른 불필요한 코드가 늘어나게 됩니다.
코틀린은 함수에 추출한 함수를 원 함수 내부에 중첩시킬 수 있습니다.
다음 예제는 사용자를 저장하기 전 검증 로직입니다.
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")
}
// ...
}
위처럼 중복되는 검증 로직을 하나의 함수로 개선한다면 다음과 같이 구현할 수 있습니다.
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")
//...
}
코틀린은 확장 함수 기능이 있습니다. 이를 통해 검증 로직을 확장 함수로 만들어 좀 더 개선할 수 있습니다.
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()
//...
}
요약
- 코틀린은 자체 컬렉션 클래스를 정의하지 않고 자바 클래스를 확장하여 더 풍부한 API를 제공한다.
- 함수 파라미터의 디폴트 값을 정의하면 `overloading` 함수의 필요성이 줄어든다.
- 코틀린은 파일에서 클래스 멤버가 아닌 최상위 함수와 프로퍼티를 직접 선언할 수 있다.
- 확장 함수와 프로퍼티를 사용하면 외부 라이브러리에 정의된 클래스를 포함해 모든 클래스의 API를 해당 클래스의 코드를 바꿀 필요없이 유연하게 확장할 수 있다. 확장 함수를 사용해도 실행 시점에 부가 비용이 들지 않는다.
- 중위 호출을 통해 인자가 하나 밖에 없는 경우 더 깔끔한 구문을 호출할 수 있다.
- 로컬 함수를 써서 코드를 더 깔끔하게 유지하면서 중복을 제거할 수 있다.
'Kotlin' 카테고리의 다른 글
| [Kotlin in action 2/e] 코틀린 타입 시스템 (0) | 2026.01.07 |
|---|---|
| [Kotlin in action 2/e] 람다로 프로그래밍 (0) | 2025.12.31 |
| [Kotlin in Action 2/e] 클래스, 객체, 인터페이스 (0) | 2025.12.30 |
| [Kotlin in Action 2/e] 코틀린의 기초 (0) | 2025.12.12 |
| [Kotlin in Action 2/e] 코틀린이란 무엇이며, 왜 필요한가? (1) | 2025.12.12 |