본문 바로가기

안드로이드

안드로이드로이드에서 Constants를 효율적으로 사용하려 어떻게 해야할까?

반응형

1. 서론

Android/Kotlin 프로젝트를 진행하다 보면, 상수를 선언할 때 크게 두 가지 방식 중 하나를 사용합니다.

  • 최상위 레벨(top-level) 상수
  • companion object 내부에 상수

두 방식이 실제로 어떤 차이가 있는지, 성능이나 APK 크기, 메모리 사용량에 차이가 있는지 궁금해질 때가 있습니다. 이번 글에서는 컴파일 과정에서 생성되는 바이트코드와 R8 최적화 과정을 살펴보며, 어떤 방식을 쓰는 것이 효과적인지 알아보겠습니다.

2. Kotlin에서 상수 선언하기

Kotlin에서는 상수를 만들기 위해 const val 키워드를 사용합니다. 대표적인 선언 패턴은 다음과 같습니다.

  1. 최상위 레벨 상수
// MyConstants.kt 
const val MAX\_RETRY\_COUNT = 3
  1. 클래스 내부 companion object
    class NetworkManager {
    companion object {
        const val DEFAULT_TIMEOUT = 5000
    }
}

 

주의: val 자체로도 불변(fixed reference)이지만, const val은 컴파일 시점에 상수로 간주되어 인라인될 수 있어 더 효율적으로 처리됩니다.

3. Kotlin 바이트코드 & D8/R8 개념 간단 정리

  • Kotlin 바이트코드:
    Kotlin 코드는 우선 JVM 바이트코드로 컴파일됩니다. Android 스튜디오에서는 Show Kotlin Bytecode 기능을 통해 이 중간 단계를 확인할 수 있습니다.
  • D8:
    JVM 바이트코드를 Dalvik(또는 ART)용 바이트코드로 변환하는 도구입니다. (이전에는 dx를 사용했으나, 현재는 D8이 주로 사용됩니다.)
  • R8:
    불필요한 코드를 제거하고 최적화하는 도구(코드 축소, tree shaking). ProGuard의 후속 도구로 보며, 보통 minifyEnabled 옵션을 통해 활성화합니다.
buildTypes {
    release {
        minifyEnabled true
        // ...
    }
}

4. 코드 예시 & 디컴파일 결과 살펴보기

(1) 예시 코드

// 최상위 레벨 상수
const val TOP_LEVEL_CONST = "MY_TOP_LEVEL_CONST"

class SampleActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        println(TOP_LEVEL_CONST)
        println(COMPANION_CONST)
    }

    companion object {
        const val COMPANION_CONST = "MY_COMPANION_CONST"
    }
}

(2) 바이트코드 디컴파일 결과 (자바 형태)

Android Studio에서 Show Kotlin Bytecode -> Decompile 버튼을 통해 자바 코드를 확인해보면, 핵심 부분이 다음과 같이 표현됩니다.

// SampleActivity.java (디컴파일된 부분)
public final class SampleActivity extends ComponentActivity {
   public static final String COMPANION_CONST = "MY_COMPANION_CONST";

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      System.out.println("MY_TOP_LEVEL_CONST");
      System.out.println("MY_COMPANION_CONST");
   }
   ...
}

// SampleActivityKt.java
public final class SampleActivityKt {
   public static final String TOP_LEVEL_CONST = "MY_TOP_LEVEL_CONST";
}
  • 상수들이 직접 문자열로 인라인됩니다.
  • 최상위 레벨 상수를 위한 SampleActivityKt 클래스가 생성되지만, 실제로는 코드 상에서 참조될 일이 거의 없습니다(직접 상수만 사용).

5. companion object 내 상수 선언 시 문제점

companion object로 상수를 정의하면 다음과 같은 결과가 나타납니다.

  1. 정적 내부 클래스(Companion) 생성
    Kotlin 컴파일러는 companion object를 별도의 정적 내부 클래스로 만들어냅니다.
  2. 생성된 내부 클래스 객체 할당
    public static final Companion Companion = new Companion(...) 식으로, 주(outer) 클래스가 이 companion 객체를 보관하게 됩니다.
  3. 메모리 오버헤드
    불필요한 객체가 하나 생기는 셈입니다. 인라인된 상수와 별개로, 자바 바이트코드 수준에서 추가된 클래스와 객체가 생길 수 있습니다.

R8이 최적화 과정에서 제거해주면 좋겠지만, companion object가 사용되는 상황(즉, 해당 상수가 실제로 쓰이거나 해당 내부 클래스가 참조되는 로직)이 조금이라도 있으면, 클래스 자체가 남아 있는 경우가 많습니다.

6. 최상위 레벨(top-level) 상수 활용 시 장점

최상위 레벨 상수를 사용하면 다음과 같은 이점이 있습니다.

  1. 불필요한 클래스 제거 가능
    R8 최적화에서 실제로 상수만 인라인하고, 최상위 레벨 상수를 담던 클래스 자체(*.kt를 디컴파일하면 생기는 XxxKt 클래스)는 더 이상 참조가 없으면 제거될 수 있습니다.
  2. 간단한 코드 구조
    동반 객체(companion)을 추가로 만들지 않으므로, 코드 가독성도 개선됩니다.

7. 여러 클래스에서 상수 공유하기

상수를 여러 곳에서 사용해야 할 경우, 최상위 레벨 상수를 모아 놓은 별도의 파일을 만드는 것이 좋습니다.

// Constants.kt
package com.example.shared

const val API_BASE_URL = "https://example.com/api"
const val DEFAULT_PAGE_SIZE = 20

 

그 후 필요한 곳에서 import하여 사용하면, R8 관점에서는 전역 상수로서 인라인되고, Constants.kt를 디컴파일한 클래스 자체가 최종적으로 쓰이지 않는다면 제거됩니다.

8. 상수 그룹화: object 활용

여러 상수를 계층적으로 묶어 관리하고 싶을 때는 object를 사용할 수 있습니다.

object AppInfo {
    const val APP_NAME = "MyApp"

    object Versions {
        const val API_LEVEL = 33
        const val MIN_SUPPORTED_VERSION = 21
    }
}

 

이렇게 하면

println(AppInfo.APP_NAME)
println(AppInfo.Versions.API_LEVEL)

 

형태로 접근할 수 있습니다. 다만, 이 또한 R8 관점에서는 사용되는 상수만 인라인하고 실제 object 클래스가 남지 않을 수도 있습니다.
(object 특성상 런타임 초기화 과정이 있을 수 있으나, 상수만 인라인되고 해당 object의 다른 로직이 없으면 클래스가 제거될 가능성이 큽니다.)

9. 결론 및 권장 사항

  1. companion object 내부 상수는 가능하면 피하는 편이 좋습니다. 바이트코드상 내부 클래스와 객체가 불필요하게 생성됩니다.
  2. 최상위 레벨 상수를 사용하는 것이 간단하며, R8 최적화(코드 축소) 시 불필요한 클래스(*.kt -> XxxKt.class)가 제거될 수 있어 유리합니다.
  3. 여러 클래스에서 공유되어야 하는 상수들은 별도의 파일에 최상위 레벨로 선언해두고, 필요할 때 import 하여 사용하는 구조가 좋습니다.
  4. 상수를 여러 그룹으로 묶어서 관리하려면 object로 계층화할 수 있지만, 이 역시 실제로 사용되지 않는다면 R8이 제거합니다.
  5. 프로덕션 빌드에서는 반드시 minifyEnabled = true를 통해 R8 최적화를 적용하세요. APK 크기가 줄고 성능 개선에도 도움이 됩니다.

10. 참고 자료

반응형