본문으로 바로가기

Kotlin In Action #3

category Programming/Kotlin 2021. 12. 22. 00:00

Kotlin In Action #3


클래스, 객체, 인터페이스

코틀린의 클래스와 인터페이슨느 자바의 클래스, 인터페이스와 약간 다름.

자바와 달리 코틀린 선언은 기본적으로 public, final 임.

코틀린은 중첩 클래스는 기본적으로 내부 클래스가 아님.

클래스 계층 정의

코틀린 인터페이스

  • 코틀린은 자바 8 인터페이스와 비슷.
  • 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현 메서드도 정의 가능. (자바 8의 디폴트 메서드와 비슷)
  • 인터페이스에 아무런 필드가 들어갈 수 없음.
interface Clickable {
    fun click()
}
class Button: Clickable {
    override fun click() = println("Button Click")
}
  • 자바에서는 extends와 implements 키워드를 사용하지만, 코틀린에서는 클래스 이름 뒤에 콜론(:)을 사용해 클래스 확장과 인터페이스 구현을 모두 처리.
  • 자바와 동일하게 인터페이스는 개수 제한 없이 구현 가능하지만, 클래스는 오직 하나만 확장 가능.
  • override 변경자는 자바의 @Override 어노테이션과 비슷하지만 필수적으로 사용해야 함.
interface Clickable {
    fun click()
    fun showOff() = println("Show Off")
}

자바와 다르게 메서드에 디폴트 구현을 할 때 특별한 키워드가 필요 없음.

interface Focusable {
    fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
    fun showOff() = println("Focusable")
}
class Button: Clickable, Focusable {
    override fun click() = println("Button Click")
    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

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

open, final, abstract 변경자: 기본적으로 final

자바에서 final로 명시적으로 상속을 금지하지 않은 모든 클래스는 다른 클래스가 상속 가능.

기본적으로 상속이 가능하면 편리한 경우도 있지만 문제가 생기는 경우도 많음.

  • 취약한 기반 클래스라는 문제는 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우.
  • 코틀린은 클래스와 메서드는 기본적으로 final임.

열린 메서드를 포함하는 열린 클래스 정의

open class RichButton : Clickable { // 해당 클래스는 다른 클래스가 상속 가능

    fun disable() {} // final 함수, 하위 클래스가 오버라이드할 수 없음

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

    override fun click() {} // 오버라이드한 함수는 기본적으로 열려있음
}​
  • 오버라이드한 함수는 기본적으로 열려있음. 이를 방지하려면 앞에 final을 붙여줘야 함.

추상 클래스 정의

abstract class Animated { // 추상 클래스는 인스턴스를 만들 수 없음. (구현이 없는 추상 멤버가 있기 때문)

    abstract fun animate() // 추상 함수는 기본적으로 open

    open fun stopAnimating() {} // 추상 클래스에 속했더라도 비추상 함수는 기본적으로 final

    fun animateTwice() {}
}

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

