[Kotlin] 클래스 9 — 봉인된 클래스(sealed class), 열거형 클래스(enum class)

dEpayse
dEpayse_publication
14 min readDec 6, 2020

Kotlin의 Sealed class와 Enum class를 잘 이용하면 코드의 가독성을 높여주고 유지 보수를 쉽게 만들어 줄 수 있다. Sealed class 와 Enum class에 대해 알아보자.

Sealed class

Kotlin에서는 when이라는 키워드를 사용하여 조건을 나눌 수 있다. 이렇게 조건을 나누면서도 조건마다 값을 반환할 수 있는데, when이 값을 반환해야 하는 경우라면, 예외없이 모든 경우를 다뤄야 한다.

다음과 같은 경우를 보자.

Example1. when을 통한 분기 처리와 값 반환interface Fruit
class Apple():Fruit
class Banana():Fruit

fun main(){
val fruits = arrayListOf(Apple(), Banana())
val someFruit = when(fruits[0]){
is Apple -> "Apple"
is Banana -> "Banana"
else -> "No Fruit" //빠지면 컴파일 오류
}
println(someFruit)
}
//결과 : Apple

Example1에서는 someFruit에 when을 통하여 문자열 값을 넣어주고 있다. Fruit을 상속받는 것은 실제로 Apple과 Banana 뿐이지만, Example1의 when에서 else가 빠지면 컴파일 오류로 실행되지 않는다. Sealed class는 ‘Fruit의 자식 클래스는 Apple과 Banana가 전부야!’라는 사실을 컴파일러에게 알려주는 역할을 수행하며, 이것이 Sealed class의 핵심 기능이기도 하다.

Fig1. Sealed class의 장점

Sealed class 정의와 관리

Sealed class를 정의하는 방법은 다음과 같다. Example1의 interface였던 Fruit을 Sealed class로 바꿔보자.

Example1. Sealed class 정의하기sealed class Fruit

아주 간단하다.

Sealed class를 관리하는 방법에는 두 가지가 있다.

하나는 Sealed class의 중첩 클래스로 포함시키는 것과, 다른 하나는 중첩 클래스로 포함시키지 않는 것이다. 두 경우 모두 Sealed class에 포함시킬 클래스는 Sealed class의 자식 클래스로 정의한다.

Example2-1. nested class로 Sealed class에 포함시키기sealed class Fruit{
class Apple():Fruit()
class Banana():Fruit()
}

fun main(){
val fruits = arrayListOf(Fruit.Apple(), Fruit.Banana())
val someFruit = when(fruits[0]){
is Fruit.Apple -> "Apple"
is Fruit.Banana -> "Banana"
}
println(someFruit)
}
//결과 : AppleExample2-2. nested class를 이용하지 않고 Sealed class에 포함시키기sealed class Fruit
class Apple():Fruit()
class Banana():Fruit()

fun main(){
val fruits = arrayListOf(Apple(), Banana())
val someFruit = when(fruits[0]){
is Apple -> "Apple"
is Banana -> "Banana"
}
println(someFruit)
}
//결과 : Apple

물론 두 가지 경우를 혼합하여도 실행은 되는데, 특별한 경우가 아니면 한 가지의 방법만 선택하는 것이 유지, 보수에 좋을 것이라 생각한다. 중첩 클래스를 이용하면 분기할 클래스 앞에 Sealed class의 이름을 적어주어야 하는 것이 차이점이다.

Sealed class 특징

  • 기본 가시성은 public, open이다.
  • Sealed class는 추상 클래스이고, 객체화할 수 없으며 private 생성자를 갖고 있다.
  • Sealed class의 하위 클래스들은 동일한 파일에 정의되어야 한다. 서로 다른 파일에서 정의할 수 없다.
  • Sealed interface도 있다.(2020.02.22 기준으로 kotlin 1.5 시험 버전에서 사용 가능한 상태이다.)
  • Sealed class를 사용하면 타입 분기로 사용되는 when이 반드시 값을 반환해야 하고, Sealed class타입을 인자로 받을 때, 나눠지는 조건은 Sealed class의 하위 클래스 타입 전부이어야하며, else가 없어도 정상 작동한다.

Enum class

