프로그래머를 위한 Kotlin 부트캠프 4: 객체 지향 프로그래밍

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

소개

이 Codelab에서는 Kotlin 프로그램을 만들고 Kotlin의 클래스와 객체를 알아봅니다. 다른 객체 지향 언어를 알고 있다면 이 콘텐츠 대부분이 익숙할 것입니다. 그러나 Kotlin에는 작성해야 하는 코드의 양을 줄이기 위해 몇 가지 중요한 차이점이 있습니다. 추상 클래스 및 인터페이스 위임에 관해서도 알아봅니다.

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

기본 요건

  • 유형, 연산자, 반복을 포함한 Kotlin의 기본사항
  • Kotlin의 함수 구문
  • 객체 지향 프로그래밍의 기본사항
  • IntelliJ IDEA 또는 Android 스튜디오와 같은 IDE의 기본사항

학습할 내용

  • Kotlin으로 클래스를 만들고 속성에 액세스하는 방법
  • Kotlin에서 클래스 생성자를 만들고 사용하는 방법
  • 서브클래스를 만드는 방법 및 상속이 작동하는 방식
  • 추상 클래스, 인터페이스, 인터페이스 위임에 관한 정보
  • 데이터 클래스 만들기 및 사용 방법
  • 싱글톤, enum 및 봉인 클래스 사용 방법

실습할 내용

  • 속성이 포함된 클래스 만들기
  • 클래스의 생성자 만들기
  • 서브클래스 만들기
  • 추상 클래스 및 인터페이스 예시 검토
  • 간단한 데이터 클래스 만들기
  • 싱글톤, enum 및 봉인 클래스 알아보기

다음 프로그래밍 약관은 이미 익숙할 것입니다.

  • 클래스는 객체의 청사진입니다. 예를 들어 Aquarium 클래스는 수족관 객체를 만드는 청사진입니다.
  • 객체는 클래스의 인스턴스이며 수족관 객체는 하나의 실제 Aquarium입니다.
  • 속성Aquarium의 길이, 너비, 높이와 같은 클래스의 특성입니다.
  • 메서드라고도 하는 메서드는 클래스의 기능입니다. 메서드는 객체를 사용하여 실행할 수 있는 작업입니다. 예를 들어 Aquarium 객체를 fillWithWater()할 수 있습니다.
  • 인터페이스는 클래스에서 구현할 수 있는 사양입니다. 예를 들어 청소는 수족관이 아닌 물건에 공통적이며, 청소는 보통 다른 물체에 관해 비슷한 방식으로 이루어집니다. 따라서 clean() 메서드를 정의하는 Clean라는 인터페이스가 있을 수 있습니다. Aquarium 클래스는 Clean 인터페이스를 구현하여 부드러운 스펀지로 수족관을 청소합니다.
  • 패키지는 관련 코드를 그룹화하여 정리하거나 코드 라이브러리를 만드는 방법입니다. 패키지가 생성되면 패키지 콘텐츠를 다른 파일로 가져와서 코드 및 클래스를 재사용할 수 있습니다.

이 작업에서는 일부 속성과 메서드를 사용하여 새 패키지와 클래스를 만듭니다.

1단계: 패키지 만들기

패키지를 사용하면 코드를 체계적으로 정리할 수 있습니다.

  1. Project 창의 Hello Kotlin 프로젝트에서 src 폴더를 마우스 오른쪽 버튼으로 클릭합니다.
  2. New > Package를 선택하고 이름을 example.myapp로 지정합니다.

2단계: 속성이 포함된 클래스 만들기

클래스는 키워드 class로 정의되며 규칙에 따라 클래스 이름은 대문자로 시작합니다.

  1. example.myapp 패키지를 마우스 오른쪽 버튼으로 클릭합니다.
  2. New > Kotlin File / Class를 선택합니다.
  3. Kind에서 Class를 선택하고 클래스 이름을 Aquarium로 지정합니다. IntelliJ IDEA는 파일에 패키지 이름을 포함하고 빈 Aquarium 클래스를 생성합니다.
  4. Aquarium 클래스 내에서 너비, 높이, 길이 (센티미터 단위)로 var 속성을 정의하고 초기화합니다. 기본값으로 속성을 초기화합니다.
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

내부적으로 Kotlin은 Aquarium 클래스에서 정의한 속성의 getter 및 setter를 자동으로 생성하므로 속성에 직접 액세스할 수 있습니다(예: myAquarium.length).

3단계: main() 함수 만들기

