스위프트 증분 빌드는 어떻게 동작하고 있나

Jung Kim
7 min readMar 24, 2021

--

이 글을 작성하게 된 계기는 자바 gradle 빌드 과정에서 SOLID 원칙에서 말하는 ISP 인터페이스 분리를 적용하면 증분 빌드가 동작한다는 블로그 때문이었다. 이 글을 보고 궁금한 점은 스위프트 컴파일러도 동일하게 동작할까? 였다.

그래서 직접 빌드해봤다.

우선 Xcode에서 스위프트 증분 빌드가 지원된 게 Xcode 10부터 llbuild 기반으로 새로운 빌드 시스템 덕분이라고 했으니 관련 자료를 찾았다. 역시 친절하게도 zedd님 블로그가 먼저 노출되더군요 (🤓 대단한 분…) 블로그 내용은 WWDC18 415세션 Behind the Scenes of the Xcode Build Process 앞부분을 포함하고 있다. 뒷부분은 안쓰셨네요? 이 세션에 중요한 정보가 많이 있지만 스위프트 내용보다는 Objective-C와 관련된 내용이 좀 더 많다.

가장 궁금한 부분은 스위프트 증분 빌드를 위해서 의존성(dependency)를 찾는 방식에 대한 설명인데 상세하게 설명하지 않고 넘어간다. (아마도 WWDC 당시에는 베타라서 다 만들지 않았기 때문일꺼다. 새 빌드 시스템이 잘 동작하지도 않았던 것 같다…)

WWDC18 415. Behind the scene of the Xcode Build Process (173P)

Xcode 9까지는 모든 스위프트 파일을 계속 빌드하는 구조였다면, Xcode 10부터는 스위프트 파일 이외에 부가적인 파일을 생성하고 그 파일을 공유하며, 의존성에 따라서 그룹을 나눠서 병렬로 컴파일해서 빌드가 빨라진다는 내용이다.

Xcode 12기준으로 소스 파일을 빌드하는 과정에서 어떤 파일들이 생기고, 어떻게 증분 빌드가 동작하는 지 살펴보자.

새로운 macOS CLI 프로젝트를 하나 만들고 나면 main.swift에 Hello World만 있는 프로젝트가 생긴다.

새로운 프로젝트들은 SWIFT_COMPILATION_MODE 디버그 환경에 대해서 Incremental (singlefile값)으로 되어 있어서 증분 빌드를 지원하는 상태다. 릴리스 환경처럼 Whole Module로 되어 있으면 모듈 전체 스위프트 파일을 컴파일하기 때문에 증분 빌드가 동작하지 않는다.

이 프로젝트를 처음 클린상태에서 빌드해하면 빌드 로그는 다음과 같다.

빌드에 필요한 디렉토리를 생성하고 WWDC18 영상에서도 설명이 나오는 헤더맵(.hmap) 파일이 7개가 생긴다. 추가적으로 .SwiftFileList, .OutputFileMap.json, .LinkFileList 파일을 생성한다. 그리고 나서야 하나 뿐인 main.swift 소스 파일을 컴파일한다. 당장은 증분 빌드가 관심이 있기 때문에 링크 이후 단계(phase)는 생략하자.

위의 파일들은 어디에 생성될까? 🤔

빌드 순서상 가장 처음에 하는 동작은 /Users/godrm/Library/Developer/Xcode/DerivedData/Sampler-cunmxdgrqwvmldgynerpmjtlgoda/Build/Intermediates.noindex 디렉토리를 생성하는 것이다. 바로 여기 아래에 /Sampler.build/Debug/Sampler.build/Objects-normal/x86_64 를 더 붙이면 .o 파일이 생기는 경로가 된다.

위에서 말한 경로에서 main.o 오브젝트 파일을 찾았다. 이 경로에 있는 파일들은 분류하면 다음과 같다.

