[Kotlin]클래스 3—상속(Inheritance)

dEpayse
dEpayse_publication
11 min readSep 18, 2020

상속에 관하여 처음 접한다면 이번 포스트가 다소 어렵게 느껴질 수 있다. 처음 객체 지향 프로그래밍을 배울 때 생소해서 어려웠던 개념이 클래스와 객체의 개념이었는데, 그 다음 과정으로 어려웠던 것이 상속이라는 개념이었던 것을 기억한다. 그러나 내가 이해하기 어려웠던 부분들을 쉽게 풀어보려 노력할 것이고, 이 글을 읽는 독자가 있다면 상속이라는 개념이 내가 처음 접할 때보다 더 친근했으면 하는 바람으로 글을 적어본다.

Kotlin의 상속 개념

상속(Inheritance)이란?

상속은 사전적 의미로는 뒤를 잇는 것 또는 친족 관계에서 재산 권리를 이어 받는 것이라는 뜻이다. 프로그래밍에서는 클래스 간의 관계를 구축하는 방법인데, 부모 클래스와 자식 클래스의 관계가 기본이고 자식 클래스는 부모 클래스의 속성과 동작을 포함한다. 부모 클래스상위 클래스, 기반 클래스라고도 하고 자식 클래스하위 클래스, 파생 클래스라고도 한다.

Fig1. 부모 클래스, 자식 클래스의 동의어 정리

상속 관계

그럼 부모 클래스와 자식 클래스의 관계는 프로그래밍에서 구체적으로 어떤 특징들을 갖고 있을까? 자식 클래스는 부모 클래스의 private를 제외한 메소드, 프로퍼티를 물려 받고 자신만의 생성자, 메소드, 프로퍼티 또한 가질 수 있다. 즉, 자식 클래스는 부모 클래스의 기능을 재사용할 수 있다는 관점에서 효율성을 보일 수 있다.

자식 클래스는 부모 클래스의 기능을 전부 포함하기 때문에, ‘자식 클래스는 부모 클래스이다.’ 라는 명제는 참이다. 그러나 부모 클래스는 자식 클래스의 기능을 전부 포함하지 않기 때문에 ‘부모 클래스는 자식 클래스가 아니다.’ 라는 명제가 참이다. 이런 상속 관계를 ‘is-a’ 관계라고도 한다.

Fig2. parent class와 child class의 관계

Fig2는 부모 클래스와 자식 클래스의 관계를 도식화한 그림이다.

Fig2의 좌측 그림은 상속 관계를 표현한 건데, 화살표 표시가 상속 관계를 나타낸다. 화살표의 끝이 향하는 쪽이 부모 클래스, 화살표가 시작되는 쪽이 자식 클래스이다. 그림에서 확인할 수 있듯이 여러 자식 클래스가 하나의 부모 클래스를 상속받을 수 있다. 그러나 자식 클래스가 여러 부모 클래스를 두는 것은 불가하다.

Fig2의 우측 그림은 포함 관계를 나타내는데, 헷갈리는 부분 중 하나가 집합 관계랑 그림의 의미가 다르다는 것이다. 집합 관계로 생각하면 parent is-a child가 맞지만, 실제로는 그 반대이다. 위에서도 언급했듯이 Kotlin에서 관점은 다음과 같다. 자식 클래스는 부모 클래스의 정보를 모두 포함하기 때문에 child is-a parent가 참이고, 그 역인 parent is-a child는 거짓이다. 실제로 child1이나 child2 클래스의 객체를 생성할 때 parent 클래스 생성자가 반드시 호출된다.

이러한 parent와 child와의 관계는 다음과 같은 경우에 이용할 수 있다. 아래 예제에서 parent class와 child클래스는 상속 관계라고 가정한다.

Example1. child is-a parent
//parent class와 child클래스는 상속 관계라고 가정하고, parent class는
//String 타입의 객체 "I am parent"를 반환하는 whoAmI()라는 메소드를
//갖는 다고 가정한다.
val p:Parent = Child()
println(p.whoAmI())
//결과 : I am parent

Example1에서는 child is-a parent 이기 때문에 Parent 클래스 타입으로 선언했지만 Child클래스의 객체를 할당해도 문제없다는 걸 볼 수 있다. 또한, child클래스의 객체이지만, parent 클래스의 메소드를 사용할 수 있는 것을 볼 수 있다.

자료형 변환에 관하여 덧붙이자면, 형변환을 할 때 업캐스팅(up-casting)이라고도 불리는 child에서 parent 형변환은 정보를 잘라내면 되기 때문에 자연스럽지만, 다운캐스팅(down-casting)이라고도 불리는 parent에서 child 형변환은 없던 정보를 덧붙여야 하기 때문에 더 엄격한 과정을 거쳐야 한다.

