본문으로 바로가기

[Kotlin] 코틀린 클래스, 인터페이스

category 카테고리 없음 2018. 4. 19. 00:30
반응형


이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다

코틀린은 Java와 같은 형태의 상속개념을 가집니다.

인터페이스와 객체등의 성격도 그대로 물려받습니다만, 실효성을 강조한 언어인 만큼 자바에서 불필요하다고 생각됐던 부분들과 좀더 편한게 쓸수있도록 확장한 부분들이 존재합니다.

예를들면 interface에 구현부를 갖는 함수가 들어갈 수 있으며, class의 기본값이 final 이면서 public 입니다.

그외에 어떤점들이 다른지 다른부분에 초점을 맞춰 설명하겠습니다.


4.1 클래스의 계층

4.1.1 kotlin interface

코틀린의 interface에는 자바와 다르게 구현부가 있는 함수가 정의될 수 있습니다.
단 어떤 멤버(field)도 가질 수 없습니다.
interface Clickable {
    fun click()
}

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

fun main(args: Array) {
    Button().click()
}

interface키워드를 이용하여 interface를 정의하고 이를 구현하는 클래스는 콜론 : 을 이용합니다.

그리고 자식 클래스에서 부모의 함수를 override 하려면 override 라는 키워드를 반드시 사용해야만 합니다.

자바에서 @Override 어노테이션은 옵션이지만 코틀린에서 override 키워드는 필수 입니다.

따라서 override 없이 function을 정의하면 컴파일 에러가 발생합니다.


interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}
인터페이스에 구현부가 들어갈 수도 있습니다.

자바8부터 default method로 지원하는 기능을 kotlin에서도 사용할 수 있습니다.

default란 키워드를 붙일 필요도 없습니다.

기본 구현부를 같는 showOff()는 자식 클래스가 override해도 되고, 안하고 그대로 정의된 기본코드를 사용하는것도 가능합니다.


만약 다른 interface에 showOff()라는 기본 메서드가 정의되고 위에 있는 Clickable과 함께 상속받는다면, 자식 클래스에서는 두개의 부모 인터페스에 같은 메서드가 존재하게 됩니다.

따라서 이런경우 자식 클래스는 반드시 해당 method를 override해야 합니다.

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(args: Array) {
    val button = Button()
    button.showOff()
    button.setFocus(true)
    button.click()
}

부모의 특정 클래스를 호출하기 위해 super 키워드르 사용합니다.

super<인터페이스명>.함수명

참고로 자바였다면 Clickable.super.showOff(); 로 표현해야 합니다.


여기서 잠깐...

코틀린은 JDK1.6을 지원합니다.

단 default method는 java8에서 부터 들어간 기능입니다.

따라서 코틀린 컴파일러는 해당 함수를 컴파일할때 interface안에 static class로 한번더 wrapping해서 해당 함수를 처리합니다.

응?? 이해가 안가신다구요?

java8에서 지원하는 Function<T,R>, Predicate<T> 같은 functional interface들은 내부적으로 default method를 가지고 있습니다.

이 functional interface를 java7 이하 버전에서도 사용 가능하도록 하는 library들이 있는데요.

이런 library를 따라가 보시면 이런 default method를 호환성을 위해 static class로 표현한 코드를 확인해 볼 수 있습니다.

자세한건 직접 해보시는걸로~~


4.1.2 class 한정자

class 앞에 붙을수 있는 한정자에는 open, final, abstract가 있습니다.
  • open: 상속이 가능한 클래스로 명명함
  • final: 상속이 불가능한 클래스로 명명함. (java와 동일) - 기본값
  • abstract: 추상 클래스임 (java와 동일)
java와 다른점은 한정자 없이 사용한 class는 final이 기본 속성이라는것과 상속이 가능한 클래스로 만드려면 open 키워드를 명시적으로 붙여야 합니다.

상속은 OOP 개념에서 코드를 재활용하는 편리한 수단지만 fragile base class problem을 일으킵니다.

※  fragile base class problem: 부모 클래스가 명확하게 상속하는 방법과 규칙에 대해 정의하지 않는다면 해당 부모를 상속받는 자식들은 부모 클래스 작성당시의 의도와 다르게 상속받아 사용될 수 있다. 이런 경우 부모 클래스가 바뀌면 하위 클래스가 영향을 받아 side-effect 발생하는 경우가 발생한다. (부모 클래스 변경시 이를 상속받는 모든 하위 클래스의 구현을 일일이 확인할수 없으므로 발생함.)

코틀린에서는 기본값이 "상속못함!!!" 입니다. 따라서 상속할수 있도록 만들어 주려면 명시적으로 class 앞에 open을 붙여야 합니다.

