티스토리 뷰

안녕하세요 Mobile Application Team 에서 iOS 개발을 하고 있는 강수진입니다.

이번 글에서는 지마켓 iOS 프로젝트에서 사용하고 있는 프레임워크들의 관계와, Framework 에서 다른 Framework 를 사용할 때 Mach-O type 에 따른 주의 사항을 알아보겠습니다.

상황

Gmarket 에서 사용하고 있는 framework 는 다양하지만, 문제에 집중하기 위해 UI 와 Util 이라는 두 가지 framework 를 사용한다고 상황을 간략화해보겠습니다.

이때 Util.framework 는 유틸성 코드를 담고 있어서 UI.framework 에서도 사용합니다. 따라서 아래와 같이 사용 관계를 표현할 수 있습니다.

위 상황을 관리하기 위해 프레임워크들의 관계를 어떻게 설정할 수 있을까요?

하나의 방법으로는 양쪽에서 사용되는 Util 을 dynamic framework 로 설정하고, Gmarket 과 UI 각각의 Frameworks & Libraries 에서 Util 을 사용한다고 명시할 수 있을 것 같습니다.

또 다른 방법으로는 Util 을 Static framework 로 설정하고, 우선 UI 의 Frameworks & Libraries 에서만 Util 을 사용한다고 명시합니다. 그리고 Gmarket 의 Frameworks & Libraries 에서는 UI 에 대해서만 명시합니다. 그러면 최종적으로 UI 에서는 Util 을, 지마켓에서는 Util 과 UI 모두 사용할 수 있을 텐데요, 왜냐하면 Util 은 static framework 라서 어차피 UI 의 최종 파일에 같이 포함되어 있을 것이기 때문입니다.

참고로 UI 프레임워크는 코드 수정이 빈번해 dynamic framework 로 설정이 되어있고, Util 프레임워크는 static 으로 할지, dynamic 으로 할지 고민 중인 상황이었는데요, 각 케이스 별로 설정 시 어떤 점을 주의해야 할지 살펴보겠습니다.

Util as Dynamic Framework

첫 번째로 "양쪽에서 사용되는 Util 을 dynamic framework 로 설정하고, Gmarket과 UI 각각의 Frameworks & Libraries 에서 Util 사용을 명시" 의 상황을 조금 더 구체적으로 살펴보겠습니다.

Util 프레임워크의 Mach-O type 을 Dynamic 으로 설정했기 때문에, output 은 별도의 executable binary 로 생성됩니다. 따라서 Gmarket 과 UI 프레임워크 두 곳에서 모두 해당 프레임워크를 사용한다고 명시해야 합니다.

In Gmarket

In UI framework

여기서 Util 에 대한 Embed 옵션이 Gmarket 과 UI 에서 다르게 설정이 되어있는데요, Gmarket 에서는 Embed & Sign 으로, UI 에서는 Do Not Embed 로 Util 을 설정합니다. 그 이유는 잠시 후에 살펴보겠습니다.

이렇게 빌드를 하고 Gmarket.app 패키지 내용을 보면 Frameworks 폴더 하위에 Gmarket에서 Embed & Sign 으로 설정한 UI, Util 프레임워크가 있는 것을 볼 수 있습니다.

느낌상 이 경로에 있는 동적 라이브러리들을 사용할 것 같죠? 그럼 실제로 각 프로그램에서 사용하고 있는 동적 라이브러리의 path 를 어떻게 확인할 수 있을까요? 바로 otool 명령어의 -L 옵션을 통해 사용하고 있는 shared library 의 목록을 프린트해보겠습니다.

우선 Gmarket 부터 확인해 봤을 때 @rpath 에 있는 Util 과 UI 를 사용하고 있네요

@rpath 는 Xcode 에서 Runpath Search Paths 에 해당하는데 이는 @exeutable_path/Frameworks 로 설정이 되어있습니다.

즉, 우리가 방금 살펴봤던 경로에 있는 Util 과 UI 프레임워크들 사용하는 것을 알 수 있습니다.

다음으로 UI 프레임워크에서 사용 중인 shared library 의 목록을 프린트해보겠습니다. 마찬가지로 @rpath 에 있는 Util 과 UI 를 사용하고 있네요

