[iOS] static framework로 해야할까 dynamic framework로 해야할까?

Youth·2024년 12월 30일
2

고찰 및 분석

목록 보기
20/21

안녕하세요 연말을 맞이하여 열심히 글을 쓰고있는 킴스캐슬입니다

오늘은 tuist 4.x.x을 사용하면서 마주했던 경고메세지를 통해 알게된 제 나름대로의 결론(?)에 대해 적어볼까합니다. 솔직히 노션에 정리해놓은 이 글을 블로그에 써도 될지 고민을 많이 했습니다. 이유는 여러가지가 있지만 가장 큰 이유는 이게 맞는걸까?였습니다

그래도 거진 1~2주동안 끊임 없이 공부해보고 찾아봤던 주제이기도하고 만약에 그렇게까지 공부하고 적은 내용중에 틀린 부분이 있는데 그걸 제가 모르고있어서 오히려 잘못된 지식을 맞는거라고 생각하고 있는게 더 위험하지 않을까라는 생각이들어서 불꽃 피드백과 지적을 받고자 글을 올리게되었습니다

tuist를 제대로 써보는게 처음이고 심지어 4.x.x버전은 처음써보는거라 제가 공부한 내용중에 틀린 부분이 있다면 언제든지 댓글로 남겨주시면 감사드리겠습니다! 해당부분은 글 마지막부분에 계속해서 업데이트를 하면 좋을거같습니다:)

그럼 서론은 이정도로 마무리해보고 본론으로 넘어가보겠습니다

tuist v4를 사용하며 생긴 일

최근 진행중인 프로젝트를 모듈화하는 과정에서 최신 버전인 tuist v4를 사용하고 있습니다. 제가 했다기보다는 팀원분들이 제가 입사하기전에 프로젝트 모듈화를 어느정도 마무리를 해놓으셔서 저는 사용만 하면되는 상황이었습니다

그러다가 갑자기 iOS 15버전 미만에서 release버전으로 build를 했을때 navigation이 동작하지 않는 문제를 마주하게됩니다. 처음엔 라이브러리 문제인가 싶어서 라이브러리도 들어내보고 이런저런 시도를 많이 해봤는데 해결이 되지 않았습니다... 귀신이 곡할 노릇이었습니다 정말...

그러다가 다른 팀원분이 곰곰히 보시더니 기존에 모듈들을 static framework로 바꿔보라고 제안해주셨고 tuist generate를 했을 때 문제가 없었기에 네...?하는 표정으로 바꿨는데 해결이 되었었습니다

모듈을 static으로 바꿔야한다는걸 어떻게 아셨어요...?

저는 당시에 framework에 대한 지식이 전혀 없었고 static이면 어떤 장점이있고 dynamic이면 어떤장점이있는지정도만 알고있는 상태였다보니 선택 기준에 대한 감과 지식이 전혀없었습니다

이론을 실제로 사용하는데 전혀 써먹을 수 없는 그런 상황이었기 때문에 제대로 공부를 해봐야겠다라는 생각을 하게되었습니다

tuist의 경고

그러면 tuist가 어떤 경고를 해줬고 그 경고를 조금 분석해보겠습니다

Target 'CommonUI' has been linked from target 'Forecast', target 'Home', and target 'Search', it is a static product so may introduce unwanted side effects

해석을 해보면 CommonUI라는 static product가 Forecast, Home와 link되어있어서 원치 않는 side effect를 야기시킬 수 있다고 합니다. CommonUI랑 Forecase랑 Home이 어떤 관계인지를 모르는상태로 경고메세지를 보면 당연히 1도 감이 안오기때문에 시각화된 이미지를 보시죠

의존관계그래프는 위와같이 표현됩니다. 의존관계 화살표가 너무 많은거같으니 오른쪽으로 간략하게 바꿔보겠습니다

여기서 확인해봐야할 부분은 Home과 Forecast가 dynamic framework여서 발생한 문제인지 입니다. CommonUI가 static임을 메세지에서 표현해줬지만 Home과 Forecast는 linked되었다는부분만 표현해줬기 때문이죠

