오늘도 더 나은 코드를 작성하였습니까?

클래스 객체 인터페이스(클래스 계층 정의) 본문

Kotlin in Action/코틀린 기초

클래스 객체 인터페이스(클래스 계층 정의)

hik14 2020. 8. 12. 16:58

코틀린 인터페이스

- 코틀린 인터페이스는 자바 8 인터페이스와 유사하다.

- 추상 메서드뿐 아니라 구현이 있는 메서드도 정의 가능하다(자바 Default method와 유사)

- 어떤 필드도 선언 불가능하다.

 

- 자바와 동일하게 인터페이스는 여러 개를 구현할 수 있지만 클래스는 1개만 상속 가능하다.

- override 변경자는 반드시 사용해야 된다. 

 

아래와 같이 2개의 인터페이스를 1개의 클래스에서 구현하다면 둘 다 default showOff 메서드가 구현되어 있는데 어떻게 될까?

 

Class 'Button' must override public open fun showOff(): Unit defined in Clickable because it inherits multiple interface methods of it

 

코틀린은 이름과 시그니처가 동일한 멤버 메서드에 대해 둘 이상의 디폴트 구현이 있는 경우 하위 클래스에서 명시적으로 새로운 구현을 해야 된다.

interface Clickable {
    
    fun click()
    
    fun showOff() = println("I'm clickable!")
}

interface Focusable {

    fun setFocus(b: Boolean) =
            println("I ${if (b) "got" else "lost"} focus")

    fun showOff() = println("I'm focusable!")
}
class Button: Clickable, Focusable{

    override fun click() = println("I was clicked")

    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }

}


fun main() {

    val button = Button()
    
    button.showOff()
    button.setFocus(true)
    button.click()
}

* 자바 : Clickable.super.showOff()

 

  코틀린 :  super <Clickable>. showOff()

 

* 자바에서 코틀린 인터페이스를 구현한다면???

코틀린은 자바 6과 호환되도록 설계되었다 따라서 인터페이스의 디폴트 메서드(자바 8)를 지원하지 않는다. 

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

 

인터페이스에는 메서드 선언만 들어가고 인터페이스와 함께 생성되는 클래스에 디폴트 메서드의 구현이 정적 메서드로 생성된다. 

 

Open final abstract 변경자 : 기본적으로 final

 

취약한 기반 클래스  -  하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져 버린 경우

 

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

모든 하위 클래스를 분석하는 건 힘들기 때문에 기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수 있다.

 

상속을 위한 설계와 문서를 갖추거나 그렇지 않다면 상속을 금지해라. Effective Java (조슈아 블로크)

---> 오버라이드 되도록 설계된 클래스와 메서드가 아닌 이상 모두 Final로 만들어라. 

 

코틀린 클래스는 자바의 클래스와 달리 기본적으로 final이다.

 

open class RichButton: Clickable{ // 이 클래스는 열려있다. 다른 클래스가 상속가능하다.

    fun disable() {} // 이함수는 final 이다 하위 클래스가 이메소드를 오버라이드 할 수 없다.

    open fun animate(){} // 이함수는 열려있다. 하위 클래스에서 이메소드를 오버라이드 가능하다.

    override fun click() {} // 이 함수는 Clickable 메소드를 오버라이딩 했다 오버라이드한 메소드는 열려있다.
}

만약 click() 더 이상 RichButton을 상속받는 클래스에서 재정의 못하게 하려면 

final override fun click() { }으로 final을 명시해야 된다. 

 

코틀린은 override 한 메서드 프로퍼티는 기본적으로 열린 상태다. 

 

*열린 클래스와 스마트 캐스트

클래스의 기본 상속 가능을 final로 함으로써 얻는 이익중 하나는 스마트 캐스트이다.

스마트 캐스트는 타입 검사 뒤에 반드시 변경될 수 없는 변수에만 적용된다. 

 

즉 클래스의 프로퍼티가 val이며 getter가 없는 경우에만 스마트 캐스트를 쓸 수 있다.

 

프로퍼티가 final이 아니라면,

클래스의 상속하여 프로퍼티를  접근자 메서드 정의 함으로써 스마트 캐스트 요구사항이 깨진다.

클래스 프로퍼티는 기본적으로 final이다. 

 

 

추상 클래스 정의하기

abstract class Animated { // 추상클래스는 항상열려있다.
    
    abstract fun animate() //  추상 메소드 역시 open 키워드 없이 오버라이딩 가능하다.

    open fun stopAnimating(){ // 추상 클래스에 소속되어 있어도 비추상함수는 기본적으로 final이다. 
                              // open 키워드가 필요하다 오버라이딩을 위해.
    }
    
    fun animateTwice(){ // 오버라이딩 안된다. 
        
    }
}

인터페이스 멤버는 open final abstract를 사용하지 않는다.  항상 열려있다. 

인터페이스 멤버는 본문이 없으면 추상 멤버가 되기 때문에 abstract 키워드가 필요 없고 열려있게 된다.

 

제어 변경자 이변경자가 붙은 멤버는  설명
final 오버라이드 할 수없음 클래스멤버의 기본변경자
open  오버라이드 할 수있음 open을 명시해야 오버라이드 가능.
abstract 반드시 오버라이드 해야됨 추상클래스 멤버에 붙일 수있고 구현이 존재하면 안된다.
override 상위 클래스나 인터페이스를 오버라이드 한 메서드 오버라이드한 멤버는 기본적으로 open이다.
추후 오버라이드를 막으려면 final을 명시한다. 

 

