이펙티브 코틀린 Item 39: 태그 클래스보다는 클래스 계층을 사용하라

woga·2023년 10월 22일
0

코틀린 공부

목록 보기
42/54
post-thumbnail

큰 규모의 프로젝트에서는 상수 '모드'를 가진 클래스를 꽤 많이 볼 수 있다. 이런 상수 모드를 Tag라고 부르며, 태그를 포함한 클래스를 태그 클래스라고 한다.
근데 이 태그 클래스는 여러 문제가 있는데 이 문제는 서로 다른 책임을 한 클래스에 태그로 구분해서 넣는다는 점에서 시작이다.

ex)

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)
    }
}

이 코드엔 굉장히 많은 단점이 있다.

  • 보일러플레이트 추가

  • 프로퍼티가 더 많이 필요해짐, 위 예제에서는 value가 LIST_EMPTY, LIST_NOT_EMPTY일 때 아예 사용되지 않음

  • 요소는 여러 방법으로 설정할 수 있는 경우에 상태의 일관성과 정확성을 지키기 어려움

  • 팩토리 메서드를 사용해야함, 안 그러면 객체가 제대로 생성되었는지 확인하는 것 자체가 굉장히 어려움

그래서 코틀린에서는 sealed 클래스를 많이 사용한다. 한 클래스에 여러 모드를 만드는 방법 대신에 각각의 모드를 여러 클래스로 만들고 타입 시스템과 다형성을 활용하는 것이다.
클래스에는 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>(val value: T) : ValueMatcher<T>() {
    	override fun match(value: T): Boolean = value is List<*> && value.isEmpty()
    }
    
    class NotEmptyList<T>(val value: T) : ValueMatcher<T>() {
    	override fun match(value: T): Boolean = value is List<*> && value.isNotEmpty()
    }
}

이렇게 구현하면 책임이 분산되어 깔끔하다. 그리고 같은 계층을 사용하여 태그 클래스의 단점을 모두 해소한다.

sealed 한정자

그러나 이 한정자를 무조건 사용해야하는 것은 아니다. abstract 한정자도 가능하지만 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)
     }

위 예제처럼 when 을 사용해 모드를 구분해서 편리하게 사용할 수 있다

반면 abstract 키워드를 사용하면 다른 개발자가 새로운 인스터스를 만들어서 사용할 수도 있다. 이런 경우엔 함수를 abstract로 선언하고 서브클래스 내부에 구현해야 한다. when 사용하면 프로젝트 외부에서 새로운 클래스가 추가될 때 함수가 제대로 동작하지 않을 수 있다.

추상 클래스는 계층에 새로운 클래스를 추가할 수 있는 여지를 남긴다. 클래스의 서브 클래스를 제어하려면, sealed 한정자를 사용해야 한다. abstract는 상속과 관련된 설계를 할 때 사용한다.

태그 클래스와 상태 패턴의 ㅊ ㅏ이

태그 클래스와 상대 패턴을 혼동하면 안된다.

상대 패턴은 객체의 내부 상태가 변화할 때, 객체의 동작이 변하는 소프트웨어 디자인 패턴이다.
상대 패턴은 프론트엔드 컨트롤러, 프레젠터, 뷰 모델을 설계할 때 많이 사용된다 (MVC, MVP, MVVM)

만약 아침 운동을 위한 애플리케이션이 있고 운동 완료를 하면 완료했다는 화면을 출력해야한다.

그리고 여기서 상태 패턴을 사용하면 서로 다른 상태를 나타내는 클래스 계층 구조를 만들게 된다. 그리고 현재 상태를 나타내기 위한 읽고 쓸 수 있는 프로퍼티도 만들게 된다.

	sealed class WorkoutState 

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

여기에서 차이점은 다음과 같다

  • 상태는 더 많은 책임을 가진 큰 클래스다.

  • 상태는 변경할 수 있다.

구체 상태는 객체를 활용해서 표현하는 것이 일반적이며, 태그 클래스보다는 sealed 클래스 계층으로 만든다.

또한 이를 immutable 객체로 만들고 변경해야 할 때마다 state 프로퍼티를 변경하게 만든다. 그리고 뷰에서 이러한 state의 변화를 관찰하게 한다.

정리

코틀린에서는 태그 클래스보다 타입 계층을 사용하는 것이 좋다.
그리고 일반적으로 이러한 타입 계층을 만들 때는 sealed 클래스를 사용한다. 이는 상대 패턴과는 다르다.

profile
와니와니와니와니 당근당근

0개의 댓글