소스파일마다 의존성과 관련된 파일들 정보를 포함하는 파일이 생기고, 타깃(모듈)마다 의존성 정보를 포함한 파일이 생긴다. 이 중에서 .swiftdeps 파일이 의존성에 대한 내용을 담고 있다. print(“hello world”) 한 줄 있는 main.swiftdeps 파일 내용은 다음과 같다.

이것만 봐서는 잘 모르겠으니까 새로운 Essentials.swift 스위프트 파일을 추가해보자. 간단하게 문자열 배열을 리턴하는 구조체를 선언했다.

그리고 main에서 let play = Essentials() 인스턴스를 생성하고 print(play.list())를 호출하도록 수정했다. 그리고 빌드하면 main.swiftdeps 파일을 다음과 같이 바뀐다.

새로운 스위프트 파일이 추가됐으니 Essentials.swiftdeps 파일도 생겼다.

위 파일들을 보면 타입 이름, 인스턴스 이름, 변형된 mangling 함수 이름, public 인지 private 인지 명시까지 포함되어 있다. 그리고 마지막에 interface-hash 값이 있는데 이 값이 중요한 값이다.

WWDC18 408 Building Faster in Xcode 세션에서 Swift Dependency Rules에는 다음과 같이 설명한다.

Swift Dependency Rules

Compiler must be conservative
Changes in function bodies do not affect the file’s interface
Dependencies within a module are per-file
Dependencies across targets are for the whole target

설명처럼 전체 타깃을 대상으로 할 때는 타깃도 의존성을 갖게 된다. 하나의 모듈 내에서는 파일 단위로, 함수의 구현부는 바뀌어도 의존성에 영향을 주지 않는다. 의존성을 판단하는 데이터는 구현과 상관없이 선언 declarations만 관련이 있다. 의존성에 영향을 주는 선언부 — 인터페이스에 대한 해시값을 맨 마지막에 적어놓는다. 만약 Essensials.swift 파일의 list() 함수 내부 구현을 바꾸거나 main에서 호출하지는 않는 새로운 함수를 추가하면 어떻게 될까?

빌드 로그 상에서는 Essentials.swift 파일도 컴파일하고 변경사항이 없는 main.swift 파일도 컴파일하는 것처럼 보인다. 실제로 그런지 빌드 파일을 살펴보자.

Essentials.o 파일은 새로 빌드한 시각에 생성이 됐지만, main.o 파일은 새로운 파일이 만들어지지 않았다. 대신 의존성에 대한 파일들 .d, .dia, .swiftdeps 파일만 변경됐다. .swiftdeps~ 파일은 바로 직전의 의존성 파일을 백업해놓은 것이다. 새로운 의존성 파일을 만들고 나서 해시값을 비교해서 동일한 값이면 swiftc가 동작하지 않는 구조다.

요약

  • Xcode 10 이후 llbuild 기반으로 파일 단위로 증분 빌드가 동작한다.
  • Xcode 빌드 로그상으로는 차이가 없어 보이지만, 의존성을 분석하고 인터페이스 해시값을 비교해서 오브젝트 .o 파일을 생성한다. 물론 구현부가 바뀌면 해당 파일 오브젝트는 다시 생성한다.
  • 만약 의존성을 가지는 다른 소스파일에 대한 인터페이스 해시값이 동일하면 오브젝트 파일은 생성하지 않는다.
  • 스위프트 파일 의존성 정보는 .swiftdeps 파일에서 확인할 수 있다.
  • 파일 단위로 swiftmodule의 일부분 partial을 만들어 놓고 링크할 때 전체 모듈을 합친다.
  • 자바 gradle 빌드 경우처럼 ISP로 나눈다고 해서 인터페이스로 나눠지고 변경이 있는 파일만 빌드되지는 않는다.
  • 스위프트 소스 파일을 컴파일할 때는 구현부가 바뀐 파일은 다시 컴파일하지만, 선언부를 중심으로 의존성을 파악하기 때문에 선언이 바뀐 파일을 의존하는 경우만 다시 빌드한다.

--

--