Theme:

코틀린에서 모든 클래스는 기본적으로 final이다. 상속을 허용하려면 명시적으로 open을 붙여야 한다. "왜 이렇게 설계했을까?"라는 질문은 면접에서 단골로 나온다. 상속, 인터페이스, 그리고 다중 상속 문제까지 — 코틀린이 이 문제들을 어떻게 풀었는지 정리해보자.

open 키워드 — 기본이 final인 이유

KOTLIN
// 기본적으로 final — 상속 불가
class Animal

// class Dog : Animal()   // 컴파일 에러!

// open으로 상속 허용
open class Animal2 {
    open fun speak() = "..."          // 오버라이드 허용
    fun breathe() = "숨 쉬기"         // 오버라이드 불가
}

class Dog : Animal2() {
    override fun speak() = "멍멍"     // OK
    // override fun breathe() = "..."  // 컴파일 에러!
}

왜 기본이 final일까?

Effective Java에서 Joshua Bloch는 "상속을 위한 설계를 하거나, 아니면 상속을 금지하라"고 말했다. 코틀린은 이 원칙을 언어 차원에서 적용했다.

상속이 위험한 이유는 다음과 같다.

  • 상위 클래스 변경 시 하위 클래스가 깨질 수 있다 (취약한 기반 클래스 문제)
  • 개발자가 의도하지 않은 오버라이드가 발생할 수 있다
  • 클래스의 불변 조건(invariant)이 하위 클래스에 의해 깨질 수 있다

상속의 기본 규칙

KOTLIN
open class Shape(val name: String) {
    open val area: Double = 0.0

    open fun draw() {
        println("$name 그리기")
    }
}

class Circle(val radius: Double) : Shape("원") {
    // 프로퍼티도 오버라이드 가능
    override val area: Double
        get() = Math.PI * radius * radius

    override fun draw() {
        println("반지름 $radius 인 원 그리기")
    }
}

오버라이드 규칙 정리

KOTLIN
open class Parent {
    open fun method() { }          // 오버라이드 가능
    fun finalMethod() { }          // 오버라이드 불가
    open val property: Int = 0     // 오버라이드 가능
}

open class Child : Parent() {
    override fun method() { }      // 오버라이드 — 여전히 open
    final override fun method() { }  // 오버라이드하되, 하위에서는 금지

    // val을 var로 오버라이드 가능 (getter에 setter 추가)
    override var property: Int = 0
    // 반대는 불가 — var를 val로 오버라이드할 수 없음
}

주의할 점이 있다. override한 멤버는 기본적으로 open이다. 더 이상 오버라이드하지 못하게 하려면 final override를 사용해야 한다.

val을 var로 오버라이드할 수 있는 이유

val은 getter만 있다. var로 오버라이드하면 setter를 추가하는 것이므로, 기존 계약을 위반하지 않는다. 반대로 var를 val로 바꾸면 setter가 사라지므로 계약 위반이다.

생성자에서의 상속

KOTLIN
// 상위 클래스에 주 생성자가 있으면 바로 호출
open class Person(val name: String)
class Student(name: String, val grade: Int) : Person(name)

// 상위 클래스에 주 생성자가 없으면 보조 생성자에서 super 호출
open class View {
    constructor(context: Context)
    constructor(context: Context, attrs: AttributeSet)
}

class CustomView : View {
    constructor(context: Context) : super(context)
    constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
}

인터페이스 — 자바 8+와 비슷하지만 다르다

KOTLIN
interface Clickable {
    // 추상 메서드
    fun click()

    // default 구현이 있는 메서드 (자바처럼 default 키워드 불필요)
    fun showOff() = println("클릭 가능!")
}

interface Focusable {
    fun focus()
    fun showOff() = println("포커스 가능!")
}

인터페이스 구현

KOTLIN
class Button : Clickable, Focusable {
    override fun click() = println("버튼 클릭")
    override fun focus() = println("버튼 포커스")

    // showOff()가 두 인터페이스에 모두 있으므로 반드시 오버라이드 필요
    override fun showOff() {
        super<Clickable>.showOff()     // Clickable의 구현 호출
        super<Focusable>.showOff()     // Focusable의 구현 호출
    }
}

다이아몬드 문제 해결 — super<T>

두 인터페이스에 같은 시그니처의 메서드가 있으면, 구현 클래스에서 반드시 오버라이드해야 한다. super<타입>으로 특정 상위 타입의 구현을 호출할 수 있다.

KOTLIN
interface A {
    fun greet() = println("A의 인사")
}

interface B {
    fun greet() = println("B의 인사")
}

class C : A, B {
    // 반드시 오버라이드 — 어떤 구현을 쓸지 명시해야 함
    override fun greet() {
        super<A>.greet()   // "A의 인사"
    }
}