tuist edit으로 Home과 Forecast를 static framework로 바꿔주고 다시 generate를 해주면 CommonUI와 Forecast, Home과의 관계에서 발생한 오류메세지가 사라짐을 확인할 수 있습니다

결국, dynamic framework가 다른 static framework를 의존하는 관계일때 위와같은 경고메세지를 발생시킨다는걸 알았고 결론적으로 아주 단순하게 그래프를 바꿔보면 아래 그래프 형태로 의존관계가 명시되어있으면 경고메세지를 띄워준다는 결론을 내릴 수 있습니다

그렇다면 지금 부터는 왜 이런 의존관계일때 오류가 아닌 경고메세지를 띄워주는지에 대한 나름의 생각의 흐름에 대해서 말씀드려보겠습니다

뭐가 문제야 say something

App - framework

우선 위 내용에 대한 이해를 위해서는 기본적인 static framework와 dynamic framework에 대한 이해가 필요합니다

static framework와 dynamic framework는 앱이 돌아가면서 실행할 코드를 어떤 방식으로 참조할것인가에대한 방법이 다르다는게 제 개인적인 정의입니다

우선 앱이 실행할 코드를 앱이 가지고 있는것이 앱실행의 가장 기본적인 방식입니다

이런 방식이 가장 쉽고 빠른 방법이지만 앱의 규모가 커지고 실행할 코드가 많아지면 App이 가지고 있는 코드 파일의 크기가 매우 커지게됩니다

앱 자체의 크기가 커지고 그 말은 앱이 실행되는데 걸리는 시간이 길어진다는걸 의미하고 그 시간이 유의미해진다면 이탈률이 늘어나기때문에 static만 사용하는건 앱의 규모가 커질수록 좋지 않은 결과를 만들어낼 수 있음을 의미합니다

이러한 문제를 해결하기 위해 dynamic framework가 등장했습니다

두둥등장

특정 framework에서 사용되는 코드들을 직접 가지고 있지않고 해당 코드를 가지고 있는 framework의 레퍼런스를 가지고 있어 앱이 가지고 있는 코드실행파일의 몸집을 줄일 수 있습니다. 아래와같이 말이죠

물론 dynamic framework가 만능은 아닙니다. static framework가 가지는 장단점이있고 dynamic framework가 가지는 장단점이 있죠. 자세한 장단점은 구글링을 하면 너무나 많은 자료가 있으니 더 적지는 않겠습니다. 여기서는 static과 dynamic이 어떤 방식으로 코드를 실행시키는지를 보면됩니다

앱이 코드를 실행시킬때 그 코드의 출처가 static인지 dynamic인지에 따라 방식이 다른데요. 이런걸 실제로 볼 수있는 방법이 있습니다. xcode에서 앱을 빌드하고 빌드파일경로에서 앱파일의 패키지 보기를 눌러보면 아래와같은 폴더구조를 볼 수 있습니다

Unix실행파일(이하 앱 실행파일)안에 실제 코드들이 담겨있고 여기에 너무 많이 코드를 넣으면 실행파일이 너무 커져서 특정 코드의 묶음들은 실행파일 안에 넣지 않고 따로 폴더에 경로로 관리를 해주고 코드를 실행하기위해서는 경로로 이동해서 코드를 실행시켜 앱파일의 크기를 줄이는 효과를 가져올 수 있습니다

static dynamic을 이야기할때 꼭 따라오는것이 embed와 do not embed인데요. embed한다는 의미는 여기 폴더에 그 framework의 폴더를 넣고 코드를 폴더경로를 통해 접근해 실행하겠다는 의미입니다

따라서, static framework는 do not embed를 해야할 필요가 없다는 개념이 굳이 앱실행파일 안에 코드들이 있는데 굳이 외부 폴더에 해당 코드를 넣을 필요가 없기 때문입니다. dynamic framework가 embed를 해야한다고 이야기하는 이유는 embed를 하지 않으면 앱 입장에서는 정해진 경로대로 갔는데 해당경로에 폴더가 없기때문에 읽을 코드가 없어 실행을 할 수없기때문입니다