UI 프레임워크에서는 rpath 가 @exeutable_path/Frameworks 와 @loader_path/Framworks로 설정이 되어있습니다.

실제로 Util.framework 를 찾기 위해 어느 경로들을 탐색하는지 살펴보기 위해, Gmarket 에 embed 된 Util.framework 를 임시로 삭제하고 run 을 해봤습니다.

dyld[29843]: Library not loaded: @rpath/Util.framework/Util
  Referenced from: <4E332D1A-7DFD-3CBE-BDE0-D766170E343E> /private/var/containers/Bundle/Application/9F372A14-0CD8-44EF-A907-D323BA946D3A/Gmarket.app/Frameworks/UI.framework/UI

Reason: tried: 
'/private/var/containers/Bundle/Application/9F372A14-0CD8-44EF-A907-D323BA946D3A/Gmarket.app/Frameworks/Util.framework/Util' (errno=2), 
'/private/var/containers/Bundle/Application/9F372A14-0CD8-44EF-A907-D323BA946D3A/Gmarket.app/Frameworks/UI.framework/Frameworks/Util.framework/Util' (errno=2), 
'/private/preboot/Cryptexes/OS@rpath/Util.framework/Util' (errno=2), 
'/System/Library/Frameworks/Util.framework/Util' (errno=2, not in dyld cache)

중복되는 내용들을 제외해서 뽑은 로그들 중 /Gmarket.app/Frameworks/Util.framework/Util 경로에 주목을 해보면 아까 UI 프레임워크에서 Util 을 Do Not Embed 로 설정한 이유를 알 수 있습니다.

어차피 Gmarket 에서 Util 을 사용하기 위해 Embed & Sign 을 해놓은 상태이기 때문에 /Gmarket.app/Frameworks/  Util.framework 가 들어가 있고, UI 에서는 @rpath 로 Util 프레임워크를 찾는데 이슈가 없으므로 굳이 Util 을 한번 더 Embed & Sign 으로 포함할 필요가 없는 것입니다.

만약 UI 에서도 Util 을 Embed & Sign 하면 어떤 일이 발생할까요?

최종적인 폴더의 구조는 /Gmarket.app/Frameworks/UI.framework/Frameworks/Util.framework/Util 처럼 UI 프레임워크 하위에 Util 이 들어가는 형태가 될 텐데요,

빌드는 잘 되지만 Archive 후 아래와 같은 Validation Error 가 발생합니다.

그 이유는 umbrella framework (다른 framework 를 포함하는 framework) 를 iOS, watchOS, tvOS 모두에서 지원하지 않기 때문입니다. (macOS 에서는 지원되긴 하지만 권장되지 않습니다.) (참고 - Apps with Dependencies Between Frameworks)

Framework 안에 Framework 를 Embed 할 수 없기 때문에, 사실 Gmarket 에서 Util 을 바로 사용하지 않더라도 Util 프레임워크를 Embed 해야 했습니다. 이렇게 App target 은 다른 프레임워크가 의존하는 모든 프레임워크를 포함하여 모든 프레임워크를 포함할 책임이 있습니다.

Util as Static Framework

두번째 상황으로, Util 을 Static framework 로 설정하는 방법을 살펴보겠습니다.

우선 UI 의 Frameworks & Libraries 에서 Util 을 사용한다고 명시하고, Gmarket 의 Frameworks & Libraries 에서는 UI 에 대해 명시합니다.

In Gmarket

In UI framework

Util 을 Gmarket 에서 Embed 로 명시하지 않고, UI 에서는 Do Not Embed 로 설정한 이유는 Util 이 Static framework 이기 때문입니다. Util 에 대한 .o 파일들이 어차피 UI 의 executable binary 에 포함이 되어있기 때문에 (nm -debug-syms 을 통해 확인 가능) Util 을 굳이 Embed & Sign 할 필요 없습니다.

이렇게 UI 에 Util 라이브러리가 적재되었고, Gmarket 앱은 UI 에 의존성을 가지므로, Gmarket 과 UI 모두 Util 을 import 로 선언하고, Util 내의 코드를 사용할 수 있습니다.

