안드로이드의 프로세스, 쓰레드 이해하기

안드로이드 앱을 사용해봤거나 개발해봤다면 누구나 한번씩은 ANR 다이얼로그나 버벅거림, 혹은 크래시로 앱이 비정상 종료되는 상황을 겪어봤을 겁니다. 이런 현상들은 사용자에게 안좋은 경험을 주고 심한 경우 앱을 지워버리게 만드는 요인이 되기도 합니다. 반대로 빠르고 부드러운 앱은 사용자에게 좋은 인상을 줍니다.

그렇다면 빠르고 크래시 없는 앱을 개발하기 위해서는 어떻게 해야할까요?

여러가지가 있겠지만 가장 중요한 것 중 하나는 안드로이드의 프로세스와 쓰레드를 잘 이해하고, 올바르게 다루는 것일 겁니다.

이번 글에서는 안드로이드의 프로세스와 쓰레드의 개념에 대해 정리하고 이를 다룰 때 주의할 점에 대해 다루도록 하겠습니다.

1. 안드로이드 앱의 실행과 프로세스

안드로이드는 리눅스 기반의 OS입니다. 리눅스 커널위에 라이브러리와 런타임이 있고 그 위에 어플리케이션 프레임워크가 존재합니다. 안드로이드 앱 개발자는 어플리케이션 프레임워크에서 제공하는 컴포넌트와 이를 사용하기 위한 API를 이용해서 안드로이드 앱을 개발합니다.

안드로이드 앱은 앱 컴포넌트를 통해 실행됩니다. 앱 컴포넌트는 액티비티, 서비스, 컨텐츠 프로바이더, 브로드캐스트 리시버 이렇게 네 가지입니다. 앱 컴포넌트 중 하나를 실행 시켰는데 이미 실행 중인 컴포넌트가 있으면 안드로이드 시스템은 해당 컴포넌트를 기존 프로세스 내에서 실행 시키고, 없는 경우 새 리눅스 프로세스를 생성하고 여기서 컴포넌트를 실행 시킵니다.

App Components

1.1. 앱의 실행 경로

가장 기본적인 실행 경로는 안드로이드 런쳐, 즉 홈 화면이나 앱 메뉴에서 앱 아이콘을 클릭해서 실행 시키는 방법이 있습니다. 이 경우 앱은 AndroidManifest.xml에 정의한 런쳐 액티비티를 실행합니다. 이 방식은 앱 컴포넌트 중 액티비티를 통해 앱이 실행되는 경우입니다.

두 번째로 앱에서 알람을 등록했다고 합시다. 지정한 시간이 되면 알람을 받는 앱의 브로드캐스트 리시버가 호출이 됩니다. 이 경우는 브로트캐스트 리시버를 통해 앱이 실행되는 케이스입니다. 같은 방식으로 서비스, 컨텐츠 프로바이더를 통해 동일하게 앱이 실행되는 시나리오가 있을 수 있습니다.

App Launches from Components

1.2. 프로세스와 메인 쓰레드

위에 설명한 실행 경로를 통해 앱이 실행되면 하나의 리눅스 프로세스가 생성되고 프로세스 안에는 하나의 쓰레드가 기본으로 생성됩니다. 이 쓰레드가 바로 메인 쓰레드입니다. 메인 쓰레드는 아주 중요하기 때문에 어떤 일을 하고 어떻게 동작하는지 잘 이해해야 좋은 앱을 만들 수 있습니다.

먼저 앱의 네 가지 컴포넌트는 메인 쓰레드에서 실행됩니다. 따라서 앱 컴포넌트 중 하나인 액티비티의 라이프사이클 관련 콜백이나, 브로드캐스트 리시버의 onReceive()등도 모두 메인 쓰레드에서 실행됩니다.

두 번째로 UI 위젯을 통한 사용자 이벤트와 드로잉 이벤트가 메인 쓰레드에서 실행됩니다. 즉 안드로이드 UI toolkit에 있는 android.widget, android.view의 사용자 이벤트 처리나 드로잉 관련 메소드가 여기 해당됩니다.

조금 이해가 가지 않는 다면, onClick, onKeyDown, onDraw 같은 콜백들을 생각하면 됩니다.

이렇게 사용자 이벤트와 드로잉 이벤트가 메인 쓰레드에서 처리되기 때문에 메인 쓰레드를 UI 쓰레드라고도 합니다. 하지만 엄밀히 말하면 둘은 서로 같은게 아닙니다. 메인 쓰레드가 UI 쓰레드를 포함 하는 개념으로 보시는게 좋습니다.

