
클래스 계층 정의 방식, 가시성과 접근 변경자, sealed 변경자를 살펴봅니다.
코틀린 인터페이스에는 추상 메서드 뿐만 아니라 구현이 있는 메서드도 정의할 수 있다.
다만 인터페이스에는 아무런 상태도 들어갈 수 없다.
interface Clickable {
fun click()
}
click이라는 추상 메서드가 있는 인터페이스를 정의한 코드다.
이 인터페이스를 구현하는 모든 비추상 클래스는 click에 대한 구현을 제공해야 한다.
버튼을 클릭할 수 있게 만드려면 클래스 선언에서 클래스 이름 뒤에 콜론( : )을 표시하고,
그 뒤에 인터페이스 이름을 넣고 click 함수에 대한 구현을 제공해야 한다.
class Button: Clickable {
override fun click() = println("I was Clicked")
}
fun main() {
Button.click()
}
코틀린에서 상속이나 구성에서 모두 클래스 이름 뒤에 콜론( : )을 붙이고 인터페이스나 클래스 이름을 적는 방식을 사용한다.
클래스는 인터페이스를 원하는 만큼 개수 제한 없이 구현할 수 있지만 클래스는 오직 하나만 확장할 수 있다.
상위 클래스나 상위 인터페이스에 있는 프로퍼티나 메서드를 오버라이드 할 때 override 변경자를 쓴다.
자바에서는 @override 어노테이션이 선택인 반면 코틀린에서는 override 변경자를 꼭 써주어야 한다.
override 변경자는 실수로 상위 클래스의 메서드를 오버라이드하는 경우를 방지해준다.
상위 클래스에 있는 메서드와 시그니처가 같은 메서드를 우연히 하위 클래스에서 선언하는 경우 컴파일이 안되기 때문에 override를 붙이거나 메서드 이름을 바꿔야한다.
인터페이스 메서드는 디폴트 구현을 제공할 수 있다.
메서드 본문을 적기만 하면 된다.
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!")
}
이 인터페이스를 구현하는 클래스는 click에 대한 구현을 제공해야 한다.
반면 showOff 메서드의 경우 샐로운 동작을 정의하거나 정의를 생략해 디폴트 구현을 사용할 수도 있다.
이제 showOff 메서드를 정의하는 다른 인터페이스를 만들어보자
interface Focusable {
fun setFocus(b: Boolean) =
println("I'm ${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 implementation of it.
코틀린 컴파일러는 두 메서드를 아우르는 구현을 하위 클래스에 직접 구현하도록 강제한다.
class Button : Clickable, Focusable {
override fun click() = println(I'm was clickable)
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
/* 상위 타입의 이름을 홑화살괄호 사이에 넣은 super를 사용하면
어떤 상위 타입의 맴버 메서드를 호출할 지 지정할 수 있다. */
}
}
// main 에서 호출시 결과
// I'm clickable!
// I'm focusable!
상속한 구현 중 하나만 호출할 필요가 있다면 이렇게 구현할 수도 있다.
override fun showOff() = super<Clickable>.showOff()
모든 메서드를 호출하면 이런 모습이다.
fun main() {
val button = Button()
button.showOff()
button.setFocus(true)
button.click()
}
// I'm clickable!
// I'm focusable!
// I got focus
// I was Clicked
자바에서 코틀린 인터페이스를 구현할 경우 코틀린 인터페이스의 디폴트 구현을 사용할 수 없다.
코틀린 클래스는 하위 클래스를 만들수 없고, 기반 클래스의 메서드를 하위 클래스가 오버라이드 할 수 도 없다. 즉, 코틀린에서 모든 클래스와 메서드는 기본적으로 final이다.
자바에서는 final로 명시하지 않는 한 모든 클래스를 다른 클래스가 상속할 수 있고, 모든 메서드를 하위 클래스에서 오버라이드 할 수 있다.
코틀린에선 왜 안될까? 자바의 방식이 편리한 반면 문제가 될 수도 있기 때문이다.
취약한 기반 클래스라는 문제는 기반 클래스 구현을 변경함으로써 하위 클래스가 잘못된 동작을 하게 되는 경우를 뜻한다.
어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙을 제공하지 않는다면 그 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메서드를 오버라이드 할 위험이 있다.
모든 하위 클래스를 분석하는 것은 거의 불가능 하므로 기반 클래스를 변경하는 경우 하위 클래스의 동작이 예기치 않게 바뀔 수도 있다는 면에서 기반 클래스는 ‘취약’하다.
자바에서는 특별히 하위 클래스에서 오버라이드 하도록 의도된 클래스와 메서드가 아니라면 모두 final로 만들라는 철학이 있다.
코틀린도 이 철학을 따른다. 자바의 클래스와 메서드는 기본적으로 상속에 대해 열려있지만 코틀린의 클래스와 메서드는 기본적으로 final이다.
어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다. 그와 더불어 오버라이드를 허용하고 싶은 메서드나 프로퍼티 앞에도 open 변경자를 붙여야 한다.
단순한 버튼만 제공하던 UI를 개선해 클릭 가능한 RichButton을 제공하고 싶다고 하자. RichClass의 하위 클래스는 자신만의 애니메이션을 제공하되 버튼을 비활성화 하는 등의 기본 기능을 깰 수 없어야 한다.
이 클래스를 다음과 같이 정의할 수 있다.
open class RichButton: Clickable {
fun disable() { /*...*/ }
open fun animate() { /*...*/ }
override fun click() { /*...*/ }
}
class ThemedButton : RichButton {
override fun animate() { /*...*/ }
override fun click() { /*...*/ }
override fun showOff() { /*...*/ }
}
기반 클래스나 인터페이스의 맴버를 오버라이드한 경우에는 기본적으로 open으로 간주된다는 점에 유의한다.
하위 클래스가 오버라이드 하는 것을 금지하려면 명시적으로 final로 표시해야 한다.
open class RichButton: Clickable {
final override fun click() { /*...*/ }
}
클래스를 abstract로 선언할 수 있다.
abstract로 선언한 인터페이스는 인스턴스화 할 수 없다.
추상 클래스에는 구현이 없어 하위 클래스에서 오버라이드해야만 하는 추상 맴버가 있는 것이 보통이다.
추상 맴버는 항상 열려있다. 따라서 추상 맴버 앞에 open 변경자를 명시할 필요가 없다.
추상 클래스의 예제로 애니메이션 속도와 프레임 수 등의 애니메이션 속성과 애니메이션을 실행하는 동작을 정의하는 클래스를 살펴보자.
이런 프로퍼티와 메서드는 다른 객체가 구현했을 때만 의미가 있기 때문에 absract라는 표시를 붙여놓았다.
abstract class Animated { // 추상 클래스로 선언 되었으므로 인스턴스를 만들 수 없다.
abstract val animationSpeed: Double // 추상 프로퍼티이므로 값이 없고 하위 클래스에서 반드시 값이나 접근자를 제공해야 한다.
val keyFrame: Int = 20
open val frames: Int = 60
//추상 클래스의 추상이 아닌 프로퍼티는 기본적으로 열려있지 않다. 하지만 open으로 지정할 수도 있다.
abstract fun animation() // 추상 메서드 이므로 구현이 없고 하위 클래스에서 구현해줘야 한다.
open fun stopAnimating() { /*...*/ }
fu animateTwice() { /*...*/ }
// 추상 클래스의 추상이 아닌 메서드는 기본적으로 열려있지 않다. 하지만 open으로 지정할 수 있다.
}
코틀린 접근 제어자를 나열한 표를 봐보자
| 변경자 | 이 변경자가 붙은 맴버는 | 설명 |
|---|---|---|
| final | 오버라이드 할 수 없음 | 쿨래스 맴버의 기본 변경자 |
| open | 오버라이드 할 수 없음 | 반드시 open을 명시해야 오버라이드할 수 있다. |
| abstract | 반드시 오버라이드 해야함 | 추산 클래스의 맴버에만 이 변경자를 붙일 수 있다. 추상 맴버에는 구현이 있으면 안된다. |
| override | 상위 클래스나 인스턴스의맴버를 오버라이드 하는 중 | 오버라이드 하는 맴버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다. |
인터페이스 맴버의 경우 final, open, abstract를 사용하지 않는다.
인터페이스 맴버는 항상 open이며 final로 변경할 수 없다.
인터페이스 맴버에게 본문이 없으면 자동으로 추상 맴버가 되지만 그렇다고 따로 맴버 선언 앞에 abstract 키워드를 덧붙일 필요가 없다.
가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다.
어던 클래스의 구현에 대한 접근을 제한함으로써 그 클래스에 의존하는 외부 코드를 깨지 않고도 클래스 내부 구현을 변경할 수 있다.
코틀린은 public, protected, private 변경자를 제공한다. 이들은 자바의 경우에 대응한다.
코틀린에서 아무 변경자도 없는 선언은 모두 공개 즉, public이다.
모듈 안으로만 한정된 가시성을 위해 코틀린은 internal 이라는 가시성을 제공한다.
모듈을 함께 컴파일 되는 코틀린 파일의 집합이다.
코틀린에서는 최상위 선언에 대해 private 가시성을 허용한다.
이 가시성이 허용되는 최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함된다.
이 또한 하위 시스템이 자세한 구현 사항을 외부에 감추고 싶을 때 유용한 방법이다.
| 변경자 | 클래스 맴버 | 설명 |
|---|---|---|
| public | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
| internal | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서만 볼 수 있다. |
| protected | 하위 클래스 안에서만 볼 수 있다. | |
| (최상위 선언에 적용할 수 있음) | - | |
| private | 같은 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. |
internal open class TalkativeButton {
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 맴버다.
}
자바에서는 같은 패키지 안에서 protected 맴버에 접근할 수 있지만 코틀린에서는 그렇지 않다.
자바와 코틀린의 protected가 다르다는 사실을 유의하자.
코틀린에서 protexted 맴버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다.
클래스를 확장한 함수는 그 클래스의 private이나 protected 맴버에 접근할 수 없다.
코틀린의 public, protected, private 변경자는 컴파일된 자바 바이트코드 안에서도 그대로 유지된다.
유일한 예외는 private 클래스다.
자바에서는 클래스를 private로 만들 수 없으므로 내부적으로 코틀린은 private 클래스를 패키지 전용 클래스로 컴파일한다.
자바에는 internal과 비슷한 가시성 선언이 없다. 패키지 전용 가시성은 internal과는 전혀 다르다.
모듈은 보통 여러 패키지로 이뤄지며 서로 다른 모듈에 같은 패키지에 속한 선언이 들어 있을 수도 있다.
따라서 internal 변경자는 바이트 코드상에서는 public이 된다.
코틀린 선언과 자바 서언에 이런 차이가 있기 때문에 코틀린 코드에서는 접근할 수 없는 대상을 자바에서 접근할 수 있는 경우가 생긴다.
internal 클래스나 internal 최상위 선언을 모듈 외부의 자바 코드에서 접근할 수 있다.
또한 protected오 정의한 맴버를 코틀린 클래스와 같은 패키지에 속한 자바 코드에서는 접근할 수 있다.
자바처럼 코틀린 에서도 클래스 안에 다른 클래스를 선언할 수 있다.
클래스 안에 다른 클래스를 선언하면 도우미 클래스를 캡슐화하거나 코드 정의를 그 코드를 사용하는 곳 가까이에 두고 싶을 때 유용하다.
하지만 자바와 다르게 내포 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
View 요소를 하나 만든다고 상상해보자.
그 View의 상태를 직렬화 해야한다.
필요한 모든 데이터를 다른 도우미 클래스로 복사할 수 있다.
이를 위해 state 인터페이스를 선언하고 Serializable을 구현한다.
View 인터페이스 안에는 뷰의 상태를 가져와 저장할 때 사용할 getCurrentState와 restoreState 메서드 선언이 있다.
interface State: Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) { /*...*/ }
}
Button 클래스의 상태를 저장하는 클래스를 Button 클래스 내부에 선언하면 편하다.
자바에서 그런 선언을 어떻게 하는지 보자.
public class Button implments View {
@Override
public state getCurrentState() {
return new ButtonState();
}
@Override
public void restoreState(State state) { /*...*/ }
public class ButtonState implements State { /*...*/ }
}
State 인터페이스를 구현한 ButtonState 클래스를 정의해서 Button에 대해 구체적인 정보를 저장한다.
getCurrentState 메서드 안에서는 ButtonState의 새 인스턴스를 만든다.
이 코드를 실행하면 ‘java.io.NotSerializableException: Button’ 이라는 오류가 발생한다.
자바에서는 다른 클래스 안에 정의한 클래스는 자동으로 내부 클래스가 된다.
이 예제에서 ButtonState 클래스는 바깥쪽 Button 클래스에 대한 참조를 암시적으로 포함한다.
이 참조로 인해 ButtonState를 직렬화 할 수 없다.
Button을 직렬화 할 수 없으므로 버튼에 대한 ButtonState의 직렬화를 방해한다.
이 문제를 해결하려면 ButtonState를 static 클래스로 선언해야 한다.
자바에서 내포 클래스를 static으로 선언하면 그 클래스를 둘러싼 바깥쪽 클래스에 대한 암시적인 참조가 사라진다.
코틀린의 내포 클래스는 자바의 작동 방식과 정반대다.
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 |

