본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 5. 클래스 - 상속과 인터페이스

by 사바라다 2021. 6. 15.

안녕하세요. 코틀린 차곡차곡 5번째 시간입니다. 오늘은 이전 시간에 이어서 코틀린의 클래스 기본에 대해서 알아보는 시간을 가져보도록 하겠습니다.

상속(Inheritance)

상속이란 클래스를 설계할 때 외부 클래스를 틀로 가지고 와서 제작하는 기법을 말합니다. 즉, 상속을 이용하면 기 기반이 되는 클래스의 구성요소를 다시 선언하지 않고 사용할 수 있어서 중복을 막을 수 있습니디. 이러한 상속 기법을 코틀린에서는 제공하고 있습니다.

상속에는 상속을 사용하는 클래스와 상속의 대상이 되는 클래스가 필요합니다. 각각을 자식(Derived), 부모(Base) 클래스라고 부릅니다.

코틀린의 상속에는 2가지가 필요합니다. 첫번째는 부모클래스 및 함수, 그리고 맴버변수를 open 키워드로 상속을 가능하게 해주는 것입니다. 두번째는 자식클래스에서 : {baseClass}를 이용하여 부모클래스와 자식클래스를 상속 받는다고 명시해줍니다. 클래스를 상속받을때는 기본생성자의 파라미터를 포함해서 전달해야합니다. 없다면 '()'가 필수적으로 필요합니다. 이렇게 하면 자식클래스에서는 부모클래스에서 open 되어있는 요소를 사용할 수 있게됩니다.

아래 예제를 보도록 하겠습니다. MappingBaseService Class를 상속가능하게 open 키워드를 이용해서 MappingService에서 상속받았습니다.

open class MappingBaseService {
    fun service(): String = "MappingBaseService" 
}

class MappingService(val firstProperty: String) : MappingBaseService() {

}

아래처럼 사용했을 때 부모 클래스의 함수를 사용할 수 있는 것을 확인할 수 있었습니다.

val mappingService = MappingService("H")
println("service = ${mappingService.service()}") // 결과 : service = MappingBaseService

상속에서의 초기화 순서

클래스 상속에서는 어떻게 초기화 순서가 이루어지는지 한번 테스트 해보도록 하겠습니다.

open class MappingBaseService {

    init {
        println("MappingBaseService initialize")
    }

    fun service(): String = "MappingBaseService" 
}

class MappingService(val firstProperty: String) : MappingBaseService() {

    init {
        println("MappingService initialize")
    }
}

실제 호출을 통해 결과를 보도록 하겠습니다.

val mappingService = MappingService("H") 
// 결과 : MappingBaseService initialize
//       MappingService initialize

결과로 알 수 있는 사실은 클래스 상속에서 초기화는 부모 객체에서 먼저 이루어지고 이후 자식 객체에서 이루어 진다는 것을 알 수 있었습니다.

상속에서의 오버라이딩

클래스 상속에서 오버라이딩이라는 개념이 있습니다. 이것은 부모 객체에 선언되어있는 요소에 대해서 자식 객체에서 재정의 할 수 있는 기능을 말합니다. 코틀린에서는 함수와 맴버변수의 오버라이딩이 가능합니다. 오버라이딩 기능을 사용하기 위해서는 클래스가 open 되어 있어야 합니다.

또한 재정의해서 사용하는 클래스에서는 override 키워드를 반드시 붙여주어야합니다.

open class MappingBaseService {

    open fun service(): String { // 오버라이딩하여 재정의하기 위해 함수 open
        return "MappingBaseService"
    }
}

class MappingService(val firstProperty: String) : MappingBaseService() {

    override fun service(): String { // 오버라이딩
        return "MappingService"
    }
}

결과를 보도록 하겠습니다. 아래처럼 자식 객체에 재정의한 메서드가 호출된 것을 확인할 수 있습니다.

val mappingService = MappingService("H")
println("service = ${mappingService.service()}") // 결과 : service = MappingService

코틀린에서는 멤버변수도 오버라이딩할 수 있습니다. 반대로 말하면 코틀린에서는 자식 객체는 부모 객체와 동일한 이름을 변수에 사용할 수 없다는 말입니다. 동일한 이름의 멤버변수를 사용하기 위해서는 반드시 오버라이딩이 필수적입니다.

open class MappingBaseService {

    open val firstProperty: String = "MappingBaseService property"

    open fun service(): String {
        return "MappingBaseService"
    }
}

class MappingService() : MappingBaseService() {

    override val firstProperty: String = "MappingService property"

    override fun service(): String {
        return "MappingService"
    }

}

아래는 firstProperty를 호출 했을 때 결과입니다.

val mappingService = MappingService()
println("service = ${mappingService.firstProperty}") // 결과 : service = MappingService