  • 가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어함.
  • 특정 클래스의 구현에 대한 접근을 제한함으로써 그 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 잇음.
  • 가시성 변경자 종류
    • public (default)
    • protected
    • private
  • 코틀린은 패키지를 네임스페이스를 관리하기 위한 용도로만 사용하여 package-private (자바의 default)를 사용하지 않음.
    • 패키지 전용 가시성에 대한 대안으로 internal이라는 새로운 가시성 변경자를 도입.
    • internal은 "모듈 내부에서만 볼 수 있음"
      • 모듈은 한 번에 컴파일되는 코틀린 파일들을 의미.
변경자 클래스 멤버 최상위 선언
public (default) 모든 곳에서 볼 수 있음. 모든 곳에서 볼 수 있음.
internal 같은 모듈 안에서만 볼 수 있음. 같은 모듈 안에서만 볼 수 있음.
protected 하위 클래스 안에서만 볼 수 있음. 최상위 선언에 적용할 수 없음
private 같은 클래스 안에서만 볼 수 있음. 같은 파일 안에서만 볼 수 있음.
internal open class TalkativeButton : Focusable {

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

fun TalkativeButton.giveSpeech() { // "public" 멤버가 자신의 "internal" 수신 타입인 "TalkativeButton"을 노출함
    yell() // "yell"에 접근할 수 없음.
    whisper() // "whisper"에 접근할 수 없음.
}
  • public 함수 안에서 그보다 가시성이 더 낮은 타입을 참조하지 못하게 함.
    • giveSpeech 함수는 public / TalkativeButton 클래스는 internal
  • 가시성은 같거나 더 높아야 한다는 일반적인 규칙이 있음.
코틀린의 가시성 변경자와 자바
코틀린의 public, protected, private 변경자는 컴파일된 자바 바이트 코드 안에서도 그대로 유지 됨.
유일한 예외는 자바에서는 클래스를 private로 만들 수 없으므로 내부적으로 코틀린의 private 클래스를 패키지 전용 클래스로 컴파일 함.
internal 변경자는 자바에 딱 맞는 가시성이 없음. 바이트 코드상으로는 public이 됨.

코틀린과 자바 가시성 규칙의 다른 차이는 코틀린에서는 외부 클래스가 내부 클래스나 중첩 클래스의 private 멤버에 접근할 수 없음.

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

코틀린의 중첩 클래스는 자바와 다르게 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없음.

 

예시1)

직렬화할 수 있는 상태가 있는 뷰 선언

import java.io.Serializable

interface State: Serializable

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

자바에서 내부 클래스를 사용해 View 구현

import org.jetbrains.annotations.NotNull;

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

    @Override
    public void restoreState(@NotNull State state) {
        /* ... */
    }
    
    public class ButtonState implements State {
        /* ... */
    }
}
  • State 인터페이스를 구현한 ButtonState 클래스를 정의하여 Button에 대한 구체적인 정보를 저장.
  • 버튼의 상태를 직렬화하면 NotSerializableException: Button 발생.
    • 자바에서는 다른 클래스 안에 정의한 클래스는 자동으로 내부 클래스가 됨.
    • ButtonState 클래스는 바깥쪽 Button 클래스에 대한 참조를 묵시적으로 포함.
    • 해결하기 위해서는 ButtonState를 static 클래스로 선언해야 함. (묵시적인 참조가 사라짐)

중첩 클래스를 사용해 코틀린에서 View 구현하기

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

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

    class ButtonState :State {
        /* ... */
    }
}
  • 코틀린 중첩 클래스에는 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같음.
  • 반대로 바깥족 클래스에 대한 참조를 포함하게 만들고 싶으면 inner 변경자를 붙여야 함.

Inner 클래스에서 Outer 클래스의 참조에 접근

class Outer {
    val a = 1;
    
    inner class Inner {
        fun getOuterReference()  : Outer = this@Outer
        fun getValue() : Int = a
    }
}

봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한

인터페이스 구현을 통한 식 표현하기

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")
    }
  • 코틀린은 when을 사용하여 Expr 타입의 값을 검사할 때 꼭 디폴트 분기인 else 분기를 덧붙이게 강제함.
  • 새로운 하위 클래스를 추가했을때 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사가 불가능.
    • sealed 변경자를 붙이면 하위 클래스 정의를 제한할 수 있음.
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 (e) { // when 식이 모든 하위 클래스를 검사하므로 별도의 else 분기 필요 없음.
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }
  • sealed로 표시된 클래스는 자동으로 open 변경자.
  • 내부적으로 Expr 클래스는 private 생성자를 가짐.
코틀린 1.0에서는 하위 클래스는 중첩 클래스여야 하고, 데이터 클래스로 sealed 클래스를 상속할 수 없음.
1.1 부터는 해당 제한이 완화 됨.

'Programming > Kotlin' 카테고리의 다른 글

[당근마켓 밋업 정리] - Kotlin Coroutines 톺아보기  (0) 2022.01.09
Kotlin In Action #4  (0) 2022.01.05
Kotlin In Action #2  (0) 2021.12.11
Kotlin In Action #1  (0) 2021.12.06
Kotlin In Action #0  (0) 2021.12.05