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

클래스 계층 정의

webmaster 2022. 7. 30. 18:20
728x90

코틀린 인터페이스

자바 8 인터페이스와 비슷하다

interface Clickable {
    fun click()
}
  • click이라는 추상 메서드가 있는 인터페이스를 정의한다.

구현 클래스 : Button

class Button : Clickable {
    override fun click() {
        println("I was clicked")
    }
}
fun main(args: Array<String>) {
    Button().click()
}
  • 자바에서는 extends와 implement 키워드를 사용하지만 코틀린에서는 클래스 이름 뒤에 콜론(":")을 붙이고, 인터페이스와 클래스 이름을 적는 것으로 클래스 확장과 인터페이스 구현 모두 처리한다.
  • 자바와 마찬가지로 클래스는 인터페이스를 원하는 만큼 개수 제한 없이 구현 가능하지만 클래스는 오직 하나만 확장할 수 있다
  • Override 변경자는 상위 클래스나 인터페이스에 있는 프로퍼티나 메서드를 오버라이드 한다는 표시
    • 자바와는 달리 오버라이드 변경자를 코틀린에서는 반드시 써줘야 한다
    • Override변경자는 실수로 상위 클래스의 메서드를 오버라이드 하는 경우를 방지해준다
    • 상위 클래스에 있는 메서드와 시그니처가 같은 메서드를 우연히 하위 클래스에 선언하는 경우 컴파일이 안되기 때문에 Override를 붙이거나, 메서드 명을 변경하여야 한다

인터페이스 안에 본문이 있는 메서드 정의

interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}
  • 인터페이스 메서드도 디폴트 구현을 제공할 수 있다.
    • 매서드 앞에 디폴트를 붙여야 하는 자바와 달리 코틀린에서는 메서드를 특별한 키워드로 꾸미지 않아도 된다
    • 메서드 본문을 메서드 시그니처 뒤에 추가하면 된다
  • showOff 메서드 같은 경우 새로운 동작을 정의할 수도 있고, 그냥 정의를 생략해서 디폴트 구현을 사용할 수도 있다

만약 showOff 메서드를 정의하는 다른 인터페이스가 포함된다면 어떻게 할까?

interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.")
    fun showOff() = println("I'm focusable!")
}
  • 어느 쪽 인터페이스도 선택되지 않는다
  • 클래스가 구현하는 두 상위 인터페이스에 정의된 showOff구현을 대체할 오버 라이딩 메서드를 직접 제공하지 않는다면 컴파일러 오류가 발생한다

두개의 인터페이스를 구현한 클래스

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

    override fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}
fun main(args: Array<String>) {
    val button = Button()
    button.showOff()
    button.setFocus(true)
    button.click()
}
  • 두 메서드를 아우르는 구현을 하위 클래스에 직접 구현하도록 강제한다
  • 인터페이스를 구현하는 하위 클래스에서 명시적으로 새로운 구현을 제공
  • 상위 타입을 이름을 꺽쇠 괄호("<>") 사이에 넣어서 "super"를 지정하면 어떤 상위 타입의 멤버 메서드를 호출할지 지정할 수 있다
    • 상위 타입을 호출할 자바와 마찬가지고 super를 사용한다
    • 타입을 지정할 때는 꺽쇠 괄호 문법을 사용한다

TIP : 코틀린은 자바 6과 호환되게 설계됐다. 따라서 인터페이스의 디폴트 메서드를 지원하지 않는다 

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

인터페이스에는 메서드 선언만 들어가며, 인터페이스와 함께 생성되는 클래스에는 모든 디폴트 메서드 구현이 정적 메서드로 들어간다

그러므로 디폴트 인터페이스가 포함된 코틀린 인터페이스를 자바 클래스에서 상속해 구현하고 싶다면, 코틀린에서 메서드 본문을 제공하는 메서드를 포함하는 모든 메서드에 대한 본문을 작성해야 한다

