Kotlin

[Kotlin] 코틀린 인터페이스 & 상속 제어 변경자

sh1mj1 2024. 1. 4. 21:28

Kotlin in Action 을 공부하고 Effective kotlin 의 내용을 조금 참조하여 정리한 글입니다.

이미지 출처       https://commons.wikimedia.org/wiki/File:Kotlin_Icon.svg

 

이전에 코틀린 클래스와 프로퍼티 기초를 배웠다. 

이제부터 코틀린 클래스, 객체, 인터페이스에 대해 더 깊이 다룰 것이다.

 

먼저 코틀린의 클래스 계층을 정의하는 방식과 자바의 방식을 비교하자.

코틀린 인터페이스

코틀린 인터페이스는 자바 8 인터페이스와 비슷하다. 

코틀린 인터페이스 안에는 추상 메서드 뿐 아니라 구현이 있는 메서드도 정의할 수 있다. (자바 8 의 디폴트 메서드처럼)

하지만 아무런 상태도 들어갈 수는 없다.

 

간단한 인터페이스 `Clickable`

interface Clickable {
    fun click()
}

이 인터페이스를 구현하는 구현 클래스(추상 클래스가 아닌)는 `click` 에 대해 구현해야 한다.

 

`Clickable`  을 구현하는 클래스 `Button`

class Button : Clickable {
    override fun click() = println("I was clicked")
}
@Test
fun clickTest() = Button().click() 

// print
/* I was clicked */

자바에서는 클래스 상속 시 `extends` 를, 인터페이스 구현 시 `implements` 키워드를 사용하지만, 코틀린에서는 클래스 이름 뒤에 `:` 을 붙이고 클래스나 인터페이스 이름을 적으면 확장 혹은 구현된다.

이 때 클래스는 `: 클래스이름()` 처럼 주 생성자를 만들어 만들어주어야 하고 인터페이스는 `: 인터페이스이름` 의 형태로 작성하면 된다.

자바와 똑같이 인터페이스는 여러 개 구현할 수 있지만 클래스는 오직 하나만 확장 가능하다.

 

코틀린에는 자바의 `@Override` 애노테이션과 비슷한 `override` 변경자가 있다.

코틀린에서는 이 변경자를 꼭 명시해야 한다.

이는 개발자가 실수로 상위 클래스의 메서드를 오버라이드하는 경우를 방지해준다.

 

인터페이스 메서드에 디폴트 구현을 제공할 때는 자바에서는 `default` 라는 키워드를 메서드 앞에 붙여야 하지만 코틀린에서는 그럴 필요가 없다.

 

디폴트 구현을 `Clickable` 에 추가

interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!") // 디폴트 구현이 있는 메서드
}

`Clickable` 를 구현하는 구체 클래스는 `showOff` 메서드의 새로운 동작을 정의할 수도 있고 정의를 생략해서 디폴트 구현을 사용할 수도 있다.

 

동일한 메서드 `showOff` 를 구현하는 다른 인터페이스 `Focusable`

interface Focusable {
    fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus")
    fun showOff() = println("I'm focusable!")
}

그렇다면 어떤 클래스가 이 두 인터페이스(`Clickable`, `Focusable`) 을 모두 구현하면 어떻게 될까?

두 인터페이스 모두 `showOff` 라는 메서드가 디폴트 구현을 가지고 있다. 

이 경우, 두 메서드를 모두 선택하지 않는다. 

클래스는 `showOff` 메서드를 반드시 `override` 해야 한다.

그렇지 않으면 아래와 같은 컴파일러 오류가 발생한다.

Kotlin: Class 'Button' must override public open fun showOff(): Unit defined in inaction.chap4.Clickable because it inherits multiple interface methods of it
`Button` 은 showOff 함수를 오버라이드 해야 한다. 왜냐하면 그것의 여러 인터페이스 메서드를 상속받고 있기 때문이다.

 

`Clickable`, `Focusable` 을 모두 구현하는 `Button`

class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

`super<Clickable>.showOff()` 코드로 상위 타입의 구현을 호출하고 있다. 

자바에서는 `Clickable.super.showOff()` 처럼 `super` 앞에 기반 타입을 적지만, 코틀린에서는 꺾쇠 괄호 안에 기반 타입을 지정한다.

위 코드에서는 두 상위 타입의 `showOff()` 를 둘 다 호출하고 있지만 당연히 하나만 호출할 수도 있다.

 

이렇게 만든 `Button` 을 테스트해보자.

@Test
fun mainTest() {
    val button = Button()
    button.showOff()
    button.setFocus(true)
    button.click()
}

// print
/*
I'm clickable!
I'm focusable!
I got focus
I was clicked
*/

자바에서 코틀린의 메서드가 있는 인터페이스 구현하기

코틀린은 자바 6에 호환되게 설계되었다.

자바 6에서는 인터페이스의 디폴트 메서드를 지원하지 않는다.

코틀린의 디폴트 메서드가 있는 인터페이스는 사실 일반 인터페이스와 디폴트 메서드 구현이 정적 메서드로 들어있는 클래스로 조합해서 구현된다.

만약 자바 클래스에서 디폴트 메서드가 있는 코틀린 인터페이스를 구현한다고 하면?