static이 do not embed이고 dynamic은 embed해야한다라는 말은 어쩌면 당연했던거죠

지금까지 했던 이야기는 App과 framework의 관계였습니다

app-framework-framwork의 관계에 대해서도 알아볼 필요가 있습니다

App - framework - framework

총 네가지 경우입니다

  1. app - static - static
  2. app - static - dynamic
  3. app - dynamic - dynamic
  4. app - dynamic - static

네 가지 경우를 각각 살펴보겠습니다

1번 케이스(app-static1-static2)

static은 참조될때 코드가 inline으로 들어가기때문에 최종적으로는 app의 실행파일에 static1과 static2의 코드가 전체적으로 들어가게됩니다

static2가 static1에 코드가 들어가고 그 static1이 app에 코드로 들어가니까 app실행파일이 static1과 static2의 코드를 통째로 가지고 있는거죠

2번 케이스(app - static - dynamic)

해당 케이스에서는 1차적으로 이렇게생각할수있습니다.

static framework가 dynamic framework를 embed해야할거고 app입장에선 dynamic framework의 코드를 실행시키려면 static에 embed한 dynamic framework의 폴더에가서 코드를 실행시키는건가?

이 부분을 이해하기 위해서는 iOS에서 umbrella framework에 대해 약간의 사전지식이 필요합니다

Technical Note TN2435: Embedding Frameworks In An App

결론부터 말하면 iOS에서는 framework가 framework를 embed하는 umbrella framework를 지원하지 않습니다

만약 이 상황에서 static framework가 dynamic framework를 embed하게되면 build나 앱실행에서는 문제가 없지만 앱을 아카이브하고 distribute하는 과정에서 upload오류가 발생하게됩니다

오류를 읽어보면 framework가 nested된 bundle을 포함하는걸 disallowed한다는 내용입니다. umbrella framework를 허용하지 않는다는 의미죠

그렇다면 위그림에서 dynamic frame를 static framework가 embed할 수 없다면 dynamic framework는 누가 embed를 해줘야할까요?

바로 app target입니다. tuist를 사용하다보면 앱에 dependency를 걸어놓지 않는 framework가 embed되어있는걸 볼 수 있습니다

확인을 위해 위의 예시인 App-Home-CommonUI의 관계를 app-static-dyanmic으로 바꿔보겠습니다

App은 Home에 의존하고 있고 Home은 CommomUI에 의존하고 있습니다. 이상태에서 tuist generate를 해보겠습니다

Home은 CommonUI를 embed하고있을까요?

실제로 xcode에서 확인해보면 CommonUI가 dynamic임에도 embed를 하지않고있음을 알 수 있습니다. umbrellaframework를 지원하지 않기때문이죠

위의 의존성 관계를 보면 App은 CommonUI를 직접적으로 의존하고 있지 않습니다(App이 CommonUI를 직접적으로 가리키고있지 않습니다). 하지만 App target을 보면 직접의존하고있지 않아도 CommonUI를 embed하고있는걸 볼 수 있습니다

결국, umbrella framework를 지원하지않는 iOS에서는 App target이 관련된(직접적이든 간접적이든) 프레임워크를 알고있을 의무가 생기게됩니다

static의 경우엔 직접적이든 간접적이든 코드자체가 실행파일에 inline으로 들어가기때문에 embed를 신경쓰지 않아도되지만 직간접적으로 참조하는 dynamic framework는 app target이 embed를 하고 있어야합니다

실제로 해당 의존관계 상황에서 build를 해보고 빌드 파일을 뜯어보면

App빌드파일에 CommonUI가 embed되어있고

App의 앱파일내부에 Home관련 코드가 inline으로 들어가있는걸 볼 수 있습니다(by nm 명령어)

nm의 결과를 봤을떄 심볼유형이 U가 없기때문에 Home모듈(static) 코드들이 inline으로 들어가있음을 알 수있습니다