코틀린 1.5부터는 코틀린 컴파일러가 자바 인터페이스의 디폴트 메서드를 생성해 준다

Open, final, abstract 변경자

자바에서는 final로 명시적으로 상속을 금지하지 않는 모든 클래스를 다른 클래스가 상속할 수 있다

단, 이러한 상속은 취약한 기반 클래스라는 문제를 야기한다.

취약한 기반 클래스 문제는 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 깨져버린 경우 발생하는데, 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙을 제공하지 않는다면 그 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메서드를 오버라이드 할 위험이 있다.(모든 하위 클래스를 분석하는 것은 불가능하므로, 기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는 면에서 기반 클래스는 취약하다)

이러한 문제를 해결하기 위해 Effective Java에서는 "상속을 위한 설계와 문서를 갖추거나, 없다면 상속을 금지하라"라는 말이 있다

-> 이는 특별히 하위 클래스에서 오버라이드 하게 의도된 클래스와 메서드가 아니라면 모두 final로 만들어라라는 의미이다

open class RichButton : Clickable{
    fun disable(){} //해당 함수는 final이다. 하위 클래스가 이 메서드를 오버라이드 할 수 없다
    open fun animate(){} // 이 함수는 열려있다. 하위 클래스에서 메서드 오버라이드 가능하다 
    //override fun click() {} // 이 함수는 열려있는 메서드를 오버라이드 한다(오버라이드한 메서드는 기본적으로 열려있다)
    final override fun click() {} // final 이 없는 override 메서드나 프로퍼티는 기본적으로 열려있다
}
  • 자바의 클래스와 메서드는 기본적으로 상속에 열려 있지만, 코틀린의 클래스와 메서드는 기본적으로 final이다
  • 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
    • 오버라이드를 허용하고 싶은 메서드나 프로퍼티 앞에도 open 변경자를 붙여야 한다
    • 기반 클래스나 인터페이스의 멤버를 오버라이드 하는 경우 그 메서드는 기본적으로 열려있다
    • 만약, 오버라이드 하는 메서드의 구현을 하위 클래스에서 오버라이드 하지 못하도록 금지하려면, 오버라이드 하는 메서드 앞에 final을 명시하면 된다.
변경자 이 변경자가 붙은 멤버는..? 설명
final 오버라이드 할 수 없음 클래스 맴버의 기본 변경자
open 오버라이드 할 수 있음 반드시 open을 명시해야 오버라이드 할 수 있다
abstract 반드시 오버라이드 해야함 추상 클래스의 맴버에만 이 변경자를 붙일 수 있다. 
추상 맴버에는 구현이 있으면 안된다
override 상위 클래스나 상위 인스턴스의 맴버를 오버라이드 하는 중 오버라이드 하는 맴버는 기본적으로 열려있다
하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다
  • 클래스 멤버에 대해 표와 같이 적용할 수 있다.
  • 인터페이스 멤버 같은 경우 final, open, abstract를 사용하지 않는다.
    • 인터페이스 멤버는 항상 열려있으면 final로 변경할 수 없다.
    • 본문이 없으면 자동으로 추상 멤버가 되지만, 그렇다고 따로 멤버 선언 앞에 abstract 키워드를 붙일 필요 없다.

가시성 변경자

가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다.

어떤 클래스의 구현에 대한 접근을 제안함으로써 그 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있다.

