본문 바로가기

LANGUAGES, METHODLOGY/Kotlin

[안드로이드] Kotlin sealed class로 여러 클래스들을 보다 분명하게, 유연하게 활용하기

기존에 MVP 아키텍처 패턴으로 짜여진 프로젝트를 빌드업하면서 몇가지 애로사항이 생겼다.

 

 

Presenter에서 로직을 수행 후 뷰를 업데이트하기 위한 코드를 최대한 작은 단위로 한정해서,

 

서로 제약사항이 생기지 않도록 구현하고 있었는데,

 

문제는 여기서 각 뷰 단위를 업데이트하기 위한 코드가 배로 많아지고,

 

UI가 어떤 코드를 거쳐 업데이트 되는지 한 눈에 파악하기가 어려웠다.

 

수행하고자 하는 코드를 파라미터의 종류에 따라 구분지을 수는 없을까? 
그리고 이를 하나의 함수로 통일시켜 사용할 수 있을까?
특정 함수에 사용하고자 하는 클래스들을 제한해 사용할 수 있을까?

 

이런 물음들을 Kotlin의 sealed class를 활용해 상당부분 해결할 수 있었다.

1. Sealed Class란 무엇인가?

 

Sealed Classes - Kotlin Programming Language

 

kotlinlang.org

우선 Kotlin 공식 문서에서 제공하는 Selaled class의 소개는 다음과 같다.

 

  • Sealed class 내에 정의된 class 이외의 형태는 가질 수 없는, 제한된 계층구조를 정의하는 class
  • Enum class 내의 서브 클래스가 동일 클래스만을 사용할 수 있을 때, Sealed 클래스는 클래스 종류에 제약을 받지 않는다.
  • Sealed class는 추상 클래스로써 자기 자신을 사용해 생성할 수 없으며, 내부에 선언한 클래스들을 통해 생성해야 한다.
  • Sealed class는 public 생성자를 가질 수 없다(기본적으로 private 형태를 띔)
  • 프로젝트 내 어디에서나 바로 사용이 가능하다.

2. Sealed 클래스 사용하기

sealed class를 붙여 생성하고자 하는 클래스 명을 정한다.

 

코드블럭 내에서는 해당 Sealed class를 사용해 정의하고자 하는 Sub Class Group을 정의한다. 

 

Return type은 Sealed class의 이름으로 지정하면 된다.

 

아래 예시에서는 자동차를 구성하기 위한 클래스들을 sealed class로 묶어 사용하고자 구성해보았다.

sealed class CarComponent {
    data class Wheel(val wheelSize: Int, val manufacturer: String): CarComponent()
    data class Gear(val gearCount: Int, val isManual: Boolean): CarComponent()
    data class Name(val carName: String, val manufacturer: String): CarComponent()
}

 

우선, sealed class를 활용하기 이전의 방식을 먼저 살펴보자.

 

/**
*     해당 예시는 MVP 아키텍처 패턴을 기반으로 구성하였음을 가정.
**/

// 1. 각기 다르게 선언된 별개의 data class
data class Wheel(val wheelSize: Int, val manufacturer: String)
data class Gear(val gearCount: Int, val isManual: Boolean)  
data class Name(val carName: String, val manufacturer: String)

// 2. 개별 클래스들을 처리하고 이를 UI에 업데이트 하는 function (Presenter)

override fun processWheel() {
	model.loadWheel().let { wheel->
        view.updateWheel(Wheel(wheel.size, wheel.manufacturer))
	}
}

override fun processGear() {
	model.loadGear().let { gear->
        view.updateGear(Gear(gear.count, gear.isManual))
	}
}

override fun processName() {
	model.loadName().let { name->
        view.updateName(Name(name.carName, name.manufacturer))
	}
}

// 3. Presenter에서 call을 통해 UI를 업데이트하는 View code

override fun updateWheel(wheel: Wheel) {
    binding.wheelSize.text = wheel.wheelSize.toString()
    binding.manufacturer.text = wheel.manufacturer
}

override fun updateGear(gear: Gear) {
    binding.gearCount.text = gear.gearCount.toString()
    binding.isManual.text = gear.isManual.toString()
}

override fun updateName(name: Name) {
    binding.carName.text = name.carName
    binding.manufacturer.text = name.manufacturer
}

 

기본 로직 구성으로부터 나오는 효율성의 차이도 나올 수 있겠지만, 우선 각 Class들에 대한 업데이트를 해당 클래스를 업데이트 하는 각각의 로직에 파라미터로 넘겨주고, 이를 업데이트 하는 방식으로 구현하였음을 가정하자.

 

이런 구성을 두고 보면 몇가지 특징을 확인할 수 있다.

 

Wheel과 Gear가 자동차를 구현하는 구성품들 중 하나라고 확신할 수 없다.
구성품이 많아질 수록 구성품의 갯수만큼 function과 코드가 늘어난다.

 

그러면 이제 Sealed class를 활용한 예시를 살펴보자.

 

/**
*     해당 예시는 MVP 아키텍처 패턴을 기반으로 구성하였음을 가정.
**/

// 1. sealed class를 사용해 묶은 car components

sealed class CarComponent {
    data class Wheel(val wheelSize: Int, val manufacturer: String): CarComponent()
    data class Gear(val gearCount: Int, val isManual: Boolean): CarComponent()
    data class Name(val carName: String, val manufacturer: String): CarComponent()
}

// 2. 개별 클래스들을 처리하고 이를 UI에 업데이트 하는 function (Presenter)

override fun processWheel() {
	model.loadWheel().let { wheel->
        view.updateCarComponent(CarComponent.Wheel(wheel.size, wheel.manufacturer))
	}
}

override fun processGear() {
	model.loadGear().let { gear->
        view.updateCarComponent(CarComponent.Gear(gear.count, gear.isManual))
	}
}

override fun processName() {
	model.loadName().let { name->
        view.updateCarComponent(CarCompoent.Name(name.carName, name.manufacturer))
	}
}

// 3. Presenter에서 call을 통해 UI를 업데이트하는 View code

override fun updateCarComponent(component : CarComponent) {
    when (component) {
         is CarComponent.Wheel -> {
   		binding.wheelSize.text = wheel.wheelSize.toString()
                binding.manufacturer.text = wheel.manufacturer
         }
         
         is CarComponent.Gear -> {
            binding.gearCount.text = gear.gearCount.toString()
                binding.isManual.text = gear.isManual.toString()
         }
         
         is CarComponent.Name -> {
            binding.carName.text = name.carName
                binding.manufacturer.text = name.manufacturer
         }

}

 

이제 Sealed class를 활용했을 때의 장점이 확인될 것이다. 본인의 관점에서 뽑았을 때 Sealed class의 장점은 아래와 같다.

 

  • 특정 Class들이 어떤 클래스들에 대한 집합인지 명확히 확인할 수 있다.
  • when 구문을 사용할 때, 사용하지 않은 inner class에 대해 정의할 필요가 없어 enum에 비해 유연하다.
  • 동일한 행위를 필요로 하는 요구사항 안에서, 새로운 function의 추가 없이 기존 function에 추가 혹은 제거가 용이하다.

 

** 잘못된 부분, 보충하면 좋을 부분에 대해 편하게 코멘트 부탁드립니다 :) **