main() 함수를 보유할 main.kt이라는 새 파일을 만듭니다.

  1. 왼쪽의 Project 창에서 example.myapp 패키지를 마우스 오른쪽 버튼으로 클릭합니다.
  2. New > Kotlin File / Class를 선택합니다.
  3. Kind 드롭다운에서 선택 항목을 File로 유지한 다음 파일 이름을 main.kt로 지정합니다. IntelliJ IDEA에는 패키지 이름이 포함되어 있지만 파일의 클래스 정의는 포함되어 있지 않습니다.
  4. buildAquarium() 함수를 정의하고 그 내부에서 Aquarium 인스턴스를 만듭니다. 인스턴스를 만들려면 클래스를 함수인 것처럼 참조합니다. Aquarium() 이렇게 하면 클래스의 생성자를 호출하고 다른 언어로 new를 사용하는 것과 비슷하게 Aquarium 클래스의 인스턴스를 만듭니다.
  5. main() 함수를 정의하고 buildAquarium()를 호출합니다.
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

4단계: 메서드 추가하기

  1. Aquarium 클래스에서 아쿠아리움의 크기 속성을 출력하는 메서드를 추가합니다.
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. main.ktbuildAquarium()에서 myAquariumprintSize() 메서드를 호출합니다.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. main() 함수 옆의 녹색 삼각형을 클릭하여 프로그램을 실행합니다. 결과를 관찰합니다.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. buildAquarium()에서 높이를 60으로 설정하는 코드를 추가하고 변경된 크기 속성을 출력합니다.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

이 작업에서는 클래스의 생성자를 만들고 속성을 계속 사용합니다.

1단계: 생성자 만들기

이 단계에서는 첫 번째 작업에서 만든 Aquarium 클래스에 생성자를 추가합니다. 앞의 예에서 Aquarium의 모든 인스턴스는 동일한 차원으로 생성됩니다. 속성을 설정하여 측정기준을 변경할 수 있지만, 처음부터 올바른 크기를 만드는 것이 더 간단합니다.

일부 프로그래밍 언어에서는 생성자가 클래스와 같은 이름을 가진 클래스 내에 메서드를 생성하여 정의됩니다. Kotlin에서는 클래스가 메서드인 것처럼 괄호 안에 매개변수를 지정하여 클래스 선언 자체에서 생성자를 직접 정의합니다. Kotlin의 함수와 마찬가지로 이러한 매개변수에는 기본값을 포함할 수 있습니다.

  1. 앞서 만든 Aquarium 클래스에서 클래스 정의를 변경하여 length, width, height의 기본값이 있는 3개의 생성자 매개변수를 포함하고 상응하는 속성에 할당합니다.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. 좀 더 간단한 Kotlin 방법은 var 또는 val를 사용하여 생성자와 함께 속성을 직접 정의하는 것이며, Kotlin은 자동으로 getter와 setter를 생성합니다. 그런 다음 클래스 본문에서 속성 정의를 삭제할 수 있습니다.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. 이 생성자를 사용하여 Aquarium 객체를 만들 때 인수를 지정하지 않고 기본값을 가져오거나, 일부만 지정하거나, 모두 맞춤설정하고 맞춤 크기의 Aquarium를 만들 수 있습니다. buildAquarium() 함수에서 이름이 지정된 매개변수를 사용하여 Aquarium 객체를 만드는 다양한 방법을 시도해 봅니다.
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

생성자에 오버로드되고 이러한 각 경우에 대해 서로 다른 버전을 작성할 필요가 없었음을 알 수 있습니다 (또한 다른 조합에 대해서도 몇 가지 버전을 추가로 생성해야 함). Kotlin은 기본값 및 이름이 지정된 매개변수에서 필요한 항목을 생성합니다.

2단계: init 블록 추가

위의 생성자 예에서는 속성을 선언하고 표현식의 값을 할당합니다. 생성자에 더 많은 초기화 코드가 필요한 경우 하나 이상의 init 블록에 배치할 수 있습니다. 이 단계에서는 Aquarium 클래스에 init 블록을 추가합니다.

  1. Aquarium 클래스에서 init 블록을 추가하여 객체가 초기화되고 출력하는 두 번째 블록을 추가하여 리터 단위로 볼륨을 출력합니다.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

init 블록은 클래스 정의에 표시된 순서대로 실행되고 생성자가 호출될 때 모든 블록이 실행됩니다.

3단계: 보조 생성자 알아보기

이 단계에서는 보조 생성자에 관해 알아보고 클래스에 생성자를 추가합니다. Kotlin 클래스는 하나 이상의 init 블록을 포함할 수 있는 기본 생성자 외에도 생성자 오버로드를 허용하는 하나 이상의 보조 생성자, 즉 생성자가 다른 생성자를 가질 수 있습니다.

  1. Aquarium 클래스에서 constructor 키워드를 사용하여 여러 물고기를 인수로 사용하는 보조 생성자를 추가합니다. 어류의 수에 따라 수족관의 리터 단위로 계산된 val 수조 속성을 만듭니다. 물고기당 2리터 (2,000cm^3)의 물과 물이 쏟아지지 않도록 조금 더 넓은 공간을 남겨두세요.
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. 보조 생성자 내에서 (기본 생성자에 설정된) 길이와 너비를 동일하게 유지하고 탱크를 지정된 볼륨으로 만드는 데 필요한 높이를 계산합니다.
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. buildAquarium() 함수에서 새로운 보조 생성자를 사용하여 Aquarium를 만드는 호출을 추가합니다. 크기 및 볼륨을 인쇄합니다.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