추가적으로 App실행파일이 CommonUI를 어떤식으로 가지고있는지를 파악하기위해 아래와같은 명령어로 앱 실행파일을 보겠습니다

nm App.debug.dylib | grep "CommonUI"

아래와같이 commonUI관련 코드들 앞에 심볼유형이 U로되어있어 참조로 되어있음을 확인할 수 있습니다

앱입장에선 CommonUI의 코드를 읽고 실행하기 위해서는 들어있는 코드가 아닌 어딘가에 있는 프레임워크를 타고 가서 reference로 읽어야 한다는 뜻이됩니다

nm을 통한 결과 이해하기(간단 ver)

출력필드구성

  • 첫 번째 열: 심볼의 메모리 주소입니다.
    • 예: 00000000000180d4
  • 두 번째 열: 심볼 유형을 나타냅니다.
    • T: 전역 심볼로, 텍스트 섹션에 위치한 함수를 의미합니다.
    • t: 로컬 심볼로, 텍스트 섹션에 위치한 함수를 의미합니다.
    • S: 전역 심볼로, 데이터 섹션의 정적 변수를 의미합니다.
    • s: 로컬 심볼로, 데이터 섹션의 정적 변수를 의미합니다.
    • U: 정의되지 않은 심볼로, 외부에서 가져온 것을 나타냅니다.
  • 세 번째 열: 심볼의 이름입니다. Swift에서는 심볼 이름이 맹글링되어 있습니다.

심볼필드로 inline인지 참조인지 판단하기

  • 두번째열의 심볼유형이 U 가 있다면 외부에서 제공된 정의를 참조하기때문에 코드가 inline으로 들어있다면(static framework의 경우엔 앱실행파일에 inline으로 들어옴) 심볼유형이 U인 결과가 없거나 거의 없습니다. dyanmic framework는 외부정의된 정의를 참조해야하기에 U 심볼이 많습니다

3번 케이스(app - dynamic - dynamic)

3번케이스는 app-dynamic-dynamic인 경우입니다. 위의 umbrella framework를 이해하셨다면 이 경우는 꽤나 쉽게 답이 나옵니다

우선 app과 dynamic의 관계는 app이 dynamic framework를 embed해야합니다. 다만 dynamic과 dynamic의 관계에서는 프레임워크와 프레임워크관계이기때문에 embed를 할 수없고 app입장에서는 두번째 dynamic framework를 간접적으로 참조하기위해서 app타겟이 두개의 dynamic framework를 embed하고 첫번째 dynamic framework는 두번째 dynamic framework를 embed하지 않습니다(하지 않는다기보다는 할 수 없다가 좀더 정확한 표현일거같습니다)

이왕 하는김에 이번 케이스도 xcode와 build파일을 뜯어보겠습니다

우선 xcode를 보면 Home이라는 dynamic framework가 CommonUI라는 dynamic framework에 의존성을 가지고있지만 umbrella framework의 미지원 으로 인해 embed하고 있지 않음을 확인할 수 있습니다

당연히 app이 이 두개의 dynamic framework를 embed하고 있습니다

위 이미지 처럼 tuist edit으로 열어보면 app은 CommonUI를 직접적으로 의존하고 있지 않음에도 embed를 하고 있습니다

build파일에서도 app빌드 폴더에 commonUI와 Home이 embed되어있고

앱파일을 뜯어보기전에 예상을 한번 해볼까요?
앱파일이 commonUI와 Home을 참조하고있을거기때문에 심볼유형이 U일거라고 예상을 해볼 수 있습니다

Home과 CommonUI코드 모두 심볼유형이 U인 코드들이 들어가있음을 알 수있습니다
앱 입장에선 Home이나 CommonUI의 코드에 접근하기 위해서는 직접 가지고 있는 코드가 아닌 타고들어가서 읽어야한다는 의미입니다

4번 케이스(app - dynamic - static)

마지막 케이스이자 이번 글의 시작점인 tuist generate시 발생하는 warning의 원인을 제공하는 의존관계입니다

