Scala의 Enum
scala의 enum은 java와 같이 enum 키워드로 존재하지 않았다. 이 문장은 scala 3이 도입되면서 거짓이 되었다. scala 3에서 새롭게 디자인되었는데 scala 2의 문제점은 무엇이었는지 궁금해서 찾아보게 됐다.
1. Enumeration
scala 2에서는 앞서 말한 대로 enum 키워드가 존재하지 않는다. 대신 Enumeration 클래스를 상속해서 사용한다. scala API 문서에 올라온 코드를 예를 들면 아래와 같이 사용한다.
object WeekDay extends Enumeration {
type WeekDay = Value
val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value
}
별도로 지정한 이름으로 Enumeration을 생성하려면 Value 인자에 새로운 이름을 넣으면 된다.
object DayOfWeek extends Enumeration {
val Mon = Value("월요일")
val Tue = Value("화요일")
val Wed = Value("수요일")
}
그런데 Enumeration에는 단점이 몇 가지 존재한다.
2. Enumeration의 단점
2.1. 메서드 오버로딩
object Color extends Enumeration {
val Red, Green, Blue = Value
}
object Size extends Enumeration {
val S, M, L, XL = Value
}
def f(color: Color.Value) = println(color)
def f(size: Size.Value) = println(size)
Enumeration은 메서드 오버로딩을 허용하지 않는다. 그래서 같은 이름의 함수에 다른 Enumeration을 전달인자로 받으면 컴파일 오류가 발생한다. 컴파일 오류는 아래와 같이 동일한 타입이 두 번 선언됐다고 에러를 낸다.
double definition:
def f(color: Color.Value): Unit at line 9 and
def f(size: Size.Value): Unit at line 10
have same type after erasure: (color: Enumeration#Value): Unit
2.2. 패턴 매칭
첫 번째 에러보다 두 번째가 더 심각한 단점으로 생각된다. 스칼라의 큰 장점 중 하나인 패턴 매칭을 사용하면 오류를 발생한다.
object Color extends Enumeration {
val Red, Green, Blue = Value
}
def colorPattern(color: Color.Value): Unit = Color match {
case Color.Red => println("빨간색")
case Color.Green => println("녹색")
}
colorPattern(Color.Red)
위 코드를 컴파일 할 때는 오류가 생기지 않는다. 그런데 실행을 하면 scala.MatchError: Color 에러가 발생한다. Java에서도 enum을 사용해서 switch 문으로 구분하여 로직을 나눌 수 있다. 그런데 패턴 매칭에 장점을 가진 스칼라에서 Enumeration의 패턴 매칭이 안된다는건 치명적인 단점이 된다. 원인은 Enumeration을 상속했을 때, 동일 타입으로 타입이 지워지기 때문이다.
2.3. NoSuchElementException
이름으로 Enumeration 인스턴스를 얻으려면 withName 함수를 사용한다. 그런데 withName 함수는 Enumeration에 일치하는 이름이 존재하지 않으면 NoSuchElementException을 던진다. 그래서 withName을 사용할 때는 Try로 묶어서 호출해 Exception을 방지해야 한다.
final def withName(s: String): Value = values.byName.getOrElse(s,
throw new NoSuchElementException(s"No value found for '$s'"))
3. Enumeration의 대안
Enumeration의 단점에서 패턴 매칭이 불가능하다는 점이 크게 다가왔다. 그럼 대안은 없을까? Enumeration을 쓰지 않고 java의 enum과 비슷한 효과를 보는 방법이 있다. sealed case objects를 사용하는 것이다. 아래 코드를 보자.
sealed trait Color
final case object White extends Color
final case object Black extends Color
sealed trait Name
final case object Pawn extends Name
final case object Rook extends Name
final case object Knight extends Name
final case object Bishop extends Name
final case object Queen extends Name
final case object King extends Name
위 코드에서 Color는 다음과 같이 표현할 수 있다. "Color is a White or a Black". 이렇게 sealed trait로 타입을 정의하는 방법을 sum type pattern 이라고 한다. 여러 개의 서로 다른 타입을 하나의 타입으로 그룹핑 하는 것이다.
sum type pattern을 사용하면 패턴 매칭을 할 수 있다.
def isWhite(c: Color): Boolean = c match {
case White => true
case _ => false
}
4. Scala3
앞서 설명한 단점들로 인해서 scala 3에서는 새롭게 enum이 추가되었다. enum은 기존 java, c++ 등의 언어에서 사용하던 방식과 유사하다. 아래는 Scala 3 Reference 문서에 나온 enum 선언의 예이다.
enum Color:
case Red, Green, Blue
위와 같이 정의하면 Red, Green, Blue 3개의 값을 갖는 새로운 sealed class가 정의된다. 파라미터로 값을 정의할 수도 있다.
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
enum은 미리 정의된 함수를 가지고 있다. 고유 integer 값을 리턴하는 ordinal. 파라미터로 이름을 받아 enum을 생성하는 valueOf 등이 있다.
Java enum에 의해서 생성하는 호환성도 있다. java.lang.Enum을 확장하면 scala enum을 바로 생성할 수 있다.
enum Color extends Enum[Color] { case Red, Green, Blue }
Scala3에서 추가된 Enums는 다음 공식 문서를 추가로 더 확인해보자.
https://docs.scala-lang.org/scala3/reference/enums/index.html
그리고 scala의 Enumeration이 다른 개발자들도 불편함을 느꼈던 것 같다. 최근 Jetbrains에서 scala 관련 설문에서 scala3에서 사용하는 기능 중 1위로 Enums가 선정됐다.
참고 문서
- https://pedrorijo.com/blog/scala-enums/
- https://medium.com/@yuriigorbylov/scala-enumerations-hell-5bdba2c1216
- https://www.baeldung.com/scala/case-objects-vs-enumerations
- https://www.baeldung.com/scala/algebraic-data-types
- https://docs.scala-lang.org/scala3/reference/enums/enums.html
- https://docs.scala-lang.org/scala3/reference/enums/index.html
- https://www.jetbrains.com/ko-kr/lp/devecosystem-2023/scala/