면접의 단골 주제로 나오는 Sealed class 와 Enum class 의 차이점에 대해 글을 작성해보려고 한다.
흔히 UiState 와 같이 Ui 상태를 다룰 때 sealed class 를 사용하고 고정된 값을 가진 데이터 집합을 구현할 때 enum class 를 사용하는 것은 알고 있을테지만, 정확히 그렇게 나눠서 사용하고 있는 이유에 대해서 깊게 생각해보지는 않았을 것이다.
나의 경우 실무에서는 알고 썻지만 이를 이론적으로 설명하지 못하여 다시 한 번 글을 써내리는 중이다.
글을 보고 다른 이들도 나와 같이 실무에서 적용하는 것 뿐만 아니라 정확히 두 개 사이에서 어떤 차이점이 있는지를 알아내고 이를 설명할 수 있다면 좋겠다.
# Sealed Class 란?
흔히 데이터 상태의 집합을 나타낼 때 사용하는 구조로 가장 흔히 사용되는 것은 Ui 에 대한 상태이다.
# 특징1. 서브 클래스간 서로 다른 데이터 가질 수 있음
위는 Ui 의 상태를 집합시켜놓은 클래스로 Loading, Empty, Failure, Success 가 존재한다.
이러한 상태들에서 가져야 하는 데이터는 서로 다른데, sealed class 의 경우 각 서브 클래스가 서로 다른 형태의 데이터를 가질 수 있기 때문에 위와 같이 설정이 가능하다.
# 특징2. 컴파일 시 abstract sealed class 로 변환된다.
sealed class 의 경우 컴파일 시 abstract sealed class 로 변환되며 내부 subclass 들은 이 abstract sealed class 를 상속받는 final 클래스로 컴파일 된다.
참고로 자바 17 부터 abstract sealed class, 이전은 abstract class 로 컴파일된다고 하니 이 점 참고하자
아래는 위의 UiState 를 컴파일 한 결과물이다.
public abstract sealed class UiState<T> permits UiState.Loading, UiState.Empty, UiState.Failure, UiState.Success {
private UiState() {}
// Kotlin의 data object Loading에 대응 (싱글턴)
public static final class Loading extends UiState<Object> {
public static final Loading INSTANCE = new Loading();
private Loading() {}
@Override
public String toString() {
return "Loading";
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
}
// Kotlin의 data object Empty에 대응 (싱글턴)
public static final class Empty extends UiState<Object> {
public static final Empty INSTANCE = new Empty();
private Empty() {}
@Override
public String toString() {
return "Empty";
}
@Override
public boolean equals(Object obj) {
return this == obj;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
}
// Kotlin의 data class Failure(val message: String?, val throwable: Throwable)
public static final class Failure extends UiState<Object> {
private final String message;
private final Throwable throwable;
public Failure(String message, Throwable throwable) {
this.message = message;
this.throwable = throwable;
}
public String getMessage() {
return message;
}
public Throwable getThrowable() {
return throwable;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Failure)) return false;
Failure that = (Failure) o;
return java.util.Objects.equals(message, that.message) &&
java.util.Objects.equals(throwable, that.throwable);
}
@Override
public int hashCode() {
return java.util.Objects.hash(message, throwable);
}
@Override
public String toString() {
return "Failure(message=" + message + ", throwable=" + throwable + ")";
}
}
// Kotlin의 data class Success<T>(val data: T)
public static final class Success<T> extends UiState<T> {
private final T data;
public Success(T data) {
this.data = data;
}
public T getData() {
return data;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Success)) return false;
Success<?> that = (Success<?>) o;
return java.util.Objects.equals(data, that.data);
}
@Override
public int hashCode() {
return java.util.Objects.hash(data);
}
@Override
public String toString() {
return "Success(data=" + data + ")";
}
}
}
# 특징3. 런타임 때 매번 새로운 객체 생성이 가능하다. (object 제외)
fun uiState(){
// 새로운 객체 생성 가능
val state = UiState.Success(true)
val state2 = UiState.Success(false)
// 실패.
// Success 는 data class 이므로, 프로퍼티 값이 다른 객체는 서로 다른 객체로 인식한다.
assert(state == state2)
}
fun uiState2(){
// Object 는 싱글턴으로 메모리가 하나만 올라감
val state = UiState.Loading
val state2 = UiState.Loading
// 성공
// 서로 하나의 값, 메모리이기 때문에 무조건 성공
assert(state == state2)
}
# 특징4. 확장이 가능하다.
sealed class UiState <out T> {
data object Loading : UiState<Nothing>()
data object Empty : UiState<Nothing>()
data class Failure(val message: String?, val throwable: Throwable) : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
}
data object None: UiState<Nothing>()
위처럼 같은 파일 내라면 새로운 서브 클래스를 생성할 수 있다.
# 특징5. when 에서 else 없이도 안전하게 사용이 가능하다
#특징4 를 시도했다 생각하고, UiState 를 활용하여 변수에 할당하고 when 문으로 조건 검사를 실행해보자.
class DevGyuTest {
fun uiState(){
val uiState: UiState<Boolean> = getUiState()
// 'when' expression must be exhaustive,
// add necessary 'None' branch or 'else' branch instead
// 확장한 None 이 없어서 뱉는 에러
when(uiState){
is UiState.Success -> true
is UiState.Failure -> false
UiState.Loading -> false
UiState.Empty -> false
}
}
}
fun getUiState(): UiState<Boolean> = UiState.Success(true)
위와 같이 when 문으로 검사를 할 때 서브 클래스 중 하나라도 검사문에 추가하지 않는다면 에러를 내뱉게 되어 개발자가 실수로 조건을 빠뜨리지 않게 해줄 수 있다. (else 를 사용하지 않는다면 말이다.)
# Enum Class 란?
"열거형 클래스" 라고 불리며 내부적으로 상수들을 정의해놓은 상수들의 집합 클래스
내부에 선언된 열거형 상수들은 모두 고정된 값을 가진다.
# 특징1. enum 은 프로퍼티와 메소드를 가질 수 있다.
각 상수마다 서로 다른 동작(메서드 오버라이딩)이 가능하지만, 모든 상수들이 공통된 프로퍼티를 가져야한다.
enum class Animal(val sound: String) {
DOG("멍") {
override fun makeSound() = "멍멍!"
},
CAT("냥") {
override fun makeSound() = "냐옹!"
};
abstract fun makeSound(): String
}
# 특징2. 컴파일 시 각 Enum 상수들은 static final 객체로 선언되고, enum class 는 java.lang.Enum 로 변환된다.
컴파일 시 내부 상수들을 모두 static final 객체로 선언하기 때문에 싱글턴 패턴을 따른다.
즉, 런타임 때 객체가 새로 생성될 수 없다.
enum class DatabaseConnection {
INSTANCE;
fun connect() = "Connected to Database"
}
// 자바
public enum DatabaseConnection {
INSTANCE;
public String connect() {
return "Connected to Database";
}
}
# 특징3. 확장이 불가능하다.
sealed class 의 경우 같은 파일 내에서 새롭게 확장이 가능하지만 enum class 의 경우 열거형 클래스 내에서만 설정이 가능하다.
# 정리
1. 컴파일 시 변환
- sealed class : abstract sealed class 로 변환 후 서브 클래스들은 각 class 형식으로 변환
- enum class : java.lang.enum 으로 변환 후 각 상수들은 final static 선언되어 싱글턴 패턴을 따른다.
2. 서로 다른 타입의 데이터를 가질 수 있는지
- sealed class : 서브 클래스별로 서로 다른 메소드 및 프로퍼티를 가질 수 있다.
- enum class : 각 상수는 공통된 프로퍼티나 메소드를 가진다. 오버라이딩은 가능함.
3. 확장 여부
- sealed class : 같은 파일 내에서 가능
- enum class : 확장 불가능
'프로그래밍 > kotlin' 카테고리의 다른 글
[Kotlin] Coroutine Suspend 의 재개방식을 알아보자 (With CPS) (0) | 2025.02.26 |
---|---|
[Kotlin] Class 와 Data class 에 대해서 알아보고 Equals 의 작동 원리에 대해서 알아보자 (0) | 2025.02.18 |
[Kotlin] 개발자를 위한 디자인 패턴 (Design Pattern) 에 대해서 알아보자 (3/3) # 행위 패턴 (0) | 2024.09.30 |
[Kotlin] 개발자를 위한 디자인 패턴 (Design Pattern) 에 대해서 알아보자 (2/3) # 구조 패턴 (1) | 2024.09.30 |
[kotlin] 개발자를 위한 디자인 패턴 (Design Pattern) 에 대해서 알아보자 (1/3) # 생성 패턴 (1) | 2024.09.30 |