시작하기 전에

클래스는 건축가의 설계 계획, 즉 청사진과 비슷합니다. 청사진은 집을 짓는 방법에 관한 안내입니다. 집은 청사진에 따라 지은 실제 객체입니다. 클래스를 설계하는 방법을 이해하려면 객체 지향 프로그래밍(OOP)를 잘 알아야 합니다.

OOP는 복잡한 실제 문제를 더 작은 객체로 단순화하는 데 도움이 됩니다. OOP에는 4가지 기본 개념이 있습니다.

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

학습할 내용

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

클래스 정의

class 클래스이름 {
  본문
}

클래스의 정의는 class 키워드로 시작하고 그 뒤에 이름과 중괄호 쌍이 나옵니다. 중괄호 안에 클래스의 속성과 함수를 지정할 수 있습니다.

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

  • fun 키워드과 같은 Kotlin 키워드를 사용하지 마세요.
  • PascalCase로 작성되므로 각 단어는 대문자로 시작하며 단어 사이에 공백이 없어야 합니다.

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

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

Kotlin 플레이그라운드를 열고 기존 코드를 다음 프로그램으로 바꿉니다.

fun main() {
}

main() 함수 앞에 SmartDevice 클래스를 정의합니다.

class SmartDevice {
    // empty body
}

fun main() {
}

클래스 인스턴스 만들기

SmartDevice 클래스로 스마트 기기의 개념에 관한 청사진을 보유하게 됩니다. 프로그램에 실제 스마트 기기를 포함하려면 SmartDevice 객체 인스턴스를 만들어야 합니다.

클래스이름()

인스턴스화 문법은 클래스 이름으로 시작하고 그 뒤에 괄호 쌍이 나옵니다.

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

class SmartDevice {
    // empty body
}

fun main() {
    val smartTvDevice = SmartDevice()
}

클래스 메서드 정의

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

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

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

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

fun main() {
    val smartTvDevice = SmartDevice()
}

객체에서 메서드 호출

이제 SmartDevice 클래스의 메서드를 사용하여 기기를 켜거나 꺼 보겠습니다.

다음은 클래스 메서드 호출 문법입니다.

객체.메서드이름([선택사항]인수)

main() 함수에서 turnOn() 메서드와 turnOff() 메서드를 호출합니다.

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

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

fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}

코드를 실행합니다.

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

클래스 속성 정의

속성은 클래스의 특성이나 데이터 속성을 정의합니다. 예를 들어 스마트 기기에는 다음과 같은 속성이 있습니다.

  • 이름: 기기 이름
  • 카테고리: 스마트 기기의 유형
  • 기기 상태: 켜기, 끄기, 온라인, 오프라인 등

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

class SmartDevice {
    val name = "Android TV"
    val category = "Entertainment"
    val deviceStatus = "online"

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

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

fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}

smartTvDevice 변수 다음에 name을 출력합니다.

class SmartDevice {
    val name = "Android TV"
    val category = "Entertainment"
    val deviceStatus = "online"

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

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

fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}

프로그램을 실행합니다.

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 함수를 작성하면 됩니다. 다른 예로 이름이 항상 대문자여야 한다는 요구사항이 있다고 가정하겠습니다. name 속성을 대문자로 변환하는 getter 함수를 구혐할 수 있습니다.

속성을 선언하는 전체 문법입니다.

var 변수이름: 데이터유형 = 초기값
  get() {
    본문
    return 반환값
  }
  set(value) {
    본문
  }

속성에 getter 및 setter 함수를 정의하지 않으면 Kotlin 컴파일러가 내부적으로 함수를 생성합니다.

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

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

생성자 정의

생성자의 기본 목적은 클래스의 객체를 만드는 방법을 지정하는 것입니다. 생성자 내의 코드는 클래스의 객체가 인스턴스화될 때 실행됩니다.

기본 생성자

기본 생성자는 매개변수가 없는 생성자입니다.

class SmartDevice constructor() {

}

Kotlin은 간결한 것을 목표로 하므로 주석이나 공개 상태 수정자가 없는 경우 constructor 키워드를 삭제할 수 있습니다.

