오늘 포스팅 주제는 Kotlin의 유용한 기능인 sealed와 enum이다.
둘 다 컴파일 타임에 타입 집합이 결정되어 타입 안전하고 when문을 exhaustive하게 함으로써 경우에 따른 명확한 처리 구문을 작성할 수 있도록 도와준다.
오늘은 이 두 개념들의 역할을 넘어, 컴파일되었을 때의 차이점과 그로 인해 고려할 수 있는 것들에 대해 얘기해보고자 한다.
1. sealed class
코틀린의 sealed class는 모듈/패키지 외부에서 해당 클래스를 상속할 수 없도록 상속 가능 범위를 제한해서 클래스 계층 구조를 타입 안전하게 제공한다.
주로 여러 상태를 한가지 타입으로 다룸으로써 when문에서 모든 케이스를 명시적으로 처리하기 위해 사용된다.
sealed class의 모든 하위 클래스는 컴파일 타임에 검증되므로, when 문을 exhaustive하게 사용할 수 있다.
대표적으로 안드로이드에서는 sealed class를 활용한 Ui State 모델링이 있다.
sealed class UiState {
data object Loading: UiState()
data class Success<out T>(val data: T): UiState()
data class Error(val exception: Exception): UiState()
}
fun updateUI(state: UIState) {
when (state) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Success -> showData(state.data)
is UIState.Error -> showError(state.exception)
}
}
// exhaustive하지 않을 경우
fun foo(state: Int) {
when (state) {
1 -> showLoadingIndicator()
2 -> showData(1)
3 -> showError(3)
else -> // 이외 모든 케이스에 대한 일괄적인 처리를 하게 되어 디버깅이 어려워지고 제대로된 피드백을 제공하지 못한다
}
}
=================================
sealed class RunningTalkUiState {
data class MyRunningTalkUiState(
val items: List<RunningTalkItem>,
val createTime: String
): RunningTalkUiState()
data class OtherRunningTalkUiState(
val writerName: String,
val writerProfileImgUrl: String?,
val items: List<RunningTalkItem>,
val createTime: String,
var isChecked: Boolean = false,
val isReportMode: Boolean
): RunningTalkUiState()
}
sealed class RunningTalkItem(val messageId: Int) {
data class MessageTalkItem(val id: Int, val message: String) : RunningTalkItem(id)
data class ImageTalkItem(val id: Int, val imgUrl: String): RunningTalkItem(id)
}
sealed class가 상속 범위를 제한해주기 때문에, 컴파일 타임에 모든 하위 타입에 대한 정보가 파악되어 런타임에 타입 안전한 처리가 가능해진다.
2. enum class
enum class는 고정된 상수 집합을 타입 안전하게 제공하기 위해 사용된다.
sealed class와 동일하게, 미리 정의된 상수 집합만을 사용하므로 잘못된 값이 사용되는 경우를 방지할 수 있다.
필드를 가질 수 있으며, 상수 집합을 제공하는 entries, 순서를 반환하는 ordinal 등의 기능을 제공한다.
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
아래는 예시코드이다.
Enum 클래스가 제공하는 entries 메소드를 통해 상수 집합을 가져오고, 확장 프로퍼티인 ordinal을 사용해서 index를 대체하는 코드를 작성했다.(알람 탭의 종류와 순서가 고정적이기 때문에 ordinal을 사용했다)
enum class AlarmTab(val title: String) {
ARTICLE("아티클"),
NEW("새 소식");
}
// AlarmScreen.kt
// 모든 상황에 대한 명확한 처리가 가능
when (tab) {
is ARTICLE -> {
. . .
}
is NEW -> {
. . .
}
}
AlarmTab.entries.forEach { currentTab ->
val isSelected = (selectedTabIndex == currentTab.ordinal)
Tab(
selected = isSelected,
onClick = {
onTabChange(currentTab)
},
text = {
Text(
text = currentTab.title,
style = Body2Normal,
fontWeight = if (isSelected) FontWeight.Bold
else FontWeight.Medium,
color = if (isSelected) Caption_Strong
else Caption_Alternative
)
},
selectedContentColor = Primary_Normal
)
}
3. sealed class의 바이트 코드
그럼 이 둘은 컴파일되었을 때에는 어떤 차이가 있는지 바이트 코드를 통해 확인해 보자.
- sealed class는 컴파일 타임에 추상 클래스로 변환된다.
같은 파일 내에서 선언되었더라도 각각 독립적인 클래스로 컴파일된다. - 하위 타입들은 INNERCLASS(내부 클래스) 형태로 바이트 코드에 기록된다.
- 또한 data obejct, data class 등의 경우에는 hashCode, toString, equals 메소드가 자동으로 오버라이드 된 것을 확인할 수 있다.
- enum처럼 하위 타입 전체를 관리하기 위한 entries() 같은 메소드는 제공하지 않는다.
// >>> 코드 원문 <<<
sealed class UiState {
data object Loading: UiState()
data class Success<out T>(val data: T): UiState()
data class Error(val exception: Exception): UiState()
}
// >>> 자바 바이트 코드 <<<
// ================com/and/presentation/screen/mypage/UiState.class =================
public abstract class com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
. . .
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Error com/and/presentation/screen/mypage/UiState Error
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Loading com/and/presentation/screen/mypage/UiState Loading
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Success com/and/presentation/screen/mypage/UiState Success
public final static I $stable
private <init>()V
. . .
public synthetic <init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V <<< 여기 internal이라서 같은 패키지에서만 상속 가능한건가?
. . .
static <clinit>()V
. . .
}
// ================com/and/presentation/screen/mypage/UiState$Loading.class =================
public final class com/and/presentation/screen/mypage/UiState$Loading extends com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Loading com/and/presentation/screen/mypage/UiState Loading
public final static Lcom/and/presentation/screen/mypage/UiState$Loading; INSTANCE
. . .
public final static I $stable
. . .
private <init>()V
. . .
public toString()Ljava/lang/String;
. . .
public hashCode()I
. . .
public equals(Ljava/lang/Object;)Z
. . .
static <clinit>()V
. . .
}
// ================com/and/presentation/screen/mypage/UiState$Success.class =================
public final class com/and/presentation/screen/mypage/UiState$Success extends com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Success com/and/presentation/screen/mypage/UiState Success
private final Ljava/lang/Object; data
public final static I $stable
public <init>(Ljava/lang/Object;)V
. . .
public final getData()Ljava/lang/Object;
. . .
public final component1()Ljava/lang/Object;
. . .
public final copy(Ljava/lang/Object;)Lcom/and/presentation/screen/mypage/UiState$Success;
. . .
public static synthetic copy$default(Lcom/and/presentation/screen/mypage/UiState$Success;Ljava/lang/Object;ILjava/lang/Object;)Lcom/and/presentation/screen/mypage/UiState$Success;
. . .
public toString()Ljava/lang/String;
. . .
public hashCode()I
. . .
public equals(Ljava/lang/Object;)Z
. . .
static <clinit>()V
. . .
}
// ================com/and/presentation/screen/mypage/UiState$Error.class =================
public final class com/and/presentation/screen/mypage/UiState$Error extends com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Error com/and/presentation/screen/mypage/UiState Error
private final Ljava/lang/Exception; exception
public final static I $stable
public <init>(Ljava/lang/Exception;)V
. . .
public final getException()Ljava/lang/Exception;
. . .
public final component1()Ljava/lang/Exception;
. . .
public final copy(Ljava/lang/Exception;)Lcom/and/presentation/screen/mypage/UiState$Error;
. . .
public static synthetic copy$default(Lcom/and/presentation/screen/mypage/UiState$Error;Ljava/lang/Exception;ILjava/lang/Object;)Lcom/and/presentation/screen/mypage/UiState$Error;
. . .
public toString()Ljava/lang/String;
. . .
public hashCode()I
. . .
public equals(Ljava/lang/Object;)Z
. . .
static <clinit>()V
. . .
}
4. Enum 클래스의 바이트 코드
반면 enum 클래스는 컴파일 시 enum을 상속받는 final 클래스로 변환되며, 각 상수는 싱글톤 인스턴스로 정적 필드에 저장된다.
- 컴파일 타임에 상수 집합이 고정되기 때문에, 이외 다른 상수에 대한 예외 상황이 발생하지 않는다.
sealed class와 달리, 각 상수는 별도의 클래스 파일로 컴파일되지 않으며, enum 클래스 내부의 static final 필드로 생성된다. - 모든 상수가 java.lang.Enum의 메서드를(name, ordinal, toString 등) 자동으로 오버라이드한다.
- 내부적으로 상수들을 보관하는 VALUES 배열과, Kotlin 1.9.0 이후부터 지원하는 ENTRIES 배열이 존재한다.
이걸 사용해서 상수 목록을 불러와 사용할 수 있다.
// >>> 코드 원문 <<<
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
// >>> 자바 바이트 코드 <<<
// ================com/and/presentation/screen/mypage/Direction.class =================
public final enum com/and/presentation/screen/mypage/Direction extends java/lang/Enum {
@Lkotlin/Metadata;(mv={2, 1, 0}, k=1, xi=50, d1={"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0010\n\u0002\u0008\u0006\u0008\u0086\u0081\u0002\u0018\u00002\u0008\u0012\u0004\u0012\u00020\u00000\u0001B\u0007\u0008\u0002\u00a2\u0006\u0002\u0010\u0002j\u0002\u0008\u0003j\u0002\u0008\u0004j\u0002\u0008\u0005j\u0002\u0008\u0006\u00a8\u0006\u0007"}, d2={"Lcom/and/presentation/screen/mypage/Direction;", "", "(Ljava/lang/String;I)V", "NORTH", "SOUTH", "WEST", "EAST", "NewDok-Android.presentation.main"})
public final static enum Lcom/and/presentation/screen/mypage/Direction; NORTH
public final static enum Lcom/and/presentation/screen/mypage/Direction; SOUTH
public final static enum Lcom/and/presentation/screen/mypage/Direction; WEST
public final static enum Lcom/and/presentation/screen/mypage/Direction; EAST
private final static synthetic [Lcom/and/presentation/screen/mypage/Direction; $VALUES
private final static synthetic Lkotlin/enums/EnumEntries; $ENTRIES
private <init>(Ljava/lang/String;I)V
. . .
public static values()[Lcom/and/presentation/screen/mypage/Direction;
. . .
public static valueOf(Ljava/lang/String;)Lcom/and/presentation/screen/mypage/Direction;
. . .
public static getEntries()Lkotlin/enums/EnumEntries;
. . .
private final static synthetic $values()[Lcom/and/presentation/screen/mypage/Direction;
. . .
static <clinit>()V
. . .
}
Kotlin 코드는 컴파일 타임에 Java 바이트 코드로 변환되며, 바이트 코드는 많은 정보들을 담고 있다.
바이트 코드를 통해 알게 된 정보를 기반으로, 메모리 최적화와 비교 연산 성능에 대한 비교를 마지막으로 포스팅을 마무리하겠다.
5. 메모리 관점에서 생각해보기
enum 상수들은 컴파일 타임에 최초 생성되고, 해당 enum 클래스 내부의 static final 필드에 할당된다.
단일 enum 클래스에 모두 포함되기 때문에 클래스 로딩 관련 메모리 오버헤드가 줄어들고, 싱글톤 인스턴스로 관리되므로 객체 재사용이 효율적이지만, 메모리 사용량이 증가하게 될 것이다.(유의미한 수치인지는 모르겠지만, 메모리에 상주한다는 점이 중요하다)
반면 sealed class의 하위 타입들은 별도의 클래스로 컴파일되므로 클래스 로딩과 객체 생성시에 메모리 오버헤드는 상대적으로 높을 것이다.
하지만 sealed class는 enum class에 비해 훨씬 복잡하고 다양한 형태를 캡슐화하여 표현할 수 있다는 장점이 있으니 그 에 대한 트레이프 오프라고 생각하고 상황에 따라 적절한 선택을 해야 할 것 같다.
6. 비교 연산 성능을 비교
Kotlin에서 == 연산자는 equals() 메서드를 호출하지만, enum 상수 값은 싱글톤 인스턴스로 관리되기 때문에, 내부 상태 비교가 필요하지 않은 참조 비교를 수행하므로 연산 비용이 절약된다.
참조 비교는 메모리 주소를 비교하는 방법으로, 두 객체가 "같은 인스턴스를 가리키는지"를 판단하기 때문에 객체의 내부 상태를 모두 비교해야하는 방식보다 연산이 저렴하고 빠르다.
물론 sealed class의 object들도 싱글톤 인스턴스이므로 참조 비교를 수행하지만, data class나 일반 class의 경우에는 equals 메서드를 통해 내부 상태(프로퍼티)를 비교하게 되므로 비교 연산이 빈번할 경우에는 상대적으로 비용이 더 들어갈 것이다.
'개발 > Kotlin' 카테고리의 다른 글
[이펙티브 코틀린] 가변성을 제한하라 (0) | 2025.04.09 |
---|---|
참조 객체로 GC에 관여하기 (feat.WeakReference) (1) | 2025.03.19 |
[Kotlin] 예외 처리 - 예상한 예상 밖의 결과 (0) | 2025.02.19 |
[이펙티브 코틀린] 프로퍼티는 동작이 아니라 상태를 나타내야 한다 (0) | 2025.02.07 |
[Kotlin] 타입 소거와 Star projection (0) | 2024.06.27 |
오늘 포스팅 주제는 Kotlin의 유용한 기능인 sealed와 enum이다.
둘 다 컴파일 타임에 타입 집합이 결정되어 타입 안전하고 when문을 exhaustive하게 함으로써 경우에 따른 명확한 처리 구문을 작성할 수 있도록 도와준다.
오늘은 이 두 개념들의 역할을 넘어, 컴파일되었을 때의 차이점과 그로 인해 고려할 수 있는 것들에 대해 얘기해보고자 한다.
1. sealed class
코틀린의 sealed class는 모듈/패키지 외부에서 해당 클래스를 상속할 수 없도록 상속 가능 범위를 제한해서 클래스 계층 구조를 타입 안전하게 제공한다.
주로 여러 상태를 한가지 타입으로 다룸으로써 when문에서 모든 케이스를 명시적으로 처리하기 위해 사용된다.
sealed class의 모든 하위 클래스는 컴파일 타임에 검증되므로, when 문을 exhaustive하게 사용할 수 있다.
대표적으로 안드로이드에서는 sealed class를 활용한 Ui State 모델링이 있다.
sealed class UiState {
data object Loading: UiState()
data class Success<out T>(val data: T): UiState()
data class Error(val exception: Exception): UiState()
}
fun updateUI(state: UIState) {
when (state) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Success -> showData(state.data)
is UIState.Error -> showError(state.exception)
}
}
// exhaustive하지 않을 경우
fun foo(state: Int) {
when (state) {
1 -> showLoadingIndicator()
2 -> showData(1)
3 -> showError(3)
else -> // 이외 모든 케이스에 대한 일괄적인 처리를 하게 되어 디버깅이 어려워지고 제대로된 피드백을 제공하지 못한다
}
}
=================================
sealed class RunningTalkUiState {
data class MyRunningTalkUiState(
val items: List<RunningTalkItem>,
val createTime: String
): RunningTalkUiState()
data class OtherRunningTalkUiState(
val writerName: String,
val writerProfileImgUrl: String?,
val items: List<RunningTalkItem>,
val createTime: String,
var isChecked: Boolean = false,
val isReportMode: Boolean
): RunningTalkUiState()
}
sealed class RunningTalkItem(val messageId: Int) {
data class MessageTalkItem(val id: Int, val message: String) : RunningTalkItem(id)
data class ImageTalkItem(val id: Int, val imgUrl: String): RunningTalkItem(id)
}
sealed class가 상속 범위를 제한해주기 때문에, 컴파일 타임에 모든 하위 타입에 대한 정보가 파악되어 런타임에 타입 안전한 처리가 가능해진다.
2. enum class
enum class는 고정된 상수 집합을 타입 안전하게 제공하기 위해 사용된다.
sealed class와 동일하게, 미리 정의된 상수 집합만을 사용하므로 잘못된 값이 사용되는 경우를 방지할 수 있다.
필드를 가질 수 있으며, 상수 집합을 제공하는 entries, 순서를 반환하는 ordinal 등의 기능을 제공한다.
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}
아래는 예시코드이다.
Enum 클래스가 제공하는 entries 메소드를 통해 상수 집합을 가져오고, 확장 프로퍼티인 ordinal을 사용해서 index를 대체하는 코드를 작성했다.(알람 탭의 종류와 순서가 고정적이기 때문에 ordinal을 사용했다)
enum class AlarmTab(val title: String) {
ARTICLE("아티클"),
NEW("새 소식");
}
// AlarmScreen.kt
// 모든 상황에 대한 명확한 처리가 가능
when (tab) {
is ARTICLE -> {
. . .
}
is NEW -> {
. . .
}
}
AlarmTab.entries.forEach { currentTab ->
val isSelected = (selectedTabIndex == currentTab.ordinal)
Tab(
selected = isSelected,
onClick = {
onTabChange(currentTab)
},
text = {
Text(
text = currentTab.title,
style = Body2Normal,
fontWeight = if (isSelected) FontWeight.Bold
else FontWeight.Medium,
color = if (isSelected) Caption_Strong
else Caption_Alternative
)
},
selectedContentColor = Primary_Normal
)
}
3. sealed class의 바이트 코드
그럼 이 둘은 컴파일되었을 때에는 어떤 차이가 있는지 바이트 코드를 통해 확인해 보자.
- sealed class는 컴파일 타임에 추상 클래스로 변환된다.
같은 파일 내에서 선언되었더라도 각각 독립적인 클래스로 컴파일된다. - 하위 타입들은 INNERCLASS(내부 클래스) 형태로 바이트 코드에 기록된다.
- 또한 data obejct, data class 등의 경우에는 hashCode, toString, equals 메소드가 자동으로 오버라이드 된 것을 확인할 수 있다.
- enum처럼 하위 타입 전체를 관리하기 위한 entries() 같은 메소드는 제공하지 않는다.
// >>> 코드 원문 <<<
sealed class UiState {
data object Loading: UiState()
data class Success<out T>(val data: T): UiState()
data class Error(val exception: Exception): UiState()
}
// >>> 자바 바이트 코드 <<<
// ================com/and/presentation/screen/mypage/UiState.class =================
public abstract class com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
. . .
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Error com/and/presentation/screen/mypage/UiState Error
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Loading com/and/presentation/screen/mypage/UiState Loading
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Success com/and/presentation/screen/mypage/UiState Success
public final static I $stable
private <init>()V
. . .
public synthetic <init>(Lkotlin/jvm/internal/DefaultConstructorMarker;)V <<< 여기 internal이라서 같은 패키지에서만 상속 가능한건가?
. . .
static <clinit>()V
. . .
}
// ================com/and/presentation/screen/mypage/UiState$Loading.class =================
public final class com/and/presentation/screen/mypage/UiState$Loading extends com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Loading com/and/presentation/screen/mypage/UiState Loading
public final static Lcom/and/presentation/screen/mypage/UiState$Loading; INSTANCE
. . .
public final static I $stable
. . .
private <init>()V
. . .
public toString()Ljava/lang/String;
. . .
public hashCode()I
. . .
public equals(Ljava/lang/Object;)Z
. . .
static <clinit>()V
. . .
}
// ================com/and/presentation/screen/mypage/UiState$Success.class =================
public final class com/and/presentation/screen/mypage/UiState$Success extends com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Success com/and/presentation/screen/mypage/UiState Success
private final Ljava/lang/Object; data
public final static I $stable
public <init>(Ljava/lang/Object;)V
. . .
public final getData()Ljava/lang/Object;
. . .
public final component1()Ljava/lang/Object;
. . .
public final copy(Ljava/lang/Object;)Lcom/and/presentation/screen/mypage/UiState$Success;
. . .
public static synthetic copy$default(Lcom/and/presentation/screen/mypage/UiState$Success;Ljava/lang/Object;ILjava/lang/Object;)Lcom/and/presentation/screen/mypage/UiState$Success;
. . .
public toString()Ljava/lang/String;
. . .
public hashCode()I
. . .
public equals(Ljava/lang/Object;)Z
. . .
static <clinit>()V
. . .
}
// ================com/and/presentation/screen/mypage/UiState$Error.class =================
public final class com/and/presentation/screen/mypage/UiState$Error extends com/and/presentation/screen/mypage/UiState {
// compiled from: TestUser.kt
public final static INNERCLASS com/and/presentation/screen/mypage/UiState$Error com/and/presentation/screen/mypage/UiState Error
private final Ljava/lang/Exception; exception
public final static I $stable
public <init>(Ljava/lang/Exception;)V
. . .
public final getException()Ljava/lang/Exception;
. . .
public final component1()Ljava/lang/Exception;
. . .
public final copy(Ljava/lang/Exception;)Lcom/and/presentation/screen/mypage/UiState$Error;
. . .
public static synthetic copy$default(Lcom/and/presentation/screen/mypage/UiState$Error;Ljava/lang/Exception;ILjava/lang/Object;)Lcom/and/presentation/screen/mypage/UiState$Error;
. . .
public toString()Ljava/lang/String;
. . .
public hashCode()I
. . .
public equals(Ljava/lang/Object;)Z
. . .
static <clinit>()V
. . .
}
4. Enum 클래스의 바이트 코드
반면 enum 클래스는 컴파일 시 enum을 상속받는 final 클래스로 변환되며, 각 상수는 싱글톤 인스턴스로 정적 필드에 저장된다.
- 컴파일 타임에 상수 집합이 고정되기 때문에, 이외 다른 상수에 대한 예외 상황이 발생하지 않는다.
sealed class와 달리, 각 상수는 별도의 클래스 파일로 컴파일되지 않으며, enum 클래스 내부의 static final 필드로 생성된다. - 모든 상수가 java.lang.Enum의 메서드를(name, ordinal, toString 등) 자동으로 오버라이드한다.
- 내부적으로 상수들을 보관하는 VALUES 배열과, Kotlin 1.9.0 이후부터 지원하는 ENTRIES 배열이 존재한다.
이걸 사용해서 상수 목록을 불러와 사용할 수 있다.
// >>> 코드 원문 <<<
enum class Direction {
NORTH, SOUTH, WEST, EAST
}
// >>> 자바 바이트 코드 <<<
// ================com/and/presentation/screen/mypage/Direction.class =================
public final enum com/and/presentation/screen/mypage/Direction extends java/lang/Enum {
@Lkotlin/Metadata;(mv={2, 1, 0}, k=1, xi=50, d1={"\u0000\u000c\n\u0002\u0018\u0002\n\u0002\u0010\u0010\n\u0002\u0008\u0006\u0008\u0086\u0081\u0002\u0018\u00002\u0008\u0012\u0004\u0012\u00020\u00000\u0001B\u0007\u0008\u0002\u00a2\u0006\u0002\u0010\u0002j\u0002\u0008\u0003j\u0002\u0008\u0004j\u0002\u0008\u0005j\u0002\u0008\u0006\u00a8\u0006\u0007"}, d2={"Lcom/and/presentation/screen/mypage/Direction;", "", "(Ljava/lang/String;I)V", "NORTH", "SOUTH", "WEST", "EAST", "NewDok-Android.presentation.main"})
public final static enum Lcom/and/presentation/screen/mypage/Direction; NORTH
public final static enum Lcom/and/presentation/screen/mypage/Direction; SOUTH
public final static enum Lcom/and/presentation/screen/mypage/Direction; WEST
public final static enum Lcom/and/presentation/screen/mypage/Direction; EAST
private final static synthetic [Lcom/and/presentation/screen/mypage/Direction; $VALUES
private final static synthetic Lkotlin/enums/EnumEntries; $ENTRIES
private <init>(Ljava/lang/String;I)V
. . .
public static values()[Lcom/and/presentation/screen/mypage/Direction;
. . .
public static valueOf(Ljava/lang/String;)Lcom/and/presentation/screen/mypage/Direction;
. . .
public static getEntries()Lkotlin/enums/EnumEntries;
. . .
private final static synthetic $values()[Lcom/and/presentation/screen/mypage/Direction;
. . .
static <clinit>()V
. . .
}
Kotlin 코드는 컴파일 타임에 Java 바이트 코드로 변환되며, 바이트 코드는 많은 정보들을 담고 있다.
바이트 코드를 통해 알게 된 정보를 기반으로, 메모리 최적화와 비교 연산 성능에 대한 비교를 마지막으로 포스팅을 마무리하겠다.
5. 메모리 관점에서 생각해보기
enum 상수들은 컴파일 타임에 최초 생성되고, 해당 enum 클래스 내부의 static final 필드에 할당된다.
단일 enum 클래스에 모두 포함되기 때문에 클래스 로딩 관련 메모리 오버헤드가 줄어들고, 싱글톤 인스턴스로 관리되므로 객체 재사용이 효율적이지만, 메모리 사용량이 증가하게 될 것이다.(유의미한 수치인지는 모르겠지만, 메모리에 상주한다는 점이 중요하다)
반면 sealed class의 하위 타입들은 별도의 클래스로 컴파일되므로 클래스 로딩과 객체 생성시에 메모리 오버헤드는 상대적으로 높을 것이다.
하지만 sealed class는 enum class에 비해 훨씬 복잡하고 다양한 형태를 캡슐화하여 표현할 수 있다는 장점이 있으니 그 에 대한 트레이프 오프라고 생각하고 상황에 따라 적절한 선택을 해야 할 것 같다.
6. 비교 연산 성능을 비교
Kotlin에서 == 연산자는 equals() 메서드를 호출하지만, enum 상수 값은 싱글톤 인스턴스로 관리되기 때문에, 내부 상태 비교가 필요하지 않은 참조 비교를 수행하므로 연산 비용이 절약된다.
참조 비교는 메모리 주소를 비교하는 방법으로, 두 객체가 "같은 인스턴스를 가리키는지"를 판단하기 때문에 객체의 내부 상태를 모두 비교해야하는 방식보다 연산이 저렴하고 빠르다.
물론 sealed class의 object들도 싱글톤 인스턴스이므로 참조 비교를 수행하지만, data class나 일반 class의 경우에는 equals 메서드를 통해 내부 상태(프로퍼티)를 비교하게 되므로 비교 연산이 빈번할 경우에는 상대적으로 비용이 더 들어갈 것이다.
'개발 > Kotlin' 카테고리의 다른 글
[이펙티브 코틀린] 가변성을 제한하라 (0) | 2025.04.09 |
---|---|
참조 객체로 GC에 관여하기 (feat.WeakReference) (1) | 2025.03.19 |
[Kotlin] 예외 처리 - 예상한 예상 밖의 결과 (0) | 2025.02.19 |
[이펙티브 코틀린] 프로퍼티는 동작이 아니라 상태를 나타내야 한다 (0) | 2025.02.07 |
[Kotlin] 타입 소거와 Star projection (0) | 2024.06.27 |