프로그래머를 위한 Kotlin 부트캠프 5.1: 확장 프로그램

이 Codelab은 프로그래머를 위한 Kotlin 부트캠프 과정의 일부입니다. Codelab을 순서대로 진행한다면 이 과정을 통해 최대한의 가치를 얻을 수 있을 것입니다. 기존 지식에 따라 일부 섹션을 훑어볼 수도 있습니다. 이 교육 과정에서는 객체 지향 언어를 알고 Kotlin을 배우고자 하는 프로그래머를 대상으로 합니다.

소개

이 Codelab에서는 쌍, 쌍, 컬렉션, 확장 함수 등 다양한 여러 유용한 Kotlin 기능을 소개합니다.

이 과정의 강의는 하나의 샘플 앱을 빌드하는 대신 지식을 쌓을 수 있도록 만들어졌지만 서로 종속되지 않도록 익숙해져 있는 섹션을 훑어볼 수 있습니다. 이러한 사례를 연결하는 데 도움이 되는 예시는 대부분 수족관 테마를 사용합니다. 전체 수족관 이야기를 보려면 프로그래머를 위한 Kotlin 부트캠프 Udacity 과정을 확인하세요.

기본 요건

  • Kotlin 함수, 클래스, 메서드의 구문
  • IntelliJ IDEA에서 Kotlin's REPL (Read-Eval-Print Loop)을 사용하는 방법
  • IntelliJ IDEA에서 새 클래스를 만들고 프로그램을 실행하는 방법

학습할 내용

  • 쌍 및 삼중으로 작업하는 방법
  • 컬렉션 자세히 알아보기
  • 상수 정의 및 사용
  • 확장 함수 작성

실습할 내용

  • REPL의 쌍, 삼중, 해시 맵에 관해 알아보기
  • 상수를 구성하는 다양한 방법 알아보기
  • 확장 함수 및 확장 속성 작성

이 작업에서는 쌍과 삼중, 삼중 및 해체에 관해 알아봅니다. 쌍 및 삼중은 2~3개의 일반 항목을 위한 사전 제작된 데이터 클래스입니다. 이는 예를 들어 함수가 두 개 이상의 값을 반환하도록 하는 데 유용할 수 있습니다.

List의 어류가 있고 어류가 담수 또는 해수인지 확인하는 함수 isFreshWater()가 있다고 가정해 보겠습니다. List.partition()는 두 개의 목록, 즉 조건이 true인 항목과 목록이 false인 항목에 대해 목록을 반환합니다.

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

1단계: 쌍과 삼중으로 만들기

  1. REPL(Tools > Kotlin > Kotlin REPL)을 엽니다.
  2. 한 쌍을 만들어 장비와 용도를 연결한 다음 값을 출력합니다. 키워드 to를 사용하여 두 값(예: 문자열 2개)을 연결하는 표현식을 만든 다음 .first 또는 .second를 사용하여 각 값을 참조하는 표현식을 만들어 쌍을 만들 수 있습니다.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. 트리플을 만들고 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]

위의 예는 쌍 또는 삼중의 모든 부분에 동일한 유형을 사용하지만 필수는 아닙니다. 부분은 문자열, 숫자, 목록일 수 있으며, 다른 쌍 또는 삼중일 수도 있습니다.

  1. 쌍의 첫 번째 부분이 쌍일 수 있도록 쌍을 만듭니다.
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단계: 일부 쌍 및 삼중 구조

쌍과 삼중을 부품으로 분리하는 것을 해체라고 합니다. 적절한 수의 변수에 쌍 또는 3줄을 할당하면 Kotlin이 각 부분의 값을 순서대로 할당합니다.

  1. 한 쌍을 해체하고 값을 출력합니다.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. 트리플을 구조화하고 값을 출력합니다.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

참고로, 디스트릭트 쌍과 삼중은 이전 Codelab에서 다룬 데이터 클래스와 동일하게 작동합니다.

이 작업에서는 목록을 비롯한 컬렉션, 새로운 컬렉션 유형, 해시 맵에 대해 자세히 알아봅니다.

1단계: 목록에 대해 자세히 알아보기

  1. 목록 및 변경 가능한 목록은 이전 강의에서 소개했습니다. 매우 유용한 데이터 구조이므로 Kotlin은 목록에 여러 내장 함수를 제공합니다. 목록에 대한 부분 함수 목록을 검토합니다. ListMutableList의 Kotlin 문서에서 전체 목록을 확인할 수 있습니다.

함수

용도

add(element: E)

변경 가능한 목록에 항목을 추가합니다.

remove(element: E)

변경 가능한 목록에서 항목을 삭제합니다.

reversed()