class SmartDevice {

}

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

매개변수화된 생성자 정의

SmartDevice 클래스에서 기본값을 할당하지 않고 name 속성과 category 속성을 생성자로 이동합니다.

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

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

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

fun main() {
    val smartTvDevice = SmartDevice(name = "Android TV", category = "Entertainment")
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}

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

  • 기본 생성자: 클래스에는 클래스 헤더의 일부로 정의된 기본 생성자가 하나만 있을 수 있습니다. 기본 생성자에는 본문이 없습니다.
  • 보조 생성자: 한 클래스에 여러 보조 생성자가 있을 수 있습니다. 보조 생성자는 클래스를 초기화할 수 있으며 초기화 로직을 포함할 수 있는 본문을 가집니다. 클래스에 기본 생성자가 있는 경우 각 보조 생성자는 기본 생성자를 초기화해야 합니다.
class 클래스이름(매개변수1) {

    //보조 생성자
    constructor(매개변수2): this(매개변수1) {
      본문
    }
}

예를 들어 스마트 기기 제공업체에서 개발한 API를 통합하려고 합니다. 하지만 이 API는 초기 기기 상태를 Int 유형의 상태 코드로 반환합니다. 기기가 오프라인인 경우 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"
      }
    }

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

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

fun main() {
    val smartTvDevice = SmartDevice(name = "Android TV", category = "Entertainment")
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}

클래스 간의 관계 구현

상속을 사용하면 다른 클래스의 특성과 동작을 토대로 클래스를 빌드할 수 있습니다. 예를 들어 모든 스마트 기기는 다양한 기능을 갖추고 있지만 몇 가지 공통적인 특성을 공유합니다. 이러한 공통적인 특성을 각 스마트 기기 클래스에 복제하거나 상속을 통해 코드를 재사용 가능하게 만들 수 있습니다. 이렇게 하려면 SmartDevice 상위 클래스를 만들고 상위 클래스를 상속하는 하위 클래스 SmartTvDevice, SmartLightDevice를 만들 수 있습니다.

SmartDevice 슈퍼클래스에서 class 키워드 앞에 open 키워드를 추가하여 확장 가능하게 만듭니다.

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

다음은 서브클래스를 만드는 문법입니다.

class 서브클래스이름(매개변수) : 
    슈퍼클래스(매개변수) {
      본문
}

SmartDevice 슈퍼클래스를 확장하는 SmartTvDevice 서브클래스를 만듭니다.

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

SmartTvDevice 서브클래스 본문에서 speakerVolumn 속성을 추가합니다.

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

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

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
            }
        }
}

볼륨을 높이는 increaseSpeakerVolume() 메서드와 채널을 변경하는 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 $channelNumber.")
    }
}

SmartDevice 슈퍼클래스를 확장하는 SmartLightDevice 서브클래스를 정의합니다.

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

SmartLightDevice 서브클래스 본문에 brightnessLevel 속성을 정의합니다.

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

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

조명의 밝기를 높이는 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 관계

SmartDevice 슈퍼클래스와 SmartTvDevice 서브클래스 간의 IS-A 관계를 지정하면 슈퍼클래스가 할 수 있는 모든 작업을 서브클래스가 할 수 있습니다.

// Smart TV IS-A Smart Device
class SmartTvDevice : SmartDevice() {

}

HAS-A 관계

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

SmartHome 클래스를 정의합니다.

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

    ...

}

class SmartHome {
}

fun main() { 
    ...
}

SmartHome 클래스 생성자에서 val 키워드를 사용하여 SmartTvDevice 유형의 smartTvDevice 속성을 만듭니다.

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

    ...

}

// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {
}

fun main() { 
    ...
}

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

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

    ...

}

// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {
    fun turnOnTv() {
      smartTvDevice.turnOn()
    }
}

fun main() { 
    ...
}

smartTvDevice 속성에 관해 turnOff() 메서드를 호출하는 turnOffTv() 메서드를 정의합니다.

class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}

