100명의 엔지니어와 Swift

Tuomas Artman이 Uber 앱을 다시 만들면서 얻은 경험을 공유합니다.

Tuomas Artman이 2016년 San Francisco 열린 Swift Summit에서 발표한 내용을 옮긴 SkilledSwift with a hundred engineers을 번역했습니다. 발표 내용을 글로 옮긴 것이므로 원문의 동영상을 함께 보셔야 합니다.


이 토크는 Tuomas Artman이 2016년 San Francisco에서 열린 Swift Summit에서 발표한 것이다.

소개

여기까지 와줘서 고맙습니다. 저는 Tuomas Artman입니다. 저는 Uber에서 아키텍처와 프레임워크 테크 리드를 하고 있습니다. 그건 제가 굉장히 멋진 직업을 갖고 있다는 의미입니다. 아마 제가 만나본 중 가장 뛰어난 사람들과 함께 일을 하고 있습니다. 아키텍처 위에 수백만 명이 사용하고 있는 Uber 앱을 어떻게 만들지 정의하고 있습니다.

100명의 엔지니어와 Swift — 동기, 아키텍처, 배운 것

오늘 저는 100명의 엔지니어와 함께 Swift 코드를 작성하는 것이 어떤 것인지 이야기하고자 합니다. 일주일 전 수요일에 배포된 Rider 앱 새 버전이 Swift로 완전히 다시 작성되었다는 소식을 들으셨을 것입니다. Swift를 선택한 동기와 아키텍처에 대해 빠르게 이야기하고, 대부분의 시간은 이번에 앱을 다시 만들면서 배운 것을 이야기하려고 합니다.

Uber의 시작 — 왜 다시 만들었나?

4년 전 Uber의 전체 모바일 팀입니다. 그들이 우리가 지금 사용하는 앱의 기반을 만들었습니다. 지난 4년간 이 앱은 잘 동작했지만 모바일 엔지니어 팀이 기하급수적으로 늘어나면서 아키텍처가 무너지고, 기능 개발이 무척 어려워지기 시작했습니다. 우리는 여러 팀 사이에서 많은 뷰 컨트롤러를 공유했기 때문에 여러 코드 경로를 테스트해야 했습니다. 오래된 아키텍처로 인해 문제가 생기기 시작했습니다. 왜냐하면 그것을 두 명의 엔지니어가 작성했고, 그 뒤로 팀은 백 명 이상으로 늘어났기 때문입니다. 동시에 제품의 UX도 확장할 수 없음을 발견했습니다. 우리는 수많은 도시에 서비스를 개시했고, 모든 도시 팀이 새 상품을 그들의 도시에 추가하기를 원했기 때문에 바닥에 있는 프로덕트 슬라이더가 점점 촘촘해지는 문제가 발견됐습니다. 그래서 동시에 Rider 앱의 전체 UX 재설계를 원했습니다. 당연히 이 두가지 문제, 아키텍처적인 문제와 전체 UX 재설계에 대해 결정해야 했습니다. Ben이 어제 한 이야기처럼 우리는 무조건 다 바꿔버리자고 할 수 없었습니다. 아키텍처를 고쳐보지도 않고 완전히 새로 만들자고 할 수는 없었습니다.
우리는 2015년에 많은 수정을 했고, 아키텍처를 유지하려고 했습니다. 하지만 궁극적으로 안전하고 최적화한 전체 UX 재설계를 위해 앱을 완전히 다시 만들기로 했습니다.

새 아키텍처의 목표 — 신뢰도, Uber의 성장을 지원

그래서 우리는 아키텍처 목표를 정하기 시작했습니다. 다시 만들면서 우리는 무엇을 원하는가? 기본적으로 두 가지가 있습니다. 99.99% 신뢰도의 core flow. 기본적으로는 최저 수준의 크래시 프리율을 의미 하지만 명백히 더 많은 것을 의미합니다. 앱이 크래시 하지 않지만 사용자가 특정 화면에서 벗어날 수 없다면 그건 신뢰할 수 있는 게 아닙니다.
또한, 앞으로 몇 년간 Uber의 성장을 지원할 수 있기를 바랐습니다. 4년 이상이었던 지난 번 것처럼 새 아키텍처도 오래 살아남기를 바랐습니다.

선택은 Swift

