D8 과 R8
안드로이드에서는 대표적인 세가지 컴파일러가 있다
- 코틀린 or 자바 컴파일러
- D8
- R8
우선, 코틀린(또는 자바) 컴파일러는 코틀린(또는 자바) 코드를 자바 프로그래밍 바이트코드로 변환한다.
하지만, 우리는 이 바이트 코드를 안드로이드기기에서 실행할 수 없다. 안드로이드에서는 바이트코드 대신 DEX 코드라고 불리는 Dalvik에서 실행가능한 파일이 필요하다. Dalvik은 본래 안드로이드 런타임이였으나, 현재는 ART라는 런타임으로 변경되었다.
자세한 내용은 JVM, DVM, ART 이해하기 편을 참조하고, 지금은 DEX만 생각하자.
둘째로 D8은 앞단에 컴파일을 거쳐 만든 자바 바이트 코드를 다시 컴파일하여 DEX코드로 변경한다. DEX 코드로 변경했다면 안드로이드에서 실행 가능한 프로그램이 된 것이다!
이미 실행 가능한 프로그램(앱)인데 R8은 왜 필요한 것일까?
R8이라 불리는 세번째 컴파일러는 선택적으로 활성화하여 코드 최적화 및 코드 축소를 수행 할 수 있다.
R8은 기본적으로는 활성화되어 있지 않다. 만약 R8을 활성화 하고 싶다면 다음과 같은 샘플을 참고하여 build.gradle을 수정할 수 있다.
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
...
}
minifyEnabled를 true로 설정하면 R8이 활성화되고, 다른 컴파일러들의 동작이 끝나면 코드 최적화와 코드 축소를 수행하게 된다.
enum에 대해서…
코틀린에서 enum 자체를 사용하는 것에 대해서는 딱히 오버헤드가 발생하지 않는다. 코틀린 enum을 사용하면 자바 프로그래밍 언어 입장에서는 동일한 종류의 enum으로 변환 될 뿐이다.
하지만, switch 문을 사용한다면 이야기가 달라진다.
예제코드를 살펴보자
BlendMode라는 enum 타입이 있다고 가정하자.
예제코드처럼 BlendMode타입을 코틀린의 when 문법과 같이사용하는 경우에 이 소스를 컴파일 한 후 바이트코드를 살펴보면 다음과 같다.
값을 바꾸는 대신 실제로는 특정 배열을 호출하는 것을 확인할 수 있다. 이 배열은 컴파일러에 의해 생성된 특정 클래스 파일에 숨겨져 있다.
왜 이런 작업을 내부적으로 수행하는 걸까?
코드에서 enum 타입의 ordinal 값을 단순히 변경하는 것은 이진 호환성(binary compatibility) 문제로 수행 할 수 없다.
예를들어 enum을 가지고 있는 라이브러리를 만들었다고 가정하고, 누군가가 이를 가져다 쓴다고 했을 때 enum 안에 있는 아이템들의 순서를 변경해버리면 누군가의 애플리케이션을 망가뜨릴수도 있는것이다. 그렇기 때문에 ordinal 값을 사용하는 것 대신 컴파일러는 enum항목을 다른 값에 매핑하게 된다. 그런 다음 개발자가 enum가지고 뭘하던간에 라이브러리의 빌드된 코드는 여전히 잘 동작하게 된다.
뭐 여기까지는 컴파일러가 특정 클래스를 생성하고 배열까지 만드는 이유가 타당하고 좋다.
흥미롭게도 지금은 하나의 when 키워드 사용에 대해서 이러한 클래스가 생성되었지만, 다른 어딘가에서 또 when과 enum을 사용하게 된다면 또 다른 매핑 클래스가 생성되게 되고, 또 다시 정적인 배열을 선언하게 된다. 이런점들이 반복되는게 사실 큰 문제는 아니다.
하지만 우리가 모르는 사이에 클래스와 배열이 생성되는것은 그만큼 빌드와 런타임에 추가적인 시간과 비용이 발생한 다는 것은 명백한 사실이다.
(티끌 모아 태산..)
만약에 R8을 사용한다면 어떻게 될까?
R8은 모든것이 제대로 동작하도록 코드를 최적화하는 아주 똑똑한 녀석이다. 앞에서 먼저 보았던 코드를 단순하게 다음의 코드처럼 변경해준다.
R8은 모든 코드를 살펴보고, 그것이 내가 작성한 코드인지 라이브러리 코드인지 확인하여 올바른 결정을 통해 코드를 최적화 한다. 그렇기 때문에 코드를 생성하고 배열을 초기화하는 오버헤드 비용을 회피할 수 있도록 돕는다.