상속 세부 개념

Override

상속 관계에서 나오는 개념 중 하나가 Override라는 개념이다. 직역하면 덮어쓰기라는 뜻인데, 위에서 자식 클래스는 부모 클래스의 정보를 포함한다고 했고, 실제로 자식 클래스의 객체를 생성할 때 부모 클래스의 생성자가 호출된다고 하였다. 즉 Example1에서 봤듯이 자식 클래스의 객체는 부모 클래스의 메소드나 프로퍼티를 사용할 수 있다는 뜻이다. 그러나 다음과 같은 예시의 경우, 부모의 메소드나 프로퍼티를 자식 클래스의 객체에서는 다른 의미로 사용하고 싶은 때가 있다.

축구 선수를 의미하는 SoccerPlayer라는 클래스가 있고, 이 클래스를 상속받는 공격수 포지션을 맡는 선수를 뜻하는 StrikerPlayer라는 클래스가 있다. 만약 SoccerPlayer라는 클래스에 “shoot!”이라는 String 객체를 반환하는 shoot()이라는 함수를 정의해주었다면, StrikerPlayer의 객체 역시 SoccerPlayer의 자식 클래스이기 때문에 같은 결과를 갖는 shoot()함수를 갖게 된다. 그러나 우리는 Striker 객체의 shoot()함수는 “Power shoot!”이라는 String객체를 반환하도록 만들고 싶다. 이럴 경우 override를 사용할 수 있다.

이번에는 상속 관계에 있는 클래스들의 정의까지 예시에 포함이 되어있다. open 이라는 생소한 키워드가 나올텐데, 본 포스트의 바로 다음 파트에서 다룰 것이니 override의 이해에 초점을 두도록 하자.

Example2. override의 이해open class SoccerPlayer{
open fun shoot():String = "shoot!"
}
class StrikerPlayer:SoccerPlayer(){
override fun shoot():String = "Power shoot!"
}

fun main() {
val p:SoccerPlayer = SoccerPlayer()
val c:StrikerPlayer = StrikerPlayer()
println(p.shoot())
println(c.shoot())
}
//결과:
shoot!
Power shoot!

상속 접근 제어자(inheritance access modifier)

그럼 두 클래스를 상속 관계로 만드려면 어떤 키워드를 사용해야 할까? Kotlin은 기본적으로 상속을 금지한다. 그래서 두 클래스를 상속 관계로 만드려면 상속 접근 제어자를 사용해야 하는데, 이번에는 상속 접근 제어자에 대하여 알아보고 넘어갈 것이다.

상속 접근 제어자는 클래스, 메소드, 프로퍼티에 사용할 수 있으며, Kotlin에는 final, open, abstract, override가 상속 접근 제어자에 속한다.

Fig3. 상속 접근 제어자

Kotlin은 기본적으로 상속이 안되고, 이말인 즉슨 기본적으로 final이라는 뜻이다. 따라서 어떤 클래스가 부모 클래스가 될 수 있도록 상속을 허용해 주려면 open이라는 키워드를 명시해야 한다는 말이다. Example2에서도 봤듯이, SoccerPlayer클래스와 shoot() 메서드 앞에 open 키워드가 있는 것을 확인할 수 있다. 메서드를 override하기 위해 클래스를 먼저 open, override 한 후 메서드도 open, override 해준 것을 볼 수 있다.

override라는 키워드는 바로 위 파트에서 개념을 이해했었는데, override한 프로퍼티나 메소드는 기본적으로 열려있다. 즉, 한 번 override한 메서드는 그 하위 클래스에서도 열려있다는 뜻이다. 따라서 상위 클래스의 프로퍼티나 메소드를 override한 프로퍼티나 메소드를 하위 클래스의 하위 클래스에서 overriding을 금지하려면 final을 명시해주어야 한다.

  • Fig3에서 생소한 개념은 interface, sealed class와 abstract이다. 본 포스트는 상속과 관련된 개념만을 다루기 때문에, 이 개념들은 본 포스트에서 다루지 않을 예정이다. final, open, override의 사용법만 알아도 상속을 이해할 수 있기 때문이다.
  • 지금 당장 궁금한 독자는 아래 링크한 포스트를 참고할 수 있다.
  • sealed class 포스트는 작성 중입니다.

상속 방법 및 규칙

상속 방법

상속을 해줄 수 있는 방법은 Example2에서도 잠깐 봤지만 이번에는 좀 더 자세히 알아보자.