1.3. 부드러운 UI 개발을 위해 알아야 하는 것

앱개발을 좀 해봤다면 메인쓰레드 내에서 16ms 이상 걸리는 작업을 하면 안된다는 얘기를 많이 들어보셨을 겁니다. 우리가 보는 화면의 움직임은 결국 프레임의 연속입니다. 조금씩 변하는 프레임이 연속으로 빠르게 이어지면 사람의 뇌는 이걸 움직임으로 인식합니다.

이 프레임이 너무 낮거나 불규칙하게 이어지면 사용자는 UI가 부드럽지 않다고 느끼게 됩니다. 안드로이드는 초당 60 프레임, 즉 60fps을 유지하도록 권장합니다. 60fps가 사람이 인지할 수 있는 선에서 가장 부드러운 수치이기 때문입니다. 1초는 1000ms 이기 때문에 60fps를 달성하려면 1000/60, 즉 프레임이 16ms당 한번씩 새로 그려질 수 있어야 60fps가 유지됩니다. 이에 대한 이야기는 아래 유튜브 영상에서 보실 수 있습니다.

PerfMatter Why 60fps
PerfMatter Why 60fps

여기서 이제 가장 중요한 개념이 등장합니다. 앞서 메인 쓰레드는 UI 업데이트 즉 드로잉 작업도 처리한다고 언급하였습니다. 그래서 메인 쓰레드를 UI 쓰레드라고 부르기도 한다고 했죠. 즉 이 메인 쓰레드에서 16ms 이상 걸리는 작업을 하면 메인 쓰레드는 16ms 간격으로 프레임을 업데이트 할 수 없게 됩니다. 즉 프레임 드랍이 일어납니다.

우리가 리사이클러뷰에서 스크롤을 하거나 화면에 애니메이션이 그려지고 있는 도중에 이런 프레임 드랍이 생기면 화면이 뚝뚝 끊기겠죠? 이러한 화면 끊김을 안드로이드에서는 Jank 라고도 부릅니다.

결국 부드러운 UI 개발을 위해 가장 중요한 것은 메인쓰레드에서는 16ms 이상 걸리는 작업은 하지 않는 것이라고도 볼 수 있습니다.

1.4. 워커 쓰레드(백그라운드 쓰레드)

앞에서 설명한 대로 메인쓰레드는 사용자 이벤트와 드로잉 이벤트를 처리하기 위해 사용되기 때문에 메인 쓰레드에서 시간이 오래걸리는 작업을 하는것은 금물입니다. 좀 더 엄밀히 말하면 16ms을 넘기지 않는게 좋습니다.

그럼 시간이 오래 걸리는 작업들은 어떻게 처리해야 할까요? 답은 간단합니다. 다른 쓰레드를 만들어서 메인 쓰레드가 아닌 곳에서 작업을 처리하면 됩니다. 이렇게 시간이 오래 걸리는 작업을 처리하기 위해 생성한 쓰레드를 워커 쓰레드 또는 백그라운드 쓰레드라고 부릅니다.

자바에서는 쓰레드를 생성하기 위해 Runnable과 Thread, Executors 클래스를 제공합니다. 이것들을 이용해서 시간이 오래 걸리는 작업을 처리해도 되고 안드로이드에서 편의를 위해 제공하는 AsyncTask, AsyncTaskLoader, ThreadHandler, ThreadPoolExecutor 등을 사용할 수도 있습니다.

1.5. 워커 쓰레드의 기본적인 사용 패턴

기본적으로 16ms 이상 걸릴 수 있는 무거운 일들은 메인 쓰레드에서 처리하면 안됩니다. 따라서 이러한 작업들은 보통 위에서 언급한 다양한 클래스를 이용해 워커 쓰레드를 생성하고 여기서 처리합니다. 또한 메인 쓰레드는 쓰레드 세이프 하지 않습니다. 따라서 메인 쓰레드가 아닌 워커 쓰레드에서 UI를 조작하면 정상적으로 반영이 되지 않을 수 있고 비정상적인 동작을 초래할 수 있습니다.

