Study/kotlin

[코틀린 프로그래밍] Chapter.08 클래스 계층과 상속

에디개발자 2021. 5. 3. 07:00
반응형

나를 닮았다고 한다...

언어를 다루는 개발자 입장에선 클래스는 쉬운 것 같으면서도 매우 복잡하다고 생각합니다. 클래스는 다른 클래스의 추상화와 연결되어 있고 이를 바탕으로 만들어집니다. 코틀린의 클래스는 기본적으로 final입니다. 베이스 클래스로 사용하려면 open 키워드를 class 앞에 선언해줘야합니다. 클래스를 sealed로 정의하여 클래스에서 확장할 수 있는 특정 클래스를 명시할 수 있습니다. 

 

인터페이스와 추상 클래스

인터페이스

코틀린의 인터페이스는 추상 메소드를 작성하는 명세에 의한 설계를 할 수 있고 Java의 default 메서드처럼 구현이 가능합니다. 또한 static 메소드가 컴패니언 객체에 들어있는 것과 유사하게 작성할 수 있습니다.

// 간단한 interface
interface Remote {
    // 구현 필수 메소드
    fun up()
    fun down()
    
    // 구현하고 싶을 경우 오버라이드하여 구현하는 메소드
    fun doubleUp() {
        up()
        up()
    }
}

 

위 인터페이스를 상속받아 사용해보겠습니다.

class TV {
    var volume = 0
}

// interface 구현 방법
class TVRemote(val tv: TV) : Remote {
    override fun up() {
        tv.volume++
    }

    override fun down() {
        tv.volume--
    }
    
    // doubleUp()은 구현하지 않아도 된다.
}

 

생성된 TVRemote 클래스를 사용해보겠습니다.

val tv = TV()
val tvRemote: TVRemote = TVRemote(tv)

println("Volume: ${tv.volume}")
tvRemote.up()

println("Volume: ${tv.volume}")
tvRemote.doubleUp()

println("Volume: ${tv.volume}")

 

그럼 interface에 static 메소드를 구현해보겠습니다. 코틀린에서는 static 메소드를 인터페이스 안에 직접 만들 수 없습니다. 이 때 컴패니언 객체를 사용하여 static 메소드를 구현할 수 있습니다.

interface Remote {
    fun up()
    fun down()
    fun doubleUp() {
        up()
        up()
    }
    
    // static method 구현
    companion object {
        fun combine(first: Remote, second: Remote): Remote = object : Remote {
            override fun up() {
                first.up()
                second.up()
            }

            override fun down() {
                first.down()
                second.down()
            }

        }
    }
}

// 사용방법
val anotherTV = TV()
Remote.combine(tvRemote, TVRemote(anotherTV))

 

추상클래스

코틀린에서 추상 클래스는 abstract를 선언하여 구현할 수 있습니다.

// 추상 클래스 선언
abstract class Musician(val name: String, val activeFrom: Int) {
    abstract fun instrumentType(): String
}

// 추상 클래스 사용
class Cellist(name: String, activeFrom: Int) : Musician(name, activeFrom) {
    override fun instrumentType() = "String"
}

// 추상 클래스를 상속받은 클래스 사용
val ma = Cellist("asdf", 564)

 

interface vs abstract

  • 인터페이스에 정의된 속성엔 백킹 필드가 없습니다. 인터페이스는 필드를 가질 수 없습니다. 반면에 추상 클래스는 백킹 필드를 가질 수 있습니다.
  • 인터페이스는 한 번에 여러 개를 구현할 수 있지만, 클래스는 추상 클래스든지 일반 클래스든지 하나만 확장 가능합니다.
  • 여러 클래스 사이에서 상태를 다시 사용해야 한다면 추상 클래스가 좋은 선택입니다.
  • 하나 이상의 명세와 요구사항을 만족하는 클래스들을 원하지만 각각의 클래스들이 각각의 구현하는 것을 원하면 인터페이스가 좋은 선택입니다.

 

상속

코틀린에서는 클래스는 기본적으로 final 상태입니다. 만약 상속을 허가하는 클래스를 만들고 싶다면 class 앞에 open이라고 명시해줘야합니다. open으로 명시된 클래스를 상속받는 클래스에서 추상 메소드를 구현하려면 override를 선언하여 작성해야합니다. 상속받은 메소드를 final override라고 명시하면 자식 메소드에게 오버라이드하는 것을 방지할 수 있습니다.

 

상속은 속성에도 적용할 수 있습니다. 클래스 내부에 정의된 속성과 생성자에 정의된 속성 모두 override가 가능합니다. val로 정의된 속성은 override할 때 val, var 모두 사용할 수 있지만 var로 정의된 속성을 override할 때는 var만 가능합니다. 이유는 val은 getter만 가지고 있고 자식 클래스에서 var로 오버라이드하면서 setter를 생성할 수 있지만 var로 생성한 속성을 val로 오버라이딩하면서 setter를 제거할 수는 없기 때문입니다.

open class Vehicle(val year: Int, open var color: String) {
    open val km = 0
    fun repaint(newColor: String) {
        color = newColor
    }
}

 

생성된 open class를 상속받는 클래스를 생성해보겠습니다.

