본문 바로가기
2023년 이전/kotlin

kotlin - 인터페이스, open, final, abstract, 가시성 변경자, 내부 클래스와 중첩 클래스, 봉인 클래스

by JeongUPark 2020. 2. 21.
반응형

[출처 -  Kotlin In Action] [아래 내용들은 Kotlin In Action을 공부하면서 스스로 정리한 내용입니다] 

이번에 정리한 내용이 정말 중요한 내용 중 하나라고 생각합니다. 왜냐면 coding을 좀 더 체계적으로 할 수 있도록 개념을 잡을 수 있기 때문입니다. 그래서 내용도 많치만 정독을 하는 것을 추천 드리겠습니다 .그럼 정리 시작하겠습니다.

 

 

인터페이스

 

코틀린의 인터페이스는 자바 8의 인터페이스와 비슷합니다. 코틀린 인터페이스 안에는 추상 메소드뿐만 아니라 구현이 있는 메소드도 정의 할 수 있습니다. 다만 아무런 상태(필드)는 들어갈 수 없습니다.

 

인터페이스 구현은 다음과 같습니다.

interface Clickable {
    fun click()
}

이 코드는 click이라는 추상 메소드가 있는 인터페이스 힙니다.  그래서 이 인터페이스를 상속받은 모든 클래스들은 이 click을 구현해야 합니다. 그러므로

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

이렇게 Button이라는 class가 Clickable을 상속받았고, 그에 따라 click 메소드를 구현하였습니다. 위의 구현을 보면 자바는 상속은 extends를 interface 사용은 implements 를 사용하엿지만 코들린에서는 class 정의 후에 콜론(:) 다음에 상속이나 확장할 인터페이스를 작성하면 됩니다. (자바와 똑같이 상속은 1개, 인터페이스 확장은 여러개 할 수 있습니다.)

자바의 @Override는 위의 code에서 처럼 단순희 override로 대체되었고, 이를 통해 상위 클래스나, 인터페이스의 프로퍼티나 메소드를 오버라이드 한다는 표시를 합니다.

그리고 코틀린에서는 override 확장자를 꼭 작성해야 합니다. 상위 클래스나 인터페이스에서 사용하는 메소드와 현재 클래스에서 사용하는 메소드 이름이 같을 경우 컴파일이 안되기 때문 입니다.

 

그리고 코틀린 인터페이스에서 구현이 있는 메소드를 제공할 때 java처럼 default를 붙일 필요 없이 fun으로 처리가 가능합니다.

Java

public interface Clickable {
    void click();
    default void showOff() {
        System.out.println("I'm clickable!");
    }
}

kotlin

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

위 코드를 보면 Click의 경우 추상메소드로 Clickable을 확장하는 클래스에서 구현을 해야하지만, showOff의 경우에는 동작을 새로 구현하거나 , 그냥 Clickable 인터페이스에서 정의된 내용을 그대로 사용할 수 있습니다.

예를 확인해 보겠습니다.

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

fun main(args: Array<String>) {
    val tBtn = TestBtn()
    tBtn.click()
    tBtn.showOff()
}

이 결과는 

I was clicked
I was showOff

하지만 위의 TestBtn class에서 showOff를 지운다면

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

fun main(args: Array<String>) {
    val tBtn = TestBtn()
    tBtn.click()
    tBtn.showOff()
}

결과는 

I was clicked
I'm clickable!

이렇게 됩니다.

그리고 다른 2개의 인터페이스에서 동일한 이름의 메소드를 가지고 있고, 하나의 class에서 이 두개의 인터페이스를 확장했을 때 어떻게 처리되는지 확인해 보겠습니다.

우선 default로 정의된 내용을 사용하도록 Button class를 만들면

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")

}

Button에서 error가 발생하는 error의 내용은 

 

Class 'Button' must overrid public open fun showOff() : Unit defined ~~.Clickable because it inherites multiple interface methods of it 

 

showOff를 override 해야하는데 여러 인터페이스 메서드를 상속하고 있다 라는 메세지를 확인 할 수 있습니다. 해결 방법은

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

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

위와 같이 명시적으로 showOff에 상위 타입 구현을 지정합니다. super의 사용은 동일 하지만 자바랑 다르게 <>안에 타입을 지정합니다. 그래서 작성한 내용을 사용해 보면

fun main(args: Array<String>) {
    val button = Button()
    button.showOff()
    button.setFocus(true)
    button.click()
}

결과는

I'm clickable!
I'm focusable!
I got focus.
I was clicked

이렇게 나옵니다. 만일 override fun showOff() 안에 super<Focusable>.showOff()가 없다면 결과에서 I'm focusable!은 나타나지 않았을 것입니다. (setFocus는 인터페이스에서 이미 구현이 되어있으므로 자동으로 상속됩니다.)그리고 그렇게 1개만 구현하게 될 경우에는 다음과 같이 구현도 가능 합니다.

override fun showOff() = super<Clickable>.showOff()

자바에서 코틀린의 인터페이스를 사용할 때 코틀린의 디폴트 메소드 구현을 사용할 수 없으므로 직접 본문에 그 내용을 작성해야 한다.

 