볼륨은 보조 생성자가 실행되기 전에 기본 생성자의 init 블록에 의해 한 번, buildAquarium()의 코드에 한 번, 이렇게 두 번 출력됩니다.

기본 생성자에도 constructor 키워드를 포함할 수 있었지만 대부분의 경우 필요하지 않습니다.

4단계: 새 속성 getter 추가

이 단계에서는 명시적인 속성 getter를 추가합니다. 속성을 정의할 때 getter와 setter가 자동으로 정의되지만, 속성 값을 조정하거나 계산해야 하는 경우도 있습니다. 예를 들어 위에서 Aquarium의 볼륨을 출력했습니다. 변수 및 이에 대한 getter를 정의하여 볼륨을 속성으로 사용할 수 있습니다. volume를 계산해야 하므로 getter는 한 줄 함수로 처리할 수 있는 계산된 값을 반환해야 합니다.

  1. Aquarium 클래스에서 volume라는 Int 속성을 정의하고 다음 줄의 볼륨을 계산하는 get() 메서드를 정의합니다.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. 볼륨을 출력하는 init 블록을 삭제합니다.
  2. buildAquarium()에서 볼륨을 출력하는 코드를 삭제합니다.
  3. printSize() 메서드에서 볼륨을 출력하는 줄을 추가합니다.
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

크기 및 볼륨은 이전과 동일하지만 볼륨은 기본 생성자와 보조 생성자에서 모두 객체가 완전히 초기화된 후에만 한 번 출력됩니다.

5단계: 속성 setter 추가

이 단계에서는 볼륨의 새 속성 setter를 만듭니다.

  1. 두 번 이상 설정할 수 있도록 Aquarium 클래스에서 volumevar으로 변경합니다.
  2. 제공된 물의 양을 기준으로 높이를 다시 계산하는 getter 아래에 set() 메서드를 추가하여 volume 속성에 setter를 추가합니다. 규칙에 따라 setter 매개변수의 이름은 value이지만 원하는 경우 변경할 수 있습니다.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. buildAquarium()에서 수족관의 볼륨을 70리터로 설정하는 코드를 추가합니다. 새 크기를 인쇄합니다.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. 프로그램을 다시 실행하고 변경된 높이와 볼륨을 확인합니다.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

지금까지 코드에 public 또는 private와 같은 공개 상태 한정자가 없습니다. 기본적으로 Kotlin의 모든 것은 공개적이기 때문에 클래스, 메서드, 속성, 멤버 변수를 비롯하여 어디에서나 액세스할 수 있기 때문입니다.

Kotlin에서 클래스, 객체, 인터페이스, 생성자, 함수, 속성 및 setter에는 공개 상태 수정자가 있을 수 있습니다.

  • public는 클래스 외부에 표시됩니다. 클래스의 변수와 메서드를 포함하여 모든 항목이 기본적으로 공개됩니다.
  • internal은 모듈 내에서만 표시됨을 의미합니다. 모듈은 함께 컴파일된 Kotlin 파일 세트입니다(예: 라이브러리 또는 애플리케이션).
  • private는 그 클래스 (또는 함수를 다루는 경우 소스 파일)에만 표시됩니다.
  • protectedprivate와 동일하지만 모든 서브클래스에 표시됩니다.

자세한 내용은 Kotlin 문서의 공개 상태 수정자를 참고하세요.

구성원 변수

클래스 내의 클래스 또는 멤버 변수는 기본적으로 public입니다. 이를 var로 정의하면 변경할 수 있습니다(즉, 읽기 및 쓰기가 가능합니다). val로 정의하면 초기화 후 읽기 전용으로 유지됩니다.

코드에서 읽거나 쓸 수 있지만 외부 코드에서만 읽을 수 있는 속성을 원하는 경우 다음과 같이 속성과 getter를 공개로 두고 setter를 비공개로 선언할 수 있습니다.

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

이 작업에서는 Kotlin의 서브클래스 및 상속이 작동하는 방식을 알아봅니다. 다른 언어로 본 내용과 유사하지만 몇 가지 차이점이 있습니다.

Kotlin에서는 기본적으로 클래스를 서브클래스로 분류할 수 없습니다. 마찬가지로 속성과 멤버 변수는 서브클래스에서 재정의할 수 없지만 액세스할 수 있습니다.

서브클래스로 분류할 수 있도록 클래스를 open로 표시해야 합니다. 마찬가지로 서브클래스에서 속성과 멤버 변수를 open로 재정의해야 합니다. 클래스 인터페이스의 일부로 실수로 구현 세부정보가 유출되지 않도록 하려면 open 키워드가 필요합니다.

1단계: 수족관 수업 열기