우선 dynamic framework와 static framework의 관계는 framework와 framework이기때문에 embed가 불가능합니다. 다만 어차피 static framework는 embed하지 않아도 코드자체가 inline으로 들어가게됩니다

즉, dynamic framework자체의 실행파일에 static framework코드가 들어가게됩니다

그렇다면 app입장에선 어떨까요? 결국 실행주체는 app이기때문에 dynamic을 embed하고 있고 static framework의 코드들을 실행파일내에 inline으로 넣어주게 될겁니다

dynamic의 실행파일 안에도 static framework코드들이 들어가게되고 app의 실행파일내에도 static framework의 코드들이 들어가게됩니다

그리고 의존관계를 일부러 이상하게만들어서 빌드를 해보면 app에 관련 코드가 없더라도 tuist는 내부 캐싱에 의해서 코드가 있을법한 경로를 모두 들러서 실행하고자하는 코드의 레퍼런스를 찾아냅니다. 같은 code가 copy되어서 같은 실행폴더 내에 들어가게됩니다

실제로도 dyanmic내의 실행파일내에서 static을 찾아보고 app의 실행파일 내에서 static을 찾아볼 수있습니다

실행파일을 뜯어보면 app실행파일내에서 static프레임워크인 CommonUI관련 결과를 확인할 수 있습니다

당연히 Home의 실행파일내에서도 CommonUI를 가지고 있을거같은데 한번 확인해보겠습니다

다만 여기서 문제가 될 수 있는부분을 같은 코드가 복사되어서 뭔가 문제가 발생할수도있지 않을까?라고 생각할 수 있겠지만 app입장에선 앱실행파일에만 있는 코드를 실행하면되기에 Home이 CommonUI관련 코드를 inline으로 들고있다는게 용량문제(굳이 안쓸 코드를 실행파일내에 포함시켰으므로)를 야기할 수는 있지만 side effect를 야기할거라는 생각이 들진 않았습니다

하지만, 앱실행파일에서 아래와같은 nm결과도 포함된걸 확인할 수 있습니다

CommonUI에 대한 심볼이 U이고 자세히 보면 CommonUI에 대한 결과지만 앞에 Home이있는것도있고 Search가있는것도 있습니다

tuist graph를 확인해보면 앱이 CommonUI에 접근할수있는 방법이 Home을 통해서 혹은 Search를 통해서라는걸 알 수있습니다

즉, 앱실행파일에 CommonUI가 inline으로 들어가있기도하고 Home이나 Search같은 dynamic을 통해서 접근할수도있다는걸 의미합니다

이 코드를 보면 app이 자체 실행파일의 CommonUI코드를 inline이 아닌 다른 경로의 CommonUI코드에 접근할 가능성이 생김을 의미합니다. 그러면 원하지 않는 side effect가 발생할 가능성이 있을 수 있다고 생각합니다

마지막 4번 케이스에 대한 결론은 저의 개인적인 의견입니다. 혹시나 다른 의견이 있으시다면 댓글로 의견 나눠봐요!

알아보면서 의문이 들었던 점(12월30일 update)

💡 서드파티라이브러리들은 static으로만 사용가능한 경우가 있는데 이경우는 어떻게 될까?

SPM으로 사용시 SPM이 static dyanmic을 선택할 수있도록 지정하지 않은 상태로 둘것을 권장(아래이미지참고)하는데 SPM이 알아서 잘 해준다는 의미가 아닐까싶습니다

추가적으로 사실 특성 3rd party라이브러리가 static밖에 지원을 하지 않고 내부 모듈들을 전부 dynamic으로 설정한다면 어쩔수없이 app - dynamic - static의 의존관계가 생길 수 밖에없고 그러면 warning이 3rd party라이브러리때문에라도 무조건 뜨겠다고 생각해서 실험을 해봤습니다

전체내부모듈을 전부 dynamic으로 변경하고 tuist graph를 만들어보면 아래와같은 그래프가 나오고

App기준으론 적어도 두개의 의존관계가 App-dyanmic-static이 됨을 확인할 수 있지만 tuist generate시 warning이 발생하지 않습니다


