코틀린의 다양한 클래스

SMJ·2024년 11월 11일

Kotlin

목록 보기
2/2
post-thumbnail

Enum class

enum class Day {
    MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

// 생성자를 이용한 초기화
enum class NetworkState(val code: Int) {
    SUCCESS(200),
    ERROR(404),
    TIMEOUT(408)
}

특징

  • 보통 열거형 타입을 정의할 때 사용한다.
    • 상수 집합을 표현하기 위해 사용하는 클래스로, 주로 고정된 상수 집합을 나타낸다.
  • enum 클래스를 이용해 각 상수를 유일한 인스턴스(객체)로 정의할 수 있다.
    • MONDAYTUESDAY 등 은 Day 클래스의 각 상수는 하나의 객체로만 존재한다.
  • 상수 각각은 해당 클래스의 인스턴스이기 때문에, 생성자를 이용하여 값을 초기화할 수 있다.
    • 각 상수에 대해 모든 열거형 값이 동일한 생성자 형식을 가져야 한다.

seaeld가 있는데 왜 쓰나요?

  • sealed class가 enum 클래스의 확장 느낌이지만 enum이 더 유용한 상황도 있다.
    • 고정된 상수 집합 표현 (요일, 색상 등 미리 정해져서 안 바뀌는 값)

    • 단순한 값이나 상태를 나열 (열거된 값들로 상태 명확히 구분할 수 있을 때)

    • 반복 가능한 값을 활용 (열거된 값 순회 또는 비교)

      enum class NetworkState(val code: Int) {
          SUCCESS(200),
          ERROR(404),
          TIMEOUT(408)
      }
      
      fun main() {
      
      	// values() 사용
          val states = NetworkState.values()
          println("All network states:")
          states.forEach { state ->
              println("State: ${state.name}, Code: ${state.code}")
          }
      
          // entries 사용
          val stateEntries = NetworkState.entries
          stateEntries.forEach { state ->
              println("State: ${state.name}, Code: ${state.code}")
          }
      }

      values()와 entries 속성으로 모든 값 가져오기

      values() 또는 entries 속성을 통해 모든 값 목록을 가져올 수 있다.

      entries 는 코틀린 1.82에서 추가된 메서드로 enum class의 상수들을 호출할 때 values()보다 entries()를 호출하는 것을 권장한다.

      코틀린 1.9.0에선 values()를 대체하는 entries 프로퍼티가 도입됐다.
      이것은 미리 할당된 enum 상수의 불변 리스트를 반환한다.
      이는 컬렉션으로 작업 시 유용하며 성능 문제를 방지하는 데 도움이 될 수 있다.

    • values()는 상수들을 선언한 순서대로 담은 Array를 리턴한다.

      • 배열은 mutable 타입이라, 외부에서 순서를 바꾸거나 내용을 변경할 수 있다.
        • 코드의 안정성이 떨어진다. (순서가 언제든 바뀌면 상수 사용 및 관리가 어렵다.)
      • 호출할 때마다 배열을 할당, 복제하기 때문에 성능 이슈의 원인이 된다.
    • entriesEnumEntries 타입의 불변 리스트로,
      enum 상수를 immutable List로 제공하여 직접 수정할 수 없고 안전하게 사용하도록 보장한다.

    • 이 기능은 sealed class로는 제공되지 않는다.

      • 모든 값 순회 할 때, 목록으로 관리해야 할 때 enum이 더 유용하다.

      entries 장점

    • 안정성, 성능 개선 : 이미 할당된 불변 리스트를 재사용하므로 메모리 효율이 높다.

    • 일관성 유지 : 상수 목록 고정되어 있으므로 수정으로 인한 에러 발생 X

Sealed class

특징

  • sealed class는 enum class처럼 특정 상태 집합을 표현하지만, 단일 타입 값만 가질 수 있는 enum과 달리 여러 속성과 다양한 형태의 하위 클래스를 가질 수 있다.

    • class, data class, objcet, data object 등을 서브 클래스로 가질 수 있다.
  • sealed 클래스는 추상 클래스로, 자신을 상속받는 여러 서브 클래스들을 가질 수 있고, 인스턴스를 직접 생성하지 않고 하위 클래스를 통해 생성한다.

  • sealed 클래스는 자신을 상속받는 하위 클래스가 동일 파일 내에 정의되어야 한다. 이렇게 하면 모든 하위 타입이 컴파일 타임에 고정된다.

    • kotlin 1.5 버전 이후에는 같은 모듈 내 다른 파일에서 선언될 경우에도 사용될 수 있도록 제약이 완화되었다.