Example3. 상속 방법open class SoccerPlayer{
open fun shoot():String = "shoot!"
}
class StrikerPlayer: SoccerPlayer() {
override fun shoot():String = "Power shoot!"
}

상속을 해주는 방법은

1. 자식에게 상속해 줄 부모 클래스는 open 클래스여야 한다. 이 이유는 위의 ‘상속 접근 제어자’ 파트에서 다뤘듯이 Kotlin의 기본 상속 접근 제어자가 final이기 때문이다.

2. 자식 클래스에서 콜론 기호(:) 뒤에 부모 클래스의 이름(+괄호)가 오는 것이다. 이 괄호는 부모 클래스의 생성자를 호출한다는 뜻으로, 부모 클래스의 생성자 중 하나가 오면 된다.

이렇게 하면 부모 클래스와 자식 클래스 관계인 상속 관계인 두 클래스를 생성할 수 있다.

상속 규칙

1. 자식 클래스가 부생성자만 있고 주생성자를 갖지 않을 때, 부모 클래스 뒤에 괄호는 빼준다.

‘클래스2’ 포스트에서 주생성자와 부생성자를 다뤘는데, 부모 클래스와 자식 클래스의 주생성자와 부생성자의 유무에 따라 괄호가 필요할 때가 있고, 괄호를 빼주어야 하는 경우가 생긴다는 것을 발견했다. 글쓴이는 부모 클래스와 자식 클래스의 주생성자와 부생성자의 유무에 따른 괄호 필요성을 알기 위해 총 16가지 경우의 수를 실험하였고, 다음과 같은 결과를 얻었다.

Fig4. 상속과 주생성자 부생성자 사이의 관계

Fig4를 보면 알 수 있듯이 자식 클래스가 부생성자만 있고 주생성자를 갖지 않을 때, 자식 클래스 뒤에 콜론과 부모 클래스의 이름만 적어주고, 부모 클래스의 생성자를 호출하는 뜻인 괄호는 빼준다. 단, 이 경우 자식 클래스의 부생성자의 다른 생성자 호출을 따라가면 끝에는 부모 클래스의 생성자가 있어야한다.

이유는 ‘클래스2-생성자와 초기화’ 포스트 부생성자 part의 2번에서 만약 클래스가 주생성자를 갖고 있다면, 부생성자를 정의할 때 그 부생성자가 호출하는 생성자를 따라가면 끝에 반드시 주생성자가 있어야한다고 했다. 그런데 주생성자가 없다면 부생성자가 클래스의 모든 생성자를 담당하는 것이고, 따라서 부모 클래스의 생성자 호출도 자식 클래스의 부생성자가 해야되는 것이다.

2. 부생성자가 호출하는 생성자를 따라가면 끝에는 부모 클래스 생성자의 호출이어야 한다.

만약 자식 클래스의 주생성자가 있다면, ‘클래스2-생성자와 초기화’ 포스트 부생성자 part의 2번처럼 부생성자가 호출하는 생성자를 따라가면 끝에 반드시 주생성자가 있어야 한다. 주생성자가 있다면 그 주생성자는 부모 클래스의 생성자를 호출해야하기 때문에, 결국은 부모 클래스의 생성자를 호출하게 되어 있다.

그러나 자식 클래스에 주생성자가 없는 경우, 부생성자가 자식 클래스의 모든 생성자를 담당하기 때문에 부생성자 끝에는 부모 클래스의 생성자의 호출도 있어야 한다. 부생성자가 부모 클래스의 생성자를 호출하는 방법은 다음과 같다.

클래스2-생성자와 초기화’ 포스트에서 부생성자가 해당 클래스의 다른 생성자를 호출할 때는 콜론 기호(:) 뒤에 this()키워드로 호출할 수 있다고 했는데, 이 부생성자는 콜론 기호(:) 뒤에 super()키워드로 부모 클래스의 생성자를 호출할 수 있다. 부모 클래스는 상위 클래스(super class)라고도 한다고 했는데, super 키워드는 여기서 온 것이다.

만약 부모 클래스에 아무 parameter도 없는 생성자가 있다면, 명시하지 않아도 컴파일러가 그 생성자를 호출하지만 그 외의 경우에는 반드시 명시해주어야 한다.

상속 방법과 규칙을 정리하면 Fig5와 같다.

Fig5. 상속 방법과 규칙 정리

Overall part

  1. Dmitry Jemerov and Svetlana Isakova. (2017). Kotlin in Action. USA: Manning

--

--

dEpayse
dEpayse_publication

나뿐만 아니라 다른 사람들도 이해할 수 있도록 작성하는, 친절한 블로그를 목표로.