변경자 클래스 멤버 최상위 선언
public(기본 가시성) 모든 곳에서 볼 수 있다 모든 곳에서 볼 수 있다
internal 같은 모듈 안에서만 볼 수 있다 같은 모듈 안에서만 볼 수 있다
protected 하위 클래스 안에서만 볼 수 있다 (최상위 선언에 적용할 수 없음)
private 같은 클래스 안에서만 볼 수 있다 같은 파일 안에서만 볼 수 있다
  • 코틀린의 가시성은 public, internal, protected, private 변경자가 있으며, 기본 가시성은 public이다.
    • 자바의 기본 가시성인 패키지 전용은 코틀린에 없다 -> 코틀린은 패키지를 네임스페이스를 관리하는 용도로만 사용하기 때문
    • 패키지 전용 가시성에 대한 대안으로 internal 개념의 가시성을 도입했다
    • internal은 모듈 내부에서만 볼 수 있다는 의미로, 모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일들을 의미한다.
    • 모듈 내부 가시성은 개발자가 모듈의 구현에 대해 진정한 캡슐화를 제공한다는 장점이 있다.
      • 자바에서는 패키지와 같은 클래스를 선언하기만 하면 어떤 프로젝트의 외부에 있는 코드라도 패키지 내부에 있는 패키지 전용 선언에 쉽게 접근할 수 있기 때문에 모듈의 캡슐화가 깨진다.
  • 코틀린에서는 최상위 선언에 대하여 private 가시성을 허용한다.
    • 최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함된다.
    • 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에 접근할 수 없다. -> yell은 TalkativeButton의 private 멤버이다
    whisper() //whisper에 접근할 수 없다 -> whisper는 TalkativeButton의 protected 멤버이다
}
  • 코틀린은 public 함수인 getSpeech 안에서 그보다 가시성이 더 낮은 타입인 TalkativeButton을 참조하지 못하게 한다.
    • 어떤 클래스의 기반 타입 목록에 들어있는 타입이나, 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 그 클래스 자신의 가시성과 같거나 더 높아야 하고, 메서드의 시그니처에 사용된 모든 타입 가시성은 그 메서드의 가시성과 같거나, 더 높아야 한다
    • 어떤 함수를 호출하거나 어떤 클래스를 확장할 때 필요한 모든 타입에 접근할 수 있도록 보장해 준다.
  • 컴파일 오류를 없애려면 getSpeech 확장 함수의 가시성을 internal로 바꾸거나 TalkativeButton 가시성을 public으로 바꿔야 한다.
  • 자바에서는 같은 패키지 안에서 protected 멤버에 접근할 수 있지만 코틀린에서는 protected가 다르다
  • 코틀린에서는 외부 클래스가 내부 클래스나 중첩된 클래스의 private 멤버에 접근할 수 없다 

TIP 코틀린의 가시성 변경자와 자바

코틀린의 public, protected, private 변경자는 컴파일된 자바 바이트 코드 안에서도 그대로 유지된다. 그렇게 컴파일된 코틀린 선언의 가시성은 마치 자바에서도 똑같은 가시성을 사용해 선언한 경우와 같은데 유일하게 private 같은 경우 자바에서 클래스를 private로 만들 수 없으므로 내부적으로 코틀린은 private 클래스를 패키지 - 전용 클래스로 컴파일한다.

internal 같은 경우, 자바에 딱 맞는 가시성이 없으므로, internal변경자는 바이트 코드상에서는 public 이 된다.

코틀린 선언과 그에 해당하는 자바 선언에는 이런 차이가 있기 때문에 코틀린에서는 접근할 수 없는 대상을 자바에서 접근할 수 있는 경우가 생긴다. 예를 들어 다른 모듈에 정의된 internal 클래스나 internal 최상위 선언을 모듈 외부의 자바 코드에서 접근할 수 있다.

또한 코틀린에서 protected로 정의한 멤버를 코틀린 클래스와 같은 패키지에 속한 자바 코드에서는 접근할 수 있다.

하지만, 코틀린 컴파일러가 internal 멤버의 이름을 보기 나쁘게 바꾼다는 사실을 기억해라. 그로 인해 기술적으로는 internal 멤버를 자바에서 문제없이 사용할 수 있지만 이름이 보기 불편하고, 코드가 이상해진다. (이름을 바꾸는 이유는 한 모듈에 속한 어떤 클래스를 모듈 밖에서 상속한 경우 그 하위 클래스 내부의 메서드 이름이 우연히 상위 클래스의 internal 메서드와 같아져서 내부 메서드를 오버라이드 하는 경우를 방지하기 위함이고, 실수로 internal클래스를 모듈 외부에서 사용하는 일을 막기 위함이다)

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

