Kotlin에서 클래스 및 객체 사용

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

1. 시작하기 전에

이 Codelab에서는 Kotlin에서 클래스와 객체를 사용하는 방법을 알아봅니다.

클래스는 객체를 구성할 수 있는 청사진을 제공합니다. 객체란 해당 객체와 관련이 있는 데이터로 구성된 클래스의 인스턴스입니다. 객체와 클래스 인스턴스를 서로 바꿔 사용할 수 있습니다.

비유를 위해, 집을 짓는다고 상상해 보겠습니다. 클래스는 건축가의 설계 계획, 즉 청사진과 비슷합니다. 청사진은 집이 아니라 집을 짓는 방법에 관한 안내입니다. 집은 청사진에 따라 지은 실제 사물 또는 객체입니다.

집의 청사진이 각각 고유한 설계와 용도를 가진 방 여러 개를 구체적으로 그려내는 것처럼, 각 클래스는 고유한 설계와 목적을 가지고 있습니다. 클래스를 설계하는 방법을 이해하려면 데이터, 로직, 동작을 객체에 포함하는 방법을 알려주는 프레임워크인 객체 지향 프로그래밍(OOP)을 잘 알아야 합니다.

OOP는 복잡한 실제 문제를 더 작은 객체로 단순화하는 데 도움이 됩니다. OOP에는 4가지 기본 개념이 있으며, 이 Codelab 후반부에서 각 개념을 자세히 알아보겠습니다.

  • 캡슐화. 관련 속성 및 이런 속성에 관해 작업을 실행하는 메서드를 클래스에 래핑합니다. 휴대전화를 예로 들어 보겠습니다. 휴대전화는 카메라, 디스플레이, 메모리 카드, 기타 하드웨어 구성요소와 소프트웨어 구성요소를 캡슐화합니다. 사용자는 휴대전화 내부에 구성요소가 어떻게 연결되어 있는지 염려할 필요가 없습니다.
  • 추상화. 캡슐화의 확장으로, 내부 구현 로직을 최대한 숨긴다는 개념입니다. 예를 들어 휴대전화로 사진을 찍으려면 카메라 앱을 열고 휴대전화로 촬영할 장면을 가리킨 다음 버튼을 클릭하여 사진을 찍기만 하면 됩니다. 카메라 앱이 빌드된 방식이나 휴대전화의 카메라 하드웨어가 실제로 작동하는 방식을 몰라도 됩니다. 간단히 말해 카메라 앱의 내부 메커니즘과 모바일 카메라가 사진을 촬영하는 방식이 추상화되어 있으므로 사용자는 사진 촬영이라는 주된 목표에 집중할 수 있습니다.
  • 상속. 상위-하위 관계를 설정하여 다른 클래스의 특성과 동작을 토대로 클래스를 빌드할 수 있습니다. 예를 들어 여러 제조업체에서 Android OS를 실행하는 다양한 휴대기기를 제작하지만, 각 기기의 UI는 서로 다릅니다. 즉, 제조업체는 Android OS 기능을 상속하고 이러한 기능 위에 맞춤설정을 빌드합니다.
  • 다형성. 이 단어는 그리스어 어원 poly-(많음을 의미함)와 -morphism(형태를 의미함)이 변형된 것입니다. 다형성은 여러 객체를 한 가지 공통 방식으로 사용하는 능력입니다. 예를 들어 휴대전화에 블루투스 스피커를 연결한다면 휴대전화는 블루투스를 통해 오디오를 재생할 수 있는 기기가 있다는 사실만 알면 됩니다. 선택할 수 있는 블루투스 스피커가 여러 개 있더라도 각각의 스피커를 사용하는 구체적인 방법을 휴대전화가 알 필요는 없습니다.

마지막으로, 간결한 문법으로 속성 값을 관리하기 위해 재사용 가능한 코드를 제공하는 속성 위임에 관해 알아보겠습니다. 이 Codelab에서는 스마트 홈 앱의 클래스 구조를 빌드하면서 이러한 개념을 학습합니다.

기본 요건

  • Kotlin 플레이그라운드에서 코드를 열고 수정하고 실행하는 방법 이해
  • 변수, 함수, println() 함수, main() 함수를 비롯한 Kotlin 프로그래밍 기본사항에 관한 지식

학습할 내용

  • OOP의 개요
  • 클래스의 정의
  • 생성자, 함수, 속성으로 클래스를 정의하는 방법
  • 객체를 인스턴스화하는 방법
  • 상속의 정의
  • IS-A 관계와 HAS-A 관계의 차이점
  • 속성 및 함수를 재정의하는 방법
  • 공개 상태 수정자의 정의
  • 위임의 정의 및 by 위임을 사용하는 방법

빌드할 항목

  • 스마트 홈 클래스 구조
  • 스마트 TV, 스마트 조명과 같은 스마트 기기를 나타내는 클래스

필요한 항목

  • 인터넷 액세스가 가능하고 웹브라우저가 있는 컴퓨터

2. 클래스 정의

클래스를 정의할 때는 해당 클래스의 모든 객체에 포함해야 하는 속성과 메서드를 지정합니다.

클래스 정의는 class 키워드로 시작하고 그 뒤에 이름과 중괄호 쌍이 나옵니다. 문법상 여는 중괄호 앞에 있는 부분을 클래스 헤더라고도 합니다. 중괄호 안에 클래스의 속성과 함수를 지정할 수 있습니다. 속성과 함수에 관해서는 곧 알아보겠습니다. 다음 다이어그램에서 클래스 정의 문법을 볼 수 있습니다.

클래스 키워드로 시작하고 그 뒤에 이름과 여는 중괄호, 닫는 중괄호의 쌍이 나옵니다. 중괄호 안에 파란색 출력 항목을 설명하는 클래스의 본문이 포함됩니다.

클래스에 권장되는 이름 지정 규칙은 다음과 같습니다.

  • 클래스 이름을 자유롭게 선택할 수 있지만 fun 키워드와 같은 Kotlin 키워드를 클래스 이름으로 사용하지 마세요.
  • 클래스 이름은 PascalCase로 작성되므로 각 단어는 대문자로 시작하며 단어 사이에 공백이 없습니다. 예를 들어 SmartDevice에서 각 단어의 첫 글자가 대문자이고 단어 사이에 공백이 없습니다.

클래스는 다음 세 가지 주요 부분으로 구성됩니다.

  • 속성. 클래스 객체의 속성을 지정하는 변수입니다.
  • 메서드. 클래스의 동작과 작업이 포함된 함수입니다.
  • 생성자. 클래스가 정의된 프로그램 전체에서 클래스의 인스턴스를 만드는 특수 멤버 함수입니다.

이전에도 클래스로 작업해 보셨을 것입니다. 이전 Codelab에서 Int, Float, String, Double 등의 데이터 유형을 알아봤습니다. 이러한 데이터 유형을 Kotlin에서는 클래스로 정의합니다. 다음 코드 스니펫과 같이 변수를 정의하면 값 1로 인스턴스화되는 Int 클래스의 객체를 만들게 됩니다.

val number: Int = 1

SmartDevice 클래스를 정의합니다.

  1. Kotlin 플레이그라운드에서 콘텐츠를 비어 있는 main() 함수로 바꿉니다.