  • sealed 키워드를 사용해 선언하며, 이를 통해 하위 클래스 집합을 강제하여 타입 안정성을 확보할 수 있다.

  • sealed 클래스를 사용하면 컴파일러가 모든 가능한 하위 타입을 알고 있기 때문에, when 구문에서 모든 경우에 대한 처리가 되었는지 검사하여 타입 안정성을 보장할 수 있다.

  • when 구문에서  else 구문이 필요하지 않다.

    • 각 상태를 빠짐없이 처리하도록 컴파일러가 체크해 안정성을 높일 수 있다.
class SignUpContract {

    // Events that user performed
    sealed class Event : UiEvent {
        data class OnUsernameChanged(val username: String) : Event()
        data class OnPasswordChanged(val password: String) : Event()
        data class OnHobbyChanged(val hobby: String) : Event()
        data object OnPasswordVisibilityToggle : Event()
        data object OnSignUpButtonClicked : Event()
    }

    // Possible states for SignUp
    enum class SignUpStatus {
        IDLE, SUCCESS, FAILURE
    }

    data class SignUpState (
        val username: String = "",
        val password: String = "",
        val hobby: String = "",
        var isPassWordVisibility: Boolean = false,
        val status: SignUpStatus = SignUpStatus.IDLE
    ) : UiState