코틀린에서도 클래스안에 다른 클래스 선언이 가능하다

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

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

interface State : Serializable

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State){}
}
  • Button 클래스의 상태를 저장하는 클래스는 Button 클래스 내부에 선언하면 편하다
public class Button implements View{
	@Override
    public State getCurrentState(){
    	return new ButtonState();
    }
    @Override
    public void restoreState(State state){...}
    public class ButtonState implement State{ ... }
}
  • State 인터페이스를 구현한 ButtonState 클래스를 정의해서 Button에 대한 구체적인 정보를 제공한다.
  • getCurrentState 메소드 안에서 ButtonState의 새 인스턴스를 만든다.
  • 해당 코드를 자바 코드에서 실행하면 java.io.NotSerializeableException : Button이라는 오류가 발생한다. 
    • Why? 자바에서 다른 클래스 안에 정의한 클래스는 자동으로 내부 클래스가 된다, ButtonState클래스는 바깥쪽 Button클래스에 대한 대한 참조를 묵시적으로 포함한다.
  • 이문제를 해결하기 위해서는 ButtonState를 static 클래스로 선언해야 한다
  • 자바에서 중 첨 클래스를 static으로 선언하면 그 클래스를 둘러싼 바깥쪽 클래스에 대한 묵시적인 참조가 사라진다.
class Button : View {
    override fun getCurrentState(): State = ButtonState()

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

 

  • 코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 Static 중첩 클래스와 같다
  • 이를 내부 클래스로 변거 애 서 바깥쪽 클래스에 대한 참조를 포함하게 만들고 싶다면 inner 변경자를 붙여야 한다.
클래스 B 안에 정의된 클래스 A 자바 코틀린
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) static class A class A
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) class A inner class A
class Outer {
    inner class Inner{
        fun getOuterReference(): Outer = this@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.left) + eval(e.right)
        else -> throw IllegalArgumentException("Unknown expression") //반드시 else 필요
    }
  • 코틀린 컴파일러는 when을 사용해 Expr타입의 값을 검사할 때는  꼭 디폴트 분기인 else 분기를 덧붙이게 강제한다
  • 디폴트 분기가 있으면 이런 클래스 계층에 새로운 하위 클래스를 추가하더라도 컴파일러가 When이 모든 경우를 처리하는지 제대로 검사할 수 없다
    • 실수록 새로운 클래스 처리를 잊어버리더라도 디폴트 분기가 선택되기 때문에 심각한 오류가 생길 수도 있다
  • 코틀린에서는 sealed 클래스를 사용하면 된다.
    • 상위 클래스에 sealed변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다
    • 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.left) + eval(e.right)
    }
  • when 식에서 sealed 클래스의 모든 하위 클래스를 처리하므로 디폴트 분기가 필요 없다.
  • sealed로 표기된 클래스는 자동으로 open이다.
    • 별도로 open 변경자를 붙일 필요가 없다.
    • 봉인된 클래스는 클래스 외부에 자신을 상속한 클래스를 둘 수 없다
  • 나중에 sealed 클래스의 상속계층에 새로운 하위 클래스를 추가해도 when식이 컴파일되지 않는다
  • 내부적으로는 Expr클래스는 private 생성자를 가진다.
    • 해당 생성자는 클래스 내부에서만 호출할 수 있다
  • sealed 인터페이스를 정의할 수는 없다
    • 봉인된 인터페이스를 만들 수 있다면 그 인터페이스를 자바 쪽에서 구현하지 못하게 막을 수 있는 수단이 코틀린 컴파일러에게 없기 때문이다

 

728x90