이 Codelab은 프로그래머를 위한 Kotlin 부트캠프 과정의 일부입니다. Codelab을 순서대로 진행하면 이 과정의 학습 효과를 극대화할 수 있습니다. 기존 지식에 따라 일부 섹션을 훑어볼 수도 있습니다. 이 과정은 객체 지향 언어를 알고 Kotlin을 배우고 싶은 프로그래머를 대상으로 합니다.
소개
이 Codelab에서는 쌍, 컬렉션, 확장 함수 등 Kotlin의 여러 유용한 기능을 소개합니다.
이 과정의 강의는 단일 샘플 앱을 빌드하는 대신 지식을 쌓을 수 있도록 설계되었지만, 서로 반독립적이므로 잘 아는 섹션은 대충 훑어볼 수 있습니다. 이러한 요소를 연결하기 위해 많은 예에서 수족관 테마를 사용합니다. 전체 수족관 스토리를 확인하려면 프로그래머를 위한 Kotlin 부트캠프 Udacity 과정을 확인하세요.
기본 요건
- Kotlin 함수, 클래스, 메서드의 문법
- IntelliJ IDEA에서 Kotlin의 REPL (Read-Eval-Print Loop)로 작업하는 방법
- IntelliJ IDEA에서 새 클래스를 만들고 프로그램을 실행하는 방법
학습할 내용
- 쌍 및 3중항을 사용하는 방법
- 컬렉션에 대해 자세히 알아보기
- 상수 정의 및 사용
- 확장 프로그램 함수 작성
실습할 내용
- REPL에서 쌍, 삼중, 해시 맵에 대해 알아보기
- 상수를 정리하는 다양한 방법 알아보기
- 확장 프로그램 함수 및 확장 프로그램 속성 작성
이 작업에서는 쌍과 트리플에 대해 알아보고 이를 구조화 해제합니다. 쌍과 삼중은 2개 또는 3개의 일반 항목을 위한 사전 제작된 데이터 클래스입니다. 예를 들어 함수가 두 개 이상의 값을 반환하도록 하는 데 유용할 수 있습니다.
물고기 List
가 있고 물고기가 담수어인지 해수어인지 확인하는 함수 isFreshWater()
가 있다고 가정해 보겠습니다. List.partition()
는 조건이 true
인 항목이 포함된 목록과 조건이 false
인 항목이 포함된 목록의 두 목록을 반환합니다.
val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")
1단계: 페어 및 트리플 만들기
- REPL을 엽니다 (Tools > Kotlin > Kotlin REPL).
- 장비를 사용 용도와 연결하는 쌍을 만든 다음 값을 출력합니다.
to
키워드를 사용하여 두 값(예: 두 문자열)을 연결하는 표현식을 만든 다음.first
또는.second
를 사용하여 각 값을 참조하여 쌍을 만들 수 있습니다.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
- 3개를 만들어
toString()
로 출력한 다음toList()
로 목록으로 변환합니다. 3개의 값을 사용하여Triple()
로 트리플을 만듭니다..first
,.second
,.third
를 사용하여 각 값을 참조합니다.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42) [6, 9, 42]
위의 예에서는 쌍 또는 삼중의 모든 부분에 동일한 유형을 사용하지만 필수는 아닙니다. 부분은 문자열, 숫자, 목록 등일 수 있으며 다른 쌍이나 3중일 수도 있습니다.
- 쌍의 첫 번째 부분이 쌍 자체인 쌍을 만듭니다.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment ⇒ catching fish
2단계: 일부 쌍과 삼중항 구조 분해
쌍과 삼중항을 부분으로 분리하는 것을 구조 분해라고 합니다. 쌍 또는 트리플을 적절한 수의 변수에 할당하면 Kotlin에서 각 부분의 값을 순서대로 할당합니다.
- 쌍을 구조화 해체하고 값을 출력합니다.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
- 3중 구조를 해체하고 값을 출력합니다.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42
구조 분해 쌍과 3개는 이전 Codelab에서 다룬 데이터 클래스와 동일하게 작동합니다.
이 작업에서는 목록을 비롯한 컬렉션과 새로운 컬렉션 유형인 해시 맵에 대해 자세히 알아봅니다.
1단계: 목록에 대해 자세히 알아보기
- 이전 강의에서 목록과 변경 가능한 목록을 소개했습니다. 목록은 매우 유용한 데이터 구조이므로 Kotlin에서는 목록을 위한 여러 기본 제공 함수를 제공합니다. 목록에 대한 함수의 일부 목록을 검토하세요.
List
및MutableList
의 전체 목록은 Kotlin 문서에서 확인할 수 있습니다.
함수 | 목적 |
| 변경 가능한 목록에 항목을 추가합니다. |
| 변경 가능한 목록에서 항목을 삭제합니다. |
| 요소가 역순으로 정렬된 목록의 사본을 반환합니다. |
| 목록에 항목이 포함된 경우 |
| 첫 번째 색인부터 두 번째 색인까지(두 번째 색인 제외) 목록의 일부를 반환합니다. |
- REPL에서 계속 작업하면서 숫자 목록을 만들고 목록에서
sum()
를 호출합니다. 이렇게 하면 모든 요소가 합산됩니다.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
- 문자열 목록을 만들고 목록을 합산합니다.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
- 문자열과 같이
List
에서 직접 합산하는 방법을 모르는 요소인 경우 람다 함수와 함께.sumBy()
를 사용하여 합산하는 방법을 지정할 수 있습니다(예: 각 문자열의 길이로 합산). 람다 인수의 기본 이름은it
이며 여기서it
는 목록이 순회될 때 목록의 각 요소를 나타냅니다.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
- 목록으로 할 수 있는 작업은 훨씬 더 많습니다. 사용 가능한 기능을 확인하는 한 가지 방법은 IntelliJ IDEA에서 목록을 만들고 점을 추가한 다음 도움말에서 자동 완성 목록을 살펴보는 것입니다. 이는 모든 객체에 적용됩니다. 목록으로 사용해 보세요.
- 목록에서
listIterator()
를 선택한 다음for
문으로 목록을 살펴보고 공백으로 구분된 모든 요소를 출력합니다.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
println("$s ")
}
⇒ a bbb cc
2단계: 해시 맵 사용해 보기
Kotlin에서는 hashMapOf()
를 사용하여 거의 모든 것을 다른 것으로 매핑할 수 있습니다. 해시 맵은 첫 번째 값이 키 역할을 하는 쌍 목록과 같습니다.
- 증상(키)과 물고기 질병(값)이 일치하는 해시 맵을 만듭니다.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
- 그런 다음
get()
또는 더 짧은 대괄호[]
를 사용하여 증상 키를 기반으로 질병 값을 검색할 수 있습니다.
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
- 지도에 없는 증상을 지정해 보세요.
println(cures["scale loss"])
⇒ null
키가 지도에 없으면 일치하는 질병을 반환하려고 하면 null
가 반환됩니다. 지도 데이터에 따라 가능한 키가 일치하지 않는 경우가 많을 수 있습니다. 이러한 경우 Kotlin은 getOrDefault()
함수를 제공합니다.
getOrDefault()
를 사용하여 일치하는 항목이 없는 키를 조회해 보세요.
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know
값을 반환하는 것 이상의 작업을 해야 하는 경우 Kotlin에서는 getOrElse()
함수를 제공합니다.
getOrDefault()
대신getOrElse()
를 사용하도록 코드를 변경합니다.
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this
단순한 기본값을 반환하는 대신 중괄호 {}
사이에 있는 코드가 실행됩니다. 이 예시에서 else
은 문자열을 반환하지만 치료법이 있는 웹페이지를 찾아 반환하는 등 더 복잡한 작업을 할 수도 있습니다.
mutableListOf
와 마찬가지로 mutableMapOf
도 만들 수 있습니다. 변경 가능한 맵을 사용하면 항목을 넣고 삭제할 수 있습니다. 변경 가능은 변경할 수 있다는 의미이고 변경 불가능은 변경할 수 없다는 의미입니다.
- 장비 문자열을 항목 수에 매핑하는 수정 가능한 인벤토리 지도를 만듭니다. 물고기 그물이 있는 상태로 만든 다음
put()
를 사용하여 인벤토리에 탱크 스크러버 3개를 추가하고remove()
를 사용하여 물고기 그물을 삭제합니다.
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}
이 작업에서는 Kotlin의 상수와 상수를 정리하는 다양한 방법을 알아봅니다.
1단계: const와 val의 차이점 알아보기
- REPL에서 숫자 상수를 만들어 보세요. Kotlin에서는 최상위 상수를 만들고
const val
을 사용하여 컴파일 시간에 값을 할당할 수 있습니다.
const val rocks = 3
값이 할당되고 변경할 수 없다는 점은 일반 val
을 선언하는 것과 매우 유사합니다. 그렇다면 const val
과 val
의 차이점은 무엇일까요? const val
의 값은 컴파일 시간에 결정되는 반면 val
의 값은 프로그램 실행 중에 결정됩니다. 즉, val
는 런타임에 함수에 의해 할당될 수 있습니다.
즉, val
에는 함수에서 값을 할당할 수 있지만 const val
에는 할당할 수 없습니다.
val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok
또한 const val
는 최상위 수준에서만 작동하며 일반 클래스가 아닌 object
로 선언된 싱글톤 클래스에서만 작동합니다. 이를 사용하여 상수만 포함하는 파일이나 싱글톤 객체를 만들고 필요에 따라 가져올 수 있습니다.
object Constants {
const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2
2단계: 동반 객체 만들기
Kotlin에는 클래스 수준 상수라는 개념이 없습니다.
클래스 내에서 상수를 정의하려면 companion
키워드로 선언된 컴패니언 객체로 래핑해야 합니다. 컴패니언 객체는 기본적으로 클래스 내의 싱글톤 객체입니다.
- 문자열 상수가 포함된 동반 객체가 있는 클래스를 만듭니다.
class MyClass {
companion object {
const val CONSTANT3 = "constant in companion"
}
}
컴패니언 객체와 일반 객체의 기본적인 차이점은 다음과 같습니다.
- 동반 객체는 포함 클래스의 정적 생성자에서 초기화됩니다. 즉, 객체가 생성될 때 생성됩니다.
- 일반 객체는 해당 객체에 처음 액세스할 때, 즉 처음 사용될 때 지연 초기화됩니다.
더 많은 내용이 있지만 지금은 동반 객체의 클래스에 상수를 래핑하는 것만 알아두면 됩니다.
이 작업에서는 클래스의 동작을 확장하는 방법을 알아봅니다. 클래스의 동작을 확장하는 유틸리티 함수를 작성하는 것은 매우 일반적입니다. Kotlin은 이러한 유틸리티 함수를 선언하기 위한 편리한 문법인 확장 함수를 제공합니다.
확장 함수를 사용하면 소스 코드에 액세스하지 않고도 함수를 기존 클래스에 추가할 수 있습니다. 예를 들어 패키지에 포함된 Extensions.kt 파일에서 선언할 수 있습니다. 실제로 클래스를 수정하지는 않지만 클래스의 객체에서 함수를 호출할 때 점 표기법을 사용할 수 있도록 합니다.
1단계: 확장 프로그램 함수 작성
- REPL에서 계속 작업하면서 문자열에 공백이 포함되어 있는지 확인하는 간단한 확장 함수
hasSpaces()
를 작성합니다. 함수 이름에는 함수가 작동하는 클래스가 앞에 붙습니다. 함수 내에서this
는 호출되는 객체를 참조하고it
는find()
호출의 반복자를 참조합니다.
fun String.hasSpaces(): Boolean {
val found = this.find { it == ' ' }
return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
hasSpaces()
함수를 단순화할 수 있습니다.this
는 명시적으로 필요하지 않으며 함수를 단일 표현식으로 줄여 반환할 수 있으므로 중괄호{}
도 필요하지 않습니다.
fun String.hasSpaces() = find { it == ' ' } != null
2단계: 확장 프로그램의 제한사항 알아보기
확장 함수는 확장하는 클래스의 공개 API에만 액세스할 수 있습니다. private
인 변수에는 액세스할 수 없습니다.
private
로 표시된 속성에 확장 함수를 추가해 보세요.
class AquariumPlant(val color: String, private val size: Int)
fun AquariumPlant.isRed() = color == "red" // OK
fun AquariumPlant.isBig() = size > 50 // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
- 아래 코드를 살펴보고 출력되는 내용을 파악하세요.
open class AquariumPlant(val color: String, private val size: Int)
class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)
fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")
val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print() // what will it print?
⇒ GreenLeafyPlant AquariumPlant
plant.print()
인쇄물 GreenLeafyPlant
aquariumPlant.print()
에 plant
값이 할당되었으므로 aquariumPlant.print()
도 GreenLeafyPlant
을 출력할 것으로 예상할 수 있습니다. 하지만 유형은 컴파일 시간에 확인되므로 AquariumPlant
가 출력됩니다.
3단계: 확장 프로그램 속성 추가
확장 함수 외에도 Kotlin을 사용하면 확장 속성을 추가할 수 있습니다. 확장 함수와 마찬가지로 확장할 클래스를 지정하고 점을 입력한 후 속성 이름을 입력합니다.
- REPL에서 계속 작업하면서 색상이 녹색인 경우
true
인 확장 프로그램 속성isGreen
을AquariumPlant
에 추가합니다.
val AquariumPlant.isGreen: Boolean
get() = color == "green"
isGreen
속성은 일반 속성과 마찬가지로 액세스할 수 있습니다. 액세스하면 isGreen
의 getter가 호출되어 값을 가져옵니다.
aquariumPlant
변수의isGreen
속성을 출력하고 결과를 관찰합니다.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true
4단계: null 허용 수신기 알아보기
확장하는 클래스를 리시버라고 하며, 이 클래스를 null 허용으로 만들 수 있습니다. 이렇게 하면 본문에 사용된 this
변수가 null
가 될 수 있으므로 이를 테스트해야 합니다. 호출자가 null 허용 변수에서 확장 메서드를 호출하기를 원하거나 함수가 null
에 적용될 때 기본 동작을 제공하려는 경우 null 허용 수신자를 사용해야 합니다.
- REPL에서 계속 작업하면서 null 허용 수신자를 사용하는
pull()
메서드를 정의합니다. 이는 유형 뒤에 점 앞에 물음표?
로 표시됩니다. 본문 내에서 물음표-점-apply?.apply.
를 사용하여this
가null
이 아닌지 테스트할 수 있습니다.
fun AquariumPlant?.pull() {
this?.apply {
println("removing $this")
}
}
val plant: AquariumPlant? = null
plant.pull()
- 이 경우 프로그램을 실행해도 출력이 없습니다.
plant
가null
이므로 내부println()
가 호출되지 않습니다.
확장 함수는 매우 강력하며 Kotlin 표준 라이브러리의 대부분이 확장 함수로 구현됩니다.
이 과정에서는 컬렉션에 관해 자세히 알아보고, 상수에 관해 배우고, 확장 함수와 속성의 강력한 기능을 맛보았습니다.
- 쌍과 3중을 사용하면 함수에서 두 개 이상의 값을 반환할 수 있습니다. 예를 들면 다음과 같습니다.
val twoLists = fish.partition { isFreshWater(it) }
- Kotlin에는
reversed()
,contains()
,subList()
과 같은List
에 유용한 함수가 많이 있습니다. HashMap
를 사용하여 키를 값에 매핑할 수 있습니다. 예:val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
const
키워드를 사용하여 컴파일 시간 상수를 선언합니다. 최상위 수준에 배치하거나, 싱글톤 객체로 정리하거나, 동반 객체에 배치할 수 있습니다.- 컴패니언 객체는 클래스 정의 내의 싱글톤 객체로,
companion
키워드로 정의됩니다. - 확장 함수와 확장 속성을 사용하면 클래스에 기능을 추가할 수 있습니다. 예를 들면 다음과 같습니다.
fun String.hasSpaces() = find { it == ' ' } != null
- null 허용 수신자를 사용하면
null
일 수 있는 클래스에 확장 프로그램을 만들 수 있습니다.?.
연산자를apply
와 페어링하여 코드를 실행하기 전에null
를 확인할 수 있습니다. 예:this?.apply { println("removing $this") }
Kotlin 문서
이 과정의 주제에 관해 자세히 알아보거나 막히는 부분이 있다면 https://kotlinlang.org에서 시작하는 것이 좋습니다.
Kotlin 튜토리얼
https://try.kotlinlang.org 웹사이트에는 Kotlin Koans라는 풍부한 튜토리얼, 웹 기반 인터프리터, 예시가 포함된 전체 참고 문서 세트가 포함되어 있습니다.
Udacity 과정
이 주제에 관한 Udacity 과정을 보려면 프로그래머를 위한 Kotlin 부트캠프를 참고하세요.
IntelliJ IDEA
IntelliJ IDEA 문서는 JetBrains 웹사이트에서 확인할 수 있습니다.
이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.
- 필요한 경우 과제를 할당합니다.
- 과제 제출 방법을 학생에게 알립니다.
- 과제를 채점합니다.
강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.
이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.
질문에 답하세요
질문 1
다음 중 목록의 사본을 반환하는 것은 무엇인가요?
▢ add()
▢ remove()
▢ reversed()
▢ contains()
질문 2
class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean)
의 다음 확장 함수 중 컴파일러 오류가 발생하는 함수는 무엇인가요?
▢ fun AquariumPlant.isRed() = color == "red"
▢ fun AquariumPlant.isBig() = size > 45
▢ fun AquariumPlant.isExpensive() = cost > 10.00
▢ fun AquariumPlant.isNotLeafy() = leafy == false
질문 3
다음 중 const val
로 상수를 정의할 수 없는 곳은 어디인가요?
파일의 최상위 수준에 ▢
▢ 일반 수업
싱글톤 객체에서 ▢
▢ 컴패니언 객체
다음 강의로 진행하세요.
다른 Codelab 링크를 비롯한 과정 개요는 프로그래머를 위한 Kotlin 부트캠프: 과정에 오신 것을 환영합니다를 참고하세요.