fun main() {
}
  1. main() 함수 앞에 있는 줄에서 // empty body 주석이 포함된 본문으로 SmartDevice 클래스를 정의합니다.
class SmartDevice {
    // empty body
}

fun main() {
}

3. 클래스 인스턴스 만들기

여기서 배운 것처럼 클래스는 객체의 청사진입니다. Kotlin 런타임은 클래스(청사진)를 사용하여 특정 유형의 객체를 만듭니다. SmartDevice 클래스로 스마트 기기의 개념에 관한 청사진을 보유하게 됩니다. 프로그램에 실제 스마트 기기를 포함하려면 SmartDevice 객체 인스턴스를 만들어야 합니다. 인스턴스화 문법은 다음 다이어그램에서 볼 수 있듯이 클래스 이름으로 시작하고 그 뒤에 괄호 쌍이 나옵니다.

caaabfce58c08886.png

객체를 사용하려면 변수를 정의하는 방법과 유사하게 객체를 만들어 변수에 할당합니다. 변경 불가능한 변수를 만들려면 val 키워드를, 변경 가능한 변수를 만들려면 var 키워드를 사용합니다. val 키워드나 var 키워드 뒤에 변수 이름, = 할당 연산자, 인스턴스화된 클래스 객체가 차례로 나옵니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

43e12b40338d1254.png

SmartDevice 클래스를 객체로 인스턴스화합니다.

  • main() 함수에서 val 키워드를 사용하여 smartTvDevice라는 변수를 만들고 이 변수를 SmartDevice 클래스의 인스턴스로 초기화합니다.
fun main() {
    val smartTvDevice = SmartDevice()
}

4. 클래스 메서드 정의

1단원에서 학습한 내용은 다음과 같습니다.

  • 함수 정의에는 fun 키워드 다음에 괄호 쌍과 중괄호 쌍을 차례로 사용합니다. 중괄호 안에는 코드, 즉 작업을 실행하는 데 필요한 안내가 포함됩니다.
  • 함수를 호출하면 함수에 포함된 모든 코드가 실행됩니다.

클래스가 실행할 수 있는 작업은 클래스의 함수로 정의됩니다. 예를 들어 휴대전화로 켜고 끌 수 있는 스마트 기기, 스마트 TV 또는 스마트 조명이 있다고 가정하겠습니다. 스마트 기기는 프로그래밍에서 SmartDevice 클래스로 변환되며 켜고 끄는 작업은 켜기/끄기 동작을 사용 설정하는 turnOn() 함수와 turnOff() 함수로 표현됩니다.

클래스에서 함수를 정의하는 문법은 이전에 알아본 것과 동일합니다. 유일한 차이점은 함수가 클래스 본문에 배치된다는 것입니다. 클래스 본문에 정의된 함수를 멤버 함수 또는 메서드라고 하며 클래스의 동작을 나타냅니다. 이 Codelab의 나머지 부분에서는 클래스 본문에 함수가 나올 때마다 이를 메서드라고 부릅니다.

SmartDevice 클래스에서 다음과 같이 turnOn() 메서드와 turnOff() 메서드를 정의합니다.

  1. SmartDevice 클래스의 본문에서 turnOn() 메서드를 본문을 비운 채로 정의합니다.
class SmartDevice {
    fun turnOn() {

    }
}
  1. turnOn() 메서드의 본문에 println() 문을 추가한 후 이 문에 "Smart device is turned on." 문자열을 전달합니다.
class SmartDevice {
    fun turnOn(){
        println("Smart device is turned on.")
    }
}
  1. turnOn() 메서드 뒤에 "Smart device is turned off." 문자열을 출력하는 turnOff() 메서드를 추가합니다.
class SmartDevice {

    fun turnOn(){
        println("Smart device is turned on.")
    }

    fun turnOff(){
        println("Smart device is turned off.")
    }
}

객체에서 메서드 호출

지금까지 스마트 기기의 청사진 역할을 하는 클래스를 정의하고, 클래스의 인스턴스를 만들고, 이 인스턴스를 변수에 할당했습니다. 이제 SmartDevice 클래스의 메서드를 사용하여 기기를 켜거나 꺼 보겠습니다.

클래스의 메서드를 호출하는 작업은 이전 Codelab에서 main() 함수에서 다른 함수를 호출한 방법과 비슷합니다. 예를 들어 turnOn() 메서드에서 turnOff() 메서드를 호출해야 한다면 다음 코드 스니펫과 유사한 코드를 작성하면 됩니다.

class SmartDevice {