자연스레 이 두 가지 목표는 우리가 Swift를 선택하게 만들었습니다. 우리는 Swift가 좀 더 안전하다고 생각했습니다. 아직 누구도 프로덕션에서 사용하지 않았습니다.
우리는 컴파일러의 타입 안정성(type safety)이 프로덕션에서의 크래시 보다 먼저 문제를 찾아내는 데 도움이 될 것이라고 생각했습니다.
우리는 이미 몇 년 전부터 Swift의 전성기가 올 것이고, Apple에서 발전시킬 유일한 언어라는 것을 알았습니다.

타임라인
우리는 상당히 공격적인 타임라인을 설정했습니다. 연초, 2월에 시작했습니다. 제대로 해내고 싶었습니다. 이전 회사에서 재작성하는 데에 굉장히 많은 시간을 소모했거나 재작성에 실패했던 엔지니어가 있었기 때문입니다.
우리는 이것이 성공했는지 확인하기 바랐습니다. 5개월이 지나고 나서 플랫폼 팀의 핵심 엔지니어를 데려와서 아키텍처를 보기 시작했습니다. 우리는 5개월 동안 아키텍처, 프레임워크, 기초적인 것들을 완성하고, 린트를 넣는 것을 제외하고 아무 것도 하지 않았습니다. 모든 사람이 필요로 할 프레임워크를 작성하고, 기반이 완벽한지 확인했습니다.
6월에는 좋은 아키텍처를 갖게 됐다고 생각했고, core flow 팀의 온보딩(Onboarding)을 시작했습니다. core flow는 우리에게 UberX Rider 또는 UberPOOL Rider를 가져간다는 의미입니다. 그래서 큰 팀을 만들기 위해 20명 가량의 엔지니어를 추가했습니다. 두 달 동안 그들과 함께 아키텍처를 조사하고 우리가 생각한 것이 제품을 만드는 데 정말 적합한지 확인했습니다. 그리고 우리가 몇 가지 놓친 것을 발견했습니다. 화면 단에서 엔지니어가 트랜지션을 하며 복잡한 화면 조작을 하면 그들의 요구를 수용하기 위해 약간의 아키텍처를 변경해야 했습니다. 하지만 두 달 뒤, 코드베이스에서 더 이상 큰 마이그레이션이 없을 거라고 느꼈고, 모두에게 플랫폼을 개방하고 원한다면 기능을 포팅하라고 말했습니다.
Uber의 프로그램 팀은 상당히 독립적이기 때문에 우리는 이렇게 이야기했습니다. 우리는 11월 출시를 원한다. 기능을 추가할지 말지는 당신에게 달려있다. 당신이 그것을 해야만 하는지 알려줄 수 없다. 예전 Rider 앱을 언제 지원 종료해야하는 지 알려줄 수 없다. 일부 팀은 즉시 작업에 들어가 세 달을 다 사용해서 그들의 기능을 향상시켰고, 다른 팀들은 지난 주에 시작해서 겨우 만들었습니다. 어쨌든 우리는 지난 주, 11월에 출시했고, 모두가 흥분했던 성공적인 출시였습니다.
저는 아키텍처 자체에 대해 많은 시간을 쓰고 싶지 않습니다. 저희가 많은 엔지니어들과 Swift를 사용하면 배운 것을 나누는 것이 여러분에게 가장 좋다고 생각하기 때문입니다. 하지만 저희 아키텍처에 대해 빠르게 소개하겠습니다.

Uber의 아키텍처

우리는 그것을 “Riblets”라고 부릅니다. 그것은 Router, Interaction, Builder, 그리고 Presenter와 View를 의미합니다. 그것은 하나의 앱의 주요 컴포넌트들입니다. VIPER의 일종입니다. 우리는 MVVM을 보고, VIPER를 보고, MVC를 봤습니다. 그리고 혁신을 VIPER 위에서 생각해냈습니다. 우리가 하고 싶은 가장 큰 일은 모든 것을 분류하고, 모든 것을 테스트 가능하게 만드는 것이었습니다. Riblets에 포함된 각 컴포넌트는 프로토콜 인터페이스를 가지고 있고, 각 유닛을 꺼낼 수 있으며 완전히 테스트 할 수 있습니다. 모든 Riblets는 트리(tree)에 의해 관리됩니다. 그래서 우리는 상태 머신 대신 상태 트리를 가집니다. 이 박스들은 Riblets를 나타내고 있습니다. 저희 아키텍처의 가장 중요한 부분은 뷰 기반이 아닌 비즈니스 로직 기반이었으면 하며 모든 비즈니스 로직 결정이 매우 지역적이었으면 합니다.