코틀린에서 바깥쪽 클래스의 인스턴스를 가리키는 참조를 표기하는 방법도 자바와 다르다.
내부 클래스 Inner 안에서 바깥쪽 클래스 Outer 참조에 접근하려면 this@Outer라고 써야 한다.
코트린의 when은 else를 사용해 디폴트 분기를 사용할 수 있다.
하지만 디폴트 분기가 항상 편하진 않다.
만약 클래스 계층에 새로운 하위 클래스를 추가하면 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사할 수 없다.
이는 새로운 하위 클래스에 대해 처리해주지 않으면 디폴드 분기가 불리기 때문에 심각한 버그가 발생할 수 있다.
코틀린은 봉인된 클래스를 통해 이 문제를 해결한다.
상위 클래스에 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)
}
when 식에서 sealed 클래스의 모든 하위 클래스를 처리한다면 디폴트 분기가 필요 없다.
sealed 변경자는 클래스가 추상 클래스임을 명시한다.
따라서 sealed 클래스에는 abstract를 붙일 필요가 없으며, 추상 맴버를 선언할 수 있다.

sealed 클래스에 속한 값에 대해 디폴트 분기를 사용하지 않고 나중에 sealed 클래스의 상속 계층에 새로운 하위 클래스를 추가하면 when 식이 컴파일되지 않으면서 변경해야만 하는 코드를 알려준다.
하위 클래스를 추가했는데 when 식을 변경하지 않은 경우에 오류가 발생하며 추가해야 할 코드를 알려준다.
sealed class Expr
class Num(val value: Int): Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
class Mul(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)
}
// ERROR: 'when' expression must be exhaustive,
// add necessary 'is Mul' branch or 'else' branch instead
클래스가 아니라 interface 앞에 sealed 변경자를 붙여 sealed interface를 만들 수 있다.
봉인된 인터페이스도 똑같은 규칙을 따른다.
봉인된 인터페이스가 속한 모듈의 컴파일되고 나면 이 인터페이스에 대한 새로운 구현을 추가할 수 없다.
sealed interface Toggleable {
fun toggle()
}
class LightSwitch: Toggleable {
override fun toggle() = println("Lights!")
}
class Camera: Toggleable {
override fun toggle() = println("Camera!")
}