    fun turnOn(){
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

클래스 외부에서 클래스 메서드를 호출하려면 클래스 객체로 시작하고 그 뒤에 . 연산자, 함수 이름, 괄호 쌍을 사용합니다. 해당하는 경우 괄호에는 메서드에 필요한 인수가 포함됩니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

2b30399f31d97757.png

객체에서 turnOn() 메서드와 turnOff() 메서드를 호출합니다.

  1. smartTvDevice 변수 다음 줄의 main() 함수에서 turnOn() 메서드를 호출합니다.
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. turnOn() 메서드 다음 줄에서 turnOff() 메서드를 호출합니다.
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 코드를 실행합니다.

출력은 다음과 같습니다.

Smart device is turned on.
Smart device is turned off.

5. 클래스 속성 정의

1단원에서는 단일 데이터의 컨테이너인 변수에 관해 알아봤습니다. val 키워드로 읽기 전용 변수를 만들고 var 키워드로 변경 가능한 변수를 만드는 방법을 배웠습니다.

메서드는 클래스가 실행할 수 있는 작업을 정의하고, 속성은 클래스의 특성이나 데이터 속성을 정의합니다. 예를 들어 스마트 기기에는 다음과 같은 속성이 있습니다.

  • 이름. 기기 이름입니다.
  • 카테고리. 스마트 기기의 유형입니다(예: 엔터테인먼트, 유틸리티, 요리).
  • 기기 상태. 켜기, 끄기, 온라인, 오프라인과 같은 기기의 상태입니다. 기기가 인터넷에 연결되어 있으면 온라인 상태로, 그렇지 않으면 오프라인으로 간주됩니다.

속성은 기본적으로 함수 본문이 아닌 클래스 본문에 정의된 변수입니다. 즉, 속성과 변수를 정의하는 문법은 동일합니다. val 키워드로 변경 불가능한 속성을 정의하고 var 키워드로 변경 가능한 속성을 정의합니다.

앞서 언급한 특성을 SmartDevice 클래스의 속성으로 구현합니다.

  1. turnOn() 메서드 앞에 있는 줄에서 name 속성을 정의하고 "Android TV" 문자열에 할당합니다.
class SmartDevice {

    val name = "Android TV"

    fun turnOn(){
        println("Smart device is turned on.")
    }

    fun turnOff(){
        println("Smart device is turned off.")
    }
}
  1. name 속성 다음 줄에서 category 속성을 정의하고 "Entertainment" 문자열에 할당한 다음 deviceStatus 속성을 정의하고 "online" 문자열에 할당합니다.
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn(){
        println("Smart device is turned on.")
    }

    fun turnOff(){
        println("Smart device is turned off.")
    }
}
  1. smartDevice 변수 다음 줄에서 println() 함수를 호출한 다음 이 함수에 "Device name is: ${smartTvDevice.name}" 문자열을 전달합니다.
fun main(){
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. 코드를 실행합니다.

출력은 다음과 같습니다.

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

속성의 getter 함수와 setter 함수

속성은 변수보다 더 많은 작업을 할 수 있습니다. 예를 들어 스마트 TV를 나타내는 클래스 구조를 만든다고 가정하겠습니다. 흔히 실행하는 작업 중 하나는 볼륨을 높이거나 줄이는 것입니다. 프로그래밍에서 이 작업을 표현하려면 speakerVolume이라는 속성을 만들면 됩니다. 이 속성은 TV 스피커에 설정된 현재 볼륨 수준을 포함하지만, 볼륨 값에는 범위가 있습니다. 설정할 수 있는 최소 볼륨은 0이고 최대 볼륨은 100입니다. speakerVolume 속성이 100을 초과하거나 0 미만으로 떨어지지 않도록 하려면 setter 함수를 작성하면 됩니다. 속성 값을 업데이트할 때 값이 0~100 범위에 있는지 확인해야 합니다. 다른 예로, 이름이 항상 대문자여야 한다는 요구사항이 있다고 가정하겠습니다. name 속성을 대문자로 변환하는 getter 함수를 구현할 수 있습니다.

이러한 속성을 구현하는 방법을 자세히 알아보기 전에 먼저 이 속성을 선언하는 전체 문법을 이해해야 합니다. 변경 가능한 속성을 정의하는 전체 문법은 변수 정의로 시작하고 그 뒤에 선택 항목인 get() 함수와 set() 함수가 나옵니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

4cad686a6fcca35b.png

속성에 getter 및 setter 함수를 정의하지 않으면 Kotlin 컴파일러가 내부적으로 함수를 생성합니다. 예를 들어 var 키워드를 사용하여 speakerVolume 속성을 정의하고 2 값을 할당하는 경우 컴파일러는 다음 코드 스니펫에서 볼 수 있듯이 getter 함수와 setter 함수를 자동으로 생성합니다.

var speakerVolume = 2
    get() = field
    set(value) {
        field = value
    }

위의 줄은 백그라운드에서 컴파일러가 추가하므로 코드에 표시되지 않습니다.

변경 불가능한 속성의 전체 문법에는 다음과 같은 두 가지 차이점이 있습니다.

  • val 키워드로 시작합니다.
  • val 유형의 변수는 읽기 전용이므로 set() 함수가 없습니다.

Kotlin 속성은 지원 필드를 사용하여 메모리에 값을 보유합니다. 지원 필드는 기본적으로 속성에 내부적으로 정의된 클래스 변수입니다. 지원 필드는 범위가 속성으로 지정되므로 get() 속성 함수나 set() 속성 함수를 통해서만 액세스할 수 있습니다.

get() 함수에서 속성 값을 읽거나 set() 함수에서 값을 업데이트하려면 속성의 지원 필드를 사용해야 합니다. 이 필드는 Kotlin 컴파일러에서 자동으로 생성되며 field 식별자로 참조됩니다.

예를 들어 set() 함수에서 속성 값을 업데이트하려는 경우 다음 코드 스니펫에서 보듯이 set() 함수의 매개변수(value 매개변수라고 함)를 사용하고 이를 field 변수에 할당합니다.

var speakerVolume = 2
    set(value) {
        field = value
    }

예를 들어 speakerVolume 속성에 할당된 값이 0~100 범위에 속하게 하려면 다음 코드 스니펫과 같이 setter 함수를 구현합니다.

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

set() 함수는 in 키워드 뒤에 있는 값 범위를 사용하여 Int 값이 0~100 범위에 속하는지 확인합니다. 값이 예상 범위 내에 있으면 field 값이 업데이트되고 그렇지 않으면 속성 값이 변경되지 않습니다.

이 Codelab의 클래스 간의 관계 구현 섹션에서 이 속성을 클래스에 포함할 것입니다. 따라서 지금은 코드에 setter 함수를 추가할 필요가 없습니다.

6. 생성자 정의

생성자의 기본 목적은 클래스의 객체를 만드는 방법을 지정하는 것입니다. 즉, 생성자는 객체를 초기화하고 객체를 사용할 수 있도록 준비합니다. 이 작업은 객체를 인스턴스화할 때 처리되었습니다. 생성자 내의 코드는 클래스의 객체가 인스턴스화될 때 실행됩니다. 매개변수를 포함하거나 포함하지 않고 생성자를 정의할 수 있습니다.

기본 생성자

기본 생성자는 매개변수가 없는 생성자입니다. 다음 코드 스니펫과 같이 기본 생성자를 정의할 수 있습니다.

class SmartDevice constructor() {
    ...
}

Kotlin은 간결한 것을 목표로 하므로 주석이나 공개 상태 수정자가 없는 경우 constructor 키워드를 삭제할 수 있습니다. 이 생성자에 관해 곧 알아봅니다. 다음 코드 스니펫과 같이 생성자에 매개변수가 없는 경우 괄호를 삭제할 수도 있습니다.

class SmartDevice {
    ...
}

Kotlin 컴파일러는 기본 생성자를 자동으로 생성합니다. 자동 생성된 기본 생성자는 컴파일러가 백그라운드에서 추가하므로 코드에 표시되지 않습니다.

매개변수화된 생성자 정의

SmartDevice 클래스에서 name 속성과 category 속성은 변경할 수 없습니다. SmartDevice 클래스의 모든 인스턴스가 name 속성과 category 속성을 초기화하도록 해야 합니다. 현재 구현에서 namecategory 속성의 값은 하드코딩됩니다. 즉, 모든 스마트 기기는 "Android TV" 문자열로 이름이 지정되며 "Entertainment" 문자열로 분류됩니다.

불변성은 유지하되 하드코딩된 값을 피하려면 매개변수화된 생성자를 사용하여 초기화합니다.

  • SmartDevice 클래스에서 기본값을 할당하지 않고 name 속성과 category 속성을 생성자로 이동합니다.
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn(){
        println("Smart device is turned on.")
    }

    fun turnOff(){
        println("Smart device is turned off.")
    }
}

이제 생성자가 속성을 설정하는 매개변수를 허용하므로 이러한 클래스의 객체를 인스턴스화하는 방법도 변경됩니다. 다음 다이어그램에서 객체를 인스턴스화하는 전체 문법을 확인할 수 있습니다.

46890255031c3fa4.png

코드 표현은 다음과 같습니다.

SmartDevice("Android TV", "Entertainment")

생성자의 두 인수는 모두 문자열입니다. 어느 매개변수에 값을 할당해야 하는지 다소 명확하지 않습니다. 이 문제를 해결하려면 함수 인수를 전달한 방법과 유사하게 다음 코드 스니펫과 같이 이름이 지정된 인수를 사용하여 생성자를 만들면 됩니다.

SmartDevice(name = "Android TV", category = "Entertainment")

Kotlin의 생성자에는 두 가지 기본 유형이 있습니다.

