24.02.02 Sealed Class vs Sealed Interface

KSang·2024년 2월 4일
0

TIL

목록 보기
48/101

sealed interface에 대해 알아보자


유효성검사의 에러메세지, 액티비티 혹은 프래그먼트에 진입하는 entry type등을 정해줄때 주로 enum class를 사용한다.
관련있는 상수들끼리 모아주기때문에 데이터 관리에 효과적이나, 서로의 타입이 다르면 안된다는 한계가 있다.

sealed class

그런 단점을 보완하기 위해 sealed class를 사용해 왔다.
대표적으로 리사이클러뷰의 뷰타입을 나눠 줄때 유용하게 사용했는대

sealed class SearchViewType {
    data class SortType(val sort: String): SearchViewItem()
    data class ItemType(val type: String): SearchViewItem()
    data class Contents(val content: SearchModel) : SearchViewItem()
}

sealed class UiEvent {
    object OnClick : UiEvent()
    data class OnInput(val text: String) : UiEvent()
}

이렇게 다양한 타입의 데이터를 하나로 묶어 줄 수 있어 리사이클러뷰 타입, 이벤트 처리 등 관리하기 쉽고 안정적으로 만들 수 있다.

하지만 제약사항으로 하위 클래스의 정의는 같은 패키지나 파일 내부에 기술되어야 한다.

하위 타입을 좀더 세분화 할 필요가 있을땐 Nested sealed class를 사용해서 구분해주기도한다

Nested sealed class

sealed class Action {
    object Stand : Action()

    object Move : Action() 

    data class Sit(val position: String) : Action()
}

움직임을 나타낸 실드 클래스를 만들었다.

여기선 Stand, Move, Sit 세가지 서브 클래스를 가지고 있는데

움직임을 더 다양하게 나타내고 싶다면 Move를 실드클래스로 선언해 세분화해서 나타낼 수 있다.

sealed class Action {
    object Stand : Action()

    sealed class Move : Action() {
        object Walk : Move()
        object Run : Move()
        data class Jump(val height: Int) : Move()
    }

    data class Sit(val position: String) : Action()
}

fun performAction(action: Action) {
    when (action) {
        is Action.Stand -> println("일어난다.")
        is Action.Sit -> println("${action.position}번 위치에 앉는다")
        is Action.Move.Walk -> println("걷는다")
        is Action.Move.Run -> println("뛴다")
        is Action.Move.Jump -> println("${action.height}만큼 점프 했다.")
    }
}

fun main() {
    performAction(Action.Move.Run)
}

이처럼 네스티드 실드 클래스를 사용하면, 복잡한 계층 구조를 명확하게 표현할 수 있다.

하지만 내부 클래스의 인스턴스화가 제한되고, 계층 구조 내에서 공통의 부모 클래스를 공유하는 것이 어려운 문제가 있는데 이를 해결 할 수 있는게 sealed interface다

sealed interface

sealed interface는 sealed class의 계층적 한계를 극복하기 위해서 추가되었는데

클래스 계층 구조를 더 유연하게 표현할 수 있으며, 인터페이스기 때문에 인스턴스화나, 다중상속에서 자유롭다

실드 인터페이스를 사용하면 코드의 유연성을 높이고, 복잡한 계층 구조를 쉽게 관리할 수 있는데

실드 클래스와 비교해 보겠다.

Nested sealed class 예제

sealed class Action {
    object Stand : Action()

    sealed class Move : Action() {
        object Walk : Move()
        object Run : Move()
        data class Jump(val height: Int) : Move()
    }

    data class UseAbility(val ability: String) : Action()
    data class Shield(val strength: Int) : Action()
    data class Strike(val power: Int) : Action()
}

fun performAction(action: Action) {
    when (action) {
        is Action.Stand -> println("일어난다.")
        is Action.Move -> when (action) {
            is Action.Move.Walk -> println("걷는다")
            is Action.Move.Run -> println("뛴다")
            is Action.Move.Jump -> println("{action.height}만큼 점프 했다.")
        }
        is Action.UseAbility -> println("능력발동: ${action.ability}")
        is Action.Shield -> println("방어한다 \n 방어력: ${action.strength}")
        is Action.Strike -> println("공격한다 \n 공격력: ${action.power}")
    }
}

