안드로이드 ENUM과 Typedef 어노테이션(Annotation)

이 글은 ENUM 사용 시 주의할 점과 어노테이션을 통해 이를 보완할 수 있는 방법에 대해 정리한 글입니다.

주로 참고한 자료는 유튜브 perfmatter 시리즈 중 The price of ENUMs (100 Days of Google Dev) 영상과 Improve Code Inspection with Annotations 문서입니다.

안드로이드 앱 실행 시 메모리 할당

ENUM 사용 시 주의점을 알기 전에 먼저 안드로이드 앱 실행 시 메모리가 어떻게 할당되는지 간단히 짚고 넘어가겠습니다.

앱을 실행시키면 안드로이드는 시스템 메모리 곁에 앱을 위한 메모리를 할당합니다. 이렇게 할당된 메모리는 앱의 코드와 실행중에 동적으로 할당하는 메모리를 위해 사용됩니다. 여기서 앱의 코드는 안드로이드 앱을 빌드할 때 생성되는 DEX 파일입니다.

아래는 The price of ENUMs 영상에서 캡쳐한 이미지입니다.

Android Memory Allocation

ENUM 사용 시 주의점

ENUM은 앱의 코드(DEX) 크기와 런타임 메모리 사용량을 증가시킵니다.

앱의 DEX 크기가 증가된다는건 APK 파일도 커지고 앱이 실행됐을 때 사용하는 메모리의 양도 그만큼 늘어난다는걸 의미합니다. 또한 ENUM은 Integer나 String에 비해 더 많은 메모리를 런타임에 사용합니다.

ENUM은 얼마나 DEX 크기를 더 증가 시킬까?

The price of ENUMs (100 Days of Google Dev) 영상을 보면 그 차이를 분명히 알 수 있습니다.

먼저 상수를 아래와 같이 Integer로 선언한 경우, DEX의 크기는 124 bytes가 늘어납니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final int VALUE1 = 1;
public static final int VALUE2 = 2;
public static final int VALUE3 = 3;

int func(int value) {
switch(value) {
case VALUE1:
return -1;
case VALUE2:
return -2;
case VALUE3:
return -3;
}
return 0;
}

반면에 이를 ENUM으로 선언하면 DEX의 크기는 1,632 bytes가 증가합니다. 무려 13배나 차이가 납니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static enum Value {
VALUE1,
VALUE2,
VALUE3
}

int func(Value value) {
switch(value) {
case VALUE1:
return -1;
case VALUE2:
return -2;
case VALUE3:
return -3;
}
return 0;
}

여기서 바로 ENUM을 사용할 때 왜 주의해야 하는지를 알 수 있습니다. ENUM 사용으로 인한 DEX의 크기가 커지면 앱과 시스템이 사용하는 메모리는 줄어들게 됩니다.

ENUM을 사용함으로써 증가하는 메모리의 양은 어찌보면 그리 크지 않다 여길 수 있습니다. 하지만 ENUM을 무심코 사용하다보면 그 크기는 결코 무시할 수 없게 됩니다.

해결방법 첫 번째, Proguard 사용

프로가드를 사용하면 빌드 시 enum을 Integer로 바꾸는 최적화를 수행해 줍니다.

해결방법 두 번째, Typedef 어노테이션 사용

위의 예제처럼 ENUM의 대안으로 간단히 Integer를 사용함으로써 위의 이슈를 피해갈 수 있습니다. 하지만 ENUM이 괜히 있는게 아닙니다. ENUM을 사용하면 컴파일 및 런타임에 타입 및 값을 체크할 수 있습니다.

API의 인자나 반환 값에 제약을 줌으로써 API가 오동작을 일으킬 여지를 최대한 줄이는 것은 좋은 코드 작성하는 원칙중에 하나입니다. 하지만 단순히 Integer로 상수를 만들어 사용하는 것 만으로는 이런 제약을 줄 수 없습니다.

하지만 Typedef 어노테이션을 사용하면 컴파일 시에 warning을 통해 이를 미리 확인할 수 있게됩니다.

Typedef 어노테이션이란?

Typedef 어노테이션은 어노테이션을 통해 특정 값의 유효성을 컴파일 시 확인할 수 있는 기능입니다. 자세한 사용법은 Improve Code Inspection with Annotations 이 곳에서 확인 할 수 있습니다.

여기서는 간단한 예제를 가지고 사용법을 확인해보도록 하겠습니다.

먼저 어노테이션을 사용하기 위해 build.gradle 파일에 서포트 라이브러리를 추가해줍니다.

1
dependencies { compile 'com.android.support:support-annotations:24.2.0' } 