(자바와는 기본값이 반대입니다.)


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

open class RichButton : Clickable { // open -> 이클래스는 다른 클래스가 상속받을 수 있다.

    fun disable() {} // 자식 클래스가 override 할수 없다.

    open fun animate() {} // 자식 클래스가 override 할수 있다.

    override fun click() {} // 자식 클래스가 또다시 override 할수 있다. (override 함수는 기본값이 open이다)
}


만약 override 한 함수를 자식클래스에서 다시 override 하지 못하도록 하려면 앞에 final을 붙여줘야 합니다.

open class RichButton : Clickable {
    final override fun click() {}
}


abstract class의 경우는 자바와 성격이 동일합니다.

단 abstract 함수는 open을 붙이지 않아도 기본값이 open 입니다.


마지막으로 interface에 정의된 함수 역시 자바와 성격이 같습니다.

내부 함수는 명시하지 않아도 open이 기본이며, final을 붙일수 습니다.


4.1.3 가시성 (visibility modifier)

public, private, protected등 기본적인 성격은 java와 같습니다.
다른점만 나열해 보면 아래와 같습니다.
  • 기본값은 public 이다. 
    • 자바에서는 기본값은 package-private 입니다만 코틀린에서는 package-private 속성이 없습니다.
  • internal이란 키워드가 추가되었으며, 이는 모듈한정이다.
    • 코틀린에서 새로 추가된 키워드로 사용 범위를 모듈로 한정해 줍니다. android studio를 사용하면 코드를 모듈단위로 분리할 수 있고, 모듈은 각각 complie 될수 있는 하나의 단위 입니다.
  • Top-level 에 대해서도 가시성을 제공한다.
    • 최상위 함수, 변수, 클래스에도 가시성을 사용할 수 있습니다.
    • private으로 지정하면 해당 파일 안에서만 접근이 가능합니다.

추가적으로 protected로 정의된 멤버는 같은 package 안에서 접근할수 없습니다. (자바는 가능합니다.)




그렇다면...
해당 한정자는 java와 성격이 조금씩 다릅니다.
기본적으로 컴파일시에 해당 한정자는 그대로 바이트 코드로 전환됩니다.
단. 이런경우를 알아두면 이해의 폭을 좀더 넓힐 수 있습니다.
  1. private class는 컴파일되면 package-private으로 변경된다.
  2. internal 값은 바이트 코드에서 public로 변경된다.
internal의 경우 public으로 컴파일되기 때문에 자바에서 호출시 같은 모듈에 있지 않더라도 호출이 됩니다.
또한 protected 역시 자바 파일이 같은 package에 들어가 있다면 java에서는 호출할 수있습니다.

이런 예외적인 상황을 방지하기 위해서 일단 이런 함수나 클래스들은 자바에서 호출될때 이상한 이름으로 변경되어 보입니다.
(Tool의 코드 자동완성 기능을 통해 이상한 이름을 확인해 볼수 있습니다.)
자동완성 기능을 이용하여 자바에서 호출하도록 코드를 만들수도 있으나, 실제 컴파일은 오류가 납니다.


4.1.4 내부 클래스와 정적 클래스.

코틀린에서는 내부 클래스를 정의하면 기본적으로 static class가 됩니다.
따라서 내부 클래스에서 외부 클래스의 변수나 함수에 접근할 수 없습니다.

자바에서 처럼 내부 클래스를 사용하려면 명시적으로 inner 키워드르 붙여서 내부 class를 선언해야 합니다.

또한 inner class에서 외부 클래스의 참조를 얻으려면 this@외부클래스 형태로 사용해야 합니다.

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


4.1.5 sealed class 봉인된 class

sealed 클래스는 말대로 자식 클래스의 범위를 한정하는 경우를 말합니다.

부모 클래스를 sealed class로 만들고, 내부에 이 부모를 상속받을 수 있는 자식 class를 명시적으로 지정합니다. (한정한다고 표현해도 맞을거 같네요.)

즉, 명시적으로 자식클래스를 묶어서 그룹처럼 관리한다고 보면 됩니다.
따라서 sealed class를 쓰면, 아래와 같은 코드에서 when을 이용해 class type 분기시 예외처리(else)를 하지 않아도 됩니다.

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.right) + eval(e.left)
        else ->
            throw IllegalArgumentException("Unknown expression")
    }

//sealed class 사용시
sealed class Expr {
    class Num(val value: Int) : Expr()
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }

참고로 sealed class는 기본이 open 속성을 같습니다. (상속을 해줄수 있는 부모역할이니 당연하겠죠?)

반응형