서론
안드로이드 스튜디오에서 실행 버튼을 누르면 내부적으로 어떤 동작이 일어나는 걸까? '빌드 전달 부탁드려요', '빌드가 아직 안나왔어요' 관용적으로 표현하는 빌드는 도대체 무엇일까?
고급 언어와 저급 언어
흔히 우리가 코드를 작성할 때 사용하는 프로그래밍 언어인 Kotlin, Java, C, Python과 같이 사람이 쉽게 이해하고 작성하기 위해 만든 언어를 고급 언어(high-level programming language)라고 한다. 하지만 컴퓨터는 저급 언어(low-level programming language)만을 직접 이해하고 실행할 수 있다. 따라서 고급 언어로 작성된 소스 코드가 실행되기 위해서는 반드시 저급 언어로 변환하는 과정이 필요하다. 저급 언어에는 기계어와 어셈블리어가 있고 기계어는 0과 1로 이루어져 있기 때문에 사람이 이해하기 힘들어 이를 보다 쉽게 이해하기 위해 만들어진 것이 어셈블리어이다.
고급 언어에서 저급 언어로 변화하는 과정은 크게 컴파일 방식과 인터프리터 방식으로 나뉜다. 각 방식을 이용해 저급 언어로 변환되는 고급 언어를 컴파일 언어, 인터프리터 언어라고 한다. 컴파일 언어로 작성된 소스 코드는 컴파일러에 의해 소스 코드 전체가 저급 언어인 목적 코드로 컴파일된다. 반면 인터프리터 언어는 소스 코드를 한 줄씩 실행하면서 저급 언어로 변환한다. 따라서 컴파일 언어로 작성된 소스 코드의 경우 컴파일 중 오류가 발생하면 소스 코드 전체가 실행되지 않고, 인터프리터 언어로 작성된 소스 코드의 경우 오류 발생 이전까지의 코드는 실행된다.
위 그림이 적절한 예시인데 컴파일러는 책 전체를 번역해서 전달하는 방식이라면 인터프리터는 실시간으로 통역을 해주는 방식에 비유할 수 있다. 그렇다면 안드로이드 개발에 사용되는 Kotlin은 컴파일 언어일까, 인터프리터 언어일까?
정답은 둘 다 이다. 우리가 Kotlin을 이용해 소스 코드(.kt)를 작성하면 코틀린 컴파일러(kotlinc)는 소스 코드를 JVM에서 실행 가능한 바이트코드(.class)로 변환한다. 이 단계까지는 명확한 컴파일 언어의 특징을 보여준다. 이후 생성된 바이트 코드는 JVM(Java Virtual Machine)에서 실행되는데, JVM은 바이트 코드를 인터프리터 방식과 JIT 컴파일러의 컴파일 방식을 같이 사용해 네이티브 코드로 변환시켜준다. 참고로 네이티브 코드는 특정 운영체제에서 직접 실행이 가능한 기계어이다. 나는 이 부분에서 한 가지 의문이 들었는데, 왜 Kotlin은 굳이 소스 코드를 바이트 코드를 거쳐 네이티브 코드로 변환시키는 것일까?
Java의 철학(WORA)
Kotlin의 기반인 Java는 "한 번 작성하면, 어디서든 실행할 수 있다"는 WORA(Write Once, Run Anywhere)의 철학을 가지고 설계된 언어이다. 운영체제나 하드웨어에 상관없이 JVM이 설치되어 있다면 다양한 환경에서 동일하게 실행된다. 기존에는 동일한 코드를 다양한 플랫폼에서 실행시키기 위해서는 플랫폼 종속적인 네이티브 코드로 변환하는 과정을 거쳐야 했다. 하지만 바이트 코드는 플랫폼 독립적이다. 이러한 바이트 코드를 JVM에서 해석해서 알아서 플랫폼에 맞는 네이티브 코드로 변환시켜주기 때문에 개발자는 플랫폼을 고려하지 않아도 된다. 따라서 개발자는 한 번 작성한 Java 소스 코드를 플랫폼 독립적으로 실행시킬 수 있다.
DVM(Dalvik Virtual Machine)
그렇다면 Android 개발에서 Java/Kotlin을 사용하므로 Android도 JVM을 사용하는 것일까? 그렇지 않다. 초기의 안드로이드는 DVM(Dalvik Virtual Machine)을 사용했다. Android가 JVM이 아닌 DVM을 선택한 이유는 라이센스 문제와 메모리 효율성 문제 때문이다.
JVM은 오라클의 상용 라이센스 하에 배포되고 있기 때문에 JVM을 사용할 때에도 해당 라이센스를 따라야 한다. JVM은 GPL 라이센스이며 안드로이드는 Apache 라이센스가 대부분이었기 때문에 안드로이드에서 JVM을 사용할 경우 라이센스와 관련된 문제들이 존재했다.
메모리 관련해서도 문제가 되었다. JVM은 스택 기반의 가상머신으로 PC에서 돌아가는 것을 고안하고 만들어졌다. 반면 초기의 안드로이드 운영체제는 메모리의 제약이 많았기 때문에 이에 최적화된 가상머신이 필요했다. DVM은 레지스터 기반의 가상머신으로 적은 메모리로 빠른 연산이 가능하다. 스택 기반의 모델의 경우 하드웨어 아키텍쳐에 종속되지 않아 다양한 환경에서 실행될 수 있다는 장점이 있지만 연산 시 데이터를 모두 스택에 저장하기 때문에 스택의 크기에 따라 추가적인 메모리가 필요할 뿐 아니라 간단한 연산이어도 스택에 팝, 푸시 하는 등의 추가적인 명령어가 필요하기 때문에 연산 속도가 느리다. 반면 레지스터 기반 모델의 경우, 하드웨어에 종속적일 수 있지만 레지스터를 바로 참조하기 때문에 명령어의 수가 줄어들고 레지스터는 CPU 내부에 있어 접근 속도가 훨씬 빠르다. 또한 JVM은 단일 인스턴스로 여러 애플리케이션에 공유되어 사용되지만 DVM은 다중 인스턴스로 애플리케이션마다 자체적인 가상머신 인스턴스를 제공한다.
안드로이드 스튜디오에서 실행 버튼을 누르면 Java/Kotlin 컴파일러를 통해 만들어진 바이트 코드(.class)들을 R8과 같은 DEX 컴파일러가 압축하여 하나의 .dex(Dalvik 바이트 코드) 파일로 변환(하나로 묶기 때문에 .jar보다 크기도 작다.)하고 Gradle과 Android SDK 도구들이 리소스와 기타 라이브러리 파일들을 압축해 APK(Android application package)를 만들어낸다. 이 과정이 우리가 관용적으로 말하는 빌드이다.
이렇게 만들어진 APK를 실행하면 Android 시스템이 새로운 프로세스를 생성하고 DVM이 해당 프로세스의 코드를 실행하고 해석한다. DVM은 런타임에 인터프리터와 Trace JIT Compiler를 사용해 .dex 파일의 바이트코드를 네이티브 코드로 변환하며 자주 사용되는 바이트코드를 미리 기계어로 해석해 속도를 향상시킨다. 그러나 후술할 AOT(Ahead-Of-Time) 컴파일러보다는 느리다.
ART(Android Run Time)
ART는 Android 4.4(API 19)에서 처음 등장했으며, 등장했을 당시에는 DVM과 선택적으로 사용이 가능했다. 이후 Android 5.0(API 21) 이상에서는 ART가 기본적으로 사용된다. ART는 AOT 컴파일러를 사용하여 앱을 설치하는 과정에서 바이트 코드를 기계어로 해석 완료한다. 따라서 설치 시간은 보다 오래 걸리지만 실행 시 빠르다는 장점이 있다. 또한 JIT Complier는 자주 사용되는 바이트코드를 캐싱해놓기 때문에 별도의 메모리 공간이 필요하지만 ART는 별도의 메모리가 필요 없다. 그 외에도 ART에는 DVM에 비해 GC가 개선되거나 개발 및 디버깅에 필요한 기능들이 추가된다는 개선점이 있다.
'Computer Science' 카테고리의 다른 글
[CS, Android] 해시테이블 / HashMap, ArrayMap, SparseArray (1) | 2024.10.06 |
---|