코틀린에서는 프로젝트 전역, 모듈 전역에서 문자열 값을 공유하기 위해 상수 정의를 해서 사용하곤 하는데요.
원활한 코드 리뷰, 운영 등을 위해 이 상수 정의를 어떻게 하면 깔끔하게 정의할지 고민들을 많이합니다.
코틀린에서는 어떠한 방식으로 상수 정의를 하는지 대표적인 케이스에 대해서 알아보겠습니다.
object 선언은 싱글턴 패턴을 사용하여 상수를 관리하는 방식입니다. 상수들을 하나의 객체로 묶어두고, 프로젝트 전역에서 참조할 수 있습니다.
object ColorConstants {
const val YELLOW = "#E0AA1D"
const val RED = "#FF4C4C"
const val WHITE = "#FFFFFF"
const val BLACK = "#000000"
}
장점
단점
클래스 안에서 companion object를 사용하여 상수를 정의하는 방법입니다. companion object는 클래스의 인스턴스를 생성하지 않고도 상수에 접근할 수 있게 해줍니다.
class ColorConstants {
companion object {
const val YELLOW = "#E0AA1D"
const val RED = "#FF4C4C"
const val WHITE = "#FFFFFF"
const val BLACK = "#000000"
}
}
장점
단점
Top-Level 상수는 파일의 최상위에 상수를 정의하는 방식입니다. 이는 클래스나 객체 없이도 직접 파일 내에서 상수를 정의할 수 있습니다
// ColorConstants.kt 파일 내에서 정의된 상수들 (top-level 에 아래와 같은 코드가 위치합니다.)
const val YELLOW = "#E0AA1D"
const val RED = "#FF4C4C"
const val WHITE = "#FFFFFF"
const val BLACK = "#000000"
장점
단점
| 방식 | 메모리 관리 방식 | 설명 |
|---|---|---|
| object Constants | 싱글턴 패턴: 객체 하나만 메모리에 로드되어 상수들이 관리됨. | object 선언은 메모리에 단 하나의 객체만 존재하고, 그 객체에 속한 상수는 중복 없이 관리됩니다. |
| Class + companion object | 클래스 로딩 시 companion object가 메모리에 로드됨. | 클래스와 함께 로딩되는 단 하나의 companion object에 상수가 포함되며, 해당 객체는 중복 없이 관리됩니다. |
| Top-Level const val | 컴파일 타임 리터럴 치환: 값이 메모리에 한 번만 로드됨. | 상수는 리터럴 값으로 컴파일되어 메모리 상에 중복 없이 존재하며, 객체나 클래스 없이 사용됩니다. |
object Constants와 Class + companion object는 모두 싱글턴 패턴처럼 메모리에서 객체를 한 번만 로드하는 방식으로 관리됩니다. 두 방식 모두 단 하나의 객체나 컴패니언 객체만 메모리에 존재하므로, 메모리 상에서 중복 생성이 없다는 점에서 유사합니다.
Top-Level const val은 컴파일 타임에 값이 리터럴로 치환되어 메모리에 상수 값이 저장됩니다. 이 방식은 객체나 클래스의 존재 없이 상수 값을 메모리에 한 번만 로드하므로, 메모리 측면에서는 리터럴 값이 중복되지 않게 처리됩니다.
결국, object Constants, Class + companion object, Top-Level const val 은 모두 메모리 관리 측면에서 중복 없이 한 번만 로드되지만, 각각 메모리 관리의 세부 구현 방식은 약간 다르다는 점을 참고하면 좋을 것 같고, 무엇보다도 Class + companion object 와 같은 방식은 단순 상수 처리를 넘어서 클래스 자체가 갖는 비즈니스 로직과 상태 값들을 처리할 때 사용되는 것이 바람직하다고 볼 수 있습니다. (이 부분은 아래에서 예시를 추가로 들어보겠습니다)
Class + companion object는 주로 클래스가 비즈니스 로직이나 상태를 관리하는 방식으로 사용될 때 유리합니다. 따라서 상수 정의를 비교할 때 이 점을 강조하는 것이 더 적합합니다. companion object는 클래스의 인스턴스를 생성하지 않고도 클래스와 관련된 상수나 함수에 접근할 수 있는 장점이 있지만, 본래는 비즈니스 로직과 관련된 멤버 변수나 메서드를 관리하는 목적이 큽니다.
아래 코드 예시를 보겠습니다.
class Car(val brand: String, val model: String) {
fun drive(speed: Int) {
if (isValidSpeed(speed)) {
println("Driving at $speed km/h")
} else {
println("Invalid speed")
}
}
companion object {
const val MAX_SPEED = 200
const val MIN_SPEED = 0
fun isValidSpeed(speed: Int): Boolean {
return speed in MIN_SPEED..MAX_SPEED
}
}
}
이 예시에서 MAX_SPEED, MIN_SPEED와 같은 상수는 companion object 내에 정의되어 있지만, isValidSpeed와 같은 비즈니스 로직은 실제로 클래스를 위한 기능이므로, companion object에 포함하는 것이 자연스럽습니다.