코틀린은 implements, extends를 구분하지 않고 상속(ingeritance)라고 칭합니다.
// Java와는 다르게 extends, implements로 구분하지 않고 그냥 상속!
open class Car(year: Int, color: String) : Vehicle(year, color) {
    override var km: Int = 0  // 백킹 필드
        set(value) {  // 백킹 필드에 데이터 셋
            if (value < 1) {
                throw RuntimeException("can't...")
            }
            field = value
        }

    fun drive(distance: Int) {
        km += distance
    }
}

 

한 번 상속받은 클래스를 다시 한번 상속받아 생성해보겠습니다.

// 부모 클래스에서 private or protected 멤버를 자식 클래스에서는 public으로 만들 수 있다.
class FamilyCar(year: Int, color: String) : Car(year, color) {
    override var color: String
        get() = super.color
        set(value) {
            if (value.isEmpty()) {
                throw RuntimeException("Color required")
            }
            super.color = value
        }
}

이 클래스는 open class로 선언되지 않았기 때문에 더 이상 상속이 불가능합니다.

 

 

씰드 클래스

위에서 코틀린의 상속에 대해서 알아보았습니다. open class라면 모두 베이스 클래스가 될 수 있습니다. 하지만 자식 클래스를 일부 선택된 클래스로 제한할 수 있습니다. 

 

간단한 예제를 살펴보겠습니다. 

sealed class Card(val suit: String)  // sealed class

// child class
class Ace(suit: String) : Card(suit)

class King(suit: String) : Card(suit) {
    override fun toString() = "King of $suit"
}

class Queen(suit: String) : Card(suit) {
    override fun toString() = "Queen of $suit"
}

class Jack(suit: String) : Card(suit) {
    override fun toString() = "Jack of $suit"
}

class Pip(suit: String, val number: Int) : Card(suit) {
    init {
        if (number < 2 || number > 10) {
            throw RuntimeException("error")
        }
    }
}

 

위처럼 씰드 클래스를 상속받는 5개의 클래스를 생성하였습니다. 그리고 씰드 클래스의 자식 클래스의 인스턴스 생성을 해보겠습니다.

fun process(card: Card) = when (card) {
    is Ace -> "${card.javaClass.name} of ${card.suit}"
    is King, is Queen, is Jack -> "$card"
    is Pip -> "${card.number} of ${card.suit}"
}

fun main() {
    println(process(Ace("Diamond")))        // Ace of Diamond
    println(process(Queen("Clubs")))        // Queen of Clubs
    println(process(Pip("Spades", 2)))      // 2 of Spades
    println(process(Pip("Hearts", 6)))      // 6 of Hearts
}

씰드 클래스의 When 표현식을 사용할 떄는 else를 사용하지 말자. when에서는 else를 사용하는 것을 권장합니다. 하지만 else를 사용하면 추 후에 씰드 클래스를 상속받는 새로운 클래스가 추가되었을 때 새로운 케이스가 처리되지 않음을 알 수 없게 된다.

 

 

Enum의 생성과 사용

이전 예제에서 suit를 표현하기 위해 String을 사용했습니다. 하지만 하드코딩은 항상 예상치 못한 버그를 발생하게 합니다. 그리고 코드도 지저분해집니다. 이런 지저분한 코드를 Enum을 사용하여 변경해보겠습니다.

// enum 선언
enum class Suit {
    CLUBS, DIAMONDS, HEARTS, SPADES
}

 

위 Enum을 사용하여 기존의 String을 enum을 변경합니다.

// Sealed Class 
sealed class Card(val suit: Suit)

// Child Class
class Ace(suit: Suit) : Card(suit)

class King(suit: Suit) : Card(suit) {
    override fun toString() = "King of $suit"
}

class Queen(suit: Suit) : Card(suit) {
    override fun toString() = "Queen of $suit"
}

class Jack(suit: Suit) : Card(suit) {
    override fun toString() = "Jack of $suit"
}

class Pip(suit: Suit, val number: Int) : Card(suit) {
    init {
        if (number < 2 || number > 10) {
            throw RuntimeException("error")
        }
    }
}
fun main() {
    println(process(Ace(Suit.DIAMONDS)))                // Ace of Diamond
    println(process(Queen("Suit.CLUBS)))                // Queen of Clubs
    println(process(Pip(Suit.SPADES, 2)))               // 2 of Spades
    println(process(Pip(Suit.HEARTS, 6)))               // 6 of Hearts
}

코드가 훨씬 더 깔끔하게 변경된 것을 확인할 수 있습니다. 

 

enum은 String이 주어지면 valueOf()메소드를 이용해서 해당하는 enum을 얻을 수 있습니다.

val diamonds = Suit.valueOf("DIAMONDS")

 

정리

- 코틀린은 default 키워드가 없고, 인터페이스에서도 static 메소드를 컴패니언 객체로 사용
- 코틀린의 중첩 클래스와 내부 클래스는 Java와 다르다.
- 코틀린의 클래스의 디폴트는 final이다. 모든 open 클래스가 상속가능하진 않다.
- sealed 클래스를 사용하면 상속을 제한할 수 있다.
- enum 클래스의 인스턴스는 enum 클래스의 인스턴스는 static 멤버로 생성된다.

반응형