상속

상속

객체 지향 프로그래밍에서 상속(inheritance)은 클래스 간에 공통된 동작을 물려주고 확장하는 핵심 수단이에요. Kotlin의 상속은 다른 언어에서 익숙한 패턴과 대체로 비슷하지만, 몇 가지 중요한 규칙이 달라요. 특히 클래스가 기본적으로 닫혀 있다는 점을 꼭 기억해야 해요.

출처: Kotlin 공식 문서

본문

참고: 클래스로 상속 계층을 만들기 전에 추상 클래스인터페이스를 쓰는 것도 고려해 보세요. 추상 클래스와 인터페이스는 기본적으로 상속할 수 있고, 다른 클래스가 그들의 멤버를 상속하고 구현하도록 설계되어 있어요.

Kotlin의 모든 클래스는 공통 슈퍼클래스 Any를 가져요. 이 Any는 수퍼타입을 선언하지 않은 클래스의 기본 슈퍼클래스예요.

class Example // 암시적으로 Any를 상속해요

Any에는 equals(), hashCode(), toString() 세 가지 메서드가 있어요. 그래서 이 메서드들은 모든 Kotlin 클래스에 정의되어 있어요.

기본적으로 Kotlin 클래스는 final이라 상속할 수 없어요. 클래스를 상속 가능하게 만들려면 open 키워드로 표시해야 해요.

open class Base // 상속을 위해 열린 클래스

(자세한 내용은 Open keyword를 참고하세요.)

명시적 수퍼타입을 선언하려면 클래스 헤더에서 콜론 뒤에 타입을 두면 돼요.

open class Base(p: Int)

class Derived(p: Int) : Base(p)

파생 클래스에 주 생성자(primary constructor)가 있으면, 기반 클래스는 그 주 생성자에서 자신의 파라미터에 따라 초기화될 수 있고(그리고 초기화되어야 합니다), 그래요.

파생 클래스에 주 생성자가 없으면, 각 보조 생성자(secondary constructor)는 super 키워드를 사용해 기반 타입을 초기화하거나, 그렇게 하는 다른 생성자에 위임해야 해요. 이 경우 서로 다른 보조 생성자가 기반 타입의 서로 다른 생성자를 호출할 수 있다는 점에 주의하세요.

class MyView : View {
    constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

Open 키워드

Kotlin에서 open 키워드는 클래스나 멤버(함수 또는 프로퍼티)를 서브클래스에서 오버라이드할 수 있게 한다는 뜻이에요. 기본적으로 Kotlin 클래스와 그 멤버는 final이라서, 명시적으로 open으로 표시하지 않는 한 클래스는 상속받을 수 없고(클래스의 경우) 멤버는 오버라이드할 수 없어요(멤버의 경우).

// 상속을 허용하려고 open 키워드를 붙인 기반 클래스
open class Person(
    val name: String
) {
    // 서브클래스에서 오버라이드할 수 있는 open 함수
    open fun introduce() {
        println("Hello, my name is $name.")
    }
}

// Person을 상속하고 introduce() 함수를 오버라이드하는 서브클래스
class Student(
    name: String,
    val school: String
) : Person(name) {
    override fun introduce() {
        println("Hi, I'm $name, and I study at $school.")
    }
}

기반 클래스의 멤버를 오버라이드한 경우, 그 오버라이딩 멤버도 기본적으로 open이에요. 이를 바꿔서 자신의 클래스의 서브클래스가 자신의 구현을 오버라이드하지 못하게 하려면, 오버라이딩 멤버를 final로 명시적으로 표시하면 돼요.

// 상속을 허용하려고 open 키워드를 붙인 기반 클래스
open class Person(val name: String) {
    // 서브클래스에서 오버라이드할 수 있는 open 함수
    open fun introduce() {
        println("Hello, my name is $name.")
    }
}

// Person을 상속하고 introduce() 함수를 오버라이드하는 서브클래스
class Student(name: String, val school: String) : Person(name) {
    // final 키워드는 서브클래스에서의 추가 오버라이드를 막아요
    final override fun introduce() {
        println("Hi, I'm $name, and I study at $school.")
    }
}

메서드 오버라이딩

Kotlin은 오버라이드 가능한 멤버와 오버라이드 양쪽 모두에 명시적 수정자가 필요해요.

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}

class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

override 수정자는 Circle.draw()에 필수예요. 없으면 컴파일러가 불만을 표시해요. Shape.fill()처럼 함수에 open 수정자가 없다면, 서브클래스에서 같은 시그니처의 메서드를 선언하는 것은 override를 붙이든 붙이지 않든 허용되지 않아요. open 수정자는 final 클래스(즉 open 수정자가 없는 클래스)의 멤버에 붙여도 아무 효과가 없어요.

override로 표시된 멤버는 그 자체가 open이라 서브클래스에서 오버라이드될 수 있어요. 재오버라이드를 막으려면 final을 쓰면 돼요.

open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
}

프로퍼티 오버라이딩