fun main() {
    performAction(Action.Move.Run)
    performAction(Action.UseAbility("Fireball!"))
    performAction(Action.Shield(100))
    performAction(Action.Strike(50))
}

위에서 만든 액션에서 능력사용, 방어, 공격을 추가해봤다.

기존의 클래스가 점점 커지면서 복잡해지는걸 볼 수 있는데, 유지 보수가 힘들고 더 많은 기능을 추가하기가 점점 힘들어져 확장성이 떨어진다

그리고 한 객체가 여러 상태나 행동을 동시에 나타내는 것을 표현하기에도 어려운대

예를 들어, 캐릭터가 달리면서 공격하는등 복합적인 행동을 모델링하기 힘들다.

sealed interface 예제

sealed interface Action

sealed interface Move : Action {
    object Walk : Move
    object Run : Move
    data class Jump(val height: Int) : Move
}

sealed interface Ability : Action {
    data class UseAbility(val ability: String) : Ability
}

sealed interface Defense : Action {
    data class Shield(val strength: Int) : Defense
}

sealed interface Attack : Action {
    data class Strike(val power: Int) : Attack
}

class Character : Move, Ability, Defense, Attack

fun performAction(action: Action) {
    when (action) {
        is Move.Walk -> println("걷는다")
        is Move.Run -> println("뛴다")
        is Move.Jump -> println("J{action.height}만큼 점프 했다.")
        is Ability.UseAbility -> println("능력발동: ${action.ability}")
        is Defense.Shield -> println("방어한다 \n 방어력: ${action.strength}")
        is Attack.Strike -> println("공격한다 \n 공격력: ${action.power}")
    }
}

fun main() {
    val actions = listOf<Action>(
        Move.Run,
        Ability.UseAbility("Invisibility"),
        Defense.Shield(100),
        Attack.Strike(50)
    )

    actions.forEach { action ->
        performAction(action)
    }
}

다중 상속을 받으면서 계속 상속할 수 있어 굉장히 유연해진다.

다중 상속으로 인한 확장성뿐만 아니라 다양한 행동을 동시에 나타낼 수 있는데

sealed interface Action

...

// 복합 행동을 위한 인터페이스
sealed interface CombinedAction : Action

// 달리면서 공격하는 행동을 표현하는 클래스
data class RunAndStrike(val postion: Int, val power: Int) : Move, Attack, CombinedAction

fun performAction(action: Action) {
    when (action) {
        is Move.Walk -> println("걷는다")
        ...
        is RunAndStrike -> println("달리면서 공격한다. \n 이동속도 ${action.speed}, 공격력 ${action.power}")
    }
}

fun main() {
    val actions = listOf<Action>(
        Move.Run,
        Ability.UseAbility("Invisibility"),
        Defense.Shield(100),
        Attack.Strike(50),
        RunAndStrike(10, 60) // 달리면서 공격하는 행동 추가
    )

    actions.forEach { action ->
        performAction(action)
    }
}

Move, Attack, 그리고 CombinedAction 인터페이스를 구현하여, 달리면서 동시에 공격하는 복합 행동을 나타낼수 있다.

Sealed Class vs Sealed Interface

sealed interface를 보면 확장성과 유연성에서 sealed 클래스에 비해 훨씬 뛰어난걸 볼 수 있다.

그렇다면 이걸 언제 사용해야 할까?

실드 클래스를 전부 대체 할 수 있는건가?

실드 클래스는 실드 인터페이스로 대체 할 수 없는 특징이 있는데

생성자를 가질 수 있다.

그렇기 때문에 생성자를 통해 초기화 해줄 필요가 있다면 실드 클래스를 사용하는게 좋은 방법이 될 수 있다.

생성자가 필요없다면 sealed interface를 쓰자.

확장성과 유연성이 뛰어나서 생성자가 필요없다면 sealed interface를 쓰면 된다.

하지만 생성자를 통해 초기화 해줄 필요가 있다면 sealed Class를 쓰자,

sealed interface는 생성자를 가질 수 없으니 초기화가 필요하다면 sealed Class를 쓰면 된다.

0개의 댓글