그런데 워커 쓰레드에서 작업을 하고 이를 UI에 업데이트 하려면 어떻게 해야할까요? 워커 쓰레드에서 시간이 오래걸리는 작업을 하고 그 결과를 UI에 반영해서 사용자에게 결과를 보여주는 것은 앱 개발에서 가장 많이 보이는 패턴중에 하나입니다. 서버에 데이터를 요청하고 이를 화면에 보여주거나 이미지를 가공해서 이를 사용자에게 보여주는 경우 등, 사실 대부분의 경우 워커 쓰레드는 UI 업데이트를 동반하는 경우가 많습니다.

이를 위해 워커 쓰레드와 메인 쓰레드 간에 통신하는 가장 기본적인 방법은 핸들러(Handler)를 이용하는 방법입니다.

두 번째로, 안드로이드는 이를 좀 더 간편하게 할 수 있도록 AsyncTask라는 유틸 클래스를 제공합니다. AsysncTask는 워커 쓰레드에서 처리할 일을 doInBackground()에 작성하고 진행 사항이나 결과를 UI에 반영 하는 코드는 onProgressUpdate(), onPostExecute()에 작성하여 메인 쓰레드에서 이를 간편히 실행 시킬 수 있습니다.

이 외에도 액티비티의 runOnUiThread(), 뷰의 post(), postDelayed()를 이용할 수도 있습니다.

Worker thread

2. 메인 쓰레드와 워커 쓰레드를 잘못 다루었을 때 생길 수 있는 일들

2.1. ANR(Application Not Responding)

메인 쓰레드는 이름처럼 아주 중요합니다. 앞서 말했던 것처럼 사용자 이벤트와 드로잉 이벤트를 처리하기 때문이죠. 때문에 메인 쓰레드에서 시간이 오래 걸리는 작업을 하면 그만큼 메인 쓰레드는 사용자 이벤트와 드로잉 이벤트를 처리할 수 없게 됩니다.

즉 메인 쓰레드 내에서 시간이 오래 걸리는 작업을 하게 되면 사용자가 앱의 버튼을 클릭하거나 키보드 입력도 할 수 없고, 앱의 UI도 업데이트가 되지 않아 멈춘 것처럼 보이게 됩니다. 이것이 바로 ANR입니다.

그럼 ANR을 한번 직접 만들어볼까요? 아주 간단합니다. onClick()과 같은 사용자 인풋 이벤트는 메인 쓰레드에서 실행된다고 언급하였습니다. 그럼 여기서 5초 이상 걸리는 작업을 하도록 하면 바로 ANR을 발생 시킬수 있겠죠?

1
2
3
4
5
6
7
8
9
10
11
class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

button.setOnClickListener {
Thread.sleep(10000)
}
}
}

안드로이드 시스템은 안드로이드 버전에 따라 다르지만 약 5초 이상 UI 쓰레드가 차단되면 ANR 대화 상자를 띄움으로써 사용자가 앱을 강제로 종료할 수 있게합니다.

ANR

2.2 워커 쓰레드와 UI 처리

앱 개발을 하다보면 워커 쓰레드의 작업 결과를 메인 쓰레드에서 UI에 반영하는 경우가 많은데 이 때 자주 하는 실수가 있습니다.

하나는 워커 쓰레드에서 작업을 끝내고 이를 UI에 반영할 때 해당하는 View가 View hierachy에서 제거 되었거나 객체 자체가 삭제 되었을 수 있습니다. 안드로이드에선 네트워크 호출은 워커 쓰레드에서 호출하도록 하고 있는데 서버로 API 호출을 한 후 이 응답값을 이용해서 UI를 수정하는 경우 흔히 발생 할 수 있는 경우입니다.

좀 더 구체적인 예를 들면 액티비티 위에 프레그먼트가 있고 여기서 네트워크 콜을 합니다. 이후 네트워크 응답이 오기 전에 사용자가 백키를 눌러서 프레그먼트를 프레그먼트 스택에서 제거합니다. 이렇게 되면 프레그먼트는 액티비티의 View hierachy에서 제거 됩니다. 이제 네트워크 응답이 오고 액티비티의 View hierachy에서 제거된 프레그먼트의 UI를 수정합니다. 생각해보면 특별한 경우가 아니고 앱 개발 시 굉장히 자주 있는 시나리오입니다.

두 번째는 워커 쓰레드에서 액티비티의 레퍼런스를 갖고 있는 경우 입니다. 이렇게 되면 해당 액티비티는 워커 쓰레드가 종료될 때까지 가비지 컬렉터에 의해 제거 될 수 없습니다. 워커 쓰레드가 종료되기 전에 configuration change 이벤트가 발생하거나 하면 동일한 액티비티가 여러개 생길 수도 있는 상황이 발생합니다.

