반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item39. 태그 클래스보다는 클래스 계층을 사용하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item39. 태그 클래스보다는 클래스 계층을 사용하라

어리둥절범고래 2023. 12. 31. 13:17
반응형

태그를 포함한 클래스를 태그 클래스(tagged class)라고 부릅니다.

상수(constant) '모드'를 태그라고 부르며, 태그 클래스는 서로 다른 책임을 한 클래스에 태그로 구분해서 넣는다는 문제를 내포합니다.

예를 들어 테스트에 사용되는 클래스로서 어떤 값이 기준에 만족하는지 확인하기 위해 사용되는 클래스를 살펴보겠습니다.

class ValueMatcher<T> private constructor(
    private val value: T? = null,
    private val matcher: Matcher
) {
    fun match(value: T?) = when(matcher) {
        Matcher.EQUAL -> value == this.value
        Matcher.NOT_EQUAL -> value != this.value
        Matcher.LIST_EMPTY -> value is List<*> && value.isEmpty()
        Matcher.LIST_NOT_EMPTY -> value is List<*> && value.isNotEmpty()
    }
    
    enum class Matcher {
        EQUAL,
        NOT_EQUAL,
        LIST_EMPTY,
        LIST_NOT_EMPTY
    }
    
    companion object {
        fun <T> equal(value: T) =
            ValueMatcher<T>(value = value, matcher = Matcher.EQUAL)
        fun <T> notEqual(value: T) =
            ValueMatcher<T>(value = value, matcher = Matcher.NOT_EQUAL)
        fun <T> emptyList() =
            ValueMatcher<T>(matcher = Matcher.LIST_EMPTY)
        fun <T> notEmptyList() =
            ValueMatcher<T>(matcher = Matcher.LIST_NOT_EMPTY)
    }
}

이러한 접근 방법에는 굉장히 많은 단점이 있습니다.

 

  • 한 클래스에 여러 모드를 처리하기 위한 상용구(boilerplate)가 추가된다.
  • 여러 목적으로 사용해야 하므로 프로퍼티가 일반적이지 않게 사용될 수 있으며, 더 많은 프로퍼티가 필요하다. 예를들어 위의 예제에서 value는 모드가 LIST_EMPTY 또는 LIST_NOT_EMPTY일 때 아예 사용되지도 않는다.
  • 요소가 여러 목적을 가지고, 요소를 여러 방법으로 설정할 수 있는 경우에는 상태의 일관성과 정확성을 지키기 어렵다.
  • 팩토리 메서드를 사용해야 하는 경우가 많다. 그렇지 않으면 객체가 제대로 생성되었는지 확인하는 것 자체가 어렵다.

 

코틀린은 그래서 일반적으로 태그 클래스보다 sealed 클래스를 많이 사용합니다.

한 클래스에 여러 모드를 만드는 방법 대신, 각각의 모드를 여러 클래스로 만들고 타입 시스템과 다형성을 활용하는 것입니다.

sealed class ValueMatcher<T> {
    abstract fun match(value: T): Boolean
    
    class Equal<T>(val value: T): ValueMatcher<T>() {
        override fun match(value: T): Boolean =
            value == this.value
    }
    
    class NotEqual<T>(val value: T): ValueMatcher<T>() {
        override fun match(value: T): Boolean =
            value != this.value
    }
    
    class EmptyList<T>(): ValueMatcher<T>() {
        override fun match(value: T): Boolean =
            value is List<*> && value.isEmpty()
    }
    
    class NotEmptyList<T>(): ValueMatcher<T>() {
        override fun match(value: T): Boolean =
            value is List<*> && value.isNotEmpty()
    }
}

 

 

 

sealed 한정자

 

sealed 한정자는 외부 파일에서 서브클래스를 만드는 행위 자체를 모두 제한합니다.

외부에서 추가적인 서브클래스를 만들 수 없으므로, 타입이 추가되지 않을 거라는게 보장됩니다.

따라서 when 구문에서 else 브랜치를 따로 만들 필요가 없습니다.

fun <T> ValueMatcher<T>.reversed(): ValueMatcher<T> =
when (this) {
    is ValueMatcher.EmptyList -> ValueMatcher.NotEmptyList<T>()
    is ValueMatcher.NotEmptyList -> ValueMatcher.EmptyList<T>()
    is ValueMatcher.Equal -> ValueMatcher.NotEqual(value)
    is ValueMatcher.NotEqual -> ValueMatcher.Equal(value)
}

 

 

 

태그 클래스와 상태 패턴의 차이

 

태그 클래스와 상태 패턴(state pattern)을 혼동하면 안 됩니다.

상태 패턴은 객체의 내부 상태가 변화할 때, 객체의 동작이 변하는 소프트웨어 디자인 패턴입니다.

상태 패턴은 프런트엔드 컨트롤러(controller), 프레젠터(presenter), 뷰(view) 모델을 설계할 때 많이 사용됩니다.

상태 패턴을 사용한다면, 서로 다른 상태를 나타내는 클래스 계층 구조를 만들게 됩니다.

그리고 현재 상태를 나타내기 위한 읽고 쓸 수 있는 프로퍼티도 만들게 됩니다.

sealed class WorkoutState

class PrepareState(val exercise: Exercise): WorkoutState()

class ExerciseState(val exercise: Exercise): WorkoutState()

object DoneState : WorkoutState()

fun List<Exercise>.toState(): List<WorkoutState> = 
    flatMap { exercise ->
    listOf(PrepareState(exercise), ExerciseState(exercise))
    } + DoneState
    
class WorkoutPresenter( /*...*/ ) {
    private val state: WorkoutState = states.first()
    
    //...
}

 

 


정리

 

  • 태그 클래스보다 타입 계층을 사용하는 것이 좋다.
  • 일반적으로 이러한 타입 계층을 만들 때는 sealed 클래스를 사용한다.
  • 타입 계층과 상태 패턴은 실질적으로 함꼐 사용하는 협력 관계라고 할 수 있다.
반응형