이 트리에서 예를 들어 Signup Riblets을 보면, 부모를 알지 못합니다. 필요한 것이 주입되었다는 것만 알고 있습니다. 의존성은 부모에 의해 충족됩니다. 거기에는 아마 가입 절차가 어떠한지 관찰하는 관찰자가 있을 것입니다. 하지만 그것이 트리에서 어디에 있는지는 모릅니다. 따라서 그것은 정말 독립적이고 모든 단일 요소가 지역적으로 결정을 내립니다. App 컴포넌트에서 예를 들면, App 컴포넌트는 오로지 하나의 비즈니스 요소에만 관심이 있습니다. 세션 토큰을 가지고 있는지, 아닌지. 그것이 관찰하는 유일한 것입니다. 앱 컴포넌트가 스트림에 세션 토큰이 없다고 판단하면 Welcome Riblets로 연결됩니다. 어떠한 시점에 세션 토큰을 얻으면 Welcome 컴포넌트를 없애고 Bootstrap 컴포넌트로 이동합니다.
그 다음, 트리 오른쪽에 있는 모든 단일 요소들은 우리가 로그인 한 것을 알게됩니다. 우리는 토큰을 가지고 있습니다. 그들은 의존성 주입으로 토큰을 사용할 수 있으며 사용자의 로그아웃에 관심을 가질 필요는 없습니다. 네트워크의 어딘가에서 세션 토큰이 무효화되면 앱 컴포넌트가 알게됩니다. 스트림을 통해 호출되면 더 이상 세션 토큰이 없다는 것을 알 수 있습니다. Bootstrap 컴포넌트를 없애고 Welcome 컴포넌트로 돌아갑니다.
따라서 기본적으로 여러 팀이 다른 팀과 일일이 이야기하지 않고도 구성 요소를 개별적으로 작업 할 수 있습니다. 여러분은 지역적인 결정을 내릴 수 있고, 여러분의 의존성이 항상 충족된다는 것을 알고 있습니다.

너무 많은 파일과 너무 많은 코드
이 모든 것들은 많은 코드를 만듭니다. 우리는 모든 것에 대한 프로토콜을 가지고 있습니다. Riblets로 약속한 컴포넌트가 있고, 5개의 다른 파일로 나뉩니다. 그래서 저희 코드베이스에는 5천 개가 넘는 파일과 50만 줄이 넘는 Swift 코드가 있습니다. 추가로 약간의 Objective-C로 된 핵심 컴포넌트를 가지고 있습니다.

Swift에 대해 배운 것

우리가 얻은 일종의 교훈입니다. 여러분이 팀을 키워나길 때 신경 써야할, 우리가 Swift에 관해 배운 좋은, 나쁜 그리고 거지같은 것들입니다.

좋은 것
좋은 것부터 시작합시다. 명백히 더 나은 언어입니다. 여러분이 그렇게 생각하지 않는다면 여기에 계시지 않겠지요. 저희는 Swift가 제공하는 모든 언어 기능을 거의 다 사용합니다.

신뢰성
첫 번째 놀라운 점은 신뢰성이었습니다. 그건 아마 제가 아키텍처를 개발한 지 4개월쯤 되었을 때였습니다. IDE나 앱이 한번도 크래시 하지 않았다는 것을 깨달았습니다. 팀에게도 물어봤고, 그들은 “저희 앱도 크래시 하지 않았어요.”라고 말했습니다. 완전히 새로운 아키텍처를 개발한 5개월간 디버그 모드에서도 크래시가 없었습니다. 첫 번째 크래시는 32비트 디바이스에서 어떤 JSON을 풀다가 Integer 오버플로우(overflow)가 발생한 것이었습니다. 이것이 전체 개발 기간동안 발생한 한 번의 크래시였습니다.