  • 기본 생성자. 클래스에는 클래스 헤더의 일부로 정의된 기본 생성자가 하나만 있을 수 있습니다. 기본 생성자는 기본 또는 매개변수화된 생성자일 수 있습니다. 기본 생성자에는 본문이 없습니다. 즉, 코드를 포함할 수 없습니다.
  • 보조 생성자. 한 클래스에 여러 보조 생성자가 있을 수 있습니다. 매개변수를 포함하거나 포함하지 않고 보조 생성자를 정의할 수 있습니다. 보조 생성자는 클래스를 초기화할 수 있으며 초기화 로직을 포함할 수 있는 본문을 가집니다. 클래스에 기본 생성자가 있는 경우 각 보조 생성자는 기본 생성자를 초기화해야 합니다.

기본 생성자를 사용하여 클래스 헤더의 속성을 초기화할 수 있습니다. 생성자에 전달되는 인수는 속성에 할당됩니다. 기본 생성자를 정의하는 문법은 클래스 이름으로 시작하고 그 뒤에 constructor 키워드와 괄호 쌍이 나옵니다. 괄호에는 기본 생성자의 매개변수가 포함됩니다. 매개변수가 두 개 이상인 경우 쉼표로 매개변수 정의가 구분됩니다. 다음 다이어그램에서 기본 생성자를 정의하는 전체 문법을 확인할 수 있습니다.

fec945b80f81980.png

보조 생성자는 클래스의 본문에 포함되고 이 생성자의 문법은 세 부분으로 구성됩니다.

  • 보조 생성자 선언. 보조 생성자 정의는 constructor 키워드로 시작하고 그 뒤에 괄호가 나옵니다. 해당하는 경우 괄호에는 보조 생성자에 필요한 매개변수가 포함됩니다.
  • 기본 생성자 초기화. 초기화는 콜론으로 시작하고 그 뒤에 this 키워드와 괄호 쌍이 나옵니다. 해당하는 경우 괄호에는 기본 생성자에 필요한 매개변수가 포함됩니다.
  • 보조 생성자 본문. 기본 생성자 초기화 뒤에 중괄호 쌍이 나오며 이 중괄호에는 보조 생성자의 본문이 포함됩니다.

다음 다이어그램에서 문법을 확인할 수 있습니다.

149e1882ccad5e11.png

예를 들어 스마트 기기 제공업체에서 개발한 API를 통합하려고 하지만 이 API가 초기 기기 상태를 나타내는 Int 유형의 상태 코드를 반환합니다. API는 기기가 오프라인인 경우 0 값을, 기기가 온라인인 경우 1 값을 반환합니다. 다른 정수 값이면 상태는 알 수 없음으로 간주됩니다. 다음 코드 스니펫에서 볼 수 있듯이 SmartDevice 클래스에 statusCode 매개변수를 문자열 표현으로 변환하는 보조 생성자를 만들 수 있습니다.

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. 클래스 간의 관계 구현

상속을 사용하면 다른 클래스의 특성과 동작을 토대로 클래스를 빌드할 수 있습니다. 재사용 가능한 코드를 작성하고 클래스 간의 관계를 설정하는 데 유용한 강력한 메커니즘입니다.

예를 들어 시중에는 스마트 TV, 스마트 조명, 스마트 스위치 등 다양한 스마트 기기가 있습니다. 프로그래밍에서 스마트 기기를 표현할 때 기기는 이름, 카테고리, 상태와 같은 일반적인 속성을 공유합니다. 또한 켜기/끄기 기능과 같은 일반적인 동작도 있습니다.

하지만 각 스마트 기기를 켜거나 끄는 방법은 다릅니다. 예를 들어 TV를 켜려면 디스플레이를 켠 후 직전의 볼륨 수준과 채널을 설정해야 할 수 있습니다. 반면 조명을 켜려면 밝기를 높이거나 낮추는 작업만 필요할 수도 있습니다.

또한 각 스마트 기기에는 실행할 수 있는 더 많은 기능과 작업이 포함되어 있습니다. 예를 들어 TV에서 볼륨을 조절하고 채널을 변경할 수 있으며 조명의 경우는 밝기나 색상을 조정할 수 있습니다.

간단히 말해, 모든 스마트 기기는 다양한 기능을 갖추고 있지만 몇 가지 공통적인 특성을 공유합니다. 이러한 공통적인 특성을 각 스마트 기기 클래스에 복제하거나 상속을 통해 코드를 재사용 가능하게 만들 수 있습니다.

이렇게 하려면 SmartDevice 상위 클래스를 만들고 이러한 공통 속성과 동작을 정의해야 합니다. 그런 다음 상위 클래스의 속성을 상속하는 하위 클래스(예: SmartTvDevice, SmartLightDevice)를 만들 수 있습니다.

프로그래밍 측면에서 볼 때 SmartTvDevice 클래스와 SmartLightDevice 클래스는 SmartDevice 상위 클래스를 확장합니다. 상위 클래스를 슈퍼클래스라고, 하위 클래스를 서브클래스라고도 합니다. 다음 다이어그램에서 클래스 간의 관계를 확인할 수 있습니다.

클래스 간의 상속 관계를 나타내는 다이어그램.

그러나 Kotlin에서는 모든 클래스가 최종입니다. 즉, 클래스를 확장할 수 없으므로 클래스 간의 관계를 정의해야 합니다.

SmartDevice 슈퍼클래스와 서브클래스 간의 관계를 다음과 같이 정의합니다.

  1. SmartDevice 슈퍼클래스에서 open 키워드를 class 키워드 앞에 추가합니다.
open class SmartDevice(val name: String, val category: String) {
    ...
}

open 키워드는 클래스의 확장 가능성을 컴파일러에 알립니다. 따라서 이제 다른 클래스가 이 클래스를 확장할 수 있습니다.

서브클래스를 만드는 문법은 지금까지 배운 것처럼 클래스 헤더를 만드는 것으로 시작합니다. 생성자의 닫는 괄호 뒤에 공백, 콜론, 다시 한 번의 공백, 슈퍼클래스 이름, 괄호 쌍이 나옵니다. 필요한 경우 괄호에는 슈퍼클래스 생성자에 필요한 매개변수가 포함됩니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

3836f8e2bd95f6da.png

  1. SmartDevice 슈퍼클래스를 확장하는 SmartTvDevice 서브클래스를 만듭니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

SmartTvDeviceconstructor 정의는 속성이 변경 가능한지 불가능한지 여부를 지정하지 않습니다. 즉, deviceName 매개변수와 deviceCategory 매개변수는 클래스 속성이 아니라 constructor 매개변수입니다. 이들 매개변수를 클래스에서 사용할 수 없으며 슈퍼클래스 생성자에 전달하기만 합니다.

  1. SmartTvDevice 서브클래스 본문에서 getter 함수와 setter 함수에 관해 배울 때 만든 speakerVolume 속성을 추가합니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 0..200 범위를 지정하는 setter 함수를 사용하여 1 값에 할당된 channelNumber 속성을 정의합니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

     var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. 볼륨을 높이고 "Speaker volume increased to $speakerVolume." 문자열을 출력하는 increaseSpeakerVolume() 메서드를 정의합니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }
}
  1. 채널 수를 늘리고 "Channel number increased to $speakerVolume." 문자열을 출력하는 nextChannel() 메서드를 추가합니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $speakerVolume.")
    }
}
  1. SmartTvDevice 서브클래스 다음 줄에서 SmartDevice 슈퍼클래스를 확장하는 SmartLightDevice 서브클래스를 정의합니다.
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. SmartLightDevice 서브클래스 본문에서 0..100 범위를 지정하는 setter 함수를 사용하여 0 값에 할당된 brightnessLevel 속성을 정의합니다.
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. 조명의 밝기를 높이고 "Brightness increased to $brightnessLevel." 문자열을 출력하는 increaseBrightness() 메서드를 정의합니다.
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