open, final

만일 하나의 부모클래스가 존재하고 이 부모클래스를 상속한 여러 자식 클래스가 있다고 할 때, 부모클래스를 수정할 경우 자식 클래스의 동작이 변경될수도 있는데 이를 취약한 기반 클래스(Fragile base class)라고 합니다. (저는 부모클래스라 했지만 기반 클래스라고도 부르고 자식클래스는 하위 클래스 라고도 부릅니다. 제가 배울때 부모클래스 자식클래스 였습니다 ㅎ)

 

이런 문제를 해결하기 위해서 가장 대표적인 조언이 상속을 위한 설계와 문서를 갖추거나 아니면 상속을 금지하라는 조언 입니다.  이 말인 즉슨, 자식 클래스에서 오버라이드하게 의도된 클래스와 메소드가 아니라면 모두 final로 만들라는 뜻입니다. (부모 클래스에서 final로 메소드를 만들면 자식 클래스에서 그 메소드를 상속 받을 수 없습니다.)

 

이런 철학에 따라 코들린은 기본 적으로 클래스와 메소드가 final 입니다. (interface는 아닙니다. interface는 public 인것 같습니다.) 그래서 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 합니다.

다음 code를 보겠습니다.

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

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

class childBtn : RichButton() {
    override fun click() {
        super.click()
    }
    override fun animate() {
        super.animate()
    }
    override fun showOff() {
    	super.showOff()
    }
}

RichButton이 Clickable 인터페이스를 확장했고, childBtn class가 RichButton을 상속했습니다. 이때 RichButton이 open 으로 선언되어 있기 때문이고, RichButton에서 animate 메소드도 open으로 선언되어 있어서 childBtn에서 상속할 수 있습니다. 하지만 diable() 메소드는 open으로 선언되지 않아 childBtn에서 상속할 수 없습니다.

그리고 click()과 showOff는 Clickable 인터페이스로 부터 상속되었기 때문에 상속 할 수 있습니다. 하지만 

final override fun click() {}

이 된다면 childBtn 클래스에서 click() 메소드를 상속할 수 없게 됩니다.

 

그럼 final을 사용 함으로서 얻어지는 큰 이득을 몰까? final을 사용함으로서 얻어지는 가장큰 이득은 스마트 캐스팅이라고 합니다. 클래스 프로퍼티는 val이면서  경우 커스텀 접근자가 없을 경우 스마트 캐스팅을 할 수 있는데, final을 함으로서 다른 클래스에서 상속받아 커스텀 접근자를 정의 할 수 없게 되기 때문에 스마트 캐스팅을 할 수 있다고 합니다.

abstract

코틀린에서도 abstract로 클래스를 선언 할 수 있습니다. abstract로 선언한 클래스는 인스턴스화 할 수 없습니다. 그리고 abstract 클래스는 구현이 없는 abstract 멤버가 있어서 하위 클래스에서 그 abstarct 멤버를 반드시 오버라이드 해야합니다. 그리고 이 abstract 멤버는 항상 open이기 때문에 따로 open을 선언해주지 않아도 됩니다.

 

abstract class Animated{
    abstract fun animate()
    open fun stopAnimating(){
    }
    fun animateTwice(){
    }
}
open class RichButton : Animated() {
    override fun animate() {
    }

    override fun stopAnimating() {
        super.stopAnimating()
    }
    
}

위의 code를 보면 Animated라는 추상 클래스가 있고 RichButton 클래스에서 상속을 받았다. 그리고 animate()라는 메소드가 abstract 멤버 메소드라 RichButton에서 반드시 override 해주어야 한다. 그렇지 않으면 error나 난다. 그리고 open fun stopAnimating()은 open으로 선언되었기 때문에 상속 받을 수 있고 fun animateTwice()은 open 되지 않았기 때문에 상속 할 수 없다.

 

그럼 지금 까지 본 내용을 정리하면

변경자 이 변경자가 붙은 멤버는.. 설명
final 오버라이드 할 수 없음 클래스 멤버의 기본 변경자
open 오버라이드 할 수 있음  반드시 open을 명시해야 오버라이드 할 수 있다. (interface는 아니다)
abstract 반드시 오버라이드 해야 함 추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다.
override 상위 클래스나 상위 인스턴스의 멤버를 오버라이드 하는 중 오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다.


가시성 변경자

가시성 변경자라고 하니깐 어려운데 사실 public/protected/private를 말합니다. 이 가시성 변경자를 사용함으로서 어떤 클래스의 구현에 대한 접근을 제한하고, 그 클래스에 의존하는 외부 코드를 깨지 않고 글래스 내부 구현을 변경할 수 있습니다. 말이 어려운데 쉽게 말하면 한 클래스의 멤버필드와 메소드에 대한 다른 클래스의 접근 여부를 제어하는 것이다.

기본적으로 코틀린의 가시성 변경자는 자바와 비슷하게 public/private/protected가 있고 기본적으로 아무런 변경자도 없는 경우 모두 public 입니다.