우리는 놀랐습니다. 그리고 일주일 전에 완전히 새로운 앱을 출시했습니다. 우리는 내부적으로 테스트 했습니다. 그리고 우리 직원들과도 테스트했습니다. 하지만 아무래도… 저는 출시된 뒤에 얼마나 많은 크래시가 발생할 지 두려웠습니다. 우리의 크래시 프리 목표는 99.99% 였고, 거의 근접했습니다. 저는 이런 것을 한번도 본 적 없습니다. 새로 출시한 앱이 크래시가 거의 없었습니다.
중요한 것은 강제 언래핑을 허용하지 않는 것입니다. 그렇지 않으면 이런 크래시 프리율을 얻을 수 없습니다. 그래서 아무도 강제 언래핑을 할 수 없도록 린트를 추가합니다. 그렇게 하면 디프(diff)에서 그것을 잡아낼 수 있습니다. 그것이 괜찮은 앱을 만드는 기본일 것입니다.
반면에 모든 곳에 if-let을 넣어 놓으면 앱이 그저 동작하지 않을 뿐인 else에 대한 처리를 하지 않아도 문제가 되지 않는 것과 같은 모든 경우를 다 찾아냈는지 확인해야 합니다. 그래서 어설션(assertion)을 사용해서 사용자가 아닌 엔터프라이즈 모드에서 디버그 할 때 발견할 수 있도록 해야 합니다. 그러면 상당한 크래시 프리율을 얻을 수 있을 것입니다.

안드로이드 엔지니어들이 환영함
우리가 발견한 또다른 장점은 코틀린을 쓰는 안드로이드 엔지니어들은 특히 환영한다는 것입니다. 그들은 넘어와서 별 일 아니라는 듯이 코드를 작성합니다. 저희 아키텍처는 멀티 플랫폼 아키텍처입니다. 그래서 우리는 안드로이드와 iOS에서 같은 아키텍처를 사용하기로 결정했습니다. 우리는 모든 이름을 맞췄고, 모두 같은 관례로 작업했습니다. 그건 Swift가 가능하게 했습니다. 만약 우리가 Objective-C로 작업했다면, 안드로이드 엔지니어와 가까워지지 못했을 것이고, 같은 아키텍처를 사용하지 못했을 거라고 생각합니다.

나쁜 것
이제 나쁜 것에 대한 시간입니다. 여러분이 실수와 역경을 통해 배운다면 가장 흥미로운 내용일 것입니다.

테스팅이 어렵다
우리가 발견한 첫 번째 문제는 테스팅이 매우 어렵다는 것입니다. Swift는 정적 언어이므로 Objective-C에서 사용하던 모킹(mocking) 프레임워크에 기댈 수 없습니다. 그리고 저희 쪽은 모두 프로토콜 기반이어서 프로토콜을 테스트할 수 있는 방법을 찾아야 했습니다. 예를 들어, 여기에 키에 대한 데이터를 저장하고, 키에 대한 데이터를 가져올 수 있는 Storing 인터페이스를 만드는 프로토콜이 있습니다. 이제 여러분은 테스트하고자 하는 비즈니스 로직이 있는 인터렉터를 가지고 있습니다. 특정 입력을 받으면 뭔가를 디스크에 저장하기를 원합니다. 이제 구현이 필요합니다. 이 함수들 중 하나가 호출되는지 테스트하기 위해 저장 인터페이스의 목(mock)이 필요합니다. 코드를 작성하기 시작했을 때 우리는 이 목을 수작업으로 만들었습니다. 근본적으로 이건 확장성이 없다고 결론지었습니다. 우리는 다수의 엔지니어들을 위한 지원을 할 수 없었습니다.
그래서 우리는 작은 스크립트를 만들었습니다. 나중에 조금 더 큰 스크립트가 되었습니다. 문제는 있었지만 결국 제대로 만들었습니다. 그리고 지금은 프로토콜을 위한 목을 생성할 때 script/generate-mocks만 입력하면 됩니다. 그러면 전체 소스 코드에서 프로토콜 상단에 있는 @CreateMock 문을 찾고, 우리는 어느 시점에 Swift가 속성들을 주리라 기대합니다. 그리고 목을 생성할 것입니다. 그러면 코드 베이스를 실행할 때 이 프로토콜이 Storing을 구현한 StoringMock이 될 것입니다. 그것은 프로토콜의 모든 퍼블릭(public) 함수를 구현할 것입니다. 그리고 카운터를 제공해서 얼마나 많이 호출되었는지 알 수 있습니다. 실제 함수를 구현하고 가능하다면 기본 타입을 반환합니다. 예를 들어 dataForKey에서는 옵셔널(Optional) NSData를 얻을 수 있고, 목은 nil을 반환합니다. 인터페이스를 준수하고, 입력을 테스트할 때 dataForKeyHandlers에 클로저를 설정해서 항상 호출되게 할 수 있고, 테스트에서 올바른 정보를 얻을 수 있는지 테스트할 수 있습니다.
storageDataForKey에서는 enum인 StorageResult를 반환하고, 우리는 기본적으로 enum의 첫 번째 케이스를 반환합니다. 그래서 매우 빠르게 모든 목을 생성해서 테스트할 수 있습니다. 이러한 목을 위해 100,000줄을 작성했습니다. 손으로 만들지 않기 위한 100,000줄입니다.

