Kotlin의 클래스 및 상속

1. 시작하기 전에

기본 요건

  • Kotlin 플레이그라운드를 사용하여 Kotlin 프로그램을 수정하는 방법을 잘 알고 있어야 합니다.
  • 이 과정 단원 1에서 살펴본 Kotlin 프로그래밍의 기본 개념을 알고 있어야 합니다. 특히 main() 프로그램과 값, 변수, 데이터 유형 및 작업을 반환하는 인수가 포함된 함수, if/else 문을 알아야 합니다.
  • Kotlin의 클래스를 정의하고 그 클래스에서 객체 인스턴스를 만들어 속성과 메서드에 액세스할 수 있어야 합니다.

학습할 내용

  • 상속을 사용하여 클래스의 계층 구조를 구현하는 Kotlin 프로그램을 만듭니다.
  • 클래스를 확장하고 기존 기능을 재정의하며 새 기능을 추가합니다.
  • 변수의 올바른 공개 상태 수정자를 선택합니다.

빌드할 항목

  • 클래스 계층 구조로 구현되는 다양한 유형의 주택이 포함된 Kotlin 프로그램

필요한 항목

2. 클래스 계층 구조란 무엇인가요?

사람들은 자연스럽게 속성과 동작이 비슷한 항목을 그룹으로 분류하고 그룹 내에서도 일정 유형의 계층 구조를 만듭니다. 예를 들어 채소와 같이 광범위한 카테고리를 사용할 수 있고 그 카테고리 안에 콩류라는 좀 더 구체적인 유형의 카테고리를 사용할 수 있습니다. 콩류 내에서도 더 구체적으로 완두콩, 강낭콩, 렌즈콩, 병아리콩, 대두 등이 있을 수 있습니다.

이는 계층 구조로 표현될 수 있는데 콩류는 채소의 속성(예: 식물, 식용 가능함)을 모두 포함하거나 상속받기 때문입니다. 마찬가지로 완두콩, 강낭콩, 렌즈콩도 모두 콩류의 속성과 고유한 자체 속성이 있습니다.

이러한 관계를 프로그래밍 측면에서 어떻게 표현할지 살펴보겠습니다. Vegetable을 Kotlin의 클래스로 만들면 LegumeVegetable 클래스의 하위 클래스 또는 서브클래스로 만들 수 있습니다. 즉, Vegetable 클래스의 모든 속성과 메서드가 Legume 클래스에 상속(즉, 사용 가능함)됩니다.

아래와 같이 클래스 계층 구조 다이어그램으로 이를 표현할 수 있습니다. VegetableLegume 클래스의 상위 클래스 또는 슈퍼클래스로 지칭할 수 있습니다.

87e0a5eb0f85042d.png

Legume의 서브클래스(예: Lentil, Chickpea)를 만들어 클래스 계층 구조를 유지하고 확장할 수 있습니다. 그러면 LegumeVegetable의 하위 클래스 또는 서브클래스가 될 뿐 아니라 LentilChickpea의 상위 클래스 또는 슈퍼클래스가 됩니다. Vegetable은 이 계층 구조의 루트 또는 최상위(또는 기본) 클래스입니다.

638655b960530d9.png

Android 클래스의 상속

이전 Codelab에서 한 것처럼 클래스를 사용하지 않고도 Kotlin 코드를 작성할 수 있지만 Android의 여러 부분이 활동, 뷰, 뷰 그룹 등 클래스 형태로 제공됩니다. 따라서 클래스 계층 구조 이해는 Android 앱 개발에 중요하며 이를 통해 Android 프레임워크에서 제공하는 기능을 활용할 수 있습니다.

예를 들어 Android에는 화면의 직사각형 영역을 나타내고 그리기와 이벤트 처리를 담당하는 View 클래스가 있습니다. TextView 클래스는 View 클래스의 서브클래스입니다. 즉, TextViewView 클래스의 모든 속성과 기능을 상속받고 사용자에게 텍스트를 표시하는 특정 로직을 추가합니다.

c39a8aaa5b013de8.png

한 단계 더 나아가 EditTextButton 클래스는 TextView 클래스의 하위 클래스입니다. TextViewView 클래스의 모든 속성과 메서드를 상속받고 고유한 특정 로직을 추가합니다. 예를 들어 EditText는 화면에서 텍스트를 수정할 수 있는 자체 기능을 추가합니다.

ViewTextView 클래스의 모든 로직을 복사하여 EditText 클래스에 붙여넣지 않고도 EditTextView 클래스의 서브클래스인 TextView 클래스의 서브클래스로 분류하면 됩니다. 그러면 EditText 클래스의 코드는 이 UI 구성요소를 다른 뷰와 다르게 하는 요소에 특히 중점을 둘 수 있습니다.