smartTvDevice 속성에 관해 increaseSpeakerVolume() 메서드를 호출하는 increaseTvVolume() 메서드를 정의한 후에 nextChannel() 메서드를 호출하는 changeTvChannelToNext() 메서드를 정의합니다.

class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}

SmartHome 클래스 생성자에 val 키워드를 사용하여 SmartLightDevice 유형의 smartLightDevice 속성을 정의합니다.

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

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}

smartLightDevice 객체에 관해 turnOn() 메서드를 호출하는 turnOnLight() 메서드와 turnOff() 메서드를 호출하는 turnOffLight() 메서드를 정의합니다.

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

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

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

smartLightDevice 속성에 관해 increaseBrightness() 메서드를 호출하는 increaseLightBrightness() 메서드를 정의합니다.

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

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }
}

turnOffTv()turnOffLight() 메서드를 호출하는 turnOffAllDevices() 메서드를 정의합니다.

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

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }

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

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

SmartDevice 클래스의 turnOn() 메서드와 turnOff() 메서드를 재정의합니다. 각 메서드의 fun 키워드 앞에 open 키워드를 추가합니다.

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

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}

SmartLightDevice 클래스에 turnOn() 메서드를 본문을 다음 코드와 같이 정의합니다.

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.")
    }

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

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

SmartLightDevice 클래스에 turnOff() 메서드를 본문을 다음 코드와 같이 정의합니다.

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.")
    }

    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")
    }
}

SmartTvDevice 클레스에 다음 코드와 같이 turnOn(), turnOff() 메서드를 추가합니다.

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 $channelNumber.")
    }

    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")
    }
}

main() 함수를 다음 코드로 수정하여 프로그램을 실행합니다.

fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
    
    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

출력은 다음과 같습니다.

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

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

SmartTvDevice, SmartLightDevice 두 서브클래스의 turnOn(), turnOff() 메서드를 보면 메서드가 호출될 때마다 deviceStatus 변수가 업데이트되는 방식이 유사합니다. SmartDevice 클래스에서 상태를 업데이트하고 재사용하면 중복 코드를 제거할 수 있습니다.

SmartDevice 슈퍼클래스에 turnOn(), turnOff() 메서드를 수정합니다.

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

    var deviceStatus = "online"

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

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

super 키워드를 사용하여 SmartTvDevice, SmartLightDevice 서브클래스에서 SmartDevice 슈퍼클래스의 메서드를 호출합니다. 슈퍼클래스의 정의된 메서드를 서브클래스에서 호출하려면 super 키워드를 사용해야 합니다.

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 $channelNumber.")
    }

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

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

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.")
    }

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

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

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

메서드와 마찬가지로 속성도 재정의 할 수 있습니다. SmartDevice 슈퍼클래스에서 deviceType 속성을 정의합니다.

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

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}

SmartTvDevice, SmartLightDevice 서브클래스에서 deviceType 속성을 재정의합니다.

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

    override val deviceType = "Smart TV"

    ...
}

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

    override val deviceType = "Smart Light"

    ...

}

공개 상태 수정자

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

  • public: 기본 공개 상태 수정자입니다. 공개 상태 수정자를 지정하지 않는 경우 기본적으로 공개 상태입니다.
  • private: 동일한 클래스나 소스 파일에서 선언에 액세스할 수 있도록 합니다.
  • protected: 서브클래스에서 선언에 액세스할 수 있도록 합니다.
  • internal: 동일한 모듈에서 선언에 액세스할 수 있도록 합니다. private 수정자와 유사하지만 동일한 모듈에서 액세스된다면 클래스 외부에서 내부 속성과 메서드에 액세스할 수 있습니다.

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

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

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

    ...

    private var deviceStatus = "online"

    ...
}

공개 상태 수정자를 setter 함수로 설정할 수도 있습니다.

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

SmartDevice 슈퍼클래스의 deviceStatus 속성에서 set() 함수에 protected 수정자를 추가합니다.

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

    ...

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

    ...
}

