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

이 Codelab은 프로그래머를 위한 Kotlin 부트캠프 과정의 일부입니다. Codelab을 순서대로 진행하면 이 과정의 학습 효과를 극대화할 수 있습니다. 기존 지식에 따라 일부 섹션을 훑어볼 수도 있습니다. 이 과정은 객체 지향 언어를 알고 Kotlin을 배우고 싶은 프로그래머를 대상으로 합니다.

소개

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

이 과정의 강의는 단일 샘플 앱을 빌드하는 대신 지식을 쌓을 수 있도록 설계되었지만, 서로 반독립적이므로 잘 아는 섹션은 대충 훑어볼 수 있습니다. 이러한 요소를 연결하기 위해 많은 예에서 수족관 테마를 사용합니다. 전체 수족관 스토리를 확인하려면 프로그래머를 위한 Kotlin 부트캠프 Udacity 과정을 확인하세요.

기본 요건

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

학습할 내용

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

실습할 내용

  • 속성이 있는 클래스 만들기
  • 클래스의 생성자 만들기
  • 서브클래스 만들기
  • 추상 클래스와 인터페이스의 예 살펴보기
  • 간단한 데이터 클래스 만들기
  • 싱글톤, 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. 종류 드롭다운에서 파일을 선택한 상태로 유지하고 파일 이름을 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단계: 보조 생성자 알아보기

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

  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 추가

이 단계에서는 명시적 속성 게터를 추가합니다. 속성을 정의하면 Kotlin에서 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단계: 속성 설정자 추가

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

  1. Aquarium 클래스에서 volumevar로 변경하여 두 번 이상 설정할 수 있도록 합니다.
  2. 제공된 물의 양에 따라 높이를 다시 계산하는 set() 메서드를 getter 아래에 추가하여 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 클래스를 공개로 설정하기

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

  1. Aquarium 클래스와 모든 속성을 open 키워드로 표시합니다.
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% 를 반환하는 게터가 있는 공개 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 = 40Aquarium을 생성하도록 코드를 변경합니다.
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. 원통을 계산하기 위해 volume 속성을 재정의합니다. 원기둥의 공식은 파이 곱하기 반지름의 제곱 곱하기 높이입니다. 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의 서브클래스인 SharkPlecostomus를 만듭니다.
  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 클래스를 나타냅니다.

추상 클래스 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

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

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

위의 예는 간단하지만 상호 관련된 클래스가 많은 경우 추상 클래스와 인터페이스를 사용하면 디자인을 더 깔끔하고 체계적으로 유지하고 유지관리하기가 더 쉬워집니다.

위에서 언급한 것처럼 추상 클래스에는 생성자가 있을 수 있지만 인터페이스에는 있을 수 없습니다. 그 외에는 매우 유사합니다. 그렇다면 언제 어떤 플랫폼을 사용해야 할까요?

인터페이스를 사용하여 클래스를 구성하면 클래스에 포함된 클래스 인스턴스를 통해 클래스의 기능이 확장됩니다. 추상 클래스에서 상속하는 것보다 컴포지션을 사용하면 코드를 더 쉽게 재사용하고 추론할 수 있습니다. 또한 클래스에서 여러 인터페이스를 사용할 수 있지만 하나의 추상 클래스에서만 서브클래스를 만들 수 있습니다.

컴포지션은 종종 더 나은 캡슐화, 더 낮은 결합 (상호 의존성), 더 깔끔한 인터페이스, 더 유용한 코드로 이어집니다. 이러한 이유로 인터페이스를 사용한 컴포지션이 선호되는 설계입니다. 반면 추상 클래스에서 상속은 일부 문제에 적합한 경우가 많습니다. 따라서 컴포지션을 선호해야 하지만 상속이 적합한 경우 Kotlin에서는 상속도 허용합니다.

  • 메서드가 많고 기본 구현이 하나 또는 두 개 있는 경우 인터페이스를 사용합니다(예: 아래 AquariumAction).
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • 클래스를 완료할 수 없는 경우 언제든지 추상 클래스를 사용하세요. 예를 들어 AquariumFish 클래스로 돌아가서 모든 AquariumFishFishAction를 구현하도록 하고 eat의 기본 구현을 제공하면서 color는 추상으로 남겨둘 수 있습니다. 물고기의 기본 색상은 없기 때문입니다.
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. PlecostomusFishActionFishColor의 두 인터페이스를 구현하도록 변경합니다. FishColorcolorFishActioneat()을 재정의해야 합니다.
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")
   }
}

이 클래스에서는 모든 Pleco가 금색이지만 이 물고기는 실제로 다양한 색상으로 제공됩니다. Plecostomus의 기본 색상으로 GoldColor를 사용하여 색상의 생성자 매개변수를 추가하면 이 문제를 해결할 수 있습니다.

  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

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

인터페이스 위임은 강력하며 다른 언어에서 추상 클래스를 사용할 수 있는 경우 일반적으로 인터페이스 위임을 사용하는 방법을 고려해야 합니다. 이를 통해 각기 다른 방식으로 특화된 많은 하위 클래스가 필요한 대신 구성을 사용하여 동작을 연결할 수 있습니다.

데이터 클래스는 다른 언어의 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. 파일에서 클래스 외부에 "granite"가 있는 Decoration 인스턴스를 만들어 출력하는 makeDecorations() 함수를 추가합니다.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. makeDecorations()을 호출하는 main() 함수를 추가하고 프로그램을 실행합니다. 데이터 클래스이므로 생성된 적절한 출력을 확인하세요.
⇒ Decoration(rocks=granite)
  1. makeDecorations()에서 'slate'인 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를 비교하는 두 번째 print 문을 추가합니다. 데이터 클래스에서 제공하는 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, Color.GREEN, Color.BLUE는 각각 하나만 있을 수 있습니다. 이 예시에서는 색상 구성요소를 나타내기 위해 RGB 값이 rgb 속성에 할당됩니다. ordinal 속성을 사용하여 enum의 서수 값을 가져오고 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을 사용하여 enum을 정의합니다.

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

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

Kotlin 문서

이 과정의 주제에 관해 자세히 알아보거나 막히는 부분이 있다면 https://kotlinlang.org에서 시작하는 것이 좋습니다.

Kotlin 튜토리얼

https://try.kotlinlang.org 웹사이트에는 Kotlin Koans라는 풍부한 튜토리얼, 웹 기반 인터프리터, 예시가 포함된 전체 참고 문서 세트가 포함되어 있습니다.

Udacity 과정

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

IntelliJ IDEA

IntelliJ IDEA 문서는 JetBrains 웹사이트에서 확인할 수 있습니다.

이 섹션에는 강사가 진행하는 과정의 일부로 이 Codelab을 진행하는 학생에게 출제할 수 있는 과제가 나열되어 있습니다. 다음 작업은 강사가 결정합니다.

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

강사는 이러한 추천을 원하는 만큼 사용할 수 있으며 적절하다고 생각되는 다른 과제를 출제해도 됩니다.

이 Codelab을 직접 진행하는 경우 이러한 과제를 자유롭게 사용하여 배운 내용을 테스트해 보세요.

질문에 답하세요

질문 1

클래스에는 해당 클래스의 객체를 만드는 청사진 역할을 하는 특수 메서드가 있습니다. 메서드 이름은 무엇인가요?

▢ 빌더

▢ 인스턴스화 프로그램

▢ 생성자

▢ 청사진

질문 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 부트캠프: 과정에 오신 것을 환영합니다를 참고하세요.