swift로 iOS 개발 공부를 시작하면서, 코드 구현의 결과물을 확인하기 위해 수도 없이 xcode에서 (command + R)을 눌러왔습니다. (command + R)은 "실행(run)"의 단축키로, 우리가 작성한 코드를 빌드(build)하고 이를 특정 타겟(디바이스 or 시뮬레이터)에서 실행시킵니다.
그런데 돌아보니 단 한번도 "swift는 과연 어떤 과정을 거쳐 빌드가 되고 있는가?"에 대한 진지한 고민은 한 번도 해본 적이 없다는 것을 깨달았습니다. 그저 단축키만 무작정 눌러왔는데.. 별로 좋은 습관은 아니겠죠..? 그래서 한번 알아보았습니다!
기초개념: 컴파일? 링크? 빌드? 런?
먼저, 기본적인 개념부터 알아봐야겠죠? 정말 많이 들어봤을 위 개념들에 대해 매우 간단하게 먼저 살펴보죠!
컴파일러
컴파일러는 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 언어 번역 프로그램을 말한다.
-위키 백과
사람이 작성한 언어를 기계가 읽을 수 있도록 번역하는 것이 바로 컴파일입니다!
링크
컴파일러의 결과물인 목적(object)코드들을 최종 실행 가능한 실행파일 만들기 위해 연결 및 병합 해주는 작업
-위키 백과
컴파일시, 연관된 파일들을 엮어서 합쳐주는 과정입니다!
빌드
빌드는 소스코드 파일을 컴퓨터나 휴대폰에서 실행할 수 있는 독립 소프트웨어 가공물로 변환하는 과정을 말하거나 그에 대한 결과물을 일컫는다.
-위키 백과
빌드는 코드, 리소스 등을 하나로 합쳐 실행 가능한 파일을 만드는 과정입니다!
런(실행)
어떠한 제품을 실제 장치에서 실행시키는 프로세스이다.
-위키 백과
빌드된 파일을 시뮬레이터나 실제 디바이스에서 실행시키는 과정입니다!
정리하면,
- 런(실행) 하기 위해서는 빌드된 파일이 있어야 하고,
- 빌드 안에는 컴파일 과정이 포함되어 있으며
- 컴파일 과정 안에 링크 과정이 포함되어 있습니다!
빌드 과정
이제부터는 우리의 주 관심인 빌드 과정을 큰 틀을 따라가면서 자세히 살펴봅시다!
많이 들어봤을 전처리기, 컴파일러, 어셈블러, 링커 등이 보입니다. 위 그림의 흐름을 따라가면서 swift의 빌드 과정을 살펴봅시다!
전처리기(preprocessor)
작성한 소스코드를 컴파일러에 제공할 수 있도록, 컴파일 전에 전처리를 먼저 실행합니다.
- 매크로를 실제 정의로 바꿈(e.g. #define)
- 파일 간의 참조 관계(종속성)를 파악(e.g. #include)
- 컴파일 조건문을 파악(e.g. #if~, #if else, #endif)
그러나, swift 컴파일러에는 전처리기가 없습니다!
- 그래서, swift에서는 매크로를 사용할 수 없음..
🤔 하지만, swift에서도 파일 간의 참조 관계(종속성)나 컴파일 조건문은 분명 존재하는데, 이를 어떻게 처리하고 있는걸까요..?
- llbuild(하위 레벨 빌드 시스템)을 통해 종속성을 해결함
- 컴파일 조건문은 아래 그림과 같이 프로젝트의 Build Settings에서 flag를 설정하여 사용 가능
- Active Compliation Conditions에 RELEASE를 추가하여 DEBUG 모드냐 RELEASE 모드냐에 따라 코드가 다르게 동작하게 할 수 있음
#if RELEASE
print("RELEASE에서만 할 코드")
#elseif DEBUG
print("DEBUG에서만 할 코드")
#endif
- Other Swift Flags에 원하는 flag를 추가하여 사용 가능
- 단, -D라는 prefix를 반드시 붙여주어야 함
#if FLAG1
print("앱 타켓에서만 할 코드")
#endif
컴파일러(Complier)
소스코드에 대한 정보를 수집하는 symbol table을 작성 및 관리하며, 아직 고수준 언어를 저수준 언어인 어셈블리어로 변환합니다.
그리고, swift의 컴파일 과정은 아래 그림과 같이 LLVM을 거쳐서 실행됩니다.
🤔 LLVM이란?(약자가 아닌 프로젝트명입니다!)
- 프로그램을 컴파일, 링크, 런타임 등의 상황에서 프로그램의 작성 언어에 상관없이 최적화를 쉽게 구현할 수 있도록 구성되어 있는 컴파일러이자 툴킷
- 중간 표현(Intermediate Representation, IR), 이진(binary) 코드를 구성, 최적화 및 생성하는데 사용되는 라이브러리
본격적으로 swift의 컴파일 과정을 한번 살펴봅시다!
- 프론트엔드
- 소스코드 => swift AST
- swift AST => swift IL(SIL)
- swift IL => LLVM IR
- LLVM IR => 백엔드
- 백엔드(LLVM)
- LLVM IR => 어셈블리어
- 어셈블리어 => 기계어
즉, swift는 총 2번의 최적화 과정을 거칩니다!
- 프론트엔드: swift AST => SIL
- 백엔드: LLVM IR => 기계어
🤔 SIL(Swift Intermediate Language)이란?
1. 프론트엔드
- 두 가지 컴파일러를 지원
- Swift Compiler: swift 용
- Clang: C languagues 용
- 소스 코드에 대한 정보를 수집하는 symbol table를 생성
- 소스 코드를 분석 및 최적화하고, 이를 LLVM이 어셈블리어로 번역해 줄 중간 언어인 LLVM IR을 생성
2. 백엔드(LLVM)
- 프론트엔드로부터 넘겨받은 LLVM IR을 어셈블리어로 변환하고, 어셈블러가 이를 기계어로 변환
- 이 때, 비트코드를 생성하는 것으로 설정하면 비트코드로도 변환
- LLVM 어셈블러인 llvm-as가 LLVM IR을 LLVM 비트코드로 변환
- 아래 그림처럼 설정이 가능하고, Yes로 설정하지 않으면 비트코드 파일은 생성되지 않고 기계어만 생성됨
🤔 비트코드(bitcode, .bc)란?
- IR(Intermediate Representation) 종류 중 하나
- LLVM에 의해 컴파일된, 소스코드와 기계어의 중간단계로 어떤 아키텍처(디바이스)에서도 실행되기를 준비하는 중간단계
어셈블러
사람이 읽고 해석할 수 있는 어셈블리 코드를 재배치가능한(relocatable) 기계어로 변환합니다. 이 때, Mach-O(Mach Object file format) 파일을 생성합니다.
- 컴파일 과정에서 언급한 백엔드(LLVM)의 어셈블러가 어셈블리어를 기계어로 변환합니다!
링커
Darwin 기반 OS에서 실행할 수 있는 단일 Mach-O 실행 파일을 만들기 위해 다양한 오브젝트 파일들과 라이브러리들을 병합합니다.
- input: 오브젝트 파일(어셈블러의 결과물) + 라이브러리 파일(동적, 정적)
- 어셈블러와의 공통점: 둘 다 Mach-O 파일을 output으로 생성
- 어셈블러와의 차이점: 어셈블러의 Mach-O 파일들은 불완전한 파일
- 어셈블러: 재배치가능한 오브젝트 파일을 생성
- 다른 오브젝트 파일들이나 라이브러리들을 참조하는 부분들이 누락된 상태
- 링커: 아래 그림처럼 프로젝트의 Build Settings의 Linking에서 지정한 타입의 파일을 생성
- 컴파일 과정에서 생성된 symbol table을 활용하여 여러 오브젝트 파일들과 라이브러리들에 대한 참조를 확인하고, 이들을 연결시켜 줌
- archive를 통해 앱을 업로드하는 과정에 링킹이 포함되어 있음
- Enable Bitcode No 설정: 최종 컴파일 결과인 실행 가능한 바이너리 파일 자체를 업로드
- 앱이 실행될 수 있는 모든 환경(다양한 아키텍쳐, 디바이스)에 대한 바이너리를 생성하여 하나의 파일로! => "fat binary"
- Enable Bitcode Yes 설정: fat binary를 만들지 않고, 비트코드를 업로드
- 앱 스토어는 업로드된 비트코드를 가지고 각 환경별 최적의 바이너리를 생성(앱 시닝)
비트코드의 장점
- 비트코드는 컴파일된 프로그램의 중간 표현(IR), 따라서 다양한 방식으로 다시 컴파일을 시도할 수 있음
- 애플은 애플 서버에 제출된 비트코드를 다시 컴파일하여 사용자에게 맞는 최적의 바이너리 파일을 제공할 수 있음
- 애플은 비트코드로 인해 새로운 CPU에 대한 지원을 앱 스토어의 백엔드에 쉽게 추가가 가능해짐
- 최신 아키텍쳐로 컴파일하는 방법을 비트코드에 표시하여 이에 맞게 컴파일되게 할 수 있음
비트코드, 단점은 없을까?
crash report를 디버깅하고자 할 때(e.g. Firebase Crashlytics), 비트코드를 사용하면 "unsymbolicated crash from missing dSYMs" 에러가 발생하는 경우가 있음
- 개발자가 archive시 생성된 바이너리에 포함된 dSYM 파일과, 앱 스토어에서 비트코드로 생성한 바이너리에 포함된 dSYM 파일이 불일치하여 나타나는 현상
- 해결방법은 앱 스토어에 가서 dSYM 파일을 다운로드 받아, 업로드하여 사용
- 🤔 dSYM(debug symbol file)이란?
- 컴파일러가 소스코드를 기계어로 변환할때 생성되고, 역할은 기계어를 다시 소스코드 라인으로 매핑하는 정보를 가진 파일
- Xcode의 build setting에서 Debug Information Format의 설정에 따라 Debug Symbol을 최종 바이너리에 포함시킬지, 별도의 파일로 추출되도록 할 지 설정할 수 있음
앱 시닝(App Thinning)
서로 다른 각 디바이스에서 필요한 내용만을 선택하여 앱 번들로 만들고 전달하는 과정입니다!
위 그림처럼 범용 앱(universal app) 하나를 업로드 하면, 앱 스토어에서 슬라이스가 발생하게 됩니다!
앱 스토어가 디바이스의 특성을 보고, 아래 그림처럼 필요한 것만 조합해서 별도의 IPA를 만듭니다.
- 🤔 IPA란?
- iOS App Store Package의 줄임말로, 패키징이 끝난 압축파일
- 애플 모바일 스마트기기의 운영체제인 iOS에서 사용하는 앱의 설치 파일 - Xcode에서 앱을 만들면 .app 파일이 되는데, 이 파일을 .zip 형식으로 압축해 확장자만 .ipa로 바꾸는 방식을 사용