Kotlin - 특수 클래스(data class, enum class, sealed class)
안드로이드 스튜디오 개발자 페이지를 기반으로 작성됨.
https://developer.android.com/courses/pathways/android-basics-compose-unit-1-pathway-1?hl=ko
Kotlin 프로그래밍 소개 | Android Basics Compose - First Android app | Android Developers
Kotlin에서 Android 앱 빌드를 준비하기 위해 Kotlin의 입문 프로그래밍 개념을 알아봅니다.
developer.android.com
이번 블로그에서는 클래스 중에서도 데이터를 주로 있는 클래스들을 소개하도록 한다.
지금와서 생각해보니까, C언어의 Structure과 비슷한 느낌도 있는 것 같다.
data class
클래스의 기본 구성 요소인 본문이 제외된 데이터만 포함하는 특수한 클래스이다. 안드로이드 프로그래밍에 있어서 꽤 많이 사용되는 클래스로 ui state에 대한 부분을 data class로 정의하거나 server communication 할 때에도 data class를 활용하여 데이터를 주고 받을 수 있다.
사용 예시는 다음과 같다.
data class Person(
val name: String,
val age: Int
)
val person1 = Person("IOT", 27)
println(person1.name)
// "IOT"
println(person1.age)
// 27
굉장히 간단하다. 기존 클래스 구조에서 중괄호를 사용한 본문 구조만 빠진 형태이다.
기존 클래스로도 위와 같은 로직을 구성할 수 있지만, data class로 구성함으로써 특정 메서드를 자동으로 구현할 수 있다.
예를 들어 class에 대해서 toString()이라는 메서드를 class와 data class 각각에 적용해보자.
class Person (
val name: String,
val age: Int
)
data class DataPerson (
val name: String,
val age: Int
)
fun main() {
val person = Person("IOT", 27)
println(person.toString())
// Person@7cc355be
val dataPerson = DataPerson("IOT", 27)
println(dataPerson.toString())
// DataPerson(name=IOT, age=27)
}
class에 대해서 toString() 메서드를 적용할 경우 객체의 고유 식별자만 출력되는 반면,
data class에 대해서 toString() 메서드를 적용하면 객체의 속성을 출력해주는 것을 확인할 수 있다.
enum Class
enum class는 가능한 값의 집합을 제한하는데 사용한다. 예를 들면 질문에 대한 클래스를 정의할 때 난이도에 대한 속성을 정의할 수 있는데, 난이도는 보통 Easy, Medium, Hard로 3가지로 분류하여 구성한다. 이러한 경우 enum class를 활용하여 다음과 같이 선언하고 활용할 수 있다.
enum class Difficulty(
EASY,
MEDIUM,
HARD
)
fun main() {
val questionDifficulty: Difficulty = Difficulty.EASY
...
}
그렇다면, enum class를 사용하는 이유는 무엇일까?
대답은
- 제한된 대답이 아닌 엉뚱한 대답을 피하기 위함
- 코드의 가독성을 높임
- 리터럴 변수를 사용했을 때보다 값 변경시 코드 보수가 용이함
등이 있겠다.
마지막 이유를 좀 더 자세히 확인해보자면, 만일 enum class를 사용하지 않고 difficulty에 대한 부분을 string literal variable로 표현한다고 해보자. 예를 들어 "medium" 으로 지정한 값이 "average"로 변경할 필요가 있다면 우리는 "medium"으로 지정된 부분을 모두 찾아서 변경해야 한다.
하지만, enum의 경우는 어떨까?
enum class Difficulty(val value: String) {
EASY("easy"), MEDIUM("medium"), HARD("hard")
}
fun main() {
val getMediumStringValue = Difficulty.MEDIUM.value
...
}
위와 같은 방식으로 코드를 작성하여 변경해야할 리터럴 값을 enum class 안에서 해결할 수 있는 것이다. 이렇게 하면 변경사항에 대해서 매우 유연하게 대처할 수 있는 장점이 생긴다.
하지만, enum class의 값들은 모두 상수 값으로 어느정도 한계점이 있기는 하다.
- enum class 내부의 값들은 싱글턴 디자인 패턴을 따르는 하나의 인스턴스들이기 때문에 값을 변경하지 못한다.
- 따라서 enum class 외부에서 enum class의 내부 값들을 바꿀 수 없다는 것이다.
- enum class 에 대한 서브 클래스를 생성할 수 없다.
sealed class
enum class에서 상수 및 상속과 관련된 부분을 채워줄 수 있는 sealed class이다.
몇 가지 특징을 살펴보자.
- 자기 자신이 abstract class이며, 상속이 가능한 서브 클래스를 가질 수 있다.
- 싱글턴 디자인 패턴을 따르는 상수 값들이 아니기 때문에, class 외부에서 값을 인스턴스화 할 수 있다
- sealed의 뜻인 '봉인된'이 의미하는 바와 같이 상속 받는 서브 클래스를 제한할 수 있다.
- sealed class의 서브 클래스는 같은 파일에 반드시 선언되어야 한다.
- sealed class의 생성자는 항상 private 접근자만 가진다.
- 즉, 다른 파일에 있는 서브클래스를 sealed class의 서브클래스로 가질 수 없기 때문에 서브클래스에 대한 제한이 생긴다.
코드로 살펴보면 다음과 같다.
sealed class Difficulty {
data class Easy(val timeLimit: Int, val questionNumber: Int): Difficulty()
data class Medium(val timeLimit: Int, val questionNumber: Int): Difficulty()
data class Hard(val timeLimit: Int, val questionNumber: Int): Difficulty()
}
fun main() {
// instantiate
val difficultyOfEasyQuestion: Difficulty = Difficulty.Easy(10, 1)
}
혹은
sealed class Difficulty
data class Easy(val timeLimit: Int, val questionNumber: Int): Difficulty()
data class Medium(val timeLimit: Int, val questionNumber: Int): Difficulty()
data class Hard(val timeLimit: Int, val questionNumber: Int): Difficulty()
다만 서브 클래스를 상속하는 클래스들은 sealed class와 같은 파일에 있을 필요는 없다.
이런식으로 enum class와 같이 데이터 클래스가 필요한 상황에서 상속이 필요하다면 sealed class를 활용하면 된다.
추가적으로 enum class, sealed class는 조건문인 when 문을 활용하여 표현식을 작성할 때 컴파일러가 해당 데이터 유형에 대해 인지하고 있기 때문에 else branch를 작성하지 않아도 된다는 장점이 있다.
예를 들면
enum class Difficulty {
Easy, Medium, Hard
}
fun main() {
val difficulty = Difficulty.Easy
val stringDifficulty = when (difficulty) {
Difficulty.Easy -> "easy"
Difficulty.Medium -> "medium"
Difficulty.Hard -> "hard"
}
println(stringDifficulty)
// easy
}
이런식으로 사용할 수 있다.