도구 문제
다른 나쁜 점은 도구 문제입니다. 우리는 “무한 인덱싱”(infinity indexing)이라고 부릅니다. 여러분도 아마 본 적이 있을 것입니다. 인덱서가 멈추지 않습니다. 저는 저희 프로젝트에서 이틀을 보내봤지만 어떤 이유에서인지 완료되지 않았습니다. 동시에 보너스로 328%라는 높은 CPU 사용율도 얻었습니다. 랩탑이 뜨거워지고 전원을 연결 안했다면 한 시간 반 밖에 사용할 수 없을 것입니다. 무척 이상한 현상이고, 이 현상은 코드가 커질 수록 더 문제가 되었습니다. 이전에는 이런 문제가 없었지만, 한번 200,000 또는 300,000 줄의 코드를 넘고나서는 큰 문제가 되었습니다.
또한, IDE는 이걸 시작했습니다. 이건 제가 천천히 타이핑한 게 아닙니다. 저는 모든 문자를 다 입력했습니다. 하지만 IDE는 모든 개별 키 입력을 SourceKit을 통해 맞는 코드를 입력했는지 확인합니다. 그래서 타이핑이 불가능해졌습니다.

그러면 무엇을 할 수 있나?
여려분은 이렇게 할 수 있습니다. 여러분이 원한다면. 여러분은 다른 도구로 전환할 수 있습니다. AppCode를 사용할 수 있습니다. 우리 팀 일부가 AppCode로 전환했습니다. 약간의 과정이 있습니다. AppCode에서 코드를 작성하고 Xcode에 카피 앤 페이스트를 합니다. 굉장히 이상합니다만. Facebook의 IDE인 Nuclide에 기여할 수도 있습니다. 아직 Swift를 지원하지 않지만 여러분이 추가할 수 있습니다.
기본적으로 우리는 더 많은 프레임워크를 추가하기로 했습니다. 우리는 많은 프레임워크로 우리의 앱을 쪼갰습니다. 그건 각 프레임워크는 더 적은 파일을 갖는다는 뜻입니다. 또, 각각이 다시 빨라진다는 뜻입니다. 각 프레임워크에 더 많은 파일이 있으면 더 많은 도구 문제를 가질 거라는 것입니다.
우리는 처음부터 여러 프레임워크를 갖도록 아키텍처를 정의했습니다. 다 합쳐서 70이나 80 정도입니다. 그래서 더 쪼개는 것도 쉬웠습니다. 그냥 인덱싱만 막고 싶다면 이렇게 할 수 있습니다. 흑백 텍스트만 쓰고, 코드 완성이 필요 없다고 생각하면 가능합니다. 우리 중 몇 명이 그랬습니다.