역순으로 요소가 포함된 목록 사본을 반환합니다.

contains(element: E)

목록에 항목이 있으면 true를 반환합니다.

subList(fromIndex: Int, toIndex: Int)

첫 번째 색인부터 두 번째 색인까지 목록의 일부를 반환합니다.

  1. REPL에서 계속 작업 중일 때 숫자 목록을 만들고 그 위에 sum()를 호출합니다. 모든 요소가 합쳐져 있습니다.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. 문자열 목록을 만들고 목록의 합계를 냅니다.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. 예를 들어 요소가 List와 같이 직접 합산하는 방법을 아는 경우(예: 문자열) .sumBy()를 사용하여 람다 함수를 사용하여 요소에 합산하는 방법을 지정할 수 있습니다. 예를 들어 각 문자열의 길이를 기준으로 합산하는 방법이 있습니다. 람다 인수의 기본 이름은 it이며 여기서 it는 목록이 순회되므로 목록의 각 요소를 나타냅니다.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. 목록에서 할 수 있는 작업이 많습니다. IntelliJ IDEA에서 목록을 만들고 점을 추가한 다음 팁에서 자동 완성 목록을 살펴보면 사용 가능한 기능을 확인할 수 있습니다. 모든 객체에 적용됩니다. 목록을 통해 사용해 보세요.

  1. 목록에서 listIterator()를 선택한 다음 for 문으로 목록을 탐색하고 모든 요소를 공백으로 구분하여 출력합니다.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

2단계: 해시 맵 사용해 보기

Kotlin에서 hashMapOf()을 사용하면 거의 모든 요소를 다른 매핑에 매핑할 수 있습니다. 해시 맵은 첫 번째 값이 키 역할을 하는 쌍 목록과 같습니다.

  1. 증상, 물고기의 증상, 질환과 일치하는 해시 맵을 만듭니다.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. 그런 다음 get() 또는 더 짧은 대괄호 []를 사용하여 증상 키에 따라 질병 값을 가져올 수 있습니다.
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. 지도에 없는 증상들을 지정해 보세요.
println(cures["scale loss"])
⇒ null

키가 맵에 없는 경우 일치하는 질병을 반환하려고 시도하면 null가 반환됩니다. 맵 데이터에 따라 가능한 키와 일치하는 결과가 없는 경우가 많습니다. 이와 같은 경우 Kotlin은 getOrDefault() 함수를 제공합니다.

  1. getOrDefault()을 사용하여 일치하는 키가 없는 키를 검색해 보세요.
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

값을 반환하는 것 이상을 실행해야 하는 경우 Kotlin은 getOrElse() 함수를 제공합니다.

  1. getOrDefault() 대신 getOrElse()를 사용하도록 코드를 변경합니다.
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

간단한 기본값을 반환하는 대신 중괄호 {} 사이에 있는 코드를 실행합니다. 이 예에서 else은 단순히 문자열을 반환하지만 치료법이 있는 웹페이지를 찾아서 반환하는 것과 같은 기능을 사용할 수 있습니다.

mutableListOf와 마찬가지로 mutableMapOf를 만들 수도 있습니다. 변경 가능한 맵을 사용하면 항목을 넣고 삭제할 수 있습니다. 변경 가능이란 변경 가능함을 의미합니다. 변경 불가능은 변경할 수 없음을 의미합니다.

  1. 장비 문자열을 항목 수에 매핑하여 수정할 수 있는 인벤토리 맵을 만듭니다. 그물에 물고기 네트를 넣은 다음 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 비교 알아보기

  1. REPL에서 숫자 상수를 만들어 봅니다. Kotlin에서는 const val을 사용하여 최상위 상수를 만들고 컴파일 시간에 값을 할당할 수 있습니다.
const val rocks = 3

이 값은 할당되며 변경할 수 없습니다. 이는 일반적인 val를 선언하는 것과 매우 유사합니다. 그렇다면 const valval의 차이점은 무엇일까요? 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 키워드로 선언된 컴패니언 객체로 래핑해야 합니다. 컴패니언 객체는 기본적으로 클래스 내의 싱글톤 객체입니다.

  1. 문자열 상수가 포함된 컴패니언 객체로 클래스를 만듭니다.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

컴패니언 객체와 일반 객체의 기본적인 차이점은 다음과 같습니다.

  • 컴패니언 객체는 포함하는 클래스의 정적 생성자에서 초기화됩니다. 즉, 객체가 만들어질 때 생성됩니다.
  • 일반 객체는 해당 객체에 대한 첫 번째 액세스, 즉 처음 사용될 때 천천히 초기화됩니다.

더 많은 항목이 있지만, 지금은 컴패니언 객체의 클래스에 상수를 래핑하기만 하면 됩니다.