만약 UI 에서 Util 을 Embed & Sign 으로 설정하면 아까와 같이 Validation Error 가 발생하기 때문에 설정에 주의 해야합니다.

Util 을 Static Framework 로 사용할 때 추가 주의 사항

앞서 설명한 두 가지 방법 중 두 번째 방법인 "Util 을 static framework 으로 설정"하는 방식으로 프로젝트 구성을 하려고 했는데요, 이때 run time 에 특정 경로 재현 시 unrecognized selector 관련 에러가 발생하는 이슈가 있었습니다.

libc++abi: terminating with uncaught exception of type NSException

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[GmarketMobile.GMKTRootNavigationController popViewControllerWithAnimated:transitionType:completion:]: unrecognized selector sent to instance 0x7fc018038e00'

terminating with uncaught exception of type NSException

"unrecognized selector static library" 로 구글링을 해보면, 보통 static library 에서 Objective-C 의 category method 를 정의해놨을 때, 호출시 Category에 정의된 selector를 Objective-C Runtime에서 찾지 못하는 이슈로 많이 나옵니다.

이런 이슈가 발생하는 이유는 "UNIX static library와 Objective-C의 Dynamic한 특성간 충돌이 있어서 static library에 있는 category method들이 Application에 Link가 되지 않기 때문" 인데요, 자세한 내용은 Building Objective-C static libraries with categories 을 참고할 수 있습니다.

보통 이를 해결하기 위한 방법으로는 Other linker flags 에 -Objc, -all_load, -force_load 등을 명시해줌으로써 찾지 못했던 객체 파일을 찾을 수 있도록 합니다.

따라서 보통의 상황이라면 UI 의 Other linker flags 에 -Objc 를 설정하면 문제가 해결될 것입니다.

하지만 Util 은 현재 Lottie 와 같은 framework 를 SPM 으로 사용하고 있었는데요, 만약 UI framework에서 -Objc 플래그를 설정하면 Util 의 SPM 으로 명시해둔 Lottie 같은 프레임워크들에서 Duplicate symbols 에러가 발생했습니다.

In UI framework

In Util framework

-ObjC flag causes duplicate symbols with Swift Packages 를 참고해보면, 다른 패키지를 link 하고 있는 static library 를 -Objc 로 로드할 때는 Lottie.o 에 있는 object file 들이 static library 안에도 들어가고, static library 사용하는 쪽 (UI.framework) 에서 -Objc 플래그를 통해서 또 로드되어서 duplicate 문제가 발생한다고 추측할 수 있습니다.

 

따라서 UI.framework 에서 에서 static library 를 쓸 때는 아래 조건을 충족해야 합니다.

  1. unrecognized selector 이슈가 나올 수 있는 Objective-C 관련 코드가 있으면 안 됨. (category method 라든가, extension 에 정의된 @objc 키워드가 붙은 swift 함수라든가)
  2. 위와 같은 코드를 쓰려면 unrecognized selector 이슈를 방지하기 위해 static library 를 사용하는 곳의 Other linker flags 에 -Objc 를 설정해야함. 하지만 Other linker flags 를 설정할때 Duplicate symbol 에러를 방지하기 위해 static library 에서는 다른 라이브러리를 사용하면 안 됨.

결론

결론적으로 Framework 안에서 다른 Framework 를 사용할 때는 Embed Option 은 Do Not Embed 로 설정해야하며, 사용하고자 하는 다른 프레임워크의 Mach-O 타입이 Static 이냐 Dynamic 이냐에 따라 아래와 같은 주의 사항이 있습니다.

따라서 최종적으로는 Gmarket 프로젝트에서는 " Util 을 dynamic framework 로 설정하고, Gmarket 과 UI 각각의 Frameworks & Libraries 에서 Util 사용을 명시" 하도록 의존 관계를 구성해둔 상태입니다.

지금까지 살펴본 내용을 통해, Framework 에서 다른 Framework 사용 시 Embed 옵션 및 Mach-O type 에 따라 발생할 수 있는 문제 케이스들을 이해하고 프로젝트를 구성하는데 도움이 되길 바랍니다.

긴 글 읽어주셔서 감사합니다.

댓글