[Kotlin] sealed class에 대하여

코불·2023년 4월 4일

1. sealed class 개념

sealed class는 이름에서 알 수 있듯이 무언가를 봉인, 제한하는 클래스이다.
무엇을 봉할까?sealed class는 추상클래스로부터 상속받는 자식클래스의 종류를 제한한다.

sealed class Occupation

class Lawyer:Occupation()
class Doctor:Occupation()
class Nurse:Occupation()
class Dentist:Occupation()

sealed class는 추상클래스의 서브클래스로서, selaed class는 다음과 같은 특징들을 가진다.

  • 추상 클래스이므로, sealed class 자신은 객체화 될 수 없다. 클래스의 생성자는 private이다.
  • sealed class를 상속하고 있는 서브클래스는 반드시 같은 파일 내에 선언되어야 한다.
  • sealed class를 상속한 서브클래스는 같은 파일 내에 선언되어야 하지만,
    그 클래스를 상속하는 클래스는 다른 파일에 있어도 상관없다.
  • sealed class를 상속하는 자식 클래스는 같은 패키지 내의 클래스여야만 한다.

위 sealed class는 중첩 클래스의 형태로도 선언될 수 있다.

sealed class Occupation{
	class Lawyer:Occupation()
	class Doctor:Occupation()
	class Nurse:Occupation()
	class Dentist:Occupation()
}

IDE를 사용하다보면 sealed class를 사용할 때는 '상태 값이 없거나, equals()를 상속하지 않는다.'는 경고가 표시되기도 하는데,이는 object를 사용하라는 권고이다.
object 객체의 특성을 잘 생각해보면 이해할 수 있는데,
object의 경우 메모리에 한 번 올라가고 나서 계속해서 사용되는 싱글톤 객체이다.
그런데 서브 클래스가 변화할 수 있는 상태 값(변수)를 사용하지 않는데도 불구하고 일반 클래스로 선언되는 경우, 싱글톤으로 사용되어도 문제없이 사용할 수 있음에도 불구하고 사용될 때마다 객체 생성으로 인해 메모리를 반복해서 사용하게 되므로 이는 메모리 낭비인 것이다.
그러므로, 상태 값이 없는 경우, object를 사용하는 것이 메모리 상에서 이점이다.

2. 그래서, 어떻게 사용하는가?

sealed class의 대표적인 용도는 특정 추상 클래스의 자식 클래스 종류를 컴파일러에게 미리 가르쳐주는 것이다. 이것이 무슨 역할을 할까?
이는 when 문과 함께 사용될 때에 드러난다.
위에서 작성된 Occupation 클래스가 추상 클래스라고 가정해보자.

val value:Occupation = occupation

when(value){
	is Lawywer-> {...}
    is Doctor-> {...}
    is Nurse-> {...}
    is Dentist-> {...}
}

위 로직을 그대로 작성하면 when문에서 else 문을 작성하지 않았다는 경고문이 발생된다.
컴파일러는 추상 클래스 Occupation의 자식 클래스가 어떤 것인지 알지 못하기 때문에,
else 문을 작성해야 한다고 알리는 것이다.
그런데 위 Occupation 클래스가 sealed class인 경우, 위 로직은 경고문을 발생시키지 않는다.
Occupation 클래스의 자식 클래스에는 어떤 것들이 있는지,개수는 몇 개인지를 컴파일러가 이미 알고 있기 때문이다.

val value:Occupation = occupation

when(value){
	is Lawywer-> {...}
    is Doctor-> {...}
    is Nurse-> {...}
    // is Dentist-> {...}
}

자식 클래스인 Dentist에 대한 대응을 빼먹은 위 로직의 경우에는 경고가 발생된다.
when 문에서 자식 클래스를 전부 대응하지 않았기 때문이다. 이는 개발자에게 다른 자식 클래스를 대응하지 않는 실수를 막아줄 수 있으므로 유용하다.

sealed class는 일반적으로 State 패턴을 구현하는데에 많이 사용된다.

sealed class Status {
	object Success:Status()
    class Failed(val exception:String):Status()
    class Loading(val message:String):Status()
}

