Xcode: 비밀스러운 인덱스 저장소

Jung Kim
9 min readApr 6, 2022

그 숨겨진 비밀의 문을 열어봅시다🧐

익숙하게 만나는 Xcode 화면입니다. 나 인덱싱 중이니까 상태에 말 시키지마.

빌드Build와 인덱스Index

빌드와 인덱스는 거의 같은 동작하지만, 실제로는 조금 다릅니다. 빌드는 프로젝트에 설정되어 있는 타깃 단위로 제품을 만드는 과정입니다. 흔히 소스 파일을 컴파일해서 목적 파일을 만들고, 목적 파일들을 링크해서 바이너리를 생성합니다. 바이너리와 함께 앱 번들을 만들기 위해서 리소스도 빌드합니다.

인덱스는 빌드를 위한 게 아니라 빌드를 하는 과정에서 만들어지는 개발 도구에서 심볼을 빨리 접근하기 위한 말 그대로 색인Index입니다. Xcode를 위한 데이터인거죠.

LLVM clang은 C/C++/Objective-C 소스를 컴파일 하면서 부가적으로 Index를 만들어 냅니다. -index-store-path 옵션과 경로를 붙여서 원하는 경로를 지정할 수 있습니다. Swift 컴파일러 swiftc도 clang을 fork해서 만들었기 때문에 같은 옵션을 제공합니다.

빌드할 때 심볼에 대한 색인을 생성하는 것은 컴파일러가 3-5% 정도 더 일을 하게 된다고 합니다. clang과 swiftc는 3.2% 정도 오버헤드가 걸린다는 2017년 발표자료가 있습니다

Index 저장소

빌드 과정에서 만들어진 심볼 색인 — Index을 관리하는 것은 IndexStore DB라고 부릅니다. Index 저장소는 빌드 결과가 저장되는 DerivedData 경로 아래에 있습니다.

맨 처음이 빌드할 때마다 파일이 생성되는 Build 폴더이고, 안에는 Debug/Release Configuration에 따라 빌드된 타깃 Products가 있습니다. 예제코드는 HelloPlane 이라는 샘플 프로젝트의 DerivedData 경로입니다.

색인 저장소 경로 DataStore

두 번째 폴더가 바로 Index 저장소 경로이고, 그 아래에 DataStore 폴더가 인덱스가 저장되는 색인 저장소입니다. 거기에 있는 v5가 실제로 기록이 보관된 경로입니다.

애플의 깃헙 저장소에는 IndexStore-DB 라는 스위프트 오픈소스가 있습니다. 이 프로젝트가 바로 DataStore 아래 있는 IndexStore에 접근할 수 있는 기능을 포함하고 있습니다. 스위프트로 실제 구현체를 다 포함하고 있지는 않고, libIndexStore.dylib 에서 제공하는 API를 wrapping해서 제공하고 있습니다. 다이나믹 라이브러리 경로는 Xcode 내부에 있습니다.

/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libIndexStore.dylib

DataStore/v5/ 경로 아래에는 units와 records 경로가 있습니다.

units는 해당 프로젝트에서 사용하는 모든 유닛 파일이 목적 파일 수준에서 저장되어 있습니다. 샘플 프로젝트는 iOS 앱이 아니로 macOS 콘솔 앱이지만 작성한 소스 파일 외에 32개 목적 파일이 더 있습니다. 커널 수준의 MachO, Darwin부터 Foundation, CFNetwork, Objective-C와 SwiftShims까지 다양한 시스템 파일까지 pre-compiled 된 상태로 저장됩니다.

또 다른 폴더 records에는 해싱된 형태로 0A부터 ZZ까지 하위 폴더가 존재하고, 그 아래는 파일별로 심볼이 저장된 레코드 파일이 저장됩니다.

유닛unit과 레코드record

clang이 컴파일하면서 유닛과 레코드를 생성하는 개념을 살펴보겠습니다

captin.c와 spider.c 구현 파일이 있습니다. captin.c은 first.h, avengers.h, iron.h 를 include하고, spider.c는 avengers.h 와 iron.h만 include합니다.

C preprocessor가 #define WINTER를 처리하느냐 안하느냐에 따라서, #ifdef로 처리된 avengers.h 구현 내용을 달라질 수 있습니다.

그래서 컴파일러가 빌드하면서, captin.o 유닛은 captin.c와 first.h, avengers.h, iron.h 각각 의존성을 갖고 빌드 시점에 고유한 hash값을 붙여줍니다. 조금이라도 변경이 생겨서 다시 빌드하면 hash값이 바뀌고 바뀐 목적파일을 참조합니다

그래서 captin.o와 spider.o 목적 파일이 참조하는 레코드는 각각 hash값이 달라질 겁니다. iron.h는 둘 다 동일하게 접근한다면 같은 hash를 갖게 됩니다.