가시성 변경자: 기본적으로 공개 ( Public )

 

- 자바와 같이 private protected public이 있다.

 

- Default 가시성 변경자는 자바와 달리 public이다.

 

- 자바의 Default 가시성인 패키지 전용은 코틀린에는 없다. 대안으로 internal (모듈 내부에서 접근 가능)이 존재한다.

 

- 코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용한다. 그래서 패키지 가시성 제어가 없다.

 

*모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일을 의미한다.

 

- 코틀린 파일의 최상위 선언된 클래스 함수 프로퍼티에 private를 허용한다.

그 선언이 들어있는 파일에서만 접근 가능하다.

 

가시성 변경자 클래스 멤버 최상위 선언
public(기본 가시성 이다.) 모든 곳에서 볼수이다.  모든곳에서 볼수있다.
internal 같은 모듈 안에서 볼수있다.  같은 모듈안에서 볼수있다.
protected 하위 클래스 안에서 볼수있다. 적용불가능
private 같은 클래스에서 볼수있다.  같은 파일내에서 볼수있다.

 

예시

 

internal open class TalkativeButton : Focusable{
    
    private fun yell() = println("Hey!")

    protected fun whisper() = println("Let's talk!")
}

fun TalkativeButton.giveSpeech(){ //'public' member exposes its 'internal' receiver type TalkativeButton
    
    yell() // Cannot access 'yell': it is private in 'TalkativeButton
    
    whisper() // Cannot access 'whisper': it is protected in 'TalkativeButton'
    
}

코틀린은 public 인 함수 안에서 그보다 가시성 정도가 낮은 타입인 TalkativeButton 을 참조할 수 없다.

 

클래스의 기반 타입 목록(상위 클래스)에 들어있는 타입이나 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 클래스 자신의 가시성과 같거나 더 높아야 된다.

 

메서드(giveSpeech)의 시그니처에 사용된 모든 타입의 가시성(TalkativeButton)은 메서드(giveSpeech)의 가시성과 같거나 더 높아야 한다는 일반 규칙에 해당한다. 

즉, giveSpeech의 가시성을 internal 로변경하거나 TalkativeButton을 public으로 변경해야 된다.

 

이 규칙은 어떤 함수를 호출하거나 클래스를 확장(상속)할 경우 가능한 모든 타입에 대한 접근할 수 있게 보장한다. 

 

확장 함수는 private나 protected 멤버에는 접근 불가능하다.  확장 함수는 그 클래스 외부에 선언되는 것이기 때문이다.

 

내부 클래스와 중첩 클래스: 기본적으로 중첩 클래스 

클래스 안에 다른 클래스를 선언하는 것은 도우미 클래스를 캡슐화하거나 코드 정의를 그 코드를 사용하 곳 가까이에 두고 싶을 때 유용하다. 

 

class B안에 정의된 class A 자바 코틀린
중첩 클래스(외부클래스 참조 없음) static class A class A
내부 클래스(외부클래스 참조 저장) class A inner class A

 

코틀린 inner class에서 외부 클래스 참조 가지는 방법.

 

class Outer {
    inner class Inner{
        fun getOuterReference(): Outer = this@Outer
    }
}

 

Seal class: - 클래스 계층 정의 시 계층 확장 제한

- 상위 클래스에 sealed 변경자를 붙히면  그 상위 클래스를 상속하는 하위 클래스를 정의를 제한할 수 있다.

- sealed클래스의 하위 클래스는 반드시 그 내부에 중첩시켜야 한다.

- sealed 클래스의 변경 제한자는 항상 open이다.

- sealed 클래스의 생성자는 private이다.

- sealed interface는 자바 쪽에서 구현을 못하도록 막을 수 있는 방법이 없어 존재하지 않는다.

interface Expr

class Num(val value: Int):Expr

class Sum(val left: Expr, val right: Expr): Expr

fun eval(e: Expr): Int = 
        when (e) {
            is Num -> e.value
            is Sum -> eval(e.left) + eval(e.left)
            // else(default 분기가 없으면 에러가 난다.)
            else -> throw IllegalArgumentException("Un known expression")
        }

default 분기인 else가 있으면 새로운 Expr의 새로운 하위 클래스를 구현 시 when식이 모든 경우를 처리하는지 확인하기 어렵다. 실수로 새로운 하위 구현 클래스를 잊어버리면 디폴트 분기가 선택됨으로 런타임 시 심각한 에러가 발생한다.

 

sealed class Expr { // 기반클래스를 sealed 로 봉인한다.
  
    class Num(val value: Int) : Expr() // 하위 클래스를 중첩클래스로 나열한다.

    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
        // when 식이 Expr의 모든 하위 클래스를 검사하기때문에 else가 필요하지않다.
        when (e) {
            is Expr.Num -> e.value
            is Expr.Sum -> eval(e.left) + eval(e.left)
        }

이렇게 바꾸면 추후 Expr을 상속한 하위 클래스가 생길 시 when 식은 컴파일 시 에러가 발생해서 고칠 수 있다.