열네 번째, 상속입니다.
Kotlin의 모든 클래스는 공통 수퍼 클래스로 Any를 가지고 있습니다. Any는 어떠한 수퍼 타입도 선언돼 있지 않은 클래스로 모든 클래스의 기본적인 수퍼 클래스입니다.
class Example // 묵시적으로 Any를 상속
Any는 equals(), hashCode(), toString()이라는 3개의 메소드를 가지고 있습니다. 그러므로, Kotlin의 모든 클래스에 이 세가지의 메소드가 정의됩니다.
기본적으로 Kotlin 클래스들은 모두 final 이어서 상속 될 수 없습니다. 클래스를 상속 가능하게 하려면 open 키워드를 붙입니다.
open class Base // 클래스가 상속을 위해 열린(open) 상태입니다.
상속하는 명시적 수퍼 타입을 선언하기 위해서는 클래스 헤더 부분에 콜론(:) 뒤에 상속할 수퍼 타입을 위치시킵니다.
open class Base(p: Int)
class Derived(p: Int) : Base(p)
파생(derived) 클래스에 주 생성자가 있는 경우 기반(base) 클래스는 파생 클래스의 매개 변수에 따라, 파생 클래스의 주 생성자에서 초기화될 수 있으며 또한, 초기화되어야 합니다. 위의 코드의 경우 상속해 준 Base 클래스는 상속받은 파생 클래스인 Derived의 매개변수 p를 가지고 Base의 주 생성자가 호출되어 초기화 됩니다(당연히, Base에 부 생성자가 있는 경우, 그걸 통해 초기화가 필요하면 그렇게 할 수 있습니다).
파생(derived) 클래스
상속 받은 클래스를 상속 해 준 수퍼 클래스로 부터 유래 됐다, 파생 됐다는 뜻으로 파생 클래스라고도 부릅니다. 이런 경우 상속해 준 클래스를 기본(base) 클래스라고도 부릅니다. 용어를 의역하여 상속된 클래스로 일원화 할 수도 있지만, 원문의 표현을 그대로 전하는 것도 중요하기 때문에 원문에 따라 용어가 사용될 예정입니다.
- 상속해준 클래스: 부모 클래스, 기반(base) 클래스, 수퍼 클래스
- 상속받은 클래스: 자식 클래스, 파생(derived) 클래스, 하위 클래스
파생된 클래스에 주 생성자가 없는 경우에는 각각의 부 생성자가 super 키워드를 사용하여 기반 클래스 초기화 하거나 또는 다른 생성자에 위임하여 꼭 초기화 해야 합니다. 이런 경우 각각의 부 생성자는 각각 다른 기반 클래스의 생성자를 호출할 수 있다는 것을 주목하시면 좋습니다. 당연한 얘기 일 수 있으나, 정리하면 기반 클래스를 초기화 할 때 꼭 (기반 클래스) 주 클래스나 파생 클래스 생성자와 같은 매개변수 형태를 호출해야 하는 것이 아니라, 필요에 따라 원하는 기반 클래스의 생성자를 호출하면 됩니다.
class MyView : View {
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}
아래에 이어지는 두 주제는 상속 후 수퍼 쪽에는 멤버를 개정(override)하는 오버라이딩에 대한 얘기입니다. 즉, 상속 받은 것 중 원하는 내용이 아닐 때 이를 변경하는 것입니다. 우리 말로 하면 개정, 변경 등이 돼야겠지만 워낙에 고유 용어처럼 오버라이딩을 써 와서 오버라이딩이라고 표현하겠습니다. overriing methods 같은 경우는 '메소드를 개정하기'가 해석은 더 정확하나 우리에게 익숙한 대로 명사구 식으로 메소도 오버라이딩으로 표기합니다.
메소드 오버라이딩(Overriding methods)
메소드
메소드는 (잘 알고 있는 것처럼) 함수의 한 형태로 클래스(또는 객체)의 멤버로서의 함수를 메소드라고 부릅니다. Java 같이 모든 코드가 클래스 기반이고, 그에 따라 함수들도 모두 메소드인 상황에서는 메소드라는 용어만 사용할 것입니다. 하지만, Kotlin에서는 함수의 언어 차원에서 최상위 수준 함수 같은 예를 포함하여 함수의 위치나 용도가 더 다양화 됐기 때문에 함수라는 말을 주로 사용합니다.
공식 문서에서는 메소드 개념으로 함수를 얘기할 때도 메소드와 함수를 혼용하여 사용합니다. 본 연재도 그대로 따르고 있습니다.
그러므로, 예를 들어 '멤버로서의 함수를 명확히 나타내려고 메소드라고 했구나' 처럼 문맥에 따라 이해하시면 됩니다.
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이 아닌 클래스) 멤버에는 붙여도 아무런 영향을 미치지 않습니다. 클래스가 final이어서 상속 불가하기 때문입니다.
override가 붙은 멤버는 그 자체가 열린(open) 상태입니다. 그래서, (당연히) 하위 클래스에서 오버라이딩 될 수 있습니다. 만약, 또 오버라이딩을 되는 것을 막고 싶으면 final을 붙이면 됩니다.
open class Rectangle() : Shape() {
final override fun draw() { /*...*/ }
}
프로퍼티 오버라이딩
오버라이딩 메커니즘은 프로퍼티에도 메소드와 똑같이 동작합니다. 수퍼 클래스에 선언된 프로퍼티를 파생된 클래스에서 재선언 할 때는 반드시 override를 붙여야 하며 호환되는 타입이어야 합니다. 각각의 선언된 프로퍼티는 초기화 부분을 갖는 프로퍼티나 게터를 갖는 프로퍼티로 오버라이딩 될 수 있습니다.
open class Shape {
open val vertexCount: Int = 0
}
class Rectangle : Shape() {
override val vertexCount = 4
}
val 프로퍼티를 var 프로퍼티로 오버라이딩 할 수 있지만, 그 반대는 안 됩니다. 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 // 후에 다른 값을 설정할 수 있습니다.
}
파생된 클래스 초기화 순서
파생된 클래스의 새로운 인스턴스가 생성되는 동안, 첫 번째 단계(그 전에 기반 클래스 생성자에 전달되는 인수에 대한 평가가 선행되기는 합니다)로 기반 클래스의 초기화가 수행됩니다. 즉, 파생된 클래스의 초기화 로직을 실행하기 전에 수행됩니다.
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") }
}
fun main() {
println("Constructing the derived class(\"hello\", \"world\")")
Derived("hello", "world")
}
/* 결과: main의 println -> 기반 클래스 생성자 인수 평가 부분 -> 기반 클래스 초기화 부분 진행 -> 파생 클래스 초기화 부분 진행
Constructing the derived class("hello", "world")
Argument for the base class: Hello
Initializing a base class
Initializing size in the base class: 5
Initializing a derived class
Initializing size in the derived class: 10
*/
이 말은 기반 클래스의 생성자가 실행될 때, 선언된 프로퍼티들이나 파생된 클래스 내에서 오버라이딩된 프로퍼티들은 아직 초기화 전이라는 얘기입니다. 기반 클래스의 초기화 로직에서 이러한 프로퍼티를 (직접적으로나 또는 또는 오버라이딩된 open 멤버를 통해서 간접적으로) 사용하는 경우 잘못된 동작이나 런타임 실패로 이어질 수 있습니다. 그러므로, 기반 클래스를 설계할 때는 생성자나 초기화 블록, 프로퍼티 초기화 부분에서 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
}
내부 클래스 안에서 외부 클래스의 수퍼 클래스에 접근하기 위해서는 super@Outer식으로 바깥 클래스 이름으로 수식된 super 키워드를 사용합니다.
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}
class FilledRectangle: Rectangle() {
override fun draw() {
val filler = Filler()
filler.drawAndFill()
}
inner class Filler {
fun fill() { println("Filling") }
fun drawAndFill() {
super@FilledRectangle.draw() // Rectangle의 draw() 구현을 호출
fill()
// borderColor의 get() Rectangle에서의 구현을 사용
println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}")
}
}
}
fun main() {
val fr = FilledRectangle()
fr.draw()
}
/* 결과
Drawing a rectangle
Filling
Drawn a filled rectangle with color black
*/
오버라이딩 규칙
코틀린에서 구현 상속은 다음과 같은 규칙에 규제를 받습니다. 즉, 클래스가 같은 멤버에 대해 직접적인 수퍼 클래스들로부터 다중 구현을 상속받는 경우에는 해당 멤버는 반드시 오버라이딩해서 자신의 구현을 제공해야 합니다(아마 상속 받는 것들 중에 하나를 사용할 수도 있을 겁니다).
Java와 같이 Kotlin도 클래스 다중 상속을 지원하지 않고, 인터페이스를 통한 다중 상속을 제공합니다. 그러므로, 여기에서 '수퍼 클래스들'이라고 하는 것은 인터페이스도 포함된다고 생각하셔야 합니다. 그리고, 추후 인터페이스 부분에서 설명하지만, Kotlin의 인터페이스 메소드는 구현을 가질 수 있습니다.
구현을 상속한 수퍼 타입을 표시하기 위하여 super<Base> 처럼 super에 꺽쇄 괄호 안에 타입 이름을 지정하여 붙일 수 있습니다.
open class Rectangle {
open fun draw() { /* ... */ }
}
interface Polygon {
fun draw() { /* ... */ } // 인터페이스의 멤버는 기본적으로 open입니다.
}
class Square() : Rectangle(), Polygon {
// 컴파일러는 drwa()를 오버라이딩하라고 요구합니다.
override fun draw() {
super<Rectangle>.draw() // Rectangle.draw() 호출
super<Polygon>.draw() // Polygon.draw() 호출
}
}
Rectangle과 Polygon 모두를 상속 받는데 문제는 없지만, 둘 다 draw() 구현을 가지고 있으므로, Square에서 draw()를 오버라이딩하고 모호성 제거하기 위해 개별적인 구현을 제공해야 합니다.
'Kotlin' 카테고리의 다른 글
공식 문서로 배우는 코틀린 - 16. Interfaces (0) | 2024.03.08 |
---|---|
공식 문서로 배우는 코틀린 - 15. Properties (0) | 2024.03.07 |
공식 문서로 배우는 코틀린 - 13. Classes (0) | 2024.03.07 |
공식 문서로 배우는 코틀린 - 12. Packages and imports (0) | 2024.03.07 |
공식 문서로 배우는 코틀린 - 11. Control flow (0) | 2024.03.06 |