이 단계에서는 다음 단계에서 재정의할 수 있도록 Aquarium 클래스를 open로 만듭니다.

  1. open 클래스로 Aquarium 클래스와 모든 속성을 표시합니다.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. 값이 "rectangle"인 열린 shape 속성을 추가합니다.
   open val shape = "rectangle"
  1. Aquarium 볼륨의 90% 를 반환하는 getter를 사용하여 열려 있는 water 속성을 추가합니다.
    open var water: Double = 0.0
        get() = volume * 0.9
  1. printSize() 메서드에 코드를 추가하여 모양을 인쇄하고 물의 양은 볼륨의 백분율로 출력합니다.
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. buildAquarium()에서 코드를 변경하여 width = 25, length = 25, height = 40를 사용하여 Aquarium를 만듭니다.
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. 프로그램을 실행하고 새 출력을 관찰합니다.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

2단계: 서브클래스 만들기

  1. 직사각형 탱크 대신 둥근 실린더 탱크를 구현하는 TowerTank의 서브클래스인 Aquarium을 만듭니다. Aquarium 클래스와 동일한 파일에 다른 클래스를 추가할 수 있으므로 Aquarium 아래에 TowerTank을 추가할 수 있습니다.
  2. TowerTank에서 생성자에 정의된 height 속성을 재정의합니다. 속성을 재정의하려면 서브클래스에서 override 키워드를 사용하세요.
  1. TowerTank의 생성자가 diameter를 취하도록 합니다. Aquarium 슈퍼클래스에서 생성자를 호출할 때 lengthwidth 모두에 diameter를 사용합니다.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. 원기둥을 계산하려면 볼륨 속성을 재정의합니다. 원기둥의 수식은 파이에 반지름의 제곱과 높이를 곱한 값입니다. java.lang.Math에서 상수 PI를 가져와야 합니다.
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. TowerTank에서 water 속성을 볼륨의 80% 로 재정의합니다.
override var water = volume * 0.8
  1. shape"cylinder"으로 재정의합니다.
override val shape = "cylinder"
  1. 최종 TowerTank 클래스는 아래 코드와 같이 표시됩니다.

Aquarium.kt:

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. buildAquarium()에서 지름이 25cm, 높이가 45cm인 TowerTank를 만듭니다. 사이즈를 인쇄합니다.

main.kt:

package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium(width = 25, length = 25, height = 40)
    myAquarium.printSize()
    val myTower = TowerTank(diameter = 25, height = 40)
    myTower.printSize()
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)
aquarium initializing
cylinder
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 18 l Water: 14.4 l (80.0% full)

일부 관련 클래스 간에 공유할 공통 동작이나 속성을 정의하고자 할 때가 있습니다. Kotlin은 이를 위한 인터페이스와 추상 클래스, 두 가지 방법을 제공합니다. 이 작업에서는 모든 물고기에게 공통적인 속성에 대한 추상 AquariumFish 클래스를 만듭니다. 모든 물고기에게 공통된 동작을 정의하는 FishAction라는 인터페이스를 만듭니다.

  • 추상 클래스 및 인터페이스 자체는 인스턴스화할 수 없습니다. 즉, 이러한 유형의 객체를 직접 만들 수 없습니다.
  • 추상 클래스에는 생성자가 있습니다.
  • 인터페이스에 생성자 로직을 포함하거나 상태를 저장할 수 없습니다.

1단계: 추상 클래스 만들기

  1. example.myapp 아래에 AquariumFish.kt이라는 새 파일을 만듭니다.
  2. AquariumFish라고도 하는 클래스를 만들고 abstract로 표시합니다.
  3. String 속성 color를 추가하고 abstract로 표시합니다.
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. AquariumFish, Shark, Plecostomus의 두 개의 서브클래스를 만듭니다.
  2. color이 추상 클래스이므로 서브클래스가 구현해야 합니다. Shark를 회색과 Plecostomus 금색으로 만듭니다.
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. main.kt에서 클래스를 테스트하는 makeFish() 함수를 만듭니다. SharkPlecostomus를 인스턴스화한 다음 각 색상을 출력합니다.
  2. main()에서 이전 테스트 코드를 삭제하고 makeFish() 호출을 추가합니다. 코드는 다음 코드와 같이 표시됩니다.

main.kt:

package example.myapp

fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()

    println("Shark: ${shark.color}")
    println("Plecostomus: ${pleco.color}")
}