워커 쓰레드에서 액티비티의 레퍼런스를 직접 갖고 있지 않은 경우에도 문제가 생길 수 있습니다. 액티비티에 내부 클래스로 AsyncTask를 선언하고 사용하는 경우입니다. 내부 클래스는 외부 클래스에 대한 암시적 참조를 갖기 때문에 위의 상황처럼 AsyncTask가 종료 될 때가지 액티비티는 가비지 컬렉터에 의해 제거되지 않습니다.

여기서 암시적 참조라는 말이 조금 어렵게 느껴질 수 있는데, 내부 클래스는 외부 클래스의 멤버 변수를 참조 할 수 있습니다. 어떻게 이게 가능할까요? 직접 내부 클래스에 외부 클래스의 레퍼런스를 참조하는 코드를 작성하지 않더라도 자바는 내부 클래스에 대한 참조를 생성하기 때문입니다. 이것을 바로 암시적 참조라고 합니다.

아래 예제 코드는 암시적 참조에 의한 메모리 릭의 예를 보여줍니다. 아래 코드에서, doInBackground()에서 어떤 작업을 하고 있는 동안은 MyActivity는 메모리에 계속 남아있게 됩니다. 또한 화면 방향 전환이나 단말의 언어설정 변경 등에 의해 액티비티가 재시작이 되면 동일한 액티비티가 여러 개 생성될 수도 있으며, 액티비티가 종료된 후에 onPostExecute()에서 유효하지 않은 액티비티내의 객체나 UI 요소에 대한 상태를 변경하면 예기치 못한 동작이 발생할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class MyActivity extends AppCompatActivity {

private TextView mText;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mText = findViewById(R.id.text);
new MyAsyncTask().execute();
}

class MyAsyncTask extends AsyncTask<Void, Void, String> {

@Override
protected void onPreExecute() {
mText.setText("MyAsyncTask.onPreExecute()");
}

@Override
protected String doInBackground(Void... voids) {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "MyAsyncTask.doInBackground() is finished";
}

@Override
protected void onPostExecute(String s) {
mText.setText(s);
}
}
}

마지막으로 안드로이드 개발을 시작한지 얼마 안된 사람들이 쉽게 착각하는 것 중 하나는 액티비티와 쓰레드의 생명주기가 동일하다고 착각하는 것입니다. 하지만 액티비티의 생명주기와 쓰레드의 생명주기는 관계가 없습니다. 쓰레드가 특정 액티비티에서 생성이 되고 해당 액티비티가 종료되어도 이 쓰레드는 이와 상관없이 실행됩니다.

이를 제대로 이해하지 못하면 첫 번째로 언급한 이슈가 발생하기 쉽습니다. 이런 실수를 하게되면 결국 앱은 비정상 종료를 하게 됩니다.

Crash

3. 마무리

이번 포스팅에서는 안드로이드의 프로세스와 쓰레드에 대해 개념적인 내용들을 정리해 보았습니다.

이를 정리하자면 아래와 같습니다.

  1. 메인 쓰레드는 사용자 이벤트, 드로잉 이벤트 등을 처리하는 매우 중요한 쓰레드이며 여기서 시간이 오래 걸리는 일을 하면 앱이 끊기거나 먹통이 될 수 있다.
  2. 1과 같은 이유로 많은 시간이 걸리는 작업은 새로 쓰레드를 만들어서 처리해주어야 하며 이를 워커 쓰레드라 부른다.
  3. 사람이 느낄 수 있는 가장 부드러운 수치는 60fps 정도 이기 때문에 안드로이드에서도 앱 개발 시 60fps를 유지하도록 권장한다. 이를 위해 메인 쓰레드에서는 16ms 이상 걸리는 작업을 하지 않는다.
  4. 워커 쓰레드에 액티비티나 View의 레퍼런스를 갖는 것은 메모리 릭과 앱의 비정상 종료를 일으킬 수 있기 때문에 매우 위험하다.

개인적으로 안드로이드 앱 개발 시 가장 중요하다고 생각하는 부분이기도 합니다. 다음에는 좀 더 구체적으로 쓰레드에 대해 자세히 알아보고 상황에 따라 어떤 방식으로 쓰레드를 사용해야 하는지 다뤄보도록 하겠습니다.

참고자료

안드로이드 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

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