클래스에서 인터페이스의 모든 메서드의 바디를 명시해야 한다. (디폴트 구현을 가진 메서드도)

부연 설명 그림

 

하지만 코틀린 1.5 부터는 코틀린 컴파일러가 자바 인터페이스의 디폴트 메서드를 생성해주기 때문에 자바 클래스에서 디폴트 구현이 있는 메서드의 바디는 명시하지 않아도 된다.

코틀린에서는 기본적으로 final

자바에서는 `final` 로 명시한 클래스만이 다른 클래스가 상속할 수 없다.

즉, 기본적으로 상속이 가능하다.

이 경우 편리할 수 있지만, 문제가 자주 생긴다.

fragile base class(취약한 기반 클래스) 문제

취약한 기반 클래스라는 문제는 하위 클래스가 기반 클래스에 대해 가지고 있던 가정이 기반 클래스를 변경하면서 깨져버리는 경우에 생긴다. 

클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메서드를 오버라이드할 위험이 있다. 

모든 하위 클래스를 분석하는 것은 불가능하다.

즉, 기반 클래스를 변경하는 경우 하위 클래스의 동작이 예상치 못하게 바뀔 수 있기 때문에 "기반 클래스는 취약하다".

 

이에 대해 "Object - 코드로 이해하는 객체지향설계" 라는 책에서 자세히 다룬다. (관련하여 정리, 요약한 글)

또 Effective Java 에서는 "상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라" 라는 조언을 한다.

자바에서는 의도된 클래스와 메서드가 아니라면 모두 `final` 로 만들라는 뜻이다.

 

코틀린에서도 이와 같은 철학을 따른다. 즉,

  • 자바에서
    • 기본적으로 상속에 대해 열려있다.
    • 상속을 금지하려면 앞에 `final` 키워드를 붙인다.
  • 코틀린에서
    • 기본적으로 `final` 이다.
    • 상속을 허용하려는 클래스, 메서드, 프로퍼티 앞에 `open` 키워드를 붙인다.

상속이 가능한 `open` 클래스 `RichButton`

open class RichButton : Clickable {
    fun disable() {} // final - override 불가
    open fun animate() {} // open - override 가능
    
    override fun click() {} // 오버라이드 함수 - 기본적으로 override 가능
                            // override 앞에 final 을 붙이면 이 클래스를 상속받은 곳에서 override 불가
}

코틀린 클래스를 기본적으로 final 로 했을 때의 또 다른 장점

클래스를 기본적으로 `final` 로 함의 또 다른 장점은 다양한 경우에 smart cast(스마트 캐스트)가 가능하다는 점이다.

(스마트 캐스트에 대해서는 이전 글에서 다룬 적이 있다.)

 

이전 글에서 말한 것처럼 스마트 캐스트는 컴파일러가 변수의 타입 검사와 사용 사이에 변하지 않는다는 것을 보장할 때만 동작한다.

클래스 프로퍼티의 경우, `val` 이면서 커스텀 게터가 없는 경우에만 스마트 캐스트를 쓸 수 있다.

이는 프로퍼티가 `final` 이어야 한다는 뜻이다(`val` 이므로 ). 

만약 프로퍼티가 `final` 이 아니면 스마트 캐스트를 할 수 있는 조건이 깨진다. 

프로퍼티는 기본적으로 `final` (`val`) 이기 때문에 따로 고민할 필요없이 대부분의 프로퍼티를 스마트 캐스트에 활용할 수 있고 이는 코드를 더 이해하기 쉽게 한다. 

코틀린의 추상 키워드 abstrct

자바처럼 코틀린에서도 클래스를 `abstract` 로 선언할 수 있다.

`abstract` 로 선언한 추상 클래스는 인스턴스화할 수 없다.

추상 클래스에는 구현이 없는 추상 멤버가 있기 때문에 하위 클래스에서 그 추상 멤버를 오버라이드해야만 한다. 

그러므로 추상 멤버는 항상 `open` 이 된다.

즉, 추상 멤버 앞에 `open` 변경자를 명시하지 않아도 된다.

abstract class Animated { // 추상 클래스 -> 이 클래스의 인스턴스를 만들 수 없다.
    abstract fun animate() // 추상 함수 -> 구현이 없고 하위 클래스에서 반드시 override

    // 추상 클래스의 멤버여도 일반 함수는 기본적으로 final. 원한다면 open 으로 가능
    open fun stopAnimating() {}
    fun animateTwice() {}
}

 

아래는 코틀린의 access modifier(접근 변경자, 상속 제어 변경자)를 정리한 표이다.

access modifier 이것이 붙은 멤버는 설명
`final` `override` 불가 클래스 멤버의 기본 변경자. 생략해서 사용한다.
`open` `override` 가능 반드시 `open` 을 앞에 명시해야 `override` 가능
`abstract` 반드시 `override` 해야 함 추상 클래스의 멤버에만 붙일 수 있다. 
추상 멤버에는 구현이 없어야 한다.
`override` 상위 클래스/인스턴스의 멤버를
`override` 하는 중
`override` 하는 멤버는 기본적으로 `open` 이다.
`final override fun 함수이름` 으로 하위에서 `override` 를 금지할 수 있다.