fun main () {
    makeFish()
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
⇒ Shark: gray 
Plecostomus: gold

다음 다이어그램은 추상 클래스 AquariumFish를 서브클래스로 분류하는 Shark 클래스와 Plecostomus 클래스를 나타냅니다.

추상 클래스, 수족관 피싱, 두 개의 서브클래스 샤크 및 플레코투무스를 보여주는 다이어그램

2단계: 인터페이스 만들기

  1. AquariumFish.kt에서 메서드 eat()FishAction이라는 인터페이스를 만듭니다.
interface FishAction  {
    fun eat()
}
  1. 각 서브클래스에 FishAction를 추가하고 물고기의 역할을 출력하도록 eat()를 구현합니다.
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. makeFish() 함수에서 만든 각 물고기가 eat()를 호출하여 먹이를 가지도록 합니다.
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. 프로그램을 실행하고 출력을 관찰합니다.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

다음 다이어그램은 Shark 클래스와 Plecostomus 클래스를 나타냅니다. 두 클래스 모두 FishAction 인터페이스로 구성되어 있고 구현합니다.

추상 클래스와 인터페이스를 사용해야 하는 경우

위의 예시는 간단하지만 상호 관련된 클래스와 클래스가 많은 경우 추상 클래스 및 인터페이스를 사용하면 더 깔끔하고 체계적이며 쉽게 관리할 수 있습니다.

위에서 언급했듯이 추상 클래스에는 생성자가 포함될 수 있지만 인터페이스는 허용되지 않지만 매우 유사합니다. 각각 사용해야 하는 경우는 언제인가요?

인터페이스를 사용하여 클래스를 작성하면 클래스 기능이 포함된 클래스 인스턴스를 통해 확장됩니다. 컴포지션은 추상 클래스의 상속보다 코드를 재사용하기 쉽고 추론하는 경향이 있습니다. 한 클래스에 여러 인터페이스를 사용할 수도 있지만 한 추상 클래스에서만 서브클래스로 분류할 수 있습니다.

컴포지션을 사용하면 캡슐화가 잘 되고, 커플링(종속 항목)이 줄어들며, 인터페이스가 깔끔하고, 사용 가능한 코드가 많아집니다. 따라서 인터페이스와 함께 구성을 사용하는 것이 좋습니다. 반면에 추상 클래스의 상속은 일부 문제에 자연스러운 편입니다. 따라서 컴포지션을 선호하는 것이 좋지만, 상속이 적합하다면 Kotlin도 가능합니다.

  • 메서드 수가 많거나 인터페이스가 하나(예: 아래 AquariumAction)인 경우 인터페이스를 사용합니다.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • 수업을 완료할 수 없을 때는 추상 클래스를 사용하세요. 예를 들어 AquariumFish 클래스로 돌아가서, 모든 AquariumFishFishAction를 구현하도록 하고 color을 추상화하면서 eat의 기본 구현을 제공할 수 있습니다. 실제로 물고기의 기본 색상은 없기 때문입니다.
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

이전 작업에서는 추상 클래스, 인터페이스, 컴포지션 개념을 도입했습니다. 인터페이스 위임은 인터페이스의 메서드를 도우미 (또는 대리자) 객체가 구현하는 고급 기법이며, 그런 다음 클래스에서 사용됩니다. 이 기법은 관련 없는 일련의 클래스에서 인터페이스를 사용할 때 유용할 수 있습니다. 필요한 인터페이스 기능을 별도의 도우미 클래스에 추가하고 각 클래스에서 도우미 클래스의 인스턴스를 사용하여 기능을 구현합니다.

이 작업에서는 인터페이스 위임을 사용하여 클래스에 기능을 추가합니다.

1단계: 새 인터페이스 만들기

  1. AquariumFish.kt에서 AquariumFish 클래스를 삭제합니다. AquariumFish 클래스에서 상속받는 대신 PlecostomusShark는 물고기 작업과 색상의 인터페이스를 모두 구현합니다.
  2. 색상을 문자열로 정의하는 새 인터페이스 FishColor를 만듭니다.
interface FishColor {
    val color: String
}
  1. Plecostomus를 변경하여 두 개의 인터페이스 FishAction, FishColor를 구현합니다. FishColor에서 colorFishAction에서 eat()를 재정의해야 합니다.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. AquariumFish에서 상속받는 대신 두 인터페이스 FishActionFishColor도 구현하도록 Shark 클래스를 변경합니다.
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. 완성된 코드는 다음과 같이 표시됩니다.
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}

class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

2단계: 싱글톤 클래스 만들기

다음으로, FishColor를 구현하는 도우미 클래스를 만들어 위임 부분의 설정을 구현합니다. FishColor를 구현하는 GoldColor라는 기본 클래스를 만듭니다. 이 클래스만 사용하면 색상이 금색입니다.

이들은 모두 같은 기능을 하기 때문에 GoldColor의 인스턴스를 여러 개 만들 필요가 없습니다. 따라서 Kotlin에서는 class 대신 키워드 object를 사용하여 그 인스턴스를 하나만 만들 수 있는 클래스를 선언할 수 있습니다. Kotlin은 이러한 인스턴스 하나를 만들고 인스턴스가 인스턴스 이름으로 참조됩니다. 그러면 다른 모든 객체가 이 인스턴스 하나만 사용할 수 있습니다. 이 클래스의 다른 인스턴스를 만들 방법은 없습니다. 싱글톤 패턴에 익숙하다면 Kotlin에서 싱글톤을 구현하는 것입니다.

  1. AquariumFish.kt에서 GoldColor의 객체를 만듭니다. 색상을 재정의합니다.
