코틀린의 클래스와 인터페이스는 자바 클래스, 인터페이스와는 약간 다르다. 예를 들어 인터페이스에 프로퍼티 선언이 들어갈 수 있다.
코틀린 선언은 기본적으로 final이며, public이다. 게다가 중첩 클래스는 내부 클래스가 아니다. 즉, 코틀린 중첩 클래스는 외부 클래스에 대한 참조가 없다.
코틀린 인터페이스 안에는 추상 메소드뿐 아니라 구현이 있는 메소드도 정의할 수 있다(자바8의 디폴트 메소드와 비슷). 다만 인터페이스는 아무런 상태(필드)도 들어갈 수 없다.
4.1.1 코틀린 인터페이스
// 간단한 인터페이스 선언
interface Clickable {
fun click()
}
// 인터페이스 구현
class Button : Clickable {
override fun click() = println("I was clicked")
}
>>> Button().click()
I was clicked
자바의 @Override와 비슷한 override 변경자는 상위 클래스나 상위 인터페이스에 있는 프로퍼티나 메소드를 오버라이드한다는 표시다. 하지만 자바와 달리 코틀린에서는 override 변경자를 꼭 사용해야 한다.
코틀린은 인터페이스의 메소드에 디폴트 구현을 제공할 때, 자바와 달리 default로 꾸밀 필요가 없다.
interface Clickable {
fun click() // 일반 메소드
fun showOff() = println("I'm clickable!") // 디폴트 메소드
}
만약 Clickable과 동일한 메소드명인 showOff 메소드를 정의하는 Focusable 인터페이스가 있다고 하자.
interface Focusable {
fun setFocus(b: Boolean) =
println("I ${if (b) "got" else "lost"} focus.")
fun showOff() = println("I'm focusable!")
}
한 클래스에서 이 두 인터페이스를 함께 구현하면, 두 인터페이스에 정의된 showOff 구현을 대체할 오버라이딩 메소드를 직접 제공하지 않으면 다음과 같은 컴파일러 오류가 발생한다. 즉, 두 인터페이스에 정의된 showOff 구현을 각각 해주어야한다.
The class 'Button' must override public open fun showOff() because it inherits many implementations of it.
코틀린 컴파일러는 두 메소드를 아우르는 구현을 하위 클래스에 직접 구현하게 강제한다.
class Button : Clickable, Focusable { // 두 인터페이스를 모두 구현
override fun click() = println("I was clicked")
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
상속한 구현 중 하나만 선택해야한다면 아래와 같이도 가능하다.
override fun showOff() = super<Clickable>.showOff()
4.1.2 open, final, abstract 변경자: 기본적으로 final
코틀린의 클래스와 메소드는 기본적으로 final이다.
어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다. 그와 더불어 오버라이드를 허용하고 싶은 메소드나 프로퍼티의 앞에도 open 변경자를 붙여야한다.
open class RichButton : Clickable {
fun disable() {} // 이 함수는 파이널이기에 오버라이드 불가.
open fun animate() {} // 이 함수는 열려있기에 오버라이드 가능.
override fun click() {} // 오버라이드한 메소드는 기본적으로 열려있다.
}
// 오버라이드한 메소드는 open이지만, 금지하려면 final override fun click() {}과 같이 final을 붙여주어야한다.
코틀린의 abstract 클래스의 추상 멤버는 항상 open이다.
abstract class Animated {
abstract fun animate() // 추상 함수는 open이다. 하위 클래스에서는 반드시 override 해야한다.
open fun stopAnimating() {
} // 추상 클래스에 속했더라도 비추상 함수는 final이다. 하지만 open으로 override 허용이 가능하다.
fun animateTwice() {
}
}
4.1.3 가시성 변경자: 기본적으로 공개
변경자 | 클래스 멤버 | 최상위 선언 |
public(기본 가시성) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
internal | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서만 볼 수 있다. |
protected | 하위 클래스 안에서만 볼 수 있다. | (최상위 선언에 적용할 수 없음) |
private | 같은 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. |
자바에서는 같은 패키지 안에서 protected 멤버에 접근할 수 있지만, 코틀린에서는 그렇지 않다는 점에서 자바와 코틀린의 protected가 다르다는 사실에 유의해야한다. protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다.
4.1.4 내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스
코틀린의 중첩 클래스(nested class)는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
interface State : Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) {}
}
자바에서의 내부 클래스는 바깥쪽 클래스에 대한 참조를 묵시적으로 포함한다. 그 참조로 인해 내부 클래스를 바로 직렬화하고자 한다면 NotSerializableException 에러가 발생할 것이다. 이 문제를 해결하려면 자바에서는 내부 클래스를 static으로 선언해야한다.
코틀린에서는 이에 대한 동작 방식이 다르다.
class Button : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) { /*...*/ }
class ButtonState : State { /*...*/ } // 이 클래스는 자바의 정적 중첩 클래스와 대응한다.
}
코틀린 중첩 클래스에 아무런 변경자가 붙지 않으면 자바 static 중첩 클래스와 같다. 이를 자바의 바깥쪽 클래스에 대한 참조를 포함한 내부 클래스와 동일하게 만드려면 inner 변경자를 붙여야한다.
클래스 B 안에 정의된 클래스 A | 자바 | 코틀린 |
중첩 클래스(바깥쪽 클래스 참조x) | static class A | class A |
내부 클래스(바깥쪽 클래스 참조o) | class A | inner class A |
코틀린에서 바깥쪽 클래스의 인스턴스를 가리키는 참조를 표기하는 방법도 자바와 다르다. 내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer라고 써야 한다.
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
4.1.5 봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한
// 인터페이스 구현을 통해 식 표현하기
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 -> // "else" 분기가 꼭 있어야한다
throw IllegalArgumentException("Unknown exression")
}
코틀린 컴파일러는 when을 사용해 Expr 타입의 값을 검사할 때 꼭 디폴트 분기인 else 분기를 덧붙이게 강제한다. else 분기에서는 반환할 만한 의미 있는 값이 없으므로 예외를 던진다.
항상 디폴트 분기를 추가하는게 편하지는 않다. 그리고 디폴트 분기가 있으면 이런 클래스 계층에 새로운 하위 클래스를 추가하더라도 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사할 수 없다. 실수로 새로운 클래스 처리를 잊어버려더라도 디폴트 분기가 선택되기 때문에 심각한 버그가 발생할 수 있다.
코틀린은 이런 문제에 대한 해법으로 sealed 클래스를 제공한다. 상위 클래스에 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.eval
is Expr.Sum -> eval(e.right) + eval(e.left)
}
when 식에서 sealed 클래스의 모든 하위 클래스를 처리한다면 디폴트 분기(else)가 필요 없다. sealed로 표시된 클래스는 자동으로 open이라 별도로 open 변경자를 붙일 필요가 없다.
내부적으로 sealed class Expr은 private 생성자를 가진다(내부에서만 호출 가능).
sealed 인터페이스를 정의할 수는 없다. 봉인된 인터페이스를 만들 수 있다면 그 인터페이스를 자바 쪽에서 구현하지 못하게 막을 수 있는 수단이 코틀린 컴파일러에게 없기 때문이다.
'Kotlin' 카테고리의 다른 글
[Kotlin in Action] 4.3 컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임 (0) | 2025.03.16 |
---|---|
[Kotlin in Action] 4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언 (0) | 2025.03.15 |
[Kotlin in Action] 3.6 코드 다듬기: 로컬 함수와 확장 (0) | 2025.03.14 |
[Kotlin in Action] 3.5 문자열과 정규식 다루기 (0) | 2025.03.14 |
[Kotlin in Action] 3.4 컬렉션 처리: 가변 길이 인자, 중위 함수 호출, 라이브러리 지원 (0) | 2025.03.14 |
댓글