바이너리 크기
다음 나쁜 것. 바이너리 크기입니다. 모든 앱의 한도는 100MB입니다. 초과하면 와이파이로만 받아야 합니다. 여기에 몇 가지가 있습니다. 우선, struct가 바이너리 크기를 늘릴 수 있다는 것을 알아야 합니다. 목록에 struct가 있는 경우 스택에 생성되고 바이너리 크기가 커질 수 있습니다. 처음에 우리는 모든 모델을 struct로 만들었고 바이너리 크기는 80MB 정도였습니다. 그리 좋지는 않았습니다.
옵셔널 사용은 바이너리 크기를 증가시킵니다. 여러분은 옵셔널을 사용하고 있겠지만, 컴파일러가 많은 일을 한다는 건 모를 것입니다. 체킹(checking)하고, 언랩(unwrap)을 해야 합니다. 따라서 물음표가 있는 단 한 줄이라도 바이너리에서는 클 수 있습니다.
제네릭 특수화(generic specialization)는 우리가 마주쳤던 또 다른 문제입니다. 제네릭을 사용할 때 제네릭이 빠르길 원한다면 컴파일러는 특수화할 것이고, 바이너리 크기를 많이 증가시킵니다.
그리고 Swift 런타입 라이브러리가 앱에 포함되어야 합니다. 다들 12~20MB 정도라고 말합니다. 실제 다운로드 크기는 최소 4.5MB입니다. 왜냐하면 암호화되어 있지 않기 때문에 압축이 잘됩니다. 그래서 우리가 측정한 와치 앱을 포함한 세 아키텍처에서 실제 다운로드는 4.5MB였습니다.

그러면 무엇을 할 수 있나?
최적화 설정을 해볼 수 있습니다. Whole-module optimization을 조정할 수 있습니다. 때로는 바이너리를 줄이고, 때로는 커집니다. 가장 중요한 것은 여러분이 모든 사용량을 알아야한다는 것입니다. 그래서 우리는 그것을 위한 도구를 만들었습니다. 우리는 모든 심볼(symbol)을 파일로 맵핑한 다음 그 파일들을 결합했습니다. 그리고 이 멋진 도구를 만들었습니다. 앱의 폴더 구조를 탐색할 수 있고, 각 Swift 파일을 보면 앱에 기여하는 파일 크기를 얻을 수 있습니다.
이 오픈 소스를 보고 싶다면 크게 소리질러 주세요. 이걸 만든 엔지니어가 여기 있을 것입니다. 우리는 우리가 발견한 많은 것들을 오픈 소스로 만들고 있습니다.

시작 속도
다음 나쁜 것. 시작 속도입니다. 재미있습니다. 여러분이 빠른 시작을 원하면 Swift를 사용하라는 WWDC 토크를 봤다면 말이지요. 거기에는 일종의 현실왜곡장(reality distortion field)이 있습니다. 문제는 일반적으로 바이너리 갯수가 프리 메인에서 소모되는 시간에 선형적으로 영향을 줍니다. 시작 시간은 프리 메인(pre-main)과 포스트 메인(post-main)입니다. 프리 메인은 메인 함수가 호출되기 전에 일어납니다. 거기에서 많은 일들은 발생하고, 많은 다이나믹 라이브러리가 있다면 많은 시간을 소모합니다.
예를 들어 Swift 런타임 라이브러리는 아이폰 6s에서 250ms를 사용합니다. 그게 뜻하는 것은 그 250ms는 Swift를 사용함으로써 절대 돌려받을 수 없다는 것입니다. 안타깝습니다.
또한, 우리는 더 많은 프레임워크를 만듦으로써 도구 문제를 해결했습니다. 그리고 더 많은 프레임워크는 시작 속도를 더 느리게 만듭니다.

그러면 정말로 무엇을 할 수 있나?
전부 바이너리에 다시 링크(re-link)할 수 있습니다. 그게 우리가 한 일입니다. 그래서 우리는 모든 프레임워크를 빌드했습니다. 그 다음 그 프레임워크의 모든 심볼을 가져와서 스태틱 바이너리(static binary)에 연결하는 포스트 빌드 과정을 만들었습니다. 그게 우리가 시작 속도를 얻은 방법입니다.
이 과정이 없을 때 아이폰 6s에서 실행할 때까지 아마 4~5초는 걸렸을 것입니다. 아이폰 4S는 더 느렸을 것이고요. 이 트릭을 써서 줄일 수 있었습니다. 시작 속도에 관심이 있다면 초반부터 테스트해야 합니다. Xcode가 제공하는 툴에 의존할 수 없습니다. 예를 들어, 프리 메인에서 얼마나 사용하고 있는지 알려주는 것처럼 말입니다. 그 수치는 잘못된 것입니다. 그것은 전혀 현실성이 없습니다.
장치에 엔터프라이즈 프로비저닝 프로파일이 있는 경우 문제가 있습니다. 프로비저닝 프로파일을 얼마나 많이 가지고 있는지에 따라 읽어들이는데 10초가 걸릴 수도 있습니다. 그리고 신기하게도 유난히 느린 두 단말이 있었습니다. 아이폰 6는 어째서 다른 것보다 10배 느리게 실행되는지 알 수 없었습니다.
다시 링크를 하는 동안 포스트 메인 시간을 향상시키기 위해 무언가를 할 수 있습니다. 우리가 시도하고 있는 한 가지는 DTrace를 사용해서 어떤 심볼이 시작 과정에 접근하는지 조사하는 것입니다. 왜냐하면 그것을 다시 링크하기 때문입니다. 우리는 그것들이 올바른 순서로 링크하는지 확인합니다. 오래된 단말에서 메모리에 너무 많은 메모리 페이지를 로드할 필요가 없습니다. 대신 시작하는 동안 메모리에서 최적화된 페이지 집합을 읽어야 합니다. 예비 테스트에서 아이폰 4S의 프리 메인 시간과 포스트 메인 시간을 20% 향상시켰습니다. 이걸 넣어서요.