object GoldColor : FishColor {
   override val color = "gold"
}

3단계: FishColor 인터페이스 위임 추가하기

이제 인터페이스 위임을 사용할 준비가 되었습니다.

  1. AquariumFish.kt에서 Plecostomuscolor 재정의를 삭제합니다.
  2. Plecostomus 클래스를 변경하여 GoldColor에서 색상을 가져옵니다. 이렇게 하려면 클래스 선언에 by GoldColor를 추가하여 위임을 만들면 됩니다. 즉, FishColor를 구현하는 대신 GoldColor에서 제공하는 구현을 사용합니다. 따라서 color에 액세스할 때마다 GoldColor에 위임됩니다.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

강의는 그대로 있겠지만 모든 플레코는 금색이지만, 이 물고기는 실제로 다양한 색상으로 제공됩니다. GoldColorPlecostomus의 기본 색상으로 사용하여 색상의 생성자 매개변수를 추가하면 이 문제를 해결할 수 있습니다.

  1. Plecostomus 클래스를 변경하여 생성자와 함께 전달된 fishColor를 가져오고 기본값을 GoldColor로 설정합니다. 위임을 by GoldColor에서 by fishColor로 변경합니다.
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

4단계: FishAction에 인터페이스 위임 추가

같은 방법으로 FishAction에 인터페이스 위임을 사용할 수 있습니다.

  1. AquariumFish.kt에서 FishAction를 구현하는 PrintingFishAction 클래스를 만들어 String, food를 사용한 다음 물고기 먹이를 출력합니다.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. Plecostomus 클래스에서 재정의 함수 eat()를 삭제합니다. 이 함수를 위임으로 대체하기 때문입니다.
  2. Plecostomus 선언에서 FishActionPrintingFishAction에 위임하여 "eat algae"를 전달합니다.
  3. 모든 위임을 통해 Plecostomus 클래스의 본문에 코드가 없으므로 {}를 삭제합니다. 모든 재정의가 인터페이스 위임에 의해 처리되기 때문입니다.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

다음 다이어그램은 SharkPlecostomus 클래스를 나타냅니다. 두 클래스는 모두 PrintingFishActionFishColor 인터페이스로 구성되지만 구현을 위임합니다.

인터페이스 위임은 강력하므로 일반적으로 다른 언어로 추상 클래스를 사용할 수 있을 때마다 이를 사용하는 방법을 고려해야 합니다. 이를 통해 컴포지션을 사용하여 동작을 연결할 수 있고, 각 서브클래스를 서로 다른 방식으로 전문적으로 다룰 필요가 없습니다.

데이터 클래스는 다른 언어의 struct와 비슷하지만 주로 일부 데이터를 보유하는 역할을 하지만 데이터 클래스 객체는 여전히 객체입니다. Kotlin 데이터 클래스 객체에는 인쇄 및 복사 유틸리티와 같은 몇 가지 추가 이점이 있습니다. 이 작업에서는 간단한 데이터 클래스를 만들고 Kotlin이 데이터 클래스에 제공하는 지원을 알아봅니다.

1단계: 데이터 클래스 만들기

  1. example.myapp 패키지 아래에 새 패키지 decor를 추가하여 새 코드를 보관합니다. Project 창에서 example.myapp을 마우스 오른쪽 버튼으로 클릭하고 File > New > Package를 선택합니다.
  2. 패키지에서 Decoration라는 새 클래스를 만듭니다.
package example.myapp.decor

class Decoration {
}
  1. Decoration를 데이터 클래스로 만들려면 클래스 선언 앞에 키워드 data를 붙입니다.
  2. rocks이라는 String 속성을 추가하여 클래스에 데이터를 제공합니다.
data class Decoration(val rocks: String) {
}
  1. 파일의 클래스 외부에서 makeDecorations() 함수를 추가하여 "granite"Decoration 인스턴스를 만들고 출력합니다.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. main() 함수를 추가하여 makeDecorations()를 호출하고 프로그램을 실행합니다. 이는 데이터 클래스이므로 생성되는 적절한 출력에 주목하세요.
⇒ Decoration(rocks=granite)
  1. makeDecorations()에서 두 개 이상의 Decoration 객체를 인스턴스화하고 이들을 출력합니다.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. makeDecorations()에서 decoration1decoration2와 비교한 결과를 출력하는 print 문을 추가하고 decoration3decoration2와 비교하는 두 번째 구문을 추가합니다. 데이터 클래스에서 제공하는 equals() 메서드를 사용합니다.
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. 코드를 실행합니다.
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

2단계: 해체 사용

데이터 객체의 속성을 가져와서 변수에 할당하려면 다음과 같이 한 번에 하나씩 할당할 수 있습니다.

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