추가적으로 부모 객체의 var 타입의 멤버변수는 val로 오버라이딩할 수 있습니다. 하지만 반대는 되지 않습니다. 왜냐하면 var는 getter, setter를 모두 가지고 있지만 val는 getter만 가지고 있기 때문입니다. 또한 기본 생성자에 override를 이용하여 값을 가져오는 것도 가능합니다.

Any에 관하여

코틀린의 모든 클래스는 명시적으로 특정 클래스를 상속 받지 않으면 Any 객체를 묵시적으로 상속받습니다. 따라서 Any 클래스는 모든 객체의 부모 객체입니다. 이러한 Any 클래스에는 3가지의 메서드만이 정의되어 있습니다. 바로 equals(), hashCode(), toString() 입니다.

그렇다면 Any class의 코드를 한번 보도록 하겠습니다. 아래 코드는 Any kotlin 클래스를 번역한 부분입니다.

package kotlin

/**
 * 코틀린 클래스 계층 구조의 최상위 클래스. 모든 클래스는 [Any]를 상속 받는다.
 */
public open class Any {
    /**
     * 객체간의 동등성을 비교할 때 사용합니다. 동등성이란 동일한 값을 가지고 있는지 판단하는 것이며 같은 같은 객체라는 뜻은 아닙니다. 
     * 요구사항:
     *
     * * Reflexive: non-null value 에 대해서 `x`, `x.equals(x)` 는 true를 반환합니다..
     * * Symmetric: non-null values 에 대해서 `x`과 `y`, `x.equals(y)` 가 true 라면 `y.equals(x)` 도 true를 반환합니다.
     * * Transitive:  non-null values `x`, `y`, 그리고 `z` 에 대해 `x.equals(y)`가 true 이며 `y.equals(z)` 도 true 라면 `x.equals(z)` 도 true를 반환합니다.
     * * Consistent:  non-null values `x` 와 `y`가 있을 때 여러번 `x.equals(y)`를 반복한다고 해서 그 값이 바뀌거나 하지 않습니다.
     * * Never equal to null: non-null value `x`는 `x.equals(null)`의 경우 false를 반환합니다.
     *
     * 더 궁금하신 분은 [equality](https://kotlinlang.org/docs/reference/equality.html) 문서를 참고하세요.
     */
    public open operator fun equals(other: Any?): Boolean

    /**
     * 객체의 hash code 값을 반환 합니다. 
     * * 하나의 객체에 대해서 항상 동일한 양의 정수 값을 가집니다. 
     * * 만약 equals 함수를 통해 2 객체의 값이 동일하다면 `hashCode` 값 역시 동일합니다.
     */
    public open fun hashCode(): Int

    /**
     * 객체를 나타내는 string 표현을 반환한다.
     */
    public open fun toString(): String
}

인터페이스(interface)

코틀린에서 인터페이스는 추상화 함수의 선언과 함수의 구현이 함께 포함되어질 수 있습니다. 코틀린 클래스와 인터페이스의 차이점은 인터페이스에서는 상태를 저장할 수 없다는 것입니다. 인터페이스에서도 멤버변수는 선언할 수 있지만 이 변수들은 추상화 되어있으며 상태로써 사용하고 싶다면 implementation 받아 오버라이딩하여 사용하여야합니다.

인터페이스는 아래와 같이 선언하고 사용할 수 있습니다.

interface MyInterface {
    fun bar()
    fun foo() {
      // optional body
    }
}

이렇게 인터페이스를 선언하고 사용하기 위해서는 인터페이스를 class를 통해 구현하여야 합니다. 인터페이스 구현은 아래의 코드처럼 행해질 수 있습니다. 인터페이스는 다중 구현이 가능하며, 여러 인터페이스를 상속 받을 경우 ,를 찍어 나눕니다. 또한 클래스 상속은 기본생성자 brace인 ()가 필수 이지만 인터페이스 구현은 ()를 사용하지 않습니다.

class Child : MyInterface {
    override fun bar() {
        // body
    }
}

코틀린에서는 인터페이스에 멤버변수를 선언할 수 있습니다. 하지만 위에서도 인급했듯이 일반적으로 클래스에 선언해주는 식이 아닌 아래처럼 추상화를 시켜놓든지 아니면 get을 이용하여 구현을 해야합니다.

interface MyInterface {
    val prop: Int // abstract

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

마무리

오늘은 이렇게 class의 상속과 인터페이스에 대해서 알아보았습니다.

원래는 오늘로 코틀린 기본을 마무리하려고 하였으나 글을쓰다보니 좀 길어진거 같습니다.

다음시간을 코틀린 기본 마지막 시간으로하고 클래스를 조금더 보는 시간을 가져보겠습니다.

감사합니다.

참조

코틀린 인 액션 (Kotlin In Action)

코틀린을 다루는 기술 (The Joy Of Kotlin)

kotlinlang_classes

댓글