Kotlin에서의 상수 선언 (feat. const와 static final의 차이)

 

 

문제 상황

자바 코드를 코틀린으로 마이그레이션 하는 과정 중, 상수 선언 과정에서 문제 상황을 겪게 되었다.

 

 

public class SampleClass {
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
    //...
}

자바에서 객체를 static final을 통해 상수로 선언하는 것은 일반적으로 가능한 일이다.

 

private const val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")

class SampleClass(

하지만 상수 선언 로직을 코틀린으로 마이그레이션하는 과정에서 컴파일 에러를 마주할 수 있었다.

 

 

const 키워드에 위와 같이 primitives and String 타입만 허용된다는 에러 메시지가 나타난 것이다.

 

 

 

const 키워드

문제 해결 전, const가 무엇인지부터 먼저 알아보자.

const는 자바의 static final 과 동일한 상수 선언 역할을 수행한다.

 

 

만약 const로 선언해둔 상수에 변경을 시도하면 위와 같은 컴파일 에러가 발생할 것이다.

 

 

 

알고가면 좋은 점1) Kotlin final과 Java final의 차이

 

자바에서는 static final이 상수 선언이었는데 그럼 코틀린에서의 final은 뭘까?

 

코틀린 final의 역할은 불변이 아닌 상속을 불가능하게 만드는 역할을 수행한다.

메서드나 클래스 옆에 final 키워드를 붙이게되면 상속이 불가능해지며, 코틀린은 기본적으로 final이다.

 

때문에 JPA를 사용할 때 Entity 객체는 직접 open 키워드를 붙여서 상속을 열어주거나,
Plugin을 사용해서 JPA 관련 객체 (MappedSuperclass, Entity 등) 에 open을 자동으로 붙여주는 등의 추가적 설정이 필요하다.

 

 

알고가면 좋을 점2) val과 const의 차이

val과 const의 차이도 간혹 헷갈려하는 사람이 있는 것 같아 적는다.

 

 

[할당되는 메모리 공간]

 

우선 할당되는 JVM의 메모리 공간부터 다르다.

 

const는 글로벌한 상수로 JVM 런타임 데이터 영역(Runtime Data Area) 중 Method 영역에 할당된다.

하지만 val로 선언된 프로퍼티는 객체의 인스턴스가 생성될 때마다 매번 같은 값이 Heap 영역에 새로 할당된다는 차이점이 존재한다.

 

 

[상수와 변수]

 

const상수로 단 하나만 선언되어있고 글로벌한 값이기 때문에 여러 클래스에서 공유해서 사용된다.

반면 val는 setter를 막아 불변 설정만 되어있는 read-only 변수이다. 주생성자를 통해 주입받아 클래스마다 다른 값을 가지기도 한다.

 

 

 

원인

const가 뭔지는 알았는데, 그럼 const를 사용하면 왜 Primitive Type과 String만 선언이 가능할까?

 

그 이유는 const 키워드는 컴파일 시점에 값을 할당하기 때문이다.

컴파일 시점에는 객체의 인스턴스가 생성되지 않았기 때문에 참조 타입 등의 인스턴스를 상수로 정의할 수 없는 것이다.

 

자바의 static final런타임 시점을 지원했지만, 코틀린의 const는 그렇지 않다.

때문에 String, Primitive Type만 const를 통한 상수 선언이 가능한 것이다.

 

 

 

해결 방안

private const val CONST_STRING = "const"

class SampleClass(
) {
    companion object {
        private val COMPANION_STRING = "companion"
    }
}

코틀린에서 상수를 선언하는 방법은 2가지가 있다.

 

1. const 키워드를 통해 클래스 최상위에 상수를 선언하는 방법

2. companion object를 통해 내부 클래스를 만들어 선언하는 방법

 

객체를 상수로 선언하기 위해서는 const가 아닌 companion object를 사용하는 방법을 선택해야한다.

한 번 자세히 알아보자.

 

 

 

동반 객체 (companion object) 활용하기

class SampleClass(
) {
    companion object {
        private val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
    }
}

companion object를 활용하면 이처럼 DateTimeFormatter 와 같은 객체도 상수로 선언하는 것이 가능하다.

 

 

 

Kotlin to Java 디컴파일(decompile)을 통해 결과 확인해보기

IntelliJ의 유용한 기능인 Kotlin to Java 디컴파일(decompile)을 통해서 정말 static final로 선언이 되는지 확인해보았다.

IntelliJ Kotlin to Java 디컴파일(decompile) 사용법 알아보기

 

 

결과는 위와 같이 DateTimeFormatter가 정상적으로 상수 선언되었다.

 

 


 

결론

앞으로 객체를 상수로 등록해야하는 지금과 같은 상황이 있다면 companion object 를 활용해보려고 한다.

 

다만 상수 객체가 존재하는 경우에는 companion object에 상수가 정의되고, 그렇지 않은 경우에는 const로 정의되는 등 지저분한 코드가 발생되는 것을 우려하여 개인적으로 다음과 같은 규칙을 정의했다.

 

 

코틀린 상수 선언 규칙

1. 상수 객체는 companion object에 정의

2. Primitive Type & String은 클래스 최상위에 const로 정의

 

private const val CONST_STRING = "const"

class SampleClass(
) {
    companion object {
        private val DATE_TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss")
    }
}

그럼 결론적으로 위와 같은 코드가 만들어질 것이다.

만약 더 좋은 방안이 있다면 댓글로 남겨주면 참고해보겠습니다!