이 작업에서는 클래스의 동작을 확장하는 방법을 알아봅니다. 클래스의 동작을 확장하는 유틸리티 함수를 작성하는 것이 매우 일반적입니다. Kotlin은 이러한 유틸리티 함수(확장 함수)를 선언하는 편리한 구문을 제공합니다.

확장 함수를 사용하면 소스 코드에 액세스하지 않고도 함수를 기존 클래스에 추가할 수 있습니다. 예를 들어 패키지에 포함된 Extensions.kt 파일에서 이를 선언할 수 있습니다. 실제로 클래스를 수정하지는 않지만 클래스의 객체에서 함수를 호출할 때 점 표기법을 사용할 수 있습니다.

1단계: 확장 함수 작성

  1. REPL에서 계속 작업하는 경우 간단한 확장 함수인 hasSpaces()를 작성하여 문자열에 공백이 포함되어 있는지 확인합니다. 함수 이름에는 함수가 작동하는 클래스가 앞에 붙습니다. 함수 내에서 this는 호출되는 객체를 나타내고 itfind() 호출에서 반복자를 나타냅니다.
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. hasSpaces() 함수를 단순화할 수 있습니다. this은 명시적으로 필요하지 않으며, 함수를 단일 표현식으로 줄여 반환할 수 있습니다. 따라서 중괄호 {}도 필요하지 않습니다.
fun String.hasSpaces() = find { it == ' ' } != null

2단계: 확장 프로그램의 제한사항 알아보기

확장 함수는 확장되는 클래스의 공개 API에만 액세스할 수 있습니다. private인 변수에 액세스할 수 없습니다.

  1. 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'
  1. 아래 코드를 살펴보고 출력할 내용을 찾아봅니다.
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()GreenLeafyPlant 값이 출력될 것으로 예상할 수 있습니다. plant 값이 할당되었기 때문입니다. 그러나 컴파일 시간에 유형이 결정되므로 AquariumPlant이 출력됩니다.

3단계: 확장 속성 추가

Kotlin에서는 확장 함수 외에도 확장 속성을 추가할 수 있습니다. 확장 함수와 마찬가지로, 확장 중인 클래스와 점, 속성 이름을 차례로 지정합니다.

  1. REPL에서 계속 작동하는 경우 AquariumPlant에 확장 속성 isGreen를 추가합니다. 색상은 녹색이면 true입니다.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

isGreen 속성은 일반 속성과 마찬가지로 액세스할 수 있습니다. 액세스하면 값을 받기 위해 isGreen의 getter가 호출됩니다.

  1. aquariumPlant 변수의 isGreen 속성을 출력하고 결과를 관찰합니다.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

4단계: null을 허용하는 수신기 알아보기

확장하는 클래스를 수신자라고 하며, 이 클래스를 null 허용으로 설정할 수 있습니다. 그러면 본문에서 this 변수가 null이 될 수 있으므로 테스트해야 합니다. 호출자가 null을 허용하는 변수에서 확장 메서드를 호출할 것으로 예상되거나 함수가 null에 적용될 때 기본 동작을 제공하려는 경우 null을 허용하는 수신기를 사용하는 것이 좋습니다.

  1. REPL에서 작업하면서 null을 허용하는 수신기를 사용하는 pull() 메서드를 정의합니다. 유형 뒤의 점 ? 앞에 물음표가 표시됩니다. 본문에서는 물음표-적용 ?.apply.을 사용하여 thisnull이 아닌지 테스트할 수 있습니다.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. 이 경우 프로그램을 실행하면 출력이 없습니다. plantnull이므로 내부 println()이 호출되지 않습니다.

확장 함수는 매우 강력하며 대부분의 Kotlin 표준 라이브러리는 확장 함수로 구현됩니다.

이 강의에서는 컬렉션, 상수에 관해 자세히 알아보고 확장 함수와 속성의 기능을 알아보았습니다.

  • 쌍 및 삼중을 사용하여 함수에서 두 개 이상의 값을 반환할 수 있습니다. 예를 들면 다음과 같습니다.
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin에는 List, reversed(), contains(), subList()와 같은 여러 유용한 함수가 있습니다.
  • 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를 사용하여 상수를 정의할 수 없는 곳은 무엇인가요?

파일 최상위 수준의 ▢

일반 클래스의 ▢

싱글톤 객체의 ▢

컴패니언 객체의 ▢

다음 과정 5.2 일반으로 진행하세요.

다른 Codelab으로 연결되는 링크를 포함한 과정 개요는 "프로그래머를 위한 Kotlin 부트캠프: 교육 과정에 오신 것을 환영합니다.를 참고하세요.