클래스 간의 관계

상속을 사용할 때 두 클래스 간의 관계를 IS-A 관계로 설정합니다. 객체는 상속받는 클래스의 인스턴스이기도 합니다. HAS-A 관계에서 객체는 실제로 클래스 자체의 인스턴스가 아니더라도 다른 클래스의 인스턴스를 소유할 수 있습니다. 다음 다이어그램에서 이러한 관계를 대략적으로 확인할 수 있습니다.

HAS-A 관계와 IS-A 관계의 대략적인 표현.

IS-A 관계

SmartDevice 슈퍼클래스와 SmartTvDevice 서브클래스 간의 IS-A 관계를 지정하면 SmartDevice 슈퍼클래스가 할 수 있는 모든 작업을 SmartTvDevice 서브클래스가 할 수 있습니다. 관계는 단방향이므로, 모든 스마트 TV가 스마트 기기에 해당한다고 말할 수 있지만 모든 스마트 기기가 스마트 TV에 해당한다고 말할 수는 없습니다. IS-A 관계의 코드 표현은 다음 코드 스니펫에 나와 있습니다.

// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}

코드 재사용성을 확보하기 위해서만 상속을 사용하지는 마세요. 결정하기 전에 두 클래스가 서로 관련이 있는지 확인합니다. 관계가 어느 정도 존재하는 경우 실제로 IS-A 관계에 해당하는지 확인합니다. 서브클래스를 슈퍼클래스라고 말할 수 있을지 자문해 보세요. 예를 들어 Android는 운영체제에 해당합니다.

HAS-A 관계

HAS-A 관계는 두 클래스 간의 관계를 지정하는 또 다른 방법입니다. 예를 들어 집에서 스마트 TV를 사용하는 경우 스마트 TV와 집 사이에 관계가 있습니다. 집에 스마트 기기가 포함됩니다. 즉, 집에 스마트 기기가 존재합니다. 두 클래스 간의 HAS-A 관계를 컴포지션이라고도 합니다.

지금까지 몇 가지 스마트 기기를 생성했습니다. 이제 스마트 기기를 포함하는 SmartHome 클래스를 만듭니다. SmartHome 클래스를 사용하면 스마트 기기와 상호작용할 수 있습니다.

다음과 같이 HAS-A 관계를 사용하여 SmartHome 클래스를 정의합니다.

  1. SmartLightDevice 클래스와 main() 함수 간에 SmartHome 클래스를 정의합니다.
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

...

}

class SmartHome {
}

fun main() {
...
}
  1. SmartHome 클래스 생성자에서 val 키워드를 사용하여 SmartTvDevice 유형의 smartTvDevice 속성을 만듭니다.
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. SmartHome 클래스의 본문에서 smartTvDevice 속성에 관해 turnOn() 메서드를 호출하는 turnOnTv() 메서드를 정의합니다.
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. turnOnTv() 메서드 다음 줄에서 smartTvDevice 속성에 관해 turnOff() 메서드를 호출하는 turnOffTv() 메서드를 정의합니다.
class SmartHome(val smartTvDevice: SmartTvDevice) {

   fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. turnOffTv() 메서드 다음 줄에서 smartTvDevice 속성에 관해 increaseSpeakerVolume() 메서드를 호출하는 increaseTvVolume() 메서드를 정의한 후에 smartTvDevice 속성에 관해 nextChannel() 메서드를 호출하는 changeTvChannelToNext() 메서드를 정의합니다.
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. SmartHome 클래스 생성자에서 smartTvDevice 속성 매개변수를 자체 줄로 이동하고 그 뒤에 쉼표를 사용합니다.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

   ...

}
  1. smartTvDevice 속성 다음 줄에서 val 키워드를 사용하여 SmartLightDevice 유형의 smartLightDevice 속성을 정의합니다.
// Smart Home HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {
    ...

}
  1. SmartHome 본문에서 smartLightDevice 객체에 관해 turnOn() 메서드를 호출하는 turnOnLight() 메서드와 smartLightDevice 객체에 관해 turnOff() 메서드를 호출하는 turnOffLight() 메서드를 정의합니다.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. turnOffLight() 메서드 다음 줄에서 smartLightDevice 속성에 관해 increaseBrightNess() 메서드를 호출하는 increaseLightBrightness() 메서드를 정의합니다.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightNess()
    }
}
  1. increaseLightBrightness() 메서드 다음 줄에서 turnOffTv()turnOffLight() 메서드를 호출하는 turnOffAllDevices() 메서드를 정의합니다.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...
    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

서브클래스에서 슈퍼클래스 메서드 재정의

앞서 언급한 것처럼 켜기/끄기 기능은 모든 스마트 기기에서 지원되지만 기능을 실행하는 방법은 다릅니다. 이러나 기기별 동작을 제공하려면 슈퍼클래스에 정의된 turnOn() 메서드와 turnOff() 메서드를 재정의해야 합니다. 재정의한다는 것은 작업을 가로채서 일반적으로 수동 제어를 사용하는 것을 의미합니다. 메서드를 재정의하면 서브클래스의 메서드가 슈퍼클래스에 정의된 메서드의 실행을 중단하고 자체 실행을 제공합니다.

SmartDevice 클래스의 turnOn() 메서드와 turnOff() 메서드를 재정의합니다.

  1. 각 메서드의 fun 키워드 앞에 있는 SmartDevice 슈퍼클래스 본문에 open 키워드를 추가합니다.
open class SmartDevice {
    ...
    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. turnOn() 메서드와 turnOff() 메서드의 fun 키워드 앞에 있는 SmartLightDevice 서브클래스에 override 키워드를 추가합니다.
class SmartLightDevice(name: String, category: String) :
    SmartDevice(name = name, category = category) {

    var brightnessLevel = 0

    override fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }

    fun increaseBrightNess() {
        brightnessLevel++
    }
}

override 키워드는 서브클래스에 정의된 메서드에 포함된 코드를 실행하도록 Kotlin 런타임에 알립니다.

  1. turnOn() 메서드와 turnOff() 메서드의 fun 키워드 앞에 있는 SmartTvDevice 클래스에 override 키워드를 추가합니다.
class SmartTvDevice(name: String, category: String) :
    SmartDevice(name = name, category = category) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    override fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}
  1. main() 함수에서 var 키워드를 사용하여 "Android TV" 인수와 "Entertainment" 인수를 사용하는 SmartTvDevice 객체를 인스턴스화하는 SmartTvDevice 유형의 SmartDevice 변수를 정의합니다.