fun handleStatus(eventStatus:Status) {
	val status = eventStatus
    when(status) {
    is Success -> {...}
    is Failed -> { status.exception ...}
    is Loading -> { status.message ...}
}

위와 같이 선언된 Status class는 각기 다른 형태로 선언된 서브 클래스에 의해
when 문에서 각기 다른 형태로 대응될 수 있다. 이런 형태로 객체나 값의 상태에 따라 변화하는
UI를 구성할 수도 있어, 유지보수를 편리하게 만들 수 있다.

sealed class Status {
	object Success:Status()
    sealed class Failed(val exception:String):Status() {
    	object NetworkError:Failed()
        object UiError:Failed()
    }
    class Loading(val message:String):Status()
}

fun handleStatus(eventStatus:Status) {
	val status = eventStatus
    when(status) {
    is Success -> {...}
    is Failed -> { status.exception ...}
    is Loading -> { status.message ...}
}

selaed class는 놀랍게도 안에 중첩하여 또다른 sealed class를 선언할 수 있다.
위 방식으로 선언함으로서 자식 클래스의 내용을 좀 더 세부적으로 그룹화할 수 있다는 점에서 유용하다.

3. enum class와의 차이점

앞에서 살펴본 sealed class의 용례는 이전 글에서 다루었던 enum class에서도 충분히 할 수 있는 일들이다. 그럼 기존 enum class는 안 써도 되고, sealed class로 충분히 대체하면 문제가 없는 일인가?
내가 살펴본 두 클래스의 차이점은 다음과 같다.

Enum class

  • enum 클래스 내에 포함되는 상수(인스턴스) 들은 모두 동일한 생성자를 사용해야 한다.
  • enum 클래스 내에서 생성된 인스턴스가 포함하는 상태 변수들은 다른 인스턴스의 값과 겹칠 수 없다.
  • enum 클래스 내에 생성된 인스턴스가 포함하는 상태 변수들은 이후 변경될 수 없다.
  • enum 클래스의 인스턴스는 여러 번 생성될 수 없는 싱글톤이다.
  • valueOf() 및 values() 등의 함수를 제공한다.
  • enum class는 상속될 수 없다.

Sealed class

  • 각 자식클래스들은 서로 다른 생성자를 정의할 수 있다.
  • 상속이 가능하다.
  • 서브클래스를 여러 번 생성할 수 있다.
  • 서브클래스의 상태 변수를 변경할 수 있다. 따라서 각 객체가 구분될 수 있다.

둘 다 컴파일러에게 종속되는 값의 종류를 알려줌으로서 when 문의 else 문을 제거하는 용도로 쓰인다는 점에서 동일하다.
그러나 enum class의 경우 생성자의 형태가 제한되어 있고, 상속이 불가능하고 이후에 값 변경이 불가능하다는 점에서 일반 객체처럼 사용하기에는 무리가 있다.
그리고 추상 메소드 또는 인터페이스 상속 등의 기능이 있으나, 일반적인 개발자는 enum class에 그런 역할이 있을 거라고는 기대하지 않는 것으로 보이고, 그런 역할은 보통 sealed class를 사용하는 것으로 보이는 추세다.
그래서 작성자는 다음과 같은 용도로 둘을 분리해서 사용해볼 생각이다.

enum class

  • 값이나 인스턴스가 변하지 않는 상수처럼 사용되는 경우
  • 특정 값을 구분하기 위한 용도인 경우
  • 각 인스턴스를 정의하는 내용이 간단한 경우

sealed class

  • enum class로 구현하기에 각 클래스의 내용이 큰 경우
  • 추상 클래스로 사용하는 경우
  • 인터페이스로 사용하는 경우
  • 값 변경이 필요한 객체로 사용되어야 하는 경우

마치며

일반적으로 사용되는 sealed class에 대해 제대로 공부해본 것 같아 만족스럽다.
sealed class의 용법으로 when 문에 대해서만 설명이 되는 것 같아
추상 클래스로서의 특징이 두드러지지 않는 것 같아 아쉽다.
앞으로 추상 클래스를 사용할 일이 있다면 sealed class로 사용해 볼 생각이다.

참조

https://tourspace.tistory.com/467
https://www.charlezz.com/?p=43886
https://velog.io/@haero_kim/Kotlin-Sealed-Class-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

profile
안드로이드 3년차 개발자입니다.

0개의 댓글