결과적으로 유닛마다 의존성을 가지는 레코드를 포함하는 구조가 만들어집니다

레코드Record에서 심볼Symbol 찾기

레코드는 기본적으로 파일마다 생성되고, 고유한 hash값을 가지고 어느 유닛에서 접근하느냐에 따라 다른 심볼을 포함할 수도 있습니다. 그렇게 레코드마다 심볼 목록을 갖고 있습니다.

프로젝트에 포함되어 있는 소스 파일을 알고 있으면, IndexStore-DB API를 통해서 해당 레코드 안에 있는 심볼 목록에서 심볼을 찾을 수 있습니다.

심볼Symbol이란

심볼은 컴파일러가 문법에 맞춰서 분석하는 대상들과 대부분 일치합니다만, dynamic cast를 하는 경우를 구분하기도 하고 단위 테스트용 타깃에 있는 것을 구분하기도 합니다. 컴파일러가 편의상 생성하는 getter:x 나 속성을 let으로 했을 때 생성하는 setter:color 같은 implicit 인스턴스 메소드도 포함됩니다.

심볼은 위에 설명한 문법적인 종류Kind도 있지만 고유한 모듈과 파일, 타입이나 이름까지 포함하는 변형된 mengled USR을 갖고 있습니다. Swift 뿐만 아니라 C나 Objective-C를 포함하는 Language, SubKind와 Properties 속성도 갖고 있습니다. 속성으로는 제네릭인지, 단위 테스트용인지, IBOutlet이나 지역 변수인지 Xcode를 위한 속성을 포함합니다.

낯선 심볼과 만남 Occurrences

레코드마다 선언되어 있는 심볼 목록이 있고, 추가적으로 어떤 심볼이 다른 심볼과 관계가 있는지 나타내는 출현 Occurrences 목록이 있습니다 (어쩌다보니 occurrence만 번역한 게 되어 버렸네요 ㅎㅎ)

유닛과 레코드, 심볼 목록과 출현 목록을 그림으로 나타내면 다음과 같습니다

출현Occurrence는 특정 심볼이 다른 심볼과 어떤 관계relation인지 표현하는 relations를 포함할 수 있습니다. 선언한 definition인 경우는 relation이 없을 수도 있습니다.

예를 들어 ColorCode는 Enum Kind로 Symbol 1이 됩니다. 슈퍼 클래스인 Shape 는 Symbol 5가 되고, Line은 Symbol 6가 됩니다. 심볼 목록은 순서가 중요하지 않고 빌드할 때마다 바뀔 수 있습니다.

출현 목록에서 보면 Symbol 선언부는 relation이 없지만, Shape를 상속한 Line의 선언 부분에서는 BaseOf 형태로 참조 관계가 있는 것을 알 수 있습니다.

위 예제는 LLVM Developer Meetup 2017-10 행사에서 애플 엔지니어들이 발표한 Adding Index‐While‐Building and Refactoring to Clang https://llvm.org/devmtg/2017-10/#talk9 내용을 Swift 버전으로 바꿔본 것입니다.

Xcode 색인 저장소를 활용하면...

저는 IndexStore-DB를 활용해서 스위프트 코드들 의존성을 시각화하는 오픈소스 프로젝트 SWAN을 만들고 있습니다. Xcode 프로젝트 파일을 열어서, 특정 타깃에 빌드하는 소스 파일들 중에서 스위프트 파일을 찾고, 그 스위프트 파일들 유닛에 레코드를 뒤져서, Symbols 와 Occurrences 데이터를 탐색해서 그래프를 그립니다. 그래프는 graphviz CLI 도구를 사용하고 있습니다.

기존에는 reference 관계를 가지는 출현만 찾다가 중복된 경우가 많고 타입 단위 의존성, 파일 단위 의존성, 모듈 단위 의존성을 분석하고 싶어서 이 내용을 정리하게 됐습니다.

색인 저장소를 정리해보니 다음과 같은 내용을 알게 됐습니다

  • 빌드와 색인은 동시에 이루어지지만 같은 데이터는 아니다
  • 색인을 분석하면 원하는 심볼에 대한 활용도를 찾을 수 있다
  • 파일을 바꾸면 색인은 해시만 바뀔 뿐 계속 생긴다 (지워지지 않는다!!)
  • 클린 빌드를 해도 색인은 지워지지 않아서, 강제로 지워도 Xcode를 시작해야 바뀐다
  • 색인에 있는 심볼과 출현은 생각보다 복잡하고 중복이 많다
  • 그래프를 이쁘게 그리는 것은 디자이너가 필요하다
  • 프로젝트와 타깃에 포함된 소스 파일을 찾으려면 .xcodeproj/project.pbxroj를 분석해야 한다

--

--