개요
의문의 시작
Flutter를 통해 개발을 하면, '한 번에 두 가지, 필요에 의하면 여섯 가지의 플랫폼까지도 동시에 개발을 할 수 있다니 굉장히 편리하잖아?' 라는 생각과 '근데 웹으로 대체하던데...' 라는 생각 등등 여러가지 생각이 들곤 한다. 그런데 생각이 좀 더 진행이 되고 나니 '이걸 어떤 방식으로 수행하는 거지?' 라는 의문이 들기 시작했다.
빌드는 정해진 결과
"여섯 가지의 플랫폼으로 빌드가 가능"이라는 말은 결과지 과정이 아니다. 그리고 우리가 Dart 코드를 짜서 Flutter앱을 만드는 것도 어떻게 보면 결과에 해당되는 부분이고 과정에 해당되는 부분은 아니다. 우리는 Dart코드를 작성할 때 당연하게도 여섯가지의 플랫폼으로 빌드가 된다는 것을 상정하고 있으니 말이다. 여러가지 플랫폼으로 나온다는 결과는 정해져있고, 그 결과를 어떻게 더 짜임새 있고 효율적으로 짜는지가 Flutter 개발자가 하는 핵심적인 일이다. 물론 그 외에도 배포와 같은 중요한 것들도 있지만 Flutter라는 프레임워크 내에서만 보면 그렇다는 이야기다.
결론적으로
그래서 Flutter프레임워크는 어떻게 빌드를 해주는걸까? 라는 의문을 해소하기 위해 여러 자료들을 찾아봤다. 그리고 찾아본 결과, Dart의 컴파일 방식을 알면 그 과정을 전반적으로 이해할 수 있다는 결론에 이르렀다. 그래서 Dart의 컴파일 방식을 알아보기로 했다.
Dart 컴파일러
Dart 코드 컴파일에는 크게 2가지 종류가 있다. 하나는 JIT 컴파일, 다른 하나는 AOT 컴파일이다. JIT 컴파일은 Just-In-Time을 줄인 말이며, 개발 과정에서 코드의 변경 사항을 즉각적으로 반영할 수 있도록 하기위해 도입되었다. AOT 컴파일은 Ahead-Of-Time을 줄인 말이며, 프로덕션 빌드 시에 사용이 된다.
JIT 컴파일
JIT 컴파일이란?
JIT(Just-In-Time) 컴파일은 다른 말로 동적 번역(Dynamic Translation)이라고 부르기도 한다. 언어들을 분류할 때 그동안에는 인터프리트 방식과 정적 컴파일 방식으로 구분을 해오곤 하는데, JIT 방식은 이 두가지의 하이브리드 격으로 볼 수 있다. 실행 시점에는 인터프리트 방식으로 기계어 코드를 생성하고 그 코드를 캐싱하여 같은 함수가 여러 번 불릴 때 캐싱된 기계어 코드를 재사용한다.
JIT 컴파일 방식
소스코드는 가장 먼저 바이트코드 컴파일러에 의해 중간 언어인 바이트코드로 변환된다. 바이트코드는 기계어는 아니지만 가상 머신에 의해 기계어로 손쉽게 변환할 수 있는 코드이다. JIT 컴파일러는 바이트코드를 읽어 빠른 속도로 각 플랫폼에 맞는 기계어(네이티브 코드)를 생성할 수 있고, 이 기계어 변환은 코드가 실행되는 과정에 실시간으로 일어나며 전체 코드의 필요한 부분만 변환한다.
간단하게 요약하자면 아래와 같다.
- 개발자가 소스코드를 작성
- 바이트코드 컴파일러가 이를 중간언어인 바이트코드로 변환
- 변환된 바이트코드를 가상머신이 JIT 컴파일을 통해 실행 중에 각 플랫폼에 맞는 네이티브 코드로 변환
JIT 컴파일의 장점
JIT 방식은 실행 시점에 코드 변경을 적용할 수 있기에 즉각적인 피드백을 제공할 수 있다. 이 즉각적인 피드백은 Flutter개발에서 매우 유용한 기능인 hot reload 기능을 가능하게 한다. 또한 실행 중에 코드의 성능을 최적화 할 수 있어 자주 실행되는 코드 경로를 최적화하여 실행 성능을 향상 시킨다.
실행 중에 코드의 성능을 최적화 한다는게 다른 언어들과는 차별되는 장점인가? 라는 의문이 들 수 있는데, 인터프리터 언어는 이런 최적화를 제공하지 않는다. 기존의 인터프리터 언어는 캐싱을 하지않고 코드가 들어오는대로 번역하기에 성능이 떨어질 수 밖에 없다.
JIT 컴파일의 단점
JIT 방식은 초기 성능 비용이 크다는 단점(런타임 오버헤드)이 있다. JIT에서 제공하는 최적화라는 것은 어디까지나 한 번 실행되고 난 함수들을 캐싱한 것이기 때문에 초기 실행시 캐싱이 이루어져야 그 이후에 최적화가 가능하다. 그렇기에 처음 실행시 코드를 컴파일 하고 코드를 캐싱하는 비용이 추가되기 때문에 약간의 성능 비용이 추가될 수 있으며 런타임 오버헤드가 발생할 수 있게 된다.
AOT 컴파일
AOT 컴파일이란?
AOT(Ahead-Of-Time)컴파일은 JIT나 인터프리터 언어들이 사용하는 중간 언어(바이트코드)를 미리 목표 시스템에 맞는 기계어로 번여갛는 방식을 말한다. 일종의 정적 컴파일 방식이나, 중간 언어를 통해 컴파일 하기에 여러가지 목표 시스템에 맞게 컴파일이 가능하다.
일반적으로는 중간언어를 사용하는 시스템은 프로그램 실행 도중에 프로그램 분석 정보등을 얻을 수 있는 JIT와 같은 방식을 사용하곤 한다. 하지만 JIT 방식의 단점인 초기 성능 비용이 크다는 문제가 있기에, 이를 해결하기 위해 AOT가 나왔다.
AOT 컴파일 방식
JIT와 마찬가지로, 소스코드는 바이트코드 컴파일러에 의해 중간언어로 변환된다. 그리고 AOT 컴파일러를 통해 바이트코드를 네이티브 코드로 컴파일한다. JIT와 같이 실행도중에 컴파일하지 않고 전체 코드를 실행 전에 모두 컴파일시킨다.
- 개발자고 소스코드를 작성
- 바이트코드 컴파일러가 이를 중간언어인 바이트코드로 변환
- 변환된 바이트 코드를 각 플랫폼에 맞게 실행 전에 네이티브 코드(기계어)로 컴파일
AOT 컴파일의 장점
AOT 컴파일은 JIT에 비해 빠른 시작 시간과 일관된 성능을 보장할 수 있다. 빠른 시작 시간과 일관된 성능을 필요로 하는 프로그램은 대체로 GUI를 사용하는 프로그램이기에 해당 프로그램들에 대해서 이를 보장해줄 수 있다. 또한 컴파일을 통한 최적화가 가능하기에 계산이 많은 프로그램들을 최적화할 수 있다.
AOT 컴파일의 단점
AOT 컴파일은 실행 전에 전체 파일을 빌드해야하기 때문에 빌드 속도가 느리다는 단점이 있다. 또한 JIT에서 제공하는 실행시 컴파일 기능을 제공하지 않아 코드가 제대로 동작하는지 빠르게 확인할 수 없게 된다.
Dart가 둘 다 사용하는 이유
그렇다면 Dart 언어는 JIT만 사용하면 JIT만 사용하는거지, 왜 AOT까지 사용하는 것일까? 그 이유는 앞의 JIT와 AOT의 특징을 보면 알 수 있듯이 둘이 상호보완이 가능하기 때문이다. 세상에 만능이란게 없듯이 JIT도 장점이 많지만 몇가지 단점들도 명확하게 존재한다. JIT 특성상 실행시간에 컴파일이 되는 것은 개발자 입장에서 테스트하기에 정말 좋지만, 사용자 입장에서는 프로그램 자체의 성능이 떨어져 약간의 불편함이 나올 수 있게 된다. 하지만 JIT를 아무리 뜯어 고쳐도 이건 JIT 자체의 고질적인 문제이기 때문에 하드웨어 자체의 성능이 늘어나는게 아닌 이상 어떻게 해결할 방도가 없다.
그래서 Dart는 이 문제를 AOT를 통해 이 부분을 해결했다. 출시된 프로그램을 정적 컴파일을 통해 미리 컴파일 해놓으면 실시간 컴파일로 인해 손실되는 성능이 사라지기에 이 문제를 해결할 수 있게 된다. 사용자는 hot reload와 같은 기능이 필요없고, 그저 보장된 시작 시간과 일관된 성능을 필요로 하기에 JIT의 장점은 필요가 없다. 그렇기에 Flutter는 개발자와 소비자를 위한 2가지 방식으로 컴파일을 지원하게 된다. 개발자를 위해서는 JIT, 소비자를 위해서는 AOT를 지원하여 빠른 개발 시간과 출시 후 성능을 모두 챙기고자 하는 방식을 얻었다.
결론적으로 Flutter가 네이티브 코드를 생성하는 방식은...
결국에는 어떻게 네이티브 코드를 빌드하냐는 질문은 바이트코드를 Dart VM이 컴파일한다라는 블랙박스로밖에 답하지 못했다. Dart VM이 컴파일하는 과정, 컴파일 동작 방식에 대해서까지는 정보를 찾기가 어려웠고 빠르게 이해한다고 이해할 수 있는 부분이 아니었다. 나는 컴파일러 개발자가 아니라 모바일, 웹 개발자다. 컴파일러가 하는 일까지 알면 좋겠지만, 선택과 집중이 아닐까? 하는 생각이 들었다. 결국에는 나의 한계를 드러내는 말밖에 안되는 것 같아, 이 부분에 대해서는 추후에 더 찾아보기로 했다.
핵심적으로 요약을 하자면 이렇다.
- Flutter프레임워크를 컴파일해주는 Dart는 JIT와 AOT라는 2가지 컴파일 방식이 존재한다.
- JIT는 바이트 코드를 컴파일 하며 실행 중간에 컴파일이 가능한 인터프리터와 정적 컴파일의 혼합형 컴파일로, 컴파일 최적화를 제공하며 hot reload와 같은 기능을 제공한다.
- AOT는 바이트 코드를 컴파일하는 정적 컴파일로, 런타임 오버헤드라는 JIT의 단점을 보완한다.
- 둘 다 가상머신(Virtual Machine, VM)이 바이트코드를 컴파일 하는 방식이고, 이 방식으로 여러가지 플랫폼에 네이티브 코드를 제공할 수 있게 된다.
- Dart는 JIT로는 개발자에게 이점을(hot reload), AOT로는 소비자에게 이점을(성능 보장) 준다.
마치며
문득 든 의문인 Flutter는 어떻게 여러가지 플랫폼으로 변환될 수 있는가라는 의문에 대해서는 결국에는 바이트코드로 컴파일되고 그 바이트코드를 가상머신이 다시 번역한다 라는 블랙박스로밖에 답변을 못했지만, Dart의 컴파일 방식에 대해서도 같이 알게 되어 그나마 다행이라고 생각이 들었다.
컴파일 방식에 대해서까지 알아야 하나? 라는 생각이 아직 지배적이긴 하다. 아직 나는 Flutter도 잘 못다루는데 순서가 이게 맞는건지 생각이 들면서도, 이런 로우 레벨까지 알아야 하이 레벨 개발도 잘하는게 아닌가 하는 생각도 든다. 정석적으로는 로우 레벨부터 하이레벨까지, 바텀 업으로 학습하는게 좋겠지만 내 시간이 무한정 있는 것이 아니라 시간을 효율적으로 운용해야하는게 아닌가 싶기도 하다. 정답은 없지만 자꾸만 자기합리화를 하는거 같다는 생각이 들기도 하다. 어쨌거나 좀 더 학습을 해야겠다.
'Develop > Flutter' 카테고리의 다른 글
[Flutter] Event Bus 패턴 (0) | 2024.09.28 |
---|---|
[Flutter] Dart는 싱글 스레드 언어 (0) | 2024.08.04 |
[Flutter][Widget] CustomPaint로 나만의 위젯 만들기 (2) | 2024.04.08 |
[Flutter] 패키지 사용법 (0) | 2024.04.01 |
[Flutter][Widget] Row, Column 위젯 (0) | 2024.03.24 |