enum 은 enumeration의 줄임말로, ‘열거’를 뜻한다. Enum class를 활용하면 상수(변하지 않는 값)들을 관리할 수 있고, 각 상수들을 마치 클래스 객체처럼 사용할 수도 있는 등 유용한 기능을 제공한다.

변하지 않는 값을 다루려면 val을 사용하는 방법은 안될까? 가능하다. 하지만 그 상수가 어떤 클래스에 포함되어 있다면, 클래스의 객체를 생성할 때마다 객체는 각각의 상수를 가지게 된다.

즉 변하지 않는 값(상수)들을 다룬다면, 그 값은 클래스 객체마다 갖는 값(프로퍼티)보다 클래스에 단 하나만 있는 것이 더 낫다.

결론적으로 변하지 않으면서 클래스 하나가 상수들을 하나씩만 가지는 방법이 바로 Enum class를 사용하는 것이다. Enum class를 처음 접한다면, 더욱 생소할 것이다. 특히 java와 kotlin의 Enum은 더 다양한 기능을 제공하기 때문에 더 헷갈릴 수 있다. 그러나 이 기능을 잘 활용하면 코드의 가독성을 높이고 유지, 보수를 쉽게 만들어 줄 수 있다. Enum class를 정의하는 법 부터 차근차근히 가보자.

Enum class 정의하기

Enum class를 정의하는 방법은 다음과 같다.

Example3. Enum class 정의하기 enum class Planet{
MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE
}

Example3에서 MERCURY, VENUS, EARTH, …이 우리가 정할 상수들이고, 여기서 중요한 것은 이 상수들은 사실 Planet의 하나뿐인 Planet타입의 객체들이라는 것이다. 이 점이 Enum class를 이해하는 데 큰 부분이라고 생각한다.

즉 enum class Planet 은 Example3–1과 아주 흡사한 뜻을 갖는 것이다.

Example3-1. Enum class 내부 구조 이해하기
class Planet{
companion object{
val MERCURY = Planet()
val VENUS = Planet()
val EARTH = Planet()
val MARS = Planet()
val JUPITER = Planet()
val SATURN = Planet()
val URANUS = Planet()
val NEPTUNE = Planet()
}
}
//(companion object는 아직 다루지 않았지만 여기서는 클래스 객체 각각 프로퍼티를 갖는 것이 아닌 특정 클래스의 모든 객체가 companion object의 프로퍼티를 공유한다는 것만 알아도 충분하며, 자세한 내용은 다음 포스트에서 다룰 예정입니다.)중요한 것은 각 상수들이 Planet 타입의 객체라는 점!

Enum class의 사용하기

  1. 가장 간단한 Enum class 형태

Example4은 간단한 Enum class의 사용법이다.

Example4. Enum class 사용하기enum class Planet{
MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE
}
fun main(){
val p1 = Planet.MERCURY
val p2 = Planet.URANUS

println(p1)
println(p1.javaClass)

println(p2)
println(p2.javaClass)
}
/*결과:
MERCURY
class Planet
URANUS
class Planet
*/

Example4–1에서도 확인할 수 있듯이, 각 상수들은 그 상수가 포함된 Enum class타입이며, 출력할 시 기본적으로 그 상수가 Enum class에 정의된 상수 이름으로 출력된다.

2. Enum class의 생성자, 프로퍼티, 메서드 사용

Example5-1. Enum class 생성자 사용하기enum class Planet(mass:Double, radius:Double){
MERCURY(3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7)
}

Enum class의 상수들은 그 상수를 포함하고 있는 Enum class 타입이라는 것을 배웠다. Example5–1에서 Planet의 주생성자가 mass와 radius를 입력받기 때문에, Planet의 객체인 각 상수들은 정의할 때 mass와 radius를 위와 같이 입력받는다.

Example5-2. Enum class 생성자, 프로퍼티, 메서드 사용하기enum class Planet(val mass:Double, val radius:Double){
MERCURY(3.303e+23, 2.4397e6),
VENUS (4.869e+24, 6.0518e6),
EARTH (5.976e+24, 6.37814e6),
MARS (6.421e+23, 3.3972e6),
JUPITER (1.9e+27, 7.1492e7),
SATURN (5.688e+26, 6.0268e7),
URANUS (8.686e+25, 2.5559e7),
NEPTUNE (1.024e+26, 2.4746e7);

fun diameter():Double{ return radius*2}
}