오버라이딩 메커니즘은 프로퍼티에도 메서드와 같은 방식으로 적용돼요. 슈퍼클래스에 선언된 프로퍼티를 파생 클래스에서 다시 선언할 때는 앞에 override를 붙여야 하고, 호환되는 타입이어야 해요. 각 선언된 프로퍼티는 초기화 값을 가진 프로퍼티나 get 메서드가 있는 프로퍼티로 오버라이드할 수 있어요.

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

val 프로퍼티를 var 프로퍼티로 오버라이드할 수도 있지만, 그 반대는 안 돼요. val 프로퍼티는 본질적으로 get 메서드를 선언하는 것이고, 그것을 var로 오버라이드하면 파생 클래스에서 set 메서드가 추가로 선언되기 때문에 가능한 거예요.

override 키워드를 주 생성자의 프로퍼티 선언 일부로도 쓸 수 있다는 점도 기억하세요.

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // 항상 4개의 꼭짓점을 가져요
class Polygon : Shape {
    override var vertexCount: Int = 0  // 나중에 어떤 숫자로든 설정 가능해요
}

파생 클래스 초기화 순서

파생 클래스의 새 인스턴스를 만드는 동안, 기반 클래스 초기화가 첫 번째 단계로 수행돼요(오직 기반 클래스 생성자의 인자 평가만 앞설 뿐). 즉 파생 클래스의 초기화 로직이 실행되기 전에 기반 클래스 초기화가 일어나요.

//sampleStart
open class Base(val name: String) {

    init { println("Initializing a base class") }

    open val size: Int =
        name.length.also { println("Initializing size in the base class: $it") }
}

class Derived(
    name: String,
    val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Argument for the base class: $it") }) {

    init { println("Initializing a derived class") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in the derived class: $it") }
}
//sampleEnd

fun main() {
    println("Constructing the derived class(\"hello\", \"world\")")
    Derived("hello", "world")
}

이 말은, 기반 클래스 생성자가 실행될 때 파생 클래스에 선언되거나 오버라이드된 프로퍼티는 아직 초기화되지 않았다는 뜻이에요. 그러한 프로퍼티를 기반 클래스 초기화 로직에서 사용하면(직접이든 다른 오버라이드된 open 멤버 구현을 통한 간접이든) 잘못된 동작이나 런타임 오류로 이어질 수 있어요. 따라서 기반 클래스를 설계할 때는 생성자, 프로퍼티 초기화, init 블록에서 open 멤버를 사용하는 것을 피해야 해요.

슈퍼클래스 구현 호출하기

파생 클래스의 코드는 super 키워드를 사용해 슈퍼클래스의 함수와 프로퍼티 접근자 구현을 호출할 수 있어요.

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }
    val fillColor: String get() = super.borderColor
}

내부 클래스(inner class) 안에서는 바깥 클래스의 슈퍼클래스에 접근할 때 바깥 클래스 이름으로 한정된 super 키워드, 즉 super@Outer를 사용해요.

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

//sampleStart
class FilledRectangle: Rectangle() {
    override fun draw() {
        val filler = Filler()
        filler.drawAndFill()
    }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            [email protected]() // Rectangle의 draw() 구현을 호출해요
            fill()
            println("Drawn a filled rectangle with color ${[email protected]}") // Rectangle의 borderColor get() 구현을 사용해요
        }
    }
}
//sampleEnd

fun main() {
    val fr = FilledRectangle()
        fr.draw()
}

오버라이딩 규칙

Kotlin에서 구현 상속은 다음 규칙으로 규제돼요. 클래스가 직계 슈퍼클래스 여러 개로부터 같은 멤버의 구현을 여러 개 상속받으면, 그 멤버를 오버라이드해 자신만의 구현을 제공해야 해요(아마도 상속받은 구현 중 하나를 사용해서요).

상속된 구현을 가져온 수퍼타입을 나타내려면 각괄호 안의 수퍼타입 이름으로 한정된 super, 예를 들어 super<Base>를 사용해요.

open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // 인터페이스 멤버는 기본적으로 'open'이에요
}

class Square() : Rectangle(), Polygon {
    // 컴파일러가 draw()를 오버라이드하도록 요구해요:
    override fun draw() {
        super<Rectangle>.draw() // Rectangle.draw() 호출
        super<Polygon>.draw() // Polygon.draw() 호출
    }
}

RectanglePolygon 둘 다에서 상속받는 것은 괜찮지만, 둘 다 draw()의 구현을 갖고 있기 때문에 Square에서 draw()를 오버라이드해 모호성을 없애려면 별도의 구현을 제공해야 해요.

더 알아보기 (Learn more)

상속은 객체 지향 설계의 뼈대가 되는 개념이라서, 클래스와 인터페이스, 추상 클래스가 어떻게 얽히는지를 함께 보면 훨씬 이해가 빨라져요. 이 페이지의 openoverride, super 한정자 규칙을 직접 코드로 연습해 보면, 복잡한 상속 계층에서 헷갈리는 순서 문제를 예방할 수 있어요.