거지같은 것
거지같은 것입니다. 여러분은 어제, 또는 지난해 Swift 서밋에서 이것을 봤을 것입니다. 컴파일 속도는 끔찍해서 우리에게는 심각한 문제가 되었습니다. 우리 기본 앱을 클린 빌드하면 15~20분이 걸립니다.
우리는 심각하게 생각해서 팀 전원에게 물어봤습니다. 얼마나 큰 문제인가?
우리가 물어본 본 건 다음과 같습니다:

코드 작성과 관련하여 긍정적이고 부정적인 경험을 모두 고려했을 때, 어떤 언어가 앞으로 Uber의 iOS 개발을 위해 더 낫다고 생각하십니까?

계속해서 Swift를 사용 또는 Objective-C로 돌아간다.
이것이 결과입니다. 반반입니다.

절반의 사람들은 모든 결함, 도구 문제, 컴파일 속도를 앉고 Swift에 머무르고자 하고, 다른 사람들은 Objective-C로 돌아가고자 합니다.
우리는 다른 질문을 추가했습니다:

당신이 Swift에서 한 두가지를 바꿀 수 있다면 마음을 바꾸겠습니까?

결과는 이렇습니다.

17%는 평생 Objective-C를 한다고 했습니다. 하지만 23%는 컴파일 속도만 빠르면 된다, 그게 Swift에 있는 가장 큰 문제라고 말했습니다. 컴파일과 인덱싱이 빨라지면 된다는 게 10%. 그리고 나머지는 디버거 같은 도구 문제 그리고 다른 두 가지였습니다.
다행이도 이건 우리가 해결할 수 있는 문제입니다. 컴파일 속도에 대한 것뿐이라면 해결할 수 있습니다.

컴파일 속도 문제 해결

우리는 알아보기 시작했습니다. 첫 번째는 우리가 Swift에 기여할 수 있다는 것입니다. 여러분들도 그렇습니다. 우리는 타입 추론을 사용하지 않는 시도를 했습니다. 그리고 SourceKit을 사용해 모든 타입을 파악할 수 있는 포스트-빌드 스크립트를 작성했습니다. 그리고나서 코드가 모든 타입 정보를 포함하도록 변경했습니다.
마지막으로 우리는 파일을 합치기 시작했습니다. 그리고 우리는 200개의 모델을 하나의 파일로 합치면 컴파일 타임이 1분 35초에서 17초로 줄어든다는 것을 알았습니다. 이거다 싶었습니다. 흥미롭습니다. 모든 것을 하나로 합치면 훨씬 빨라집니다. 그 이유는 컴파일러가 모든 개별 파일에 대해 타입 체킹을 하기 때문입니다. 따라서 Swift 컴파일러의 프로세스를 200개 생성하면 올바른 타입을 사용하고 있는지 모든 파일을 200번 확인해야합니다. 그래서 모두를 하나로 합치면 훨씬 빨라집니다.
우리는 우연히 알게됐습니다. AirBnB는 이 트릭을 발견하고 2주전에 우리와 공유했습니다. 이것입니다. 굉장히 재미있습니다. Whole-module optimization이 정확히 우리가 원하는 것을 했습니다. 모든 파일을 하나로 컴파일 합니다. Whole-module optimization의 문제는 최적화를 하기 때문에 굉장히 느리다는 것입니다. 하지만 User-Defined 커스텀 플래그 SWIFT_WHOLE_MODULE_OPTIMIZATION을 추가하고 YES로 설정하고, Optimization LevelNone으로 설정하면, Whole-module optimization을 최적화 없이 실행합니다. 그러면 미친듯이 빨라집니다.
앱을 프레임워크로 나누었다면 적어도 지금은 필요한 것입니다. 애플이 속도 문제를 해결할 때까지는. 그래서 우리는 이것을 구현했고 이번 주말에 정착시켰습니다. 그리고 우리는 20분 빌드 타임에서 6분 빌드 타임이 되었습니다. 우리의 가장 큰 프레임워크는 core flow 입니다. 그것은 900개의 파일을 가지고 있고, 예전에는 4분만에 컴파일 되었지만 지금은 23초입니다. 증분 빌드(incremental builds)는 안되게 됩니다. 하지만 가장 큰 라이브러리에서 23초의 빌드 타임이라면 상관 없습니다. 다른 타켓 대부분은 파일이 훨씬 적으며 훨씬 더 빠르게 진행됩니다.