그리고 아래와 같이 Typedef 어노테이션을 사용합니다. 아래의 예제는 실제 안드로이드 ActionBar 코드 중 일부입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import android.support.annotation.IntDef;
...
public abstract class ActionBar {
...
// Define the list of accepted constants and declare the NavigationMode annotation
@Retention(RetentionPolicy.SOURCE)
@IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
public @interface NavigationMode {}

// Declare the constants
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;

// Decorate the target methods with the annotation
@NavigationMode
public abstract int getNavigationMode();

// Attach the annotation
public abstract void setNavigationMode(@NavigationMode int mode);

NavigationMode라는 Typedef 어노테이션을 생성합니다. NavigationMode에는 @IntDef를 통해 NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS 세 가지 값만 들어올 수 있음을 명시하고 있고, 각 상수는 아래 public static final int로 선언이 되어 있습니다. **@Retention(RetentionPolicy.SOURCE)**는 컴파일러가 .clss에 @NavigationMode의 데이터를 저장하지 않도록 합니다.

이제 NavigationMode는 함수의 반환값이나 인자, 객체의 필드에 사용될 수 있습니다. 코드에서 보면 getNavigationMode() 함수의 반환값과 setNavigationMode() 함수의 인자에 @NavigationMode 어노테이션이 붙어있습니다.

이를 통해 getNavigationMode()의 반환값이나 setNavigationMode()의 mode 인자에 NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS 외의 값이 들어가면 warning이 발생합니다.

이렇게 생성한 Typedef 어노테이션은 안드로이드 스튜디오의 코드 완성에도 적용되어 간편하게 사용할 수 있습니다. @StringDef를 통해 문자열도 동일하게 사용이 가능합니다.

Typedef 어노테이션 사용 시 주의점

Typedef 어노테이션을 사용한다 해도 빌드는 정상적으로 수행됩니다. 따라서 반드시 IDE 자체에서 실시간으로 보여주는 warning을 확인하거나, 안드로이드 스튜디오의 Analyze -> Code Infection을 통해 warning을 확인 하는 것이 중요합니다.

마무리

이 글은 ENUM 사용 시 주의할 점을 정리한 글이지만 ENUM을 사용하지 말라는 글은 절대 아닙니다.

ENUM이 DEX의 크기를 증가시키는 문제는 있지만 ENUM을 사용하면 코드의 가독성과 타입의 값에 대한 유효성(Type safety)이 좀 더 보장됩니다.

이는 굉장한 장점입니다. 따라서 가독성이나 안정성이 중요시 되는 코드에는 ENUM을 사용하는게 맞습니다.

비슷한 예로 C언어의 매크로 함수가 있습니다. 매크로 함수는 함수처럼 사용할 수 있지만 컴파일 시에 직접 코드로 치환되어 실행파일에 삽입됩니다. 따라서 매크로 함수를 사용하면 함수 호출의 부하를 줄일 수 있습니다.

하지만 매크로 함수도 잘 못 사용하면 오히려 성능을 저하시킵니다. 가령 코드의 너무 많은 곳에서 매크로 함수를 호출하게되면 호출 부에 모두 코드로 치환되기 때문에 코드 영역의 크기가 매우 커질 수 있습니다. 즉 경우에 따라 오히려 성능을 떨어뜨릴 수 있습니다.

ENUM과 Typedef 어노테이션 역시 비슷합니다. 각각의 장단점을 보고 필요한 곳에 잘 사용하는 것이 중요합니다. 또한 Proguard를 사용하면 위의 최적화를 대부분 알아서 수행해 줍니다. 어찌보면 굳이 Typedef 어노테이션을 사용할 필요가 없을지도 모릅니다.

이런 관점에서보면 안드로이드 플랫폼 코드에 Typedef 어노테이션을 권장하는 이유를 알 수 있습니다. 안드로이드 플랫폼 코드는 프로가드를 적용하지 않고, 전체 코드의 크기가 굉장히 크기 때문에 ENUM의 사용을 제한하지 않으면 플랫폼 코드가 대책없이 커질 수 있으니 딱 Typedef 어노테이션을 사용하기 좋은 곳이겠죠.

참고 자료

The price of ENUMs (100 Days of Google Dev)

Perfmatter 시리즈 영상 중 ENUM 사용 시 주의점에 대해 설명하는 영상입니다.

Improve Code Inspection with Annotations

구글 개발자 페이지 문서입니다. 어노테이션을 이용해서 빌드 시 코드의 잠재적인 이슈를 미리 확인하는 방법을 다룹니다.