대신 각 속성에 하나씩 변수를 만들어 데이터 객체를 변수 그룹에 할당할 수 있습니다. Kotlin은 각 변수에 속성 값을 넣습니다.

val (rock, wood, diver) = decoration

이를 해체라고 하며 유용한 약칭입니다. 변수 수는 속성 수와 일치해야 하며, 변수는 클래스에서 선언된 순서대로 할당됩니다. 다음은 Decoration.kt에서 사용해 볼 수 있는 전체 예입니다.

// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}

fun makeDecorations() {
    val d5 = Decoration2("crystal", "wood", "diver")
    println(d5)

// Assign all properties to variables.
    val (rock, wood, diver) = d5
    println(rock)
    println(wood)
    println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver)
crystal
wood
diver

하나 이상의 속성이 필요하지 않은 경우 아래의 코드와 같이 변수 이름 대신 _을 사용하여 건너뛸 수 있습니다.

    val (rock, _, diver) = d5

이 작업에서는 다음을 비롯한 Kotlin의 특수 목적 클래스를 알아봅니다.

  • 싱글톤 클래스
  • 열거형
  • 봉인 수업

1단계: 싱글톤 클래스 리콜

GoldColor 클래스를 사용하여 이전 예를 다시 생각해보세요.

object GoldColor : FishColor {
   override val color = "gold"
}

GoldColor의 모든 인스턴스가 동일한 작업을 하므로 싱글톤으로 만들기 위해 class 대신 object로 선언됩니다. 인스턴스는 하나만 있을 수 있습니다.

2단계: enum 만들기

Kotlin은 enum도 지원하므로 다른 언어와 마찬가지로 무언가를 열거하고 이름으로 참조할 수 있습니다. 선언 앞에 키워드 enum를 추가하여 열거형을 선언합니다. 기본 enum 선언에는 이름 목록만 필요하지만 각 이름과 연결된 필드를 하나 이상 정의할 수도 있습니다.

  1. Decoration.kt에서 enum의 예를 사용해 봅니다.
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

열거형은 싱글톤과 약간 유사합니다. 즉, 열거의 각 값에는 하나만 존재할 수 있습니다. 예를 들어 Color.RED 1개, Color.GREEN 1개, Color.BLUE 1개만 있을 수 있습니다. 이 예시에서는 RGB 값이 rgb 속성에 할당되어 색상 구성요소를 나타냅니다. ordinal 속성을 사용하여 열거형의 서수 값을 가져오고 name 속성을 사용하여 열거형의 이름을 가져올 수도 있습니다.

  1. 다른 열거형 예시를 시도해 봅니다.
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

fun main() {
    println(Direction.EAST.name)
    println(Direction.EAST.ordinal)
    println(Direction.EAST.degrees)
}
⇒ EAST
2
90

3단계: 봉인 클래스 만들기

봉인 클래스는 서브클래스로 분류할 수 있지만 클래스가 선언된 파일 내에서만 서브클래스로 분류될 수 있는 클래스입니다. 클래스를 다른 파일에서 서브클래스로 분류하려고 하면 오류가 발생합니다.

클래스와 서브클래스가 동일한 파일에 있으므로 Kotlin은 모든 서브클래스를 정적으로 인식합니다. 즉, 컴파일러가 컴파일 시 추가 검사를 실행할 수 있으므로 컴파일 시간에 모든 클래스와 서브클래스를 확인하고 컴파일러가 모든 클래스와 서브클래스를 인식합니다.

  1. AquariumFish.kt에서 해양 테마와 함께 봉인된 클래스의 예를 시도해 봅니다.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

Seal 클래스는 다른 파일에서 서브클래스로 분류할 수 없습니다. Seal 유형을 더 추가하려면 동일한 파일에 추가해야 합니다. 이는 봉인 클래스를 안전하게 고정한 수의 유형을 나타내는 방법입니다. 예를 들어 봉인 클래스는 네트워크 API에서 성공 또는 오류를 반환하는 데 적합합니다.

이 강의에서는 많은 내용을 다뤘습니다. 대부분은 객체 지향 프로그래밍 언어에 익숙해야 하지만, Kotlin은 코드를 간결하고 읽기 쉽게 만드는 몇 가지 기능을 추가합니다.

클래스 및 생성자

  • class를 사용하여 Kotlin에서 클래스를 정의합니다.
  • Kotlin은 속성에 관한 setter 및 getter를 자동으로 생성합니다.
  • 클래스 정의에서 직접 기본 생성자를 정의합니다. 예를 들면 다음과 같습니다.
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • 기본 생성자에 추가 코드가 필요한 경우 하나 이상의 init 블록에 코드를 작성합니다.
  • 클래스는 constructor를 사용하여 하나 이상의 보조 생성자를 정의할 수 있지만, Kotlin 스타일은 대신 팩토리 함수를 사용합니다.