fun main(){
val p1 = Planet.MERCURY
val p2 = Planet.URANUS

println("$p1 mass : ${p1.mass}, $p1 diameter : ${p1.diameter()}")
println("$p2 mass : ${p2.mass}, $p2 diameter : ${p2.diameter()}")
}
/*결과 :
MERCURY mass : 3.303E23, MERCURY diameter : 4879400.0
URANUS mass : 8.686E25, URANUS diameter : 5.1118E7
*/

Enum class는 일반 클래스처럼 프로퍼티와 메서드 역시 가질 수 있고, 따라서 각 상수들은 Enum class에 정의된 프로퍼티와 메서드를 사용할 수 있다. 단, 주의할 점은 상수들을 모두 정의한 후에 세미콜론을 반드시 적어주어야 한다.(Kotlin에서 유일하게 세미콜론이 필수인 부분이다.)

3. 컴파일러가 생성해주는 Enum class의 메서드와 프로퍼티

Enum class가 유용한 또 한 가지 이유는 자동으로 생성해주는 함수가 있기 때문이다.

Fig2. Enum class가 제공해주는 메서드
Example5-3. 컴파일러가 생성해주는 Enum class의 메서드와 프로퍼티enum class Planet{
MERCURY, VENUS, EARTH, MARS, JUPITER, SATURN, URANUS, NEPTUNE;

override fun toString(): String {
return name.first() + name.substring(1).toLowerCase()
}
}
fun printlnP(p:Planet){
println("$p ordinal : ${p.ordinal}")
println("$p declaringClass : ${p.declaringClass}")
println("$p name : ${p.name}")
println("$p toString() : ${p.toString()}")
println("$p compareTo() MARS : ${p.compareTo(Planet.MARS)}")
println("-------------------------------------------------")
}
fun main(){
for(p in Planet.values()){
printlnP(p)
}
val p = Planet.valueOf("MERCURY")
println("valueOf() test")
printlnP(p)
}
/*결과 :
Mercury ordinal : 0
Mercury declaringClass : class Planet
Mercury name : MERCURY
Mercury toString() : Mercury
Mercury compareTo() MARS : -3
-------------------------------------------------
...
Neptune ordinal : 7
Neptune declaringClass : class Planet
Neptune name : NEPTUNE
Neptune toString() : Neptune
Neptune compareTo() MARS : 4
-------------------------------------------------
valueOf() test
Mercury ordinal : 0
Mercury declaringClass : class Planet
Mercury name : MERCURY
Mercury toString() : Mercury
Mercury compareTo() MARS : -3
-------------------------------------------------
*/

Enum class의 특징

  • Enum class는 상속해줄 수 없으며 상속 받을 수도 없다. 그러나 interface를 구현할 수 있다.
  • 일반 클래스처럼 프로퍼티, 메서드, 생성자를 가질 수 있다.
  • 상수 객체와 메서드, 프로퍼티의 기본 가시성은 public이고, 생성자의 기본 가시성은 private이다.
  • Enum class는 객체화할 수 없으며 private 생성자을 정의할 수 있다.
  • Enum class의 상수들은 그 상수를 포함하고 있는 Enum class 타입이다.
  • 자동으로 생성해주는 메서드와 프로퍼티가 있다.(values(), valueOf(), ordinal, name, toString(), compareTo() 등)

Reference

Overall part

  1. Dmitry Jemerov and Svetlana Isakova. (2017). Kotlin in Action. USA: Manning

Sealed class part

2. [chacha]Kotlin —Sealed class 구현 방법 및 예제 — https://codechacha.com/ko/kotlin-sealed-classes/

Enum class part

3. Bruce Eckel. (2007). Thinking in JAVA 4/e. Prentice Hall

4.[생활코딩] Java — 상수와 enum (3/4) : enum의 문법, https://www.youtube.com/watch?v=AWEvmFs7RJ4&ab_channel=%EC%83%9D%ED%99%9C%EC%BD%94%EB%94%A9

5. Oraclehttps://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html

--

--

dEpayse
dEpayse_publication

나뿐만 아니라 다른 사람들도 이해할 수 있도록 작성하는, 친절한 블로그를 목표로.