인터페이스의 프로퍼티

KOTLIN
interface Named {
    val name: String                    // 추상 프로퍼티 — 구현 클래스에서 제공

    val greeting: String                // backing field 없는 computed 프로퍼티
        get() = "안녕, $name"

    // val state: String = "기본값"     // 컴파일 에러! 인터페이스는 backing field 불가
}

class User(override val name: String) : Named
// user.greeting → "안녕, 코틀린"

인터페이스에서 프로퍼티를 선언할 수 있지만, backing field는 가질 수 없다. 이것이 abstract class와의 핵심 차이다.

abstract class — 상태를 가진 불완전한 클래스

KOTLIN
abstract class Animal(val name: String) {
    // 상태를 가질 수 있음 (backing field)
    var energy: Int = 100

    // 추상 메서드 — open 불필요 (기본이 open)
    abstract fun speak(): String

    // 일반 메서드
    fun eat() {
        energy += 10
        println("$name 이(가) 먹이를 먹었다. 에너지: $energy")
    }
}

class Cat(name: String) : Animal(name) {
    override fun speak() = "야옹"
}

abstract class vs interface — 선택 기준

면접에서 이 질문이 나오면 다음 기준으로 답하면 된다.

interface를 선택하는 경우

  • 여러 클래스에 공통 행위(계약)를 정의할 때
  • 다중 구현이 필요할 때
  • 상태가 필요 없을 때

abstract class를 선택하는 경우

  • 공통 상태(필드)를 하위 클래스와 공유할 때
  • 생성자 파라미터가 필요할 때
  • 관련 클래스들 사이의 IS-A 관계가 명확할 때
KOTLIN
// 인터페이스가 적합한 경우 — 행위의 계약
interface Drawable {
    fun draw()
}

interface Resizable {
    fun resize(factor: Double)
}

// 추상 클래스가 적합한 경우 — 공통 상태 + 템플릿
abstract class Shape(val color: String) {
    var x: Double = 0.0      // 공통 상태
    var y: Double = 0.0

    abstract fun area(): Double

    fun moveTo(newX: Double, newY: Double) {
        x = newX
        y = newY
    }
}

// 둘 다 사용
class Circle(color: String, val radius: Double) : Shape(color), Drawable, Resizable {
    override fun area() = Math.PI * radius * radius
    override fun draw() = println("원 그리기")
    override fun resize(factor: Double) { /* ... */ }
}

상속에서 주의할 점

생성자에서 open 멤버 사용 금지

KOTLIN
open class Parent {
    open val value: Int = 1

    init {
        // 위험! 하위 클래스에서 오버라이드한 프로퍼티가 아직 초기화되지 않았을 수 있음
        println("Parent init: value = $value")
    }
}

class Child : Parent() {
    override val value: Int = 42

    init {
        println("Child init: value = $value")
    }
}

// Child() 출력:
// Parent init: value = 0    ← 예상: 42지만 아직 초기화 안 됨!
// Child init: value = 42

상위 클래스의 init 블록이 실행될 때 하위 클래스의 프로퍼티는 아직 초기화되지 않았다. 이것은 면접에서도 자주 나오는 함정 문제다.

상속 깊이를 제한하자

KOTLIN
// 과도한 상속 — 유지보수 어려움
open class A
open class B : A()
open class C : B()
open class D : C()  // 상속 체인이 너무 깊음

// 더 나은 접근 — 조합(composition) 사용
class Engine { fun start() { } }
class Car(private val engine: Engine) {
    fun start() = engine.start()
}

"상속보다 조합"이라는 원칙은 코틀린에서도 유효하다. 코틀린의 위임 패턴(by 키워드)이 이를 지원한다.

KOTLIN
interface SoundPlayer {
    fun play()
}

class DefaultSoundPlayer : SoundPlayer {
    override fun play() = println("기본 재생")
}

// 위임 — SoundPlayer 구현을 player에게 맡김
class MusicApp(player: SoundPlayer) : SoundPlayer by player

정리 — 면접에서 기억할 포인트

  • 기본 final — Effective Java 원칙. open을 붙여야 상속 가능
  • override 멤버는 기본 open — 하위에서 금지하려면 final override
  • val → var 오버라이드 가능 — getter에 setter 추가. 반대는 불가
  • super<T> — 다이아몬드 문제 해결. 두 인터페이스에 같은 메서드가 있으면 명시 필요
  • interface vs abstract — 상태가 필요하면 abstract, 계약만 필요하면 interface
  • 생성자에서 open 멤버 주의 — 상위 init 실행 시 하위 프로퍼티는 미초기화 상태
댓글 로딩 중...