빌드세팅의 Mach-O 타입을 설정하여 빌드할 타겟이 어떤 결과물이 될 것인지 설정할 수 있다.
그중 가장 대표적인 형식이 정적/동적 라이브러리이다.
여러 기술블로그들을 통해 "정적라이브러리는 리소스를 가지지 않는다." 같은 내용은 많이 봐왔다.
하지만 이러한 내용은 실제개발시 발생한 문제를 해결하는데 별로 도움이 되지 않았다.
아래 사진과 같이 복잡한 모듈 구조에 대해 각각의 모듈들이 어떤 형식을 가져야 최적화 될 수 있을까?
Tuist를 사용하여도 빌드시 코드가 중복될 수 있고, 심볼 충돌이 발생할 수 있다. 이를 어떻게 해결할 수 있을까?
이번글이 모든 문제를 해결하는 해결책은 아니지만, 개발을 하며 알아낸 사항들에 대해 다뤄 보려고한다.
빌드결과물
빌드의 가장 작은 단위는 Swift파일이다.
바이너리형식으로 컴파일된 파일들은 실행되기위해 최종적으로 실행파일에 포함되어야한다.
여기서 말하는 실행파일을 만들 수 있는 있는 형식은 2가지로 App, 동적 라이브러리 있다.
정적라이브러리
정적라이브러리는 리소스를 가지지 않는다.
왜 가지지 않을까? 정확하게는 가지지 못한다가 맞다.
정적 라이브러리는 자체 실행파일을 가지는 번들이 될 수 없기 때문에
즉, 번들이 아니기 때문에 자체적인 리소스를 가질 수 없다.
프레임워크는 리소스를 가질 수 있다고 들었는데.. 그럼 정적 프레임워크는 대체뭐냐???
정적프레임워크는 정말 미꾸라지 같은 놈이다..
정적 라이브러리도 독자적인 리소스를 가지게 하기위해 만들어진 것 같다.
정적 프레임워크의 빌드 결과물은 바이너리와 리소스를 포함한 번들로 구성된다.
해당 Bundle은 가장 가까운 실행파일속 번들에 자동으로 복사되어 포함되게 된다.
(Tuist static framework & SPM 자동으로 포함된다, 일반 Xcode로 생성시 자동으로 포함되지 못함)
그러면 해당 번들에 접근하려면 어떻게 해야할까?
먼저 Symbol을 이해해야 한다.
Symbol이란?
컴파일된 함수, 변수, 클래스등은 Symbol로 컴파일되어 구분되게 된다.
만약 A라는 모듈이 B모듈의 특정 클래스를 의존할 경우, B모듈은 해당 클래스의 심볼을 복사하여 가지게된다.
이렇게 가지게된 심볼을 토대로 링킹단계에서 적절하게 연결된다.
링킹과정에서 A모듈이 정적라이브러리일 경우 컴파일 타임에 B모듈이 의존하는 Class에 대한 바이너리 파일을 포함하게 된다.
(B모듈이 실행파일을 생성할 때)
만약 동적라이브러리라면, 링킹시 주소를 참조하게되고 런타임에 접근한다.
Swift컴파일러는 정말 똑똑하게도 같은 모듈이라도 필요한 심볼의 바이너리만 쏙쏙 뽑아서 가져온다.
모듈 B가 A모듈을 의존하여도 어떠한 타입도 사용하지 않으면 아무런 심볼도 가져오지 않고(= 링킹 되지 않음)
가져와도 필요한 심볼만 가져와 포함되는 바이너리파일을 최소화한다.
즉.. 정적라이브러리는 빌드시 갈기갈기 찢겨 최적화될 수 있다. ㅋㅋㅋ....👍
자, 다시 돌아와서
복사된 번들에 접근하기 위해선 심볼이 필요하고 심볼을 가지려면 해당 심볼을 활용해야 합니다.
직접적으로 사용하지 않더라도, 사용하는 타입이 내부적으로 특정 타입을 필요로 할 경우 해당 타입의 심볼도 포함되게 됩니다!! (중요!)
복잡하지만 다음과 같이 접근할 수 있다.
※ 정적라이브러리 역시 심볼을 가질 수 있다, 해당 라이브러리가 다른 모듈에 의존될 경우
보유한 모든 심볼(간접적으로 라도 사용되는)이 함께 복사되어 전달된다.
Tuist에서 정적프레임워크 모듈 접근하는법
Tuist는 프레임워크 형식에 상관없이 모두 자신만의 번들에 접근할 수 있는데, 어떻게 가능할까?
Tuist는 BundleFinder라는 타입을 내부적으로 생성한다. 그리고 해당 타입을 사용해 프레임워크 번들을 찾도록 한다.
외부 모듈에서 BundleFinder를 직접사용하지 않지만,
실행파일의 보유 심볼을 출력하면,
BundleFinder타입 심볼이 포함된 것을 확인할 수 있다.
간접적으로 사용되기 때문이다.
해당 심볼이 포함된 번들의 리소스 경로에 번들의 이름을 붙여 번들을 찾게된다.
때문에 사실상 실행파일에 의해 의존되는 모든 정적 프레임워크는 자신의 번들에 접근할 수 있는 구조가 된다.
실행파일로 번들이 복제되고, 심볼역시 복제되기 때문이다.
RootFeature, SplashFeature 등등 모든 Feature들은 정적 프레임워크로 선언되어 있다.
앞서언급된 대로라면 최종 실행파일인 앱모듈에 모든 번들이 존재해야한다.
앱 패키지 내부에 모든 Feature의 리소스 번들이 존재하는 것을 확인할 수 있다.
다수의 번들이 존재하지만, Bundle(for:).resourceURL + 번들이름을 통해 사용하려는 번들을 특정한다.
Tuist를 사용하면 정적프레임워크는 위와 같은 방법을 통해 자신만의 독립된 리소스번들을 관리하고 접근한다. 😎
결론
설명이 조금 복잡했지만 정리하자면 이렇다.
심볼이 어떻게 복제되는 지에 따라, 바이너리 중복 여부를 감지할 수 있다. (nm 명령어를 잘활용해보자🙌)
아래의 경우 복제된 심볼이 다시 복제되어 충돌이 발생할 수 있다.
이제 해당 문제에 대해선 이유를 설명할 수 있다.
모듈 의존성 그래프를 파악하고 적절한 프레임워크 및 라이브러리 타입을 설정해보자 😄😄
감사합니다 :D
Joys of accessing a bundle of a static framework in Xcode
As maintainers of Tuist, we get to work with so many interesting projects. Today was one of those days – this time I got to deep dive into the world of static framework symbols, resource bundle accessors, linking, and more. Whenever I hit an issue in Tui
marekfort.me
'iOS공통' 카테고리의 다른 글
[iOS] fastlane을 사용하며 겪은 일들(with Tuist) (0) | 2025.01.02 |
---|---|
[iOS] 나만의 이미지 캐싱 SDK만들기(다운샘플링, 디스크 & 메모리 캐싱) (0) | 2024.11.22 |
[iOS] 케어밋 프로젝트 기술회고2: 재사용가능한 UI에 대해서 (0) | 2024.10.26 |
[iOS] 케어밋 프로젝트 기술회고1: 클린아키텍처 (4) | 2024.10.10 |
[iOS] 첫 어플리케이션 배포 심사 회고 (1) | 2024.09.26 |