단순히 field 변수에 value 매개변수를 할당하는 setter의 기본 구현의 경우 set() 함수의 괄호와 본문을 생략할 수 있습니다.

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

    ...

    var deviceStatus = "online"
        protected set

    ...
}

SmartHome 클레스에서 비공개 setter 함수와 함께 deviceTurnOnCount 속성을 0 값으로 설정합니다.

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

    var deviceTurnOnCount = 0
        private set

    ...
}

turnOnTv(), turnOffTv(), turnOnLight(), 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()
    }

    ...

}

메서드의 공개 상태 수정자

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

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

    ...

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

    ...
}

생성자의 공개 상태 수정자

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

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

    ...

}

클래스의 공개 상태 수정자

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

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

    ...

}

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

SmartTvDevice 서브클래스에서 speakerVolume, channelNumber 속성은 각각 increaseSpeakerVolume() 메서드와 nextChannel() 메서드를 통해서만 제어해야 합니다.

SmartLightDevice 서브클래스의 brightnessLevel 속성은 increaseLightBrightness() 메서드를 통해서만 제어해야 합니다.

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

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
            }
        }

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

    ...

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

    ...
}

속성 위임 정의

지금까지 작성한 코드를 보면 SmartTvDeviceSmartLightDevice 클래스의 speakerVolume, channelNumber, brightnessLevel 속성이 해당 범위 내에 있는지 확인하는 코드가 중복 되고 있습니다.

setter 함수에 범위 확인 코드를 위임을 통해 재사용할 수 있습니다.

속성 위임을 만드는 문법은 아래와 같습니다.

var 변수이름 by 위임객체

구현을 다른 클래스에 위임하기 전에 인터페이스에 대해 이해할 필요가 있습니다. 인터페이스는 인터페이스를 구현하는 클래스들이 반드시 따라야 하는 계약입니다.

예를 들어 집을 짓기 전에 당신은 건축가에게 무엇을 원하는지를 알려줍니다. 침실, 아이 방, 거실, 주방, 욕실 두 개 정도를 원한다고 말하겠죠. 즉, 당신은 무엇을 원하는지를 명확히 하고, 건축가는 그것을 어떻게 구현할지를 결정합니다. 다음은 인터페이스를 만드는 문법입니다.

interface 인터페이스이름 {
    본문
}

클래스는 인터페이스를 구현합니다. 클래스가 인터페이스에 선언된 메서드와 속성의 구체적인 구현 내용을 제공하는 것입니다.

ReadWriteProperty 인터페이스를 이용해 위임을 만드는 작업을 해보겠습니다.

ReadWriteProperty<Any?, Int> 인터페이스를 구현하는 RangeRegulator 클래스를 만듭니다.

class RangeRegulator(): ReadWriteProperty<Any?, Int> {

}

지금은 꺾쇠괄호나 괄호 안의 콘텐츠의 의미는 몰라도 상관없습니다.

RangeRegulator 클래스의 기본 생성자에 initialValue, minValue, maxValue 속성을 추가합니다.

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

}

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) {
    }
}

코드의 맨 위 줄에서 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) {
    }
}

...

RangeRegulator 클레스에서 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) {
    }
}

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) {
    }
}

setValue() 메서드에서 할당할 value 매개변수가 범위에 있는지 확인한 후에 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
        }
    }
}

SmartTvDevice 클래스에서 위임 클래스를 사용하여 speakerVolume, channelNumber 속성을 정의합니다.

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

    override val deviceType = "Smart TV"

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

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

    ...

}

SmartLightDevice 클래스에서 위임 클래스를 사용하여 brightnessLevel 속성을 정의합니다.

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

    override val deviceType = "Smart Light"

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

    ...

}

전체 코드

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 = "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 = "Smart TV"

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

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

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

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

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

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

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

    override val deviceType = "Smart Light"

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

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

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

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

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> {

    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() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

출력은 다음과 같습니다.

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

정리

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

📚 참고: Android Developers 공식 Kotlin 학습 자료 본 글은 Android Developers의 Kotlin 교육 콘텐츠를 참고하여 재구성했습니다.

카테고리:

업데이트:

댓글남기기