글 작성자: beaniejoy

실무에서 자바 to 코틀린 변환 작업을 하면서 자바 enum class를 코틀린으로 변환하는 과정에서 있었던 이슈 하나를 기록해보고자 합니다.

 

📌 1. Java Enum static method

public enum HelloJavaStrategy {
    KOREAN(convertLang(Locale.KOREAN)),
    ENGLISH(convertLang(Locale.ENGLISH)),
    JAPANESE(convertLang(Locale.JAPANESE));

    private final String hello;

    HelloJavaStrategy(String hello) {
        this.hello = hello;
    }

    public String getHello() {
        return hello;
    }

    private static String convertLang(Locale locale) {
        if (Locale.KOREAN.equals(locale)) {
            return "안녕하세요.";
        } else if (Locale.JAPANESE.equals(locale)) {
            return "おはよう。";
        } else if (Locale.ENGLISH.equals(locale)) {
            return "hello";
        } else {
            return "no lang";
        }
    }
}

실무에서 실제 사용되는 코드를 가져올 수는 없어서 조잡한 예시로 정리해보고자 합니다.
말도 안되는 enum class이긴 하지만 enum instance 초기화 과정에서 hello 필드에 각각의 나라에 해당하는 인사말을 주입하기 위해 enum 내 static method를 사용하는 예시입니다.

Java에서는 위와 같이 enum class 내의 static method를 가지고 enum instance 초기화할 때 생성자에서 사용할 수 있습니다.

 

📌 2. Kotlin Enum companion function

코틀린에서는 자바와 다르게 동작합니다. 위의 자바 클래스를 코틀린 코드로 바꾼 내용은 다음과 같습니다.

enum class HelloStrategy(
    val hello: String
) {
    KOREAN(convertLang(Locale.KOREAN)),
    ENGLISH(convertLang(Locale.ENGLISH)),
    JAPANESE(convertLang(Locale.JAPANESE))
    ;
    
    companion object {
        fun convertLang(locale: Locale): String {
            return when (locale) {
                Locale.KOREAN -> "안녕하세요"
                Locale.JAPANESE -> "おはよう。"
                Locale.ENGLISH -> "hello"
                else -> "no lang"
            }
        }
    }
}

static method를 companion object function으로 만들어 변환해보았습니다. 하지만 위와 같이 코드를 작성하면 컴파일 단계에서 다음과 같은 에러가 발생합니다.

Companion object of enum class 'HelloStrategy' is uninitialized here

직역하면 HelloStrategy 안에 있는 companion object가 enum instance 초기화하는 생성자안에서 uninitialized(초기화 X)된 상황이라는 뜻이 됩니다.

자바의 static과 코틀린의 companion object 차이에서 이러한 에러가 발생하는데요. 자바 static은 애플리케이션 실행시 객체 생성보다 먼저 메모리에 올라가게 됩니다. 즉 자바에서는 enum instance 생성 단계에서 static method를 당연하게 사용할 수 있게 됩니다.

코틀린의 companion은 자바의 static과 다른 개념인데요. companion object 자체가 instance화 되는 시점은 속해있는 class가 instance화 되는 시점에 같이 이루어집니다. 즉 코틀린 enum instance 생성자 시점은 enum class가 instance화 되기 전이기 때문에 companion object가 아직 initialize 되지 않은 상태라 enum instance 생성자 안에 companion object function을 사용할 수 없게 됩니다.

 

📌 3. 해결 방법

enum class HelloStrategy(
    val hello: String
) {
    KOREAN(HelloStrategy.convertLang(Locale.KOREAN)),
    ENGLISH(HelloStrategy.convertLang(Locale.ENGLISH)),
    JAPANESE(HelloStrategy.convertLang(Locale.JAPANESE))
    ;
    
    //...
}

구글링하다 보면 위와 같은 방식으로 companion object를 품고있는 클래스를 직접 명시하면 된다고 언급하는 곳이 있었습니다. 하지만 위와 같은 방식으로 해도 다음과 같은 warning이 나오는데요.

Companion object of enum class 'HelloStrategy' is uninitialized here. This warning will become an error in future releases

같은 문구가 나오면서 현재 에러가 발생하지 않지만 추후에 어떠한 에러가 발생할지 모른다고 IDE가 경고하고 있습니다. 실제로 해당 enum 값을 가지고 테스트해보면 KOREAN(...) 부분에 NullPointerException이 발생하게 됩니다.

enum instance를 먼저 생성하려고 하는데 companion object가 생성되기 전에 companion object 맴버 함수에 접근하려고 하니 NullPointerException이 발생하는 것은 당연한 일입니다.
(해당 내용은 Jetbrains에서도 언급하고 있습니다. https://youtrack.jetbrains.com/issue/KT-49110)

 

Prohibit access to members of companion of enum class from initializers of entries of this enum : KT-49110

Classification Type of change: * New errors are introduced Motivation types: * User code fails with exception(s) * The implementation does not abide by a published spec or documentation * Type safety guarantees are not met (including fail-fast behavior for

youtrack.jetbrains.com

 

enum class HelloStrategy(
    val hello: String
) {
    KOREAN(LangConverter.convertLang(Locale.KOREAN)),
    ENGLISH(LangConverter.convertLang(Locale.ENGLISH)),
    JAPANESE(LangConverter.convertLang(Locale.JAPANESE))
    ;
}

class LangConverter{
    companion object {
        fun convertLang(locale: Locale): String {
            return when (locale) {
                Locale.KOREAN -> "안녕하세요"
                Locale.JAPANESE -> "おはよう。"
                Locale.ENGLISH -> "hello"
                else -> "no lang"
            }
        }
    }
}

저는 그냥 companion object 부분을 별도의 클래스를 생성해서 관리하게 하고 HelloStrategy는 해당 클래스에서 companion object function을 가져와 사용하게끔 작성을 했습니다. (다른 해결방법이 있을 수도 있습니다.)

import example.LangConverter.Companion.convertLang
import java.util.Locale

enum class HelloStrategy(
    val hello: String
) {
    KOREAN(convertLang(Locale.KOREAN)),
    ENGLISH(convertLang(Locale.ENGLISH)),
    JAPANESE(convertLang(Locale.JAPANESE))
    ;
}

class LangConverter{
    companion object {
        fun convertLang(locale: Locale): String {
            return when (locale) {
                Locale.KOREAN -> "안녕하세요"
                Locale.JAPANESE -> "おはよう。"
                Locale.ENGLISH -> "hello"
                else -> "no lang"
            }
        }
    }
}

LangConverter를 중복으로 명시해야되는 부분이 있기에 import를 통해 간결하게 사용할 수도 있습니다.
(import를 보시면 LangConverter의 Companion을 통해 convertLang function을 가져오고 있습니다.)

 

📌 4. 정리

  • Java static은 객체 생성과 무관하게 애플리케이션 실행시 메모리에 먼저 올라가는 영역이기 때문에 enum instance하는 과정에서 해당 enum class 안에 있는 static method를 사용할 수 있습니다.
  • Kotlin companion object는 java static과 다르게 속해있는 클래스가 instance화 되는 시점에 companion object가 생성이 됩니다. 그래서 enum instance 생성자 시점에서 compaion object는 생성이 되기 전 상태이기 때문에 생성자안에 companion object function을 사용할 수 없게 됩니다.