그리고 자바의 기본 가시성 변경자인 패키지 전용(package-private)는 코틀린에 없습니다. 이 대처용으로 internal이라는 새로운 가시성 변경자를 도입했습니다.

 위 4개의 가시성 변경자를 정리하면

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

 그리고 마지막은로 자바에서는 같은 패키지 않에서 protected 멤버에 접근 할 수 있지만 코틀린에서는 protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보입니다.

 

그리고 컴파일 후 코틀린의 private 경우 자바에서는 class를 private로 만들 수 없기 때문에 패키지 전용(package-private)클래스로 컴파일 됩니다. 또한 패키지 전용(package-private)을 대체하기 위해 나온 internal의 경우에는 pulic이됩니다.

이렇게 컴파일 할 경우 코틀린의 선언과 그에 해당하는 자바 선언에 차이가 생기기 때문에 코틀린에서 접근할 ㅅ ㅜ없는 대상을 자바에서는 접근 할 수 있는 경우도 발생합니다.

내부 클래스와 중첩된 클래스

자바처럼 코틀린도 클래스 안에 클래스를 선언 할 수 있습니다. 자바와 다른 점은 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다는 점입니다.

 

그럼 다음 코드를 보겠습니다.

package ch04.Button1
import java.io.Serializable

interface State: Serializable

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}
import ch04.Button1.State;
import ch04.Button1.View;
import ch04.Button1.Button.*;

public class Button implements View {
    @Override
    public State getCurrentState() {
        return new ButtonState();
    }
    @Override
    public void restoreState(State state) {   }
    public class ButtonState implements State{ }
}

이렇게 구현한 Button class를 사용하면 NotSerializableException:Button이 라는 오류가 발생합니다. 그 이유는 자바에서는 다른 클래스 안에 정의한 클래스는 자동으로 내부 클래스가 되며, 바깥 쪽 클래스를 묵시적으로 참조하기 때문입니다. 그래서 ButtonState 클래스가 Button 클래스를 참조하는데 Button 클래스는 직렬화 할 수 없으므로 위의 오류가 발생합니다. (이를 해결하려면 Button 클래스는 public static class ButtonState implements State로 생성하면 묵시적 참조가 사라집니다.)

하지만 코틀린에서의 동작은 위와 다릅니다.

import java.io.Serializable

interface State: Serializable

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}

class Button : View {
    override fun getCurrentState(): State = ButtonState()

    override fun restoreState(state: State) { /*...*/ }

    class ButtonState : State { /*...*/ }
}

 이 코드는 문제 없이 동작 합니다. 그 이유는 코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같아저 바깥 클래스에 대한 참조를 하지 않습니다. 만일 내부 클래스로 변경해서 바깥쪽 클래스를 참조하게 만들고 싶으면 inner 변경자를 붙여야 합니다.

 

클래스 B 안에 정의된 클래스 A 자바에서는 코틀린에서는
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) static class A class A
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) class A inner class A

그러므로 Java는 기본적으로 내부클래스고 코틀린은 중첩 글래스 입니다.

그리고 내부 클래스에서 바깥쪽 클래스의 참조에 접근하려면 다음과 같이 하면 됩니다.

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

 

봉인 클래스 

다음 코드를 보자

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")
    }

fun main(args: Array<String>) {
    println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}

코틀린 컴파일러는 when을 사용하여 Expr 타입 값을 검사할 때 꼭 디폴트 분기인 else분기를 덧붙이게 강제한다.

항상 디폴트 분기를 추가하는게 편하지않고, 또 새로운 하위 클래스(위에서는 Num Sum 같은)를  추가후 when이 이 추가된 하위 클래스를 처리하지 않더라도 디폴트에서 처리하기 때문에 버그를 발생 시킬 수 있습니다.

 

코틀린은 이런 문제를 해결하기 위해 sealed 클래스를 제공합니다. 상위 클래스를 sealed 변경자를 붙여서 생성 후 하위 클래스를 제한하여 상속 시킬 수 있습니다. (sealed 클래스의 하위 클래스를 정의 할 때 반드시 사우이 클래스 안에 중첩시켜야 합니다.) 코드를 보면서 확인해 보겠습니다.

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)
    }

fun main(args: Array<String>) {
    println(eval(Expr.Sum(Expr.Sum(Expr.Num(1), Expr.Num(2)), Expr.Num(4))))
}

 

sealed class Expr 안에 Num 클래스와 Sum 클래스를 중첩 시키고 when에서 Expr를 처리하면 else를 통한 디폴트 처리없이 sealed class 안의 중첩된 클래스들만 처리하면 됩니다. 그리고 sealed class 안에 새로 하위 클래스를 중첩 시키면 when에 error가 발생하고 새로 중첩된 하위 클래스를 처리하도록 합니다.

 

그리고 내부적으로 expr 클래스는 private 생성자를 가집니다. 그 생성자는 클래스 내부에서만 호출 할 수 있습니다. 그리고 sealed 인터페이스를 정의할 수 없습니다. 그 이유는 sealed 인터페이스를 만들 수 있다면 그 인터페이스를 자바 쪽에서 구현하지 못하게 막을 수 없기 때문입니다.

반응형