코틀린에서 상수 정의 스타일 3가지

Brunch Kim·2025년 1월 6일

kotlin

목록 보기
1/1

코틀린에서는 프로젝트 전역, 모듈 전역에서 문자열 값을 공유하기 위해 상수 정의를 해서 사용하곤 하는데요.
원활한 코드 리뷰, 운영 등을 위해 이 상수 정의를 어떻게 하면 깔끔하게 정의할지 고민들을 많이합니다.
코틀린에서는 어떠한 방식으로 상수 정의를 하는지 대표적인 케이스에 대해서 알아보겠습니다.


1. object Constants

object 선언은 싱글턴 패턴을 사용하여 상수를 관리하는 방식입니다. 상수들을 하나의 객체로 묶어두고, 프로젝트 전역에서 참조할 수 있습니다.

object ColorConstants {
    const val YELLOW = "#E0AA1D"
    const val RED = "#FF4C4C"
    const val WHITE = "#FFFFFF"
    const val BLACK = "#000000"
}

장점

  • 싱글턴 패턴을 활용하여 메모리 상에서 하나의 인스턴스로 상수를 관리합니다.
  • 클래스처럼 객체로 묶어 사용할 수 있어 네임스페이스를 효과적으로 관리할 수 있습니다.
  • 프로젝트 전체에서 쉽게 접근 가능하며, ColorConstants.YELLOW와 같이 호출할 수 있습니다.

단점

  • object 선언으로 인해 테스트할 때 모킹(mocking)이 어려울 수 있습니다.

2. Class + companion object

클래스 안에서 companion object를 사용하여 상수를 정의하는 방법입니다. companion object는 클래스의 인스턴스를 생성하지 않고도 상수에 접근할 수 있게 해줍니다.

class ColorConstants {
    companion object {
        const val YELLOW = "#E0AA1D"
        const val RED = "#FF4C4C"
        const val WHITE = "#FFFFFF"
        const val BLACK = "#000000"
    }
}

장점

  • class와 companion object를 사용하여 관련 상수들을 그룹화할 수 있습니다.
  • companion object는 자바의 static 변수와 유사하여, 클래스와 관련된 상수로서 그룹화됩니다.
  • 클래스 내부에서 관리할 수 있어, 코드의 구조적 명확성이 유지됩니다.

단점

  • object와 달리 ColorConstants 클래스 이름을 명시해야 하므로, 코드가 조금 더 길어질 수 있습니다. (물론 import 문에서 클래스 이름을 명시하여 처리하면 실제 참조하는 코드에서는 클래스 이름 생략이 가능합니다.)
  • 객체가 아닌 클래스 형태이므로, 객체지향적인 설계에서 상수를 클래스의 속성으로 묶고자 할 때 유용합니다.

3. Top-Level const val

Top-Level 상수는 파일의 최상위에 상수를 정의하는 방식입니다. 이는 클래스나 객체 없이도 직접 파일 내에서 상수를 정의할 수 있습니다

// ColorConstants.kt 파일 내에서 정의된 상수들 (top-level 에 아래와 같은 코드가 위치합니다.)

const val YELLOW = "#E0AA1D"
const val RED = "#FF4C4C"
const val WHITE = "#FFFFFF"
const val BLACK = "#000000"

장점

  • 코드가 간단하고 직관적입니다.
  • 불필요한 클래스나 객체를 만들지 않아 코드가 깔끔해집니다.
  • 간단한 프로젝트나 상수를 다른 클래스와 구분하지 않고 사용할 때 유용합니다.

단점

  • 전역으로 선언되어 이름 충돌이 발생할 수 있습니다.
  • 코드가 커지고, 여러 파일에서 사용해야 할 경우 관리가 어려울 수 있습니다.

비교(with 메모리 laod 관점)

방식메모리 관리 방식설명
object Constants싱글턴 패턴: 객체 하나만 메모리에 로드되어 상수들이 관리됨.object 선언은 메모리에 단 하나의 객체만 존재하고, 그 객체에 속한 상수는 중복 없이 관리됩니다.
Class + companion object클래스 로딩 시 companion object가 메모리에 로드됨.클래스와 함께 로딩되는 단 하나의 companion object에 상수가 포함되며, 해당 객체는 중복 없이 관리됩니다.
Top-Level const val컴파일 타임 리터럴 치환: 값이 메모리에 한 번만 로드됨.상수는 리터럴 값으로 컴파일되어 메모리 상에 중복 없이 존재하며, 객체나 클래스 없이 사용됩니다.

결론

object ConstantsClass + companion object는 모두 싱글턴 패턴처럼 메모리에서 객체를 한 번만 로드하는 방식으로 관리됩니다. 두 방식 모두 단 하나의 객체나 컴패니언 객체만 메모리에 존재하므로, 메모리 상에서 중복 생성이 없다는 점에서 유사합니다.

Top-Level const val은 컴파일 타임에 값이 리터럴로 치환되어 메모리에 상수 값이 저장됩니다. 이 방식은 객체나 클래스의 존재 없이 상수 값을 메모리에 한 번만 로드하므로, 메모리 측면에서는 리터럴 값이 중복되지 않게 처리됩니다.

결국, object Constants, Class + companion object, Top-Level const val 은 모두 메모리 관리 측면에서 중복 없이 한 번만 로드되지만, 각각 메모리 관리의 세부 구현 방식은 약간 다르다는 점을 참고하면 좋을 것 같고, 무엇보다도 Class + companion object 와 같은 방식은 단순 상수 처리를 넘어서 클래스 자체가 갖는 비즈니스 로직과 상태 값들을 처리할 때 사용되는 것이 바람직하다고 볼 수 있습니다. (이 부분은 아래에서 예시를 추가로 들어보겠습니다)


별첨) 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에 포함하는 것이 자연스럽습니다.


참고

profile
브런치 즐기는 여유있는 날

0개의 댓글