fun main(){
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. smartDevice 변수 다음 줄에서 smartDevice 객체에 관해 turnOn() 메서드를 호출합니다.
fun main(){
    var smartDevice : SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. 코드를 실행합니다.

출력은 다음과 같습니다.

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. turnOn() 메서드 호출 다음 줄에서 smartDevice 변수를 재할당하여 "Google Light" 인수와 "Utility" 인수를 사용하는 SmartLightDevice 클래스를 인스턴스화합니다. 그런 다음 smartDevice 객체 참조에서 turnOn() 메서드를 호출합니다.
fun main(){
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. 코드를 실행합니다.

출력은 다음과 같습니다.

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light is turned on. The brightness level is set to 2.

다형성의 예입니다. 위의 코드는 SmartDevice 유형의 변수에서 turnOn() 메서드를 호출하고 변수의 실제 값에 따라 turnOn() 메서드의 다양한 구현을 실행할 수 있습니다.

super 키워드를 사용하여 서브클래스에서 슈퍼클래스 코드 재사용

turnOn() 메서드와 turnOff() 메서드를 자세히 살펴보면 SmartTvDeviceSmartLightDevice 서브클래스에서 메서드가 호출될 때마다 deviceStatus 변수가 업데이트되는 방식에서 유사성을 발견하게 됩니다. 즉, 중복 코드입니다. SmartDevice 클래스에서 상태를 업데이트할 때 코드를 재사용할 수 있습니다.

재정의된 메서드를 슈퍼클래스에서 호출하려면 super 키워드를 사용해야 합니다. 슈퍼클래스에서 메서드를 호출하는 것은 클래스 외부에서 메서드를 호출하는 것과 비슷합니다. 객체와 메서드 간에 . 연산자를 사용하는 대신 super 키워드를 사용해야 합니다. 이 키워드는 서브클래스가 아닌 슈퍼클래스에서 메서드를 호출하도록 Kotlin 컴파일러에 알립니다.

슈퍼클래스에서 메서드를 호출하는 문법은 super 키워드로 시작하고 그 뒤에 . 연산자, 함수 이름, 괄호 쌍이 나옵니다. 해당하는 경우 괄호에는 인수가 포함됩니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

cc119b24d26d55f2.png

다음과 같이 SmartDevice 슈퍼클래스의 코드를 재사용합니다.

  1. 재사용 가능한 코드를 SmartLightDeviceSmartTvDevice 서브클래스에서 SmartDevice 슈퍼클래스로 이동합니다.
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn(){
        deviceStatus = "on"
    }

    open fun turnOff(){
        deviceStatus = "off"
    }
}
  1. super 키워드를 사용하여 SmartTvDeviceSmartLightDevice 서브클래스의 SmartDevice 클래스에서 메서드를 호출합니다.
class SmartTvDevice(name: String, category: String) :
    SmartDevice(name = name, category = category) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    override fun turnOn() {
        super.turnOn()
        println("Smart TV turned on. Speaker volume set to $speakerVolume.")
    }

    override fun turnOff() {
        super.turnOff()
        println("Smart TV turned off")
    }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
    }

    fun previousChannel() {
        channelNumber--
    }
}
class SmartLightDevice(name: String, category: String) :
    SmartDevice(name = name, category = category) {

    var brightnessLevel = 0

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("Smart Light turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }

    fun increaseBrightNess() {
        brightnessLevel++
    }
}

서브클래스에서 슈퍼클래스 속성 재정의

메서드와 마찬가지로 동일한 단계를 사용하여 속성을 재정의할 수도 있습니다.

다음과 같이 deviceStatus 속성을 재정의합니다.

  1. deviceStatus 속성 다음 줄의 SmartDevice 슈퍼클래스에서 open 키워드와 val 키워드를 사용하여 "unknown" 문자열로 설정된 deviceType 속성을 정의합니다.
open class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. SmartTvDevice 클래스에서 override 키워드와 val 키워드를 사용하여 "Smart TV" 문자열로 설정된 deviceType 속성을 정의합니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...
    override val deviceType = "Smart TV"
    ...
}
  1. SmartLightDevice 클래스에서 override 키워드와 val 키워드를 사용하여 "Smart Light" 값으로 설정된 deviceType 속성을 정의합니다.
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...
    override val deviceType = "Smart Light"
    ...

}

8. 공개 상태 수정자

공개 상태 수정자는 캡슐화를 달성하는 데 중요한 역할을 합니다.

  • 클래스에서는 클래스 외부의 무단 액세스로부터 속성과 메서드를 숨길 수 있습니다.
  • 패키지에서는 패키지 외부의 무단 액세스로부터 클래스와 인터페이스를 숨길 수 있습니다.

Kotlin은 다음 4가지 공개 상태 수정자를 제공합니다.

  • public - 기본 공개 상태 수정자입니다. 모든 위치에서 선언에 액세스할 수 있도록 합니다. 클래스 외부에서 사용하려는 속성과 메서드가 공개로 표시됩니다.
  • private - 동일한 클래스 또는 소스 파일에서 선언에 액세스할 수 있도록 합니다.

클래스 내에서만 사용되며 다른 클래스에서는 사용되지 않도록 할 속성과 메서드가 있을 수 있습니다. 이러한 속성과 메서드를 private 공개 상태 수정자로 표시하여 다른 클래스가 실수로 액세스하는 것을 방지할 수 있습니다.

  • protected - 서브클래스에서 선언에 액세스할 수 있도록 합니다. 이러한 클래스와 서브클래스를 정의하는 클래스에 사용하려는 속성과 메서드를 protected 공개 상태 수정자로 표시합니다.
  • internal - 동일한 모듈에서 선언에 액세스할 수 있도록 합니다. internal 수정자는 private 수정자와 유사하지만 동일한 모듈에서 액세스된다면 클래스 외부에서 내부 속성과 메서드에 액세스할 수 있습니다.

클래스를 정의하면 공개적으로 표시되며 클래스를 가져오는 모든 패키지가 클래스에 액세스할 수 있습니다. 즉, 공개 상태 수정자를 지정하지 않는 경우 클래스는 기본적으로 공개 상태입니다. 마찬가지로 클래스에서 속성과 메서드를 정의하거나 선언하면 기본적으로 클래스 객체를 통해 클래스 외부에서 이러한 속성과 메서드에 액세스할 수 있습니다. 주로 다른 클래스에서 액세스할 필요가 없는 속성과 메서드를 숨기기 위해 코드의 적절한 공개 상태를 정의하는 것이 중요합니다.

예를 들어 운전자가 차량에 액세스할 수 있도록 만드는 방법을 생각해 보세요. 차량을 구성하는 부분 및 자동차가 내부적으로 작동하는 방식에 관한 구체적인 정보는 기본적으로 숨겨져 있습니다. 차량은 최대한 직관적으로 작동하도록 만들어집니다. 자동차를 상용 항공기처럼 복잡하게 작동하게 만들지 않는 것처럼, 다른 개발자나 미래의 자신이 클래스의 어떤 속성과 메서드를 사용해야 하는지에 관해 혼동하는 일이 없도록 해야 합니다.

공개 상태 수정자를 사용하면 코드의 관련 부분을 프로젝트의 다른 클래스에 표시할 수 있으며 구현이 의도치 않게 사용되지 않도록 하여 이해하기 쉽고 버그 발생 가능성이 낮은 코드를 만들 수 있습니다.

다음 다이어그램에서 볼 수 있듯이 공개 상태 수정자를 선언 문법 앞에 배치하고 클래스, 메서드, 속성을 선언해야 합니다.