Uber는 Facebook의 Buck을 지원하고, Swift 지원을 추가
우리는 무언가 더 할 수 있습니다. Whole-module optimization을 하면 CPU 사용량이 30%까지 낮아지기 때문입니다. “좋아, 우리는 뭔가 더 할 수 있어.” 우리는 Objective-C 쪽에서 Buck을 사용하고 있었습니다. Buck은 우수한 의존성 관리자, 신뢰할 수 있는 증분 빌드 및 원격 빌드 캐시입니다. Facebook이 만든 빌드 시스템입니다. 빌드 타임에 문제가 있다면 이것을 알아보세요. 우리는 이전 Objective-C와 안드로이드 빌드에서 그것을 했고, 클린 빌드가 4배 빨라졌습니다. 증분 빌드 원격 빌드 캐시를 사용해서 20배 빨라졌습니다. 여러분이 여러 타켓을 컴파일하고 있고 다른 사람이 다른 장치에서 컴파일 하면, 리모트 빌드 캐시를 사용할 수 있고 그 부산물을 사용합니다. 따라서 아무 것도 다시 컴파일 하지 않습니다. 안드로이드에서는 더 빨라집니다. 클린 빌드는 6배 빨라지고, 증분 빌드는 말도 안되게 빠릅니다.
Swift 용은 아닙니다. 하지만 우리가 작업하고 있습니다. 우리는 Facebook에 Swift 지원을 기여했습니다. 우리는 Xcode 파일 생성을 위한 Swift 지원을 시작했습니다. 오늘 중에 가능하다고 생각합니다. 우리는 내부적으로 사용하고 있습니다. Buck에게 폴더 구조를 기반으로 프로젝트 파일을 생성하도록 할 수 있습니다.
다음으로 우리는 Buck 빌드에 Swift를 추가하고 있습니다. 우리 앱을 빌드하기 위해 Buck을 사용할 수 있다는 의미입니다. 마지막으로 Buck을 Xcode에 통합시키려고 합니다. cmd + B 를 누르면 Xcode 빌드를 사용하지 않고 Buck을 사용해서 빌드하게 됩니다.
우리의 생각으로는 Buck을 사용하면 6분의 빌드 타임이 2분 이하가 될 것 같습니다. 그러면 Swift 컴파일 타임 문제가 해결될 것입니다. Buck 저장소를 팔로우하기만 하면 가능합니다.

요약

요약은 여기에 있습니다.
여러분의 팀이 커지고 있다고 느낀다면 컴파일 타임과 바이너리 크기, 유닛 테스트(unit test)하는 방법, Buck 시작하기를 살펴보세요.
긍정적인 얘기를 하자면 여러분의 팀이 작다면 아마 이런 문제에 빠지지 않을 것입니다. 여러분의 팀이 크다면 이런 문제에 빠지겠지요.
하지만 여러분에게는 여러분의 도울 엔지니어가 있을 것이고, 모든 문제들에 대한 해결책은 있습니다.
이상입니다.
uber.github.io에서 오픈 소스 프로젝트를 팔로우 해주세요. 그리고 eng.uber.com에서 아키텍처에 대한 블로그 포스트를 할 것입니다.
이상입니다. 감사합니다.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.