💡 app이 static을 의존하지 않으면 뜨지 않는가

위와 같은 상황이고

App - Home(dynamic) - CommonUI(static)일때 만약에 app이 commonUI관련 코드를 하나도 사용하지 않아 App에서 import Common을 하지 않아도되면 어떻게 되는지에 대한 궁금증이 생겨서 앱 파일을 까봤습니다

우선 app에서 CommonUI의 모듈에 필요한 코드와 import를 제거한후 빌드하고 앱파일을 까서 nm명령어를 돌려보면

CommonUI코드를 사용하지 않고 import를 하지 않아도 CommonUI관련 코드들이 inline으로 앱실행파일내에 들어와있음을 알 수있습니다

여기서 특이한점은 심볼유형이 U 인 코드는 없음을 알 수있습니다

만약 app이 CommonUI코드를 사용하게되면 CommonUI관련 코드를 inline으로 들고있기도하면서 아래와같이 심볼유형이 U 인 코드를 추가적으로 볼 수 있습니다

결국 app이 CommonUI를 import해서 사용하면 copy된 코드를 사용할 가능성이 존재하게됩니다

(임시) (나만의) (정답아님) (태세전환 가능) 결론

  1. tuist에서는 코드의 복사 및 참조로부터의 side effect의 가능성을 경고하기 위해서 app-dynamic-static의 의존관계일때 warning을 띄워준다
  2. app이 dynamic에 의존하고있는 static을 사용하지 않으면 copy된 코드의 참조를 알 방법은 없다. 하지만 app이 dynamic에 의존하고있는 static을 사용한다면 copy된 코드의 참조를 알 수있기에 side effect가 발생할 가능성이 생겨버린다
  3. SPM으로 3rd party라이브러리는 static인지 dynamic일지 크게 신경쓰지 않고 내부모듈간의 의존관계만 신경쓰면 된다(SPM짱…?)

💡 tuist의존성을 고려하다보면 dynamic이 static을 의존하는 경우가 무조건 생기게될텐데 그때마다 warning이 발생할것이기에 기본적으로는 dynamic으로 앱내모듈을 만드는게 안전하지 않을까요…?


tuist예제 프로젝트는 제가 이전에 보면서 공부했던 https://github.com/socar-abel/soma-weather-ios 을 따라 만든 프로젝트를 사용했습니다. 혹시나 해서 말씀드립니다:)

dynamic과 static에 대해 전혀 몰랐던 상황에서 대체 이걸 어떻게 공부해야하고 심지어 이해할 수 있을까?가 처음 들었던 느낌이었습니다. 처음엔 같은 블로그, 문서를 보고 또 봐도 대체 무슨 말인지 모르겠고 똑같이 따라했는데 똑같은 결과가 나오지 않아서 많이 슬프기도했지만 출근할때보단 퇴근할때 조금더 이 주제에 대해 이해할 수 있는 사람이 되었고 옆에계신 iOS팀원분도 중간중간 떠오르는 생각들을 공유해주시고 방향성을 잡아주셔서 10일 정도의 삽질을 어느정도 마무리 할 수 있었던 것 같습니다ㅎㅎ...

제대로 개발 공부를 시작했던 작년 여름에 받았던 느낌을 취업하고 다시 받고 있는 느낌이 드는 요즘입니다. 여전히 공부할게 많네요. 지금 생각하면 작년 여름의 저는 개발초보느낌이었는데 1년 후의 제가 이 글을 본다면 또 비슷한 느낌이지 않을까요 ㅎㅎ...

가능하다면 이번글에는 다른 생각이나 지적들을 많이 받아서 더 견고한 지식으로 만들어나가고 싶습니다:)
우연히 들어오신 iOS개발자분들이 많은 의견을 주시길 진심으로 바래보며 오늘은 여기서 물러나보겠습니다

그럼20000!

profile
AppleDeveloperAcademy@POSTECH 1기 수료, SOPT 32기 iOS파트 수료

0개의 댓글