d5f4f2af7b2136f1.png

속성의 공개 상태 수정자 지정

속성에 공개 상태 수정자를 지정하는 문법은 private, protected, internal 수정자 중 하나로 시작하고 그 뒤에 속성을 정의하는 문법이 나옵니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

222cb4dc24ea76b1.png

예를 들어 다음 코드 스니펫에서 deviceStatus 속성을 비공개로 설정하는 방법을 확인할 수 있습니다.

open class SmartDevice(val name: String, val category: String) {

    ...
    private var deviceStatus = "online"

    ...
}

공개 상태 수정자를 setter 함수로 설정할 수도 있습니다. 이 수정자는 set 키워드 앞에 배치됩니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

7c5f42f49271b19e.png

SmartDevice 클래스의 경우 deviceStatus 속성의 값을 클래스 외부에서 클래스 객체를 통해 읽을 수 있어야 합니다. 그러나 클래스와 하위 요소만 값을 업데이트하거나 쓸 수 있어야 합니다. 이 요구사항을 구현하려면 deviceStatus 속성의 set() 함수에서 protected 수정자를 사용해야 합니다.

다음과 같이 deviceStatus 속성의 set() 함수에서 protected 수정자를 사용합니다.

  1. SmartDevice 슈퍼클래스의 deviceStatus 속성에서 set() 함수에 protected 수정자를 추가합니다.
open class SmartDevice(val name: String, val category: String) {

    ...
    var deviceStatus = "online"
        protected set(value){
           field = value
       }

    ...
}

set() 함수에서 어떠한 작업이나 검사도 하지 않습니다. 단순히 field 변수에 value 매개변수를 할당합니다. 앞서 배운 것처럼 속성 setter의 기본 구현과 유사합니다. 이 경우 set() 함수의 괄호와 본문을 생략할 수 있습니다.

open class SmartDevice(val name: String, val category: String) {

    ...
    var deviceStatus = "online"
        protected set
    ...
}
  1. SmartHome 서브클래스에서 비공개 setter 함수와 함께 deviceTurnOnCount 속성을 0 값으로 설정합니다.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. deviceTurnOnCount 속성을 추가한 후에 turnOnTv() 메서드와 turnOnLight() 메서드에 ++ 산술 연산자를 추가합니다. 그런 다음 deviceTurnOnCount 속성을 추가한 후에 turnOffTv() 메서드와 turnOffLight() 메서드에 -- 산술 연산자를 추가합니다.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    ...

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    ...

}

메서드의 공개 상태 수정자

메서드에 공개 상태 수정자를 지정하는 문법은 private, protected, internal 메서드 중 하나로 시작하고 그 뒤에 메서드를 정의하는 문법이 나옵니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

f126ad823c332643.png

예를 들어 다음 코드 스니펫에서 SmartTvDevice 클래스의 nextChannel() 메서드에 protected 수정자를 지정하는 방법을 확인할 수 있습니다.

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...
    protected fun nextChannel() {
        channelNumber++
    }
    ...
}

생성자의 공개 상태 수정자

생성자에 공개 상태 수정자를 지정하는 문법은 기본 생성자를 정의하는 것과 비슷하며 몇 가지 차이점이 있습니다.

  • 이 수정자는 클래스 이름 뒤, constructor 키워드 앞에 지정됩니다.
  • 기본 생성자에 수정자를 지정해야 하는 경우 매개변수가 없더라도 constructor 키워드와 괄호를 유지해야 합니다.

다음 다이어그램에서 문법을 확인할 수 있습니다.

e2f00ba76d7e51a9.png

예를 들어 다음 코드 스니펫에서 protected 수정자를 SmartDevice 생성자에 추가하는 방법을 확인할 수 있습니다.

open class SmartDevice protected constructor (val name: String, val category: String) {

    ...

}

클래스의 공개 상태 수정자

클래스에 공개 상태 수정자를 지정하는 문법은 private, protected, internal 메서드 중 하나로 시작하고 그 뒤에 메서드를 정의하는 문법이 나옵니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

eb41f4386d4ace9d.png

예를 들어 다음 코드 스니펫에서 SmartDevice 클래스에 internal 수정자를 지정하는 방법을 확인할 수 있습니다.

internal open class SmartDevice(val name: String, val category: String) {

    ...

}

속성과 메서드의 공개 상태를 엄격하게 유지하는 것이 이상적이므로 최대한 자주 private 수정자를 사용하여 이를 선언합니다. 비공개로 유지할 수 없는 경우 protected 수정자를 사용합니다. 보호 상태로 유지할 수 없는 경우 internal 수정자를 사용합니다. 내부에 유지할 수 없는 경우 public 수정자를 사용합니다.

적절한 공개 상태 수정자 지정

다음 표를 보면 클래스나 생성자의 속성이나 메서드에 액세스할 수 있는 위치에 따라 적절한 공개 상태 수정자를 결정하는 데 도움이 됩니다.

수정자

동일한 클래스에서 액세스 가능

서브클래스에서 액세스 가능

동일한 모듈에서 액세스 가능

모듈 외부에서 액세스 가능

private

𝗫

𝗫

𝗫

protected

𝗫

𝗫

internal

𝗫

public

SmartTvDevice 서브클래스에서 speakerVolume 속성과 channelNumber 속성이 클래스 외부에서 제어되도록 허용해서는 안 됩니다. 이러한 속성은 increaseSpeakerVolume() 메서드와 nextChannel() 메서드를 통해서만 제어해야 합니다.

마찬가지로 SmartLightDevice 서브클래스에서 brightnessLevel 속성은 increaseLightBrightness() 메서드를 통해서만 제어해야 합니다.

다음과 같이 SmartTvDeviceSmartLightDevice 서브클래스에 적절한 공개 상태 수정자를 추가합니다.

  1. SmartTvDevice 클래스에서 speakerVolume 속성과 channelNumber 속성에 private 공개 상태 수정자를 추가합니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

     private var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
     private var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    ...
}
  1. SmartLightDevice 클래스에서 brightnessLevel 속성에 private 수정자를 추가합니다.
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    private var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    ...
}

9. 속성 위임 정의

이전 섹션에서 Kotlin의 속성이 지원 필드를 사용하여 메모리에 값을 보유한다고 배웠습니다. field 식별자를 사용하여 이 필드를 참조합니다.

지금까지 작성한 코드를 보면 중복된 코드에서 값이 SmartTvDeviceSmartLightDevice 클래스의 speakerVolume, channelNumber, brightnessLevel 속성에 해당하는 범위 내에 있는지 확인할 수 있습니다. 위임을 통해 setter 함수에 범위 확인 코드를 재사용할 수 있습니다. 필드, getter 함수, setter 함수를 사용해 값을 관리하는 대신 위임을 통해 관리합니다.

속성 위임을 만드는 문법은 변수 선언으로 시작하고 그 뒤에 by 키워드 및 속성의 setter 함수와 getter 함수를 처리하는 위임 객체가 나옵니다. 다음 다이어그램에서 문법을 확인할 수 있습니다.

ec1b1498e83ed492.png