공개 상태 수정자 및 서브클래스

  • Kotlin의 모든 클래스와 함수는 기본적으로 public이지만 수정자를 사용하여 공개 상태를 internal, private, protected으로 변경할 수 있습니다.
  • 서브클래스를 만들려면 상위 클래스를 open로 표시해야 합니다.
  • 서브클래스의 메서드와 속성을 재정의하려면 상위 클래스에서 메서드와 속성을 open로 표시해야 합니다.
  • 봉인 클래스는 정의된 파일에만 서브클래스로 분류할 수 있습니다. 선언 앞에 sealed를 붙여 봉인 클래스를 만듭니다.

데이터 클래스, 싱글톤, enum

  • 선언 앞에 data를 추가하여 데이터 클래스를 만듭니다.
  • 해체data 객체의 속성을 별도의 변수에 할당하는 약어입니다.
  • class 대신 object를 사용하여 싱글톤 클래스를 만듭니다.
  • enum class를 사용하여 열거형을 정의합니다.

추상 클래스, 인터페이스, 위임

  • 추상 클래스와 인터페이스는 클래스 간에 일반적인 동작을 공유하는 두 가지 방법입니다.
  • 추상 클래스는 속성과 동작을 정의하지만 구현을 서브클래스로 남겨둡니다.
  • 인터페이스는 동작을 정의하며 동작의 일부 또는 전부에 기본 구현을 제공할 수 있습니다.
  • 인터페이스를 사용하여 클래스를 작성하면 클래스 기능이 포함된 클래스 인스턴스를 통해 확장됩니다.
  • 인터페이스 위임은 구성을 사용하지만 구현을 인터페이스 클래스에 위임합니다.
  • 컴포지션은 인터페이스 위임을 사용하여 클래스에 기능을 추가하는 강력한 방법입니다. 일반적으로 컴포지션이 선호되지만 추상 클래스에서의 상속이 일부 문제에 더 적합합니다.

Kotlin 문서

이 과정의 주제에 관해 자세히 알아보거나 도움이 필요한 경우 https://kotlinlang.org에서 시작하면 됩니다.

Kotlin 튜토리얼

https://try.kotlinlang.org 웹사이트에는 웹 기반 인터프리터라는 Kotlin Koans라는 풍부한 튜토리얼과 예시를 제공하는 완전한 참조 문서가 있습니다.

Udacity 과정

이 주제에 관한 Udacity 과정을 보려면 프로그래머를 위한 Kotlin 부트캠프를 참고하세요.

IntelliJ IDEA

IntelliJ IDEA에 관한 문서는 JetBrains 웹사이트에서 찾을 수 있습니다.

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 통해 작업하는 학생들의 숙제 과제가 나와 있습니다. 강사는 다음을 처리합니다.

  • 필요한 경우 과제를 할당합니다.
  • 학생에게 과제 과제를 제출하는 방법을 알려주세요.
  • 과제 과제를 채점합니다.

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 다른 적절한 숙제를 할당해도 좋습니다.

이 Codelab을 직접 학습하고 있다면 언제든지 숙제를 통해 지식을 확인해 보세요.

답변

질문 1

클래스에는 클래스의 객체를 만드는 청사진과 같은 특수 메서드가 있습니다. 메서드는 무엇인가요?

▢ A 빌더

▢ 인스턴트기

▢ 생성자

▢ 청사진

질문 2

인터페이스와 추상 클래스에 관한 다음 설명 중 틀린 것은 무엇인가요?

▢ 추상 클래스는 생성자를 포함할 수 있습니다.

▢ 인터페이스에는 생성자가 없을 수 있습니다.

▢ 인터페이스 및 추상 클래스를 직접 인스턴스화할 수 있습니다.

▢ 추상 속성은 추상 클래스의 서브클래스에 의해 구현되어야 합니다.

질문 3

다음 중 속성, 메서드 등에 관한 Kotlin 공개 상태 수정자가 아닌 것은 무엇인가요?

internal

nosubclass

protected

private

질문 4

다음 데이터 클래스를 살펴보세요.
data class Fish(val name: String, val species:String, val colors:String)
다음 중 Fish 객체를 만들고 해체하는 데 유효하지 않은 코드는 무엇인가요?

val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")

val (name2, _, colors2) = Fish("Bitey", "shark", "gray")

val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")

val (name4, species4, colors4) = Fish("Harry", "halibut")

질문 5

모든 동물을 돌보아야 하는 동물이 많은 동물원을 소유하고 있다고 가정해 보겠습니다. 다음 중 케어 구현에 포함되지 않는 것은 무엇인가요?

▢ 동물이 먹는 다양한 종류의 식품을 위한 interface

abstract Caretaker 클래스로, 다양한 유형의 요양보호사를 만들 수 있습니다.

▢ 동물에게 깨끗한 물을 제공하는 interface입니다.

먹이 주기 일정에서 항목의 ▢ data 클래스입니다.

다음 강의로 진행: 5.1 확장 프로그램

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