developer.android.com 웹사이트의 Android 클래스 문서 페이지 상단에서 클래스 계층 구조 다이어그램을 확인할 수 있습니다. 계층 구조 상단에 kotlin.Any가 표시된다면 그 이유는 Kotlin의 모든 클래스에 공통 슈퍼클래스 Any가 있기 때문입니다. 자세한 내용은 여기를 참고하세요.

1ce2b1646b8064ab.png

이와 같이 클래스 간 상속을 활용하는 방법을 학습하면 코드를 더 쉽게 작성하고 재사용하며 읽고 테스트할 수 있습니다.

3. 기본 클래스 만들기

주택의 클래스 계층 구조

이 Codelab에서는 바닥 면적, 층, 거주자가 있는 주택(사람이 사는 집)을 예로 사용하여 클래스 계층 구조의 작동 원리를 보여주는 Kotlin 프로그램을 빌드합니다.

다음은 빌드할 클래스 계층 구조의 다이어그램입니다. 루트에는 청사진과 유사하게 모든 주택에 적용되는 속성과 기능을 지정하는 Dwelling이 있습니다. 그런 다음 정사각형 통나무집(SquareCabin) 클래스, 둥근 오두막(RoundHut) 클래스, 여러 층이 있는 RoundHut인 둥근 타워(RoundTower) 클래스가 있습니다.

de1387ca7fc26c81.png

구현할 클래스는 다음과 같습니다.

  • Dwelling: 모든 주택에 공통으로 적용되는 정보를 담고 있는 구체적이지 않은 집을 나타내는 기본 클래스입니다.
  • SquareCabin: 바닥 면적이 정사각형인 나무로 만든 정사각형 통나무집입니다.
  • RoundHut: 바닥 면적이 원형인 짚으로 만든 둥근 오두막이고 RoundTower의 상위 요소입니다.
  • RoundTower: 바닥 면적이 원형이고 층이 여러 개인 돌로 만든 둥근 타워입니다.

추상 주택 클래스 만들기

어떤 클래스라도 클래스 계층 구조의 기본 클래스나 다른 클래스의 상위 클래스가 될 수 있습니다.

'추상' 클래스는 완전히 구현되지 않아서 인스턴스화할 수 없는 클래스입니다. 스케치라고 생각하면 됩니다. 스케치를 통해 무언가에 관한 아이디어와 계획을 통합하지만 그 무언가를 빌드하기에는 일반적으로 정보가 충분하지 않습니다. 스케치(추상 클래스)를 사용하여 청사진(클래스)을 만들고 청사진을 통해 실제 객체 인스턴스를 빌드합니다.

일반적으로 슈퍼클래스를 만들어 좋은 점은 모든 서브클래스에 공통적인 속성과 함수를 포함한다는 것입니다. 속성값과 함수 구현을 알 수 없으면 클래스를 추상으로 만듭니다. 예를 들어 Vegetables에는 모든 채소에 공통으로 적용되는 여러 속성이 있지만 구체적이지 않은 채소의 인스턴스를 만들 수는 없습니다. 모양이나 색상 등을 모르기 때문입니다. 따라서 Vegetable은 각 채소에 관한 구체적인 세부정보의 결정을 서브클래스에 맡기는 추상 클래스입니다.

추상 클래스 선언은 abstract 키워드로 시작합니다.