구현을 위임할 수 있는 클래스를 구현하려면 먼저 인터페이스를 잘 알아야 합니다. 인터페이스는 구현하는 클래스가 준수해야 하는 프로토콜로서 작업을 하는 방법이 아닌 해야 할 작업에 중점을 둡니다. 간단히 말해 인터페이스는 추상화하는 데 유용합니다.

예를 들어 집을 지을 때는 사전에 원하는 사항을 건축가에게 알려줍니다. 침실, 아이방, 거실, 주방, 욕실 몇 개를 만들고 싶습니다. 간단히 말해 원하는 항목을 지정하면 건축가가 달성할 방법을 지정합니다. 다음 다이어그램에서 인터페이스를 만드는 문법을 확인할 수 있습니다.

aa85b0a9a095cb87.png

클래스를 확장하고 클래스의 기능을 재정의하는 방법을 이미 알아봤습니다. 인터페이스의 경우 클래스가 인터페이스를 구현합니다. 클래스는 인터페이스에 선언된 메서드와 속성의 구현 세부정보를 제공합니다. ReadWriteProperty 인터페이스와 유사한 작업을 실행하여 위임을 만듭니다. 인터페이스에 관해서는 다음 단원에서 자세히 알아봅니다.

var 유형의 위임 클래스를 만들려면 ReadWriteProperty 인터페이스를 구현해야 합니다. 마찬가지로 val 유형의 경우 ReadOnlyProperty 인터페이스를 구현해야 합니다.

다음과 같이 var 유형의 위임을 만듭니다.

  1. main() 함수 앞에 ReadWriteProperty<Any?, Int> 인터페이스를 구현하는 RangeRegulator 클래스를 만듭니다.
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main(){
    ...
}

꺾쇠괄호나 괄호 안의 콘텐츠는 걱정하지 마세요. 이는 일반적인 유형을 나타내며, 이 유형은 다음 단원에서 알아봅니다.

  1. RangeRegulator 클래스의 기본 생성자에서 initialValue 매개변수, 비공개 minValue 속성과 비공개 maxValue 속성(모두 Int 유형)을 추가합니다.
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. RangeRegulator 클래스의 본문에서 getValue() 메서드와 setValue() 메서드를 재정의합니다.
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

이러한 메서드는 속성의 getter 함수 및 setter 함수 역할을 합니다.

  1. SmartDevice 클래스 앞에 있는 줄에서 ReadWriteProperty 인터페이스와 KProperty 인터페이스를 가져옵니다.
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String){
    ...
}

...

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

...
  1. RangeRegulator 클래스의 getValue() 메서드 앞에 있는 줄에서 fieldData 속성을 정의하고 initialValue 매개변수를 사용하여 초기화합니다.
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {

    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {

    }
}

이 속성은 변수의 지원 필드 역할을 합니다.

  1. getValue() 메서드의 본문에서 fieldData 속성을 반환합니다.
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {

    }
}
  1. setValue() 메서드의 본문에서 할당할 value 매개변수가 minValue..maxValue 범위에 있는지 확인한 후에 fieldData 속성에 할당합니다.
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}
  1. SmartTvDevice 클래스에서 위임 클래스를 사용하여 speakerVolume 속성과 channelNumber 속성을 정의합니다.
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    ...
}
  1. SmartLightDevice 클래스에서 위임 클래스를 사용하여 brightnessLevel 속성을 정의합니다.
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    ...

}

10. 솔루션 테스트

다음 코드 스니펫에서 솔루션 코드를 확인할 수 있습니다.

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"
        protected set

    open val deviceType: String = "unknown"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType: String = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)
    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}

class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType: String = "Smart Light"
    private var brightnessLevel by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name is turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("$name turned off")
    }

    fun increaseBrightNess() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

class SmartHome(val smartTvDevice: SmartTvDevice, val smartLightDevice: SmartLightDevice) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightNess()
    }

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    private var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

fun main() {
    val smartHome = SmartHome(
        SmartTvDevice(deviceName = "Android TV", deviceCategory = "Entertainment"),
        SmartLightDevice(deviceName = "Google light", deviceCategory = "Utility")
    )

    smartHome.turnOnTv()
    smartHome.turnOnLight()
    println("Total number of devices currently turned on: ${smartHome.deviceTurnOnCount}")
    println()

    smartHome.increaseTvVolume()
    smartHome.changeTvChannelToNext()
    smartHome.increaseLightBrightness()
    println()

    smartHome.turnOffAllDevices()
    println("Total number of devices currently turned on: ${smartHome.deviceTurnOnCount}.")
}

출력은 다음과 같습니다.

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light is turned on. The brightness level is 2.
Total number of devices currently turned on: 2.

Speaker volume increased to 3.
Channel number increased to 2.
Brightness increased to 3.

Android TV turned off.
Google light turned off.
Total number of devices currently turned on: 0.

11. 도전과제 해 보기

  • SmartDevice 클래스에서 "Device name: $name, category: $category, type: $deviceType" 문자열을 출력하는 printDeviceInfo() 메서드를 정의합니다.
  • SmartTvDevice 클래스에서 볼륨을 줄이는 decreaseVolume() 메서드와 이전 채널로 이동하는 previousChannel() 메서드를 정의합니다.
  • SmartLightDevice 클래스에서 밝기를 낮추는 decreaseBrightness() 메서드를 정의합니다.
  • SmartHome 클래스에서 각 기기의 deviceStatus 속성이 "on" 문자열로 설정된 경우에만 모든 작업을 실행할 수 있도록 합니다. 또한 deviceTurnOnCount 속성이 올바르게 업데이트되도록 합니다.

구현을 완료하면 다음과 같이 합니다.

  • SmartHome 클래스에서 decreaseTvVolume(), changeTvChannelToPrevious(), printSmartTvInfo(), printSmartLightInfo(), decreaseLightBrightness() 메서드를 정의합니다.
  • SmartHome 클래스의 SmartTvDeviceSmartLightDevice 클래스에서 적절한 메서드를 호출합니다.
  • main() 함수에서 이러한 추가된 메서드를 호출하여 테스트합니다.

12. 결론

축하합니다. 클래스를 정의하고 객체를 인스턴스화하는 방법을 알아봤습니다. 클래스 간의 관계를 만들고 속성 위임을 만드는 방법도 알아봤습니다.

요약

  • OOP에는 캡슐화, 추상화, 상속, 다형성 등 4가지 기본 원칙이 있습니다.
  • 클래스는 class 키워드로 정의되며 속성과 메서드를 포함합니다.
  • 속성은 맞춤 getter와 setter를 가질 수 있다는 점을 제외하면 변수와 비슷합니다.
  • 생성자는 클래스의 객체를 인스턴스화하는 방법을 지정합니다.
  • 기본 생성자를 정의할 때 constructor 키워드를 생략할 수 있습니다.
  • 상속을 통해 더 쉽게 코드를 재사용할 수 있습니다.
  • IS-A 관계는 상속을 의미합니다.
  • HAS-A 관계는 컴포지션을 의미합니다.
  • 공개 상태 수정자는 캡슐화를 달성하는 데 중요한 역할을 합니다.
  • Kotlin은 public, private, protected, internal 등 4가지 공개 상태 수정자를 제공합니다.
  • 속성 위임을 사용하면 여러 클래스에서 getter 및 setter 코드를 재사용할 수 있습니다.

자세히 알아보기