    // Side effects
    sealed class Effect : UiEffect {
        data class ShowToast(val message: String) : Effect()
    }

}

// 일부 코드 생략 (간략히 표현)
override fun handleEvent(event: UiEvent) {
	when (event) {
	    is SignUpContract.Event.OnUsernameChanged -> {
	        setState(currentState.copy(username = event.username))
	    }
	
	    is SignUpContract.Event.OnPasswordChanged -> {
	        setState(currentState.copy(password = event.password))
	    }
	
	    is SignUpContract.Event.OnHobbyChanged -> {
	        setState(currentState.copy(hobby = event.hobby))
	    }
	
	    is SignUpContract.Event.OnPasswordVisibilityToggle -> {
	        setState(currentState.copy(isPassWordVisibility = !currentState.isPassWordVisibility))
	    }
	
	    is SignUpContract.Event.OnSignUpButtonClicked -> {
	        if (currentState.username.isValidLength() && currentState.password.isValidLength() &&
	            currentState.hobby.isValidLength()
	        ) {
	        ...
	}
  • 이 클래스의 모든 하위 타입이 같은 파일에 정의되므로 정의된 하위 클래스의 집합이 고정된다.

  • 모든 이벤트를 처리하지 않으면 컴파일 오류가 발생하기 때문에, 실수로 특정 이벤트를 놓치는 상황을 방지할 수 있다.

  • 가독성 향상, 유지보수성 향상, 확장용이, else 문 처리할 필요 없어서 코드가 더 명확해진다.

Sealed interface

  • sealed class를 사용하면, 다중 상속이 불가능하다는 한계가 존재한다.

    • 일반 클래스와 마찬가지로 sealed class도 단일 상속만 가능하다.
    • 다중 상속으로 sealed class 보다 더 유연한 계층 구조를 구성할 수 있다.
  • 컴파일러에 자신을 구현하는 타입을 알려주는 인터페이스이다. 따라서 일반적인 interface와 같이 하나의 클래스에서 여러 sealed interface를 구현할 수 있다.


  • sealed interface 활용
//  다수의 sealed interface를 implementation 하는 서브 클래스
sealed interface GeneralError {
    object NetworkIssue : GeneralError
    object Timeout : GeneralError
    object UnknownError : GeneralError
}

// 파일 작업 관련 에러 정의
sealed interface FileError {
    data class FileNotFound(val filePath: String) : FileError
    data class FileCorrupted(val filePath: String) : FileError
    data class General(val error: GeneralError) : FileError // 공통 에러 포함
}

// 네트워크 작업 관련 에러 정의
sealed interface NetworkError {
    data class ConnectionLost(val url: String) : NetworkError
    data class InvalidResponse(val url: String) : NetworkError
    data class General(val error: GeneralError) : NetworkError // 공통 에러 포함
}

// 에러 처리 함수
fun handleFileError(error: FileError): String = when (error) {
    is FileError.FileNotFound -> "File not found at path: ${error.filePath}"
    is FileError.FileCorrupted -> "File is corrupted at path: ${error.filePath}"
    is FileError.General -> handleGeneralError(error.error)
}

fun handleNetworkError(error: NetworkError): String = when (error) {
    is NetworkError.ConnectionLost -> "Connection lost while accessing: ${error.url}"
    is NetworkError.InvalidResponse -> "Received invalid response from: ${error.url}"
    is NetworkError.General -> handleGeneralError(error.error)
}

// 공통 에러 처리 함수
fun handleGeneralError(error: GeneralError): String = when (error) {
    GeneralError.NetworkIssue -> "There is a network issue."
    GeneralError.Timeout -> "The operation timed out."
    GeneralError.UnknownError -> "An unknown error occurred."
}

// 사용 예시
fun main() {
    val fileError = FileError.General(GeneralError.Timeout)
    println(handleFileError(fileError))  // Output: The operation timed out.
    
    val networkError = NetworkError.ConnectionLost("https://example.com")
    println(handleNetworkError(networkError))  // Output: Connection lost while accessing: https://example.com
}
  • handleError 함수에서LoginErrors 중 CommonError가 발생하면 중첩된 분기 처리를 통해 공통 에러를 처리한다.
  • 공통 에러를 CommonErrors 인터페이스로 통합해서, 파일 작업과 네트워크 작업 관련 에러가 동일한 공통 에러 정의를 공유한다.
  • 코드를 더 간결하게 사용할 수 있다. (공통 에러 처리를 일관되게 관리할 수 있다.)

object class

특징

  • 싱글톤 패턴을 더 쉽게 사용하기 위해 코틀린에서 사용하는 객체 선언 키워드

    • 생성자를 사용할 수 없다.
    • 객체 생성과 동시에 자동으로 생성자 호출 없이 생성된다.
  • Singleton

  • thread-safe

  • lazy initialized (외부에서 객체가 사용되는 시점에 초기화 된다.)

  • 코틀린의 object와 companion object 내부 멤버들은 자바의 static처럼 접근되지 않는다.

    • 자바의 static 과 유사하게 사용하기 위해 @JvmStatic과 @JvmField 어노테이션 사용

    • const val

      • 자바의 static 변수로 간주

      • 고정된 상수 값을 컴파일 타임에 제공하기 때문에 자바의 static final 상수처럼 동작한다.

    • @JvmStatic과 @JvmField

      • @JvmStatic :  내부의 변수나 함수를 자바에서 static처럼 접근할 수 있도록 하기 위해 사용한다.

      • @JvmField : 필드를 자바 static 필드처럼 사용하기 위해 사용한다.

object Config {
    var apiUrl = "api.example.com"  //  직접적인 static 변수처럼 접근할 수 없다. (사용 불가)
    const val TIMEOUT = 5000  // 자바의 static final 상수처럼 동작
    @JvmStatic var versionCode = "1.0.0"
    @JvmField val connectionType = "WiFi" 
    	  
    @JvmStatic
    fun logMessage(message: String) {
        println("Log: $message")
    }
}

fun main() {
    println(KotlinClass.INTEGER_ONE)
    println(KotlinClass.BIG_INTEGER_ONE)
    KotlinClass.printVersion() // Output: Version: 1.0.0
}

디컴파일

public final class Config {
   
   @NotNull
   public static final Config INSTANCE = new Config();
   
   private Config() { }
   
   // ----------------------------------------------------
   
   // @JvmStatic var
   @NotNull
   private static String versionCode = "1.0.0";
   
   @NotNull
   public static final String getVersionCode() {
      return versionCode;
   }
   
   public static final void setVersionCode(@NotNull String var0) {
      Intrinsics.checkNotNullParameter(var0, "<set-?>");
      versionCode = var0;
   }
   // ----------------------------------------------------
	 
	 // @JvmField
   @JvmField
   @NotNull
   public static final String connectionType = "WiFi";
   
   // ---------------------------------------------------
   // var
   private static final String apiUrl = "https://api.example.com";

   @NotNull
   public final String getApiUrl() {
      return apiUrl;
   }
   
   public final void setApiUrl(@NotNull String var1) {
      Intrinsics.checkNotNullParameter(var1, "<set-?>");
      apiUrl = var1;
   }
   
   @JvmStatic
   public static void getVersionCode$annotations() {
   }
   
   // ----------------------------------------------------   
   // const val
   @NotNull
   public static final int TIMEOUT = 5000;

   // ----------------------------------------------------   
   @JvmStatic
   public static final void logMessage(@NotNull String message) {
      Intrinsics.checkNotNullParameter(message, "message");
      String var1 = "Log: " + message;
      System.out.println(var1);
   }
  • public static final Config INSTANCE = new Config();

    • new Config()를 통해 Config 클래스의 인스턴스를 생성하여 할당된다.

    • static final로 선언되었기 때문에, Config 클래스가 처음 로드될 때 한 번만 생성된다.

    • 이를 통해 싱글톤 패턴을 구현해 모든 접근에서 동일한 객체를 사용할 수 있다.

  • private Config() { }

    • 외부에서 새로운 인스턴스를 생성하는 것을 방지

  • val, var
    • getter/setter가 static이 아니다.
    • 인스턴스로 접근해야 한다.
      • staitc이 아니기 때문에  각 인스턴스마다 값을 가져 메모리를 차지한다.
  • const val
    • 컴파일 타임 상수로, static final 상수로 변환된다.
    • getter/setter 없이 Config.FIELD 형식으로 바로 접근 가능하다.
  • @JvmStatic
    • 프로퍼티가 static final로 변환된다.
    • getter/setter가 static final 메서드로 변환된다.
  • @JvmField
    • getter/setter 없이 Config.FIELD 형식으로 바로 접근 가능하다.

Companion object

  • 클래스 내부 객체 선언을 위한 object 키워드 (클래스 내부에서 싱글톤 패턴 사용하기 위해 사용한다.)

    • 클래스와 연관된 정적 멤버를 정의할 수 있다.
    • 해당 클래스의 동반 객체로 동작하여 싱글톤 인스턴스를 제공한다.
  • 해당 클래스가 로드 될 때 초기화된다.

    • 해당 클래스를 생성하면 companion object 내부 init 블럭이 호출된다.
  • companion object 내부 멤버들은 자바의 static처럼 접근되지 않는다.

    • @JvmStatic : companion object 내부의 변수나 함수를 자바에서 static처럼 접근할 수 있도록 하기 위해 사용한다.
  • 안드로이드 공식 문서에 나와있는 식

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
        @JvmStatic fun printVersion() {
            println(versionName)
        }
    }
}

fun main() {
    println(Config.TIMEOUT)
    println(Config.versionCode)
    println(Config.connectionType)
    Config.logMessage("Hello, Config!")
    
     // 일반 val 변수는 직접 접근 불가 (실제 사용할 경우 INSTANCE가 필요)
    println(Config.apiUrl)
}

디컴파일

public final class KotlinClass {
   
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
   
   // const val
   public static final int INTEGER_ONE = 1;
   
   // @JvmField
   @JvmField
   public static final BigInteger BIG_INTEGER_ONE;
	 static {
      BIG_INTEGER_ONE = BigInteger.ONE;
   }

	 // @JvmStatic
   @JvmStatic
   public static final void printVersion() {
      Companion.printVersion();
   }
   
 // 
 public static final class Companion {
      private Companion() { }

      @JvmStatic
      public final void printVersion() {
         String var1 = "Version: 1.0.0";
         System.out.println(var1);
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}
  • public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

    • KotlinClass 클래스의 정적 필드로, Companion이라는 이름을 갖는 Companion 타입의 인스턴스를 참조

    • companion object로 선언된 멤버들을 Java에서 정적 멤버처럼 접근할 수 있게 하려는 목적으로 생성된 필드이다.

    • 이 필드를 통해 Companion 클래스의 멤버에 접근할 수 있다

  • new Companion((DefaultConstructorMarker)null);

    • Companion 클래스의 인스턴스를 생성한다.

    • Companion의 private 생성자를 통해 생성되며, 외부에서 직접 생성하지 못하도록 제한한다.

    • Companion 필드는 public static final로 선언되어 KotlinClass가 처음 로드될 때 한 번만 생성된다.


  • public static final class Companion { ... }

    • companion object는 Java에서 Companion이라는 중첩 클래스로 변환되며, 이 클래스 안에 Kotlin의 companion object 멤버들이 포함된다.

    • 생성자는 private로 선언되어 있으며, 외부에서 인스턴스 생성을 하지 못하게 막는다.

data object

  • kotlin 1.9.0에 출시된 데이터 홀더 객체이다.

  • sealed class, sealed interface 에서 유용하게 사용할 수 있는 타입이다.

  • 데이터와 관련된 정보를 하나의 싱글톤 객체로 관리할 때 유용하다.

    • Singleton 이기 때문에 내부 상태가 동적으로 변하는 객체보다는 고정된 값이나 상태를 표현하는데 적합하다.
  • object + data class

    • Singleton, Immutable , data class 처럼 equalshashCodetoString 메서드가 자동으로 제공된다.

0개의 댓글