DwellingVegetable과 같은 추상 클래스가 됩니다. 여러 유형의 주택에 공통으로 적용되는 속성과 함수를 포함하지만 속성의 정확한 값과 함수 구현의 세부정보는 알 수 없습니다.

  1. Kotlin 플레이그라운드(https://developer.android.com/training/kotlinplayground)로 이동합니다.
  2. 편집기에서 main() 함수 내의 println("Hello, world!")를 삭제합니다.
  3. 그런 다음 이 코드를 main() 함수 아래에 추가하여 Dwelling이라는 abstract 클래스를 만듭니다.
abstract class Dwelling(){
}

건축 자재 속성 추가

Dwelling 클래스에서 주택마다 다를 수 있더라도 모든 주택에 적용되는 항목을 정의합니다. 모든 주택은 건축 자재로 만들어집니다.

  1. Dwelling 내에서 건축 자재를 나타내는 String 유형의 buildingMaterial 변수를 만듭니다. 건축 자재는 변경되지 않으므로 val을 사용하여 변경 불가능한 변수로 만듭니다.
val buildingMaterial: String
  1. 프로그램을 실행하면 다음 오류가 발생합니다.
Property must be initialized or be abstract

buildingMaterial 속성에 값이 없습니다. 사실 개발자가 값을 제공할 수 없습니다. 건물이 구체적이지 않아서 구체적인 자재로 만들 수 없기 때문입니다. 따라서 오류 메시지가 나타내듯이 abstract 키워드를 buildingMaterial 선언의 접두사로 지정하여 여기서 정의되지 않음을 나타낼 수 있습니다.

  1. 변수 정의에 abstract 키워드를 추가합니다.
abstract val buildingMaterial: String
  1. 코드를 실행하면 어떠한 작업도 실행되지 않지만 이제 오류 없이 컴파일됩니다.
  2. main() 함수에서 Dwelling 인스턴스를 만들고 코드를 실행합니다.
val dwelling = Dwelling()
  1. 추상 Dwelling 클래스의 인스턴스를 만들 수 없으므로 오류가 발생합니다.
Cannot create an instance of an abstract class
  1. 잘못된 코드를 삭제합니다.

지금까지 완료된 코드는 다음과 같습니다.

abstract class Dwelling(){
    abstract val buildingMaterial: String
}

수용 인원 속성 추가

주택의 또 다른 속성은 수용 인원입니다. 즉, 주택에서 살 수 있는 사람 수입니다.

모든 주택에는 변경되지 않는 수용 인원이 있습니다. 그러나 수용 인원은 Dwelling 슈퍼클래스 내에서 설정할 수 없습니다. 특정 유형의 주택에 관한 서브클래스에서 정의해야 합니다.

  1. Dwelling에서 capacity라는 abstract 정수 val을 추가합니다.
abstract val capacity: Int

거주자 수에 관한 비공개 속성 추가

모든 주택에는 주택에 거주하는 여러 residents(capacity 이하일 수 있음)가 있으므로 모든 서브클래스가 상속받아 사용하도록 Dwelling 슈퍼클래스에서 residents 속성을 정의합니다.

  1. residentsDwelling 클래스의 생성자에 전달되는 매개변수로 만들 수 있습니다. residents 속성은 var입니다. 인스턴스가 만들어진 후 거주자 수가 변경될 수 있기 때문입니다.
abstract class Dwelling(private var residents: Int) {

residents 속성은 private 키워드로 표시됩니다. 비공개는 Kotlin의 공개 상태 수정자로, residents 속성이 이 클래스에만 표시되고 이 클래스 내부에서 사용할 수 있다는 의미입니다. 프로그램의 다른 위치에서는 액세스할 수 없습니다. 속성이나 메서드를 비공개 키워드로 표시할 수 있습니다. 공개 상태 수정자가 지정되지 않은 다른 경우에는 속성과 메서드가 기본적으로 public이고 프로그램의 다른 위치에서 액세스할 수 있습니다. 건축 자재나 건물의 수용 인원에 관한 정보와 비교할 때 주택에 사는 사람 수는 일반적으로 비공개 정보이므로 이러한 판단은 합리적입니다.

주택의 capacity와 현재 residents 수가 모두 정의된 상태에서 주택의 또 다른 거주자를 위한 공간이 있는지 확인하는 hasRoom() 함수를 만들 수 있습니다. Dwelling 클래스에서 hasRoom() 함수를 정의하고 구현하면 됩니다. 공간이 있는지 계산하는 공식이 모든 주택에 동일하기 때문입니다. residents 수가 capacity보다 적으면 Dwelling에 공간이 있고 함수는 이 비교에 기반하여 truefalse를 반환해야 합니다.

  1. hasRoom() 함수를 Dwelling 클래스에 추가합니다.
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. 이 코드를 실행하면 오류가 발생하지 않습니다. 아직 눈에 띄는 작업이 실행되지 않습니다.

완성된 코드는 다음과 같이 표시됩니다.

abstract class Dwelling(private var residents: Int) {

   abstract val buildingMaterial: String
   abstract val capacity: Int

   fun hasRoom(): Boolean {
       return residents < capacity
   }
}

4. 서브클래스 만들기

SquareCabin 서브클래스 만들기

  1. Dwelling 클래스 아래에서 SquareCabin이라는 클래스를 만듭니다.
class SquareCabin
  1. 다음으로 SquareCabinDwelling과 관련이 있음을 나타내야 합니다. 코드에서 SquareCabinDwelling에서 확장된다고(또는 Dwelling)의 서브클래스라고) 나타내려 합니다. SquareCabinDwelling의 추상 부분에 관한 구현을 제공하기 때문입니다.

SquareCabin 클래스 이름 다음에 콜론(:)을 추가하고 상위 Dwelling 클래스를 초기화하는 호출을 추가하여 이 상속 관계를 나타냅니다. Dwelling 클래스 이름 뒤에 괄호를 추가해야 합니다.

class SquareCabin : Dwelling()
  1. 슈퍼클래스에서 확장할 때는 슈퍼클래스에서 예상하는 필수 매개변수를 전달해야 합니다. Dwelling에는 입력으로 residents 수가 필요합니다. 3과 같이 고정된 거주자 수를 전달할 수 있습니다.
class SquareCabin : Dwelling(3)

그러나 개발자는 프로그램의 유연성을 높여 가변적인 SquareCabins 거주자 수를 허용하기를 바랍니다. 따라서 residentsSquareCabin 클래스 정의의 매개변수로 만듭니다. residentsval,로 선언하지 마세요. 이미 상위 클래스 Dwelling에서 선언된 속성을 재사용하고 있기 때문입니다.

class SquareCabin(residents: Int) : Dwelling(residents)
  1. 코드를 실행합니다.
  2. 다음과 같이 오류가 발생합니다.
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling

추상 함수 및 변수를 선언하는 것은 나중에 값과 구현을 제공하겠다는 약속과 다름없습니다. 변수의 경우 추상 클래스의 모든 서브클래스가 값을 제공해야 한다는 의미입니다. 함수의 경우 모든 서브클래스가 함수 본문을 구현해야 한다는 의미입니다.

Dwelling 클래스에서 abstract 변수 buildingMaterial을 정의했습니다. SquareCabinDwelling의 서브클래스이므로 buildingMaterial 값을 제공해야 합니다. override 키워드를 사용하여 이 속성이 상위 클래스에서 정의되었고 이 클래스에서 재정의될 거라고 나타냅니다.

  1. SquareCabin 클래스 내에서 buildingMaterial 속성을 override하고 "Wood" 값을 할당합니다.
  2. capacity에도 같은 작업을 실행하여 거주민 6명이 SquareCabin에 살 수 있다고 나타냅니다.
class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

완성된 코드는 다음과 같이 표시됩니다.

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

코드를 테스트하려면 프로그램에서 SquareCabin 인스턴스를 만듭니다.

SquareCabin 사용

  1. DwellingSquareCabin 클래스 정의 앞에 빈 main() 함수를 삽입합니다.
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. main() 함수 내에서 거주민이 6명인 squareCabin이라는 SquareCabin 인스턴스를 만듭니다. 건축 자재, 수용 인원, hasRoom() 함수에 관한 print 문을 추가합니다.
fun main() {
    val squareCabin = SquareCabin(6)

    println("\nSquare Cabin\n============")
    println("Capacity: ${squareCabin.capacity}")
    println("Material: ${squareCabin.buildingMaterial}")
    println("Has room? ${squareCabin.hasRoom()}")
}

hasRoom() 함수는 SquareCabin 클래스에서 정의되지 않았고 Dwelling 클래스에서 정의되었습니다. SquareCabinDwelling 클래스의 서브클래스이므로 hasRoom() 함수는 무료로 상속되었습니다. 이제 hasRoom() 함수를 코드 스니펫에서 squareCabin.hasRoom()으로 표시된 대로 모든 SquareCabin 인스턴스에서 호출할 수 있습니다.

  1. 코드를 실행하면 다음과 같이 출력됩니다.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

거주민이 6으로, capacity와 같은 squareCabin을 만들었으므로 hasRoom()false를 반환합니다. 더 적은 수의 residentsSquareCabin 초기화를 시도할 수 있습니다. 그런 다음 프로그램을 다시 실행하면 hasRoom()true를 반환합니다.

with를 사용하여 코드 단순화

println() 문에서 squareCabin의 속성이나 함수를 참조할 때마다 squareCabin.을 얼마나 반복해야 하는지 확인합니다. 이 작업은 반복적이며 print 문을 복사하여 붙여넣을 때 오류의 원인이 될 수 있습니다.

클래스의 특정 인스턴스로 작업하고 이 인스턴스의 여러 속성과 함수에 액세스해야 한다면 with 문을 사용하여 '이 인스턴스 객체로 다음 작업을 모두 실행'하라고 나타낼 수 있습니다. with 키워드로 시작하고 괄호로 묶인 인스턴스 이름, 실행하려는 작업이 포함된 중괄호가 차례로 이어집니다.

with (instanceName) {
    // all operations to do with instanceName
}
  1. main() 함수에서 with를 사용하도록 print 문을 변경합니다.
  2. print 문에서 squareCabin.을 삭제합니다.
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. 다시 코드를 실행하여 오류 없이 실행되고 출력이 동일하게 표시되는지 확인합니다.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

완성된 코드는 다음과 같습니다.

fun main() {
    val squareCabin = SquareCabin(6)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

RoundHut 서브클래스 만들기

  1. SquareCabin과 같은 방식으로 또 다른 서브클래스인 RoundHutDwelling에 추가합니다.
  2. buildingMaterial을 재정의하고 "Straw" 값을 제공합니다.
  3. capacity를 재정의하고 4로 설정합니다.
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. main()에서 거주민이 3인 RoundHut 인스턴스를 만듭니다.
val roundHut = RoundHut(3)
  1. 아래 코드를 추가하여 roundHut에 관한 정보를 출력합니다.
with(roundHut) {
    println("\nRound Hut\n=========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}
  1. 코드를 실행하면 전체 프로그램이 다음과 같이 출력됩니다.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

이제 다음과 같은 클래스 계층 구조가 있습니다. Dwelling이 루트 클래스, SquareCabinRoundHutDwelling의 서브클래스입니다.

c19084f4a83193a0.png

RoundTower 서브클래스 만들기

이 클래스 계층 구조의 마지막 클래스는 둥근 타워입니다. 둥근 타워를 층이 여러 개인 돌로 만든 둥근 오두막으로 생각하면 됩니다. 따라서 RoundTowerRoundHut의 서브클래스로 만들 수 있습니다.

  1. RoundHut의 서브클래스인 RoundTower 클래스를 만듭니다. residents 매개변수를 RoundTower 생성자에 추가하고 이 매개변수를 RoundHut 슈퍼클래스 생성자에 전달합니다.
  2. buildingMaterial"Stone"으로 재정의합니다.
  3. capacity4로 설정합니다.
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. 이 코드를 실행하면 오류가 발생합니다.
This type is final, so it cannot be inherited from

이 오류는 RoundHut 클래스를 서브클래스로 분류하거나 상속할 수 없음을 의미합니다. 기본적으로 Kotlin에서 클래스는 최종 클래스이며 서브클래스로 분류할 수 없습니다. abstract 클래스나 open 키워드로 표시된 클래스에서만 상속할 수 있습니다. 따라서 상속될 수 있도록 RoundHut 클래스를 open 키워드로 표시해야 합니다.

  1. RoundHut 선언 시작 부분에 open 키워드를 추가합니다.
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. main()에서 roundTower 인스턴스를 만들고 관련 정보를 출력합니다.
 val roundTower = RoundTower(4)
with(roundTower) {
    println("\nRound Tower\n==========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}

전체 코드는 다음과 같습니다.

fun main() {
    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}

class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. 코드를 실행합니다. 이제 오류 없이 작동하고 다음과 같이 출력됩니다.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Round Tower
==========
Material: Stone
Capacity: 4
Has room? false

RoundTower에 여러 층 추가

RoundHut은 암묵적으로 단층 건물입니다. 타워에는 보통 여러 층이 있습니다.

수용 인원을 고려하면 타워에 층이 많을수록 수용 인원도 많아집니다.

여러 층이 있도록 RoundTower를 수정하고 층수에 따라 수용 인원을 조정합니다.

  1. 층수에 추가 정수 매개변수 val floors를 사용하도록 RoundTower 생성자를 업데이트합니다. residents 뒤에 삽입합니다. 이 매개변수를 상위 RoundHut 생성자에 전달하지 않아도 됩니다. floors가 여기 RoundTower에서 정의되고 RoundHut에는 floors가 없기 때문입니다.
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. 코드를 실행합니다. main() 메서드에서 roundTower를 만들 때 오류가 발생합니다. floors 인수에 숫자를 제공하지 않기 때문입니다. 누락된 인수를 추가할 수 있습니다.

또는 RoundTower의 클래스 정의에서 아래와 같이 floors의 기본값을 추가할 수 있습니다. 그런 다음 생성자에 전달되는 floors 값이 없으면 기본값을 사용하여 객체 인스턴스를 만들 수 있습니다.

  1. 코드에서 floors 선언 다음에 = 2를 추가하여 기본값 2를 할당합니다.
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    ...
}
  1. 코드를 실행합니다. 이제 RoundTower(4)가 기본값이 2층인 RoundTower 객체 인스턴스를 만들므로 컴파일됩니다.
  2. RoundTower 클래스에서 층수를 곱하도록 capacity를 업데이트합니다.
override val capacity = 4 * floors
  1. 코드를 실행하면 이제 2층 RoundTower의 수용 인원이 8입니다.

완성된 코드는 다음과 같습니다.

fun main() {

    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}

class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors
}

5. 계층 구조의 클래스 수정

바닥 면적 계산

이 연습에서는 추상 클래스에서 추상 함수를 선언한 다음 서브클래스에서 그 기능을 구현하는 방법을 알아봅니다.

모든 주택에는 바닥 면적이 있지만 주택의 형태에 따라 다르게 계산됩니다.

Dwelling 클래스에서 floorArea() 정의

  1. 먼저 abstract floorArea() 함수를 Dwelling 클래스에 추가합니다. Double을 반환합니다. Double은 String, Int와 같은 데이터 유형입니다. 소수점 뒤에 소수 부분이 오는 숫자(예: 5.8793)인 부동 소수점 숫자에 사용됩니다.
abstract fun floorArea(): Double

추상 클래스에서 정의된 모든 추상 메서드는 추상 클래스의 서브클래스에서 구현되어야 합니다. 코드를 실행하려면 먼저 서브클래스에서 floorArea()를 구현해야 합니다.

SquareCabin의 floorArea() 구현

buildingMaterial, capacity와 마찬가지로 상위 클래스에서 정의된 abstract 함수를 구현하므로 override 키워드를 사용해야 합니다.

  1. SquareCabin 클래스에서 아래와 같이 키워드 override로 시작하여 floorArea() 함수의 실제 구현 순으로 작성합니다.
override fun floorArea(): Double {

}
  1. 계산된 바닥 면적을 반환합니다. 직사각형이나 정사각형 면적은 가로 길이에 세로 길이를 곱한 값입니다. 함수의 본문은 return length * length입니다.
override fun floorArea(): Double {
    return length * length
}

길이는 클래스의 변수가 아니며 인스턴스마다 다르므로 SquareCabin 클래스의 생성자 매개변수로 추가할 수 있습니다.

  1. SquareCabin의 클래스 정의를 변경하여 Double 유형의 length 매개변수를 추가합니다. 속성을 val로 선언합니다. 건물의 길이는 변경되지 않기 때문입니다.
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {

따라서 Dwelling과 모든 서브클래스에는 생성자 인수로 residents가 있습니다. Dwelling 생성자의 첫 번째 인수이므로 모든 서브클래스 생성자의 첫 번째 인수로 만들고 모든 클래스 정의에서 동일한 순서로 인수를 배치하는 것이 좋습니다. 따라서 새 length 매개변수를 residents 매개변수 뒤에 삽입합니다.

  1. main()에서 squareCabin 인스턴스 만들기를 업데이트합니다. 50.0SquareCabin 생성자에 length로 전달합니다.
val squareCabin = SquareCabin(6, 50.0)
  1. squareCabinwith 문 내에서 바닥 면적의 print 문을 추가합니다.
println("Floor area: ${floorArea()}")

코드가 실행되지 않습니다. RoundHutfloorArea()도 구현해야 하기 때문입니다.

RoundHut의 floorArea() 구현

같은 방법으로 RoundHut의 바닥 면적을 구현합니다. RoundHutDwelling의 직접적인 서브클래스이므로 override 키워드를 사용해야 합니다.

원형 주택의 바닥 면적은 PI * 반지름 * 반지름입니다.

PI는 수학 값으로, 수학 라이브러리에 정의되어 있습니다. 라이브러리는 프로그램이 사용할 수 있는 프로그램 외부에 정의된 함수 및 값의 사전 정의된 모음입니다. 라이브러리 함수 또는 값을 사용하려면 컴파일러에 함수나 값을 사용할 거라고 알려야 합니다. 이렇게 하려면 프로그램으로 함수나 값을 가져오면 됩니다. 프로그램에서 PI를 사용하려면 kotlin.math.PI를 가져와야 합니다.

  1. Kotlin 수학 라이브러리에서 PI를 가져옵니다. 파일 상단의 main() 앞에 배치합니다.
import kotlin.math.PI
  1. RoundHutfloorArea() 함수를 구현합니다.
override fun floorArea(): Double {
    return PI * radius * radius
}

경고: kotlin.math.PI를 가져오지 않으면 오류가 발생하므로 이 라이브러리를 가져온 후 사용하세요. 또는 kotlin.math.PI * 반지름 * 반지름과 같이 PI의 정규화된 버전을 작성하면 import 문이 필요하지 않습니다.

  1. RoundHut 생성자를 업데이트하여 radius를 전달합니다.
open class RoundHut(
   residents: Int,
   val radius: Double) : Dwelling(residents) {
  1. main()에서 radius 10.0RoundHut 생성자에 전달하여 roundHut의 초기화를 업데이트합니다.
val roundHut = RoundHut(3, 10.0)
  1. roundHutwith 문 내에 print 문을 추가합니다.
println("Floor area: ${floorArea()}")

RoundTower의 floorArea() 구현

코드가 아직 실행되지 않고 다음 오류와 함께 실패합니다.

Error: No value passed for parameter 'radius'

RoundTower에서 프로그램이 컴파일되려면 RoundHut에서 상속되므로 floorArea()를 구현하지 않아도 되지만 상위 RoundHutradius 인수도 같도록 RoundTower 클래스 정의를 업데이트해야 합니다.

  1. radius도 사용하도록 RoundTower 생성자를 변경합니다. radiusresidents 뒤, floors 앞에 배치합니다. 기본값이 있는 변수는 끝에 나열하는 것이 좋습니다. radius를 상위 클래스 생성자에 전달해야 합니다.
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. main()에서 roundTower의 초기화를 업데이트합니다.
val roundTower = RoundTower(4, 15.5)
  1. floorArea()를 호출하는 print 문을 추가합니다.
println("Floor area: ${floorArea()}")
  1. 이제 코드를 실행할 수 있습니다.
  2. RoundTower의 계산이 정확하지 않습니다. RoundHut에서 상속되고 floors 수를 고려하지 않기 때문입니다.
  3. RoundTower에서 override floorArea()하여 면적에 층수를 곱하는 다른 구현을 제공할 수 있습니다. 추상 클래스(Dwelling)에서 함수를 정의하여 서브클래스(RoundHut)에서 구현한 다음 서브클래스의 서브클래스(RoundTower)에서 다시 재정의할 수 있습니다. 원하는 기능은 상속하고 원하지 않는 기능은 재정의할 수 있어서 매우 유용합니다.
override fun floorArea(): Double {
    return PI * radius * radius * floors
}

이 코드는 작동하지만 이미 RoundHut 상위 클래스에 있는 반복되는 코드를 방지하는 방법이 있습니다. 상위 RoundHut 클래스에서 PI * radius * radius를 반환하는 floorArea() 함수를 호출할 수 있습니다. 그런 다음 결과에 floors의 수를 곱합니다.

  1. RoundTower에서 floorArea()의 슈퍼클래스 구현을 사용하도록 floorArea()를 업데이트합니다. super 키워드를 사용하여 상위 클래스에 정의된 함수를 호출합니다.
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. 다시 코드를 실행하면 RoundTower에서 여러 층의 바닥 면적을 올바르게 출력합니다.

완성된 코드는 다음과 같습니다.

import kotlin.math.PI

fun main() {

    val squareCabin = SquareCabin(6, 50.0)
    val roundHut = RoundHut(3, 10.0)
    val roundTower = RoundTower(4, 15.5)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }
 }

abstract class Dwelling(private var residents: Int) {

    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
        return residents < capacity
}

    abstract fun floorArea(): Double
}

class SquareCabin(residents: Int,
    val length: Double) : Dwelling(residents) {

    override val buildingMaterial = "Wood"
    override val capacity = 6

    override fun floorArea(): Double {
       return length * length
    }
}

open class RoundHut(residents: Int,
    val radius: Double) : Dwelling(residents) {

    override val buildingMaterial = "Straw"
    override val capacity = 4

    override fun floorArea(): Double {
        return PI * radius * radius
    }
}

class RoundTower(residents: Int, radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors

    override fun floorArea(): Double {
        return super.floorArea() * floors
    }
}

출력은 다음과 같이 표시됩니다.

Square Cabin
============
Capacity: 6
Material: Wood
Has room? false
Floor area: 2500.0

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true
Floor area: 314.1592653589793

Round Tower
==========
Material: Stone
Capacity: 8
Has room? true
Floor area: 1509.5352700498956

새 거주자가 방을 갖도록 허용

거주자 수를 1씩 늘리는 getRoom() 함수를 사용하여 새 거주자가 방을 갖도록 하는 기능을 추가합니다. 이 로직은 모든 주택에 동일하므로 Dwelling에서 함수를 구현할 수 있습니다. 이를 통해 모든 서브클래스와 그 하위 요소에서 함수를 사용할 수 있습니다. 간단합니다.

참고

  • 수용 인원이 남은 경우에만 거주자를 추가하는 if 문을 사용합니다.
  • 결과 메시지를 출력합니다.
  • residents++residents = residents + 1의 약어로 사용하여 residents 변수에 1을 추가할 수 있습니다.
  1. Dwelling 클래스에서 getRoom() 함수를 구현합니다.
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. roundHutwith 문 블록에 print 문을 추가하여 getRoom()hasRoom()이 함께 사용되면 어떤 일이 발생하는지 관찰합니다.
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()

이러한 print 문은 다음과 같이 출력됩니다.

Has room? true
You got a room!
Has room? false
Sorry, at capacity and no rooms left.

자세한 내용은 솔루션 코드를 참고하세요.

둥근 주택에 카펫 맞추기

RoundHut 또는 RoundTower에 사용할 카펫의 한 면의 길이를 알아야 한다고 가정해 보겠습니다. 함수를 RoundHut에 넣어 모든 둥근 주택에서 사용할 수 있도록 합니다.

2e328a198a82c793.png

  1. 먼저 kotlin.math 라이브러리에서 sqrt() 함수를 가져옵니다.
import kotlin.math.sqrt
  1. RoundHut 클래스에서 calculateMaxCarpetLength() 함수를 구현합니다. 원에 들어갈 수 있는 정사각형 카펫의 길이를 계산하는 수식은 sqrt(2) * radius입니다. 이는 위 다이어그램에 설명되어 있습니다.
fun calculateMaxCarpetLength(): Double {

    return sqrt(2.0) * radius
}

수학 함수 sqrt(2.0)Double2.0을 전달합니다. 함수의 반환 유형이 Integer가 아닌 Double이기 때문입니다.

  1. 이제 RoundHutRoundTower 인스턴스에서 calculateMaxCarpetLength() 메서드를 호출할 수 있습니다. print 문을 main() 함수의 roundHutroundTower에 추가합니다.
println("Carpet Length: ${calculateMaxCarpetLength()}")

자세한 내용은 솔루션 코드를 참고하세요.

축하합니다. 속성과 함수가 포함된 완전한 클래스 계층 구조를 만들어 더 유용한 클래스를 만드는 데 필요한 모든 것을 알아봤습니다.

6. 솔루션 코드

다음은 주석이 포함된 이 Codelab의 전체 솔루션 코드입니다.

/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/

import kotlin.math.PI
import kotlin.math.sqrt

fun main() {
   val squareCabin = SquareCabin(6, 50.0)
   val roundHut = RoundHut(3, 10.0)
   val roundTower = RoundTower(4, 15.5)

   with(squareCabin) {
       println("\nSquare Cabin\n============")
       println("Capacity: ${capacity}")
       println("Material: ${buildingMaterial}")
       println("Floor area: ${floorArea()}")
   }

   with(roundHut) {
       println("\nRound Hut\n=========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Carpet size: ${calculateMaxCarpetLength()}")
   }

   with(roundTower) {
       println("\nRound Tower\n==========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Carpet Length: ${calculateMaxCarpetLength()}")
   }
}

/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
   abstract val buildingMaterial: String
   abstract val capacity: Int

   /**
    * Calculates the floor area of the dwelling.
    * Implemented by subclasses where shape is determined.
    *
    * @return floor area
    */
   abstract fun floorArea(): Double

   /**
    * Checks whether there is room for another resident.
    *
    * @return true if room available, false otherwise
    */
   fun hasRoom(): Boolean {
       return residents < capacity
   }

   /**
    * Compares the capacity to the number of residents and
    * if capacity is larger than number of residents,
    * add resident by increasing the number of residents.
    * Print the result.
    */
   fun getRoom() {
       if (capacity > residents) {
           residents++
           println("You got a room!")
       } else {
           println("Sorry, at capacity and no rooms left.")
       }
   }

   }

/**
* A square cabin dwelling.
*
*  @param residents Current number of residents
*  @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
   override val buildingMaterial = "Wood"
   override val capacity = 6

   /**
    * Calculates floor area for a square dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return length * length
   }

}

/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
       residents: Int, val radius: Double) : Dwelling(residents) {

   override val buildingMaterial = "Straw"
   override val capacity = 4

   /**
    * Calculates floor area for a round dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return PI * radius * radius
   }

   /**
    *  Calculates the max length for a square carpet
    *  that fits the circular floor.
    *
    * @return length of square carpet
    */
    fun calculateMaxCarpetLength(): Double {
        return sqrt(2.0) * radius
    }

}

/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
       residents: Int,
       radius: Double,
       val floors: Int = 2) : RoundHut(residents, radius) {

   override val buildingMaterial = "Stone"

   // Capacity depends on the number of floors.
   override val capacity = floors * 4

   /**
    * Calculates the total floor area for a tower dwelling
    * with multiple stories.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return super.floorArea() * floors
   }
}

7. 요약

이 Codelab을 통해 학습한 내용은 다음과 같습니다.

  • 하위 클래스가 상위 클래스에서 기능을 상속받는 클래스 트리인 클래스 계층 구조를 만드는 방법. 속성과 함수가 서브클래스에 상속됩니다.
  • 일부 기능을 서브클래스에서 구현하도록 남기는 abstract 클래스를 만드는 방법. 따라서 abstract 클래스는 인스턴스화할 수 없습니다.
  • abstract 클래스의 서브클래스를 만드는 방법
  • override 키워드를 사용하여 서브클래스의 속성과 함수를 재정의하는 방법
  • super 키워드를 사용하여 상위 클래스의 함수와 속성을 참조하는 방법
  • 서브클래스로 분류할 수 있도록 클래스를 open으로 만드는 방법
  • 속성을 private으로 만들어 클래스 내에서만 사용할 수 있도록 하는 방법
  • with 구문을 사용하여 동일한 객체 인스턴스에서 여러 호출을 실행하는 방법
  • kotlin.math 라이브러리에서 기능을 가져오는 방법

8. 자세히 알아보기