Tuist를 활용해 카드 정보를 API로 호출 받아 관리하는 프로젝트를 진행하게 되었는데 이전에 학습해 둔 Tuist를 바탕으로 각 모듈을 나눠 기능을 구현하게 되었다.
네트워크 기능을 제외한 모든 기능을 모듈 별로 구현한 다음 마지막으로 네트워킹을 넣게 되었는데 이 시점에서 Duplicate Symbol Error가 발생하여 해결하기 까지 아주 골치가 아팠다.. 하지만 결국 원인을 알게 되었고 해결 했으니 잊지 않기 위해 그 과정을 한번 정리해보자
Duplicate Symbol Error란 무엇인가
Tuist를 활용하여 모듈을 나눠 개발을 하다가 빌드 과정에서 이런 오류를 만나게 될 때가 있다
Duplicate symbol '_ExampleFunction' in:
/path/ModuleA.o
/path/ModuleB.o
이 에러는 바로 Duplicate Symbol Error인데 이 오류는 이름 그대로 링커가 빌드 과정에서 같은 이름을 가진 심볼을 여러번 발견 했을 때 발생된다. 빌드는 정상적으로 컴파일까지 진행되지만, 마지막 단계인 링크 과정에서 충돌이 발생하며 크러쉬가 나버린다..
빌드 과정 다시 한 번 확인하기
Swift를 통해 빌드를 하게 되면 크게 세가지 단계를 거쳐 빌드되게 되는데 그 중 두가지 측면에서 한번 확인해보자
- 컴파일 단계
- .swift 소스 파일 하나하나를 각 Object File(.o) 로 변환하게 된다.
- 이 Object File은 기계어로 번역된 코드 덩어리인데 이 단계에서는 이후 말하게 될 외부 의존성과 연관이 있진 않기 때문에 별다른 문제 없이 넘어갈 수 있다.
- 링크 단계
- 이 링크 단계에서는 여러 Object File과 라이브러리 파일을 하나의 실행 파일 또는 프레임워크로 묶게 되는데 이 과정에서 심볼 테이블을 만들어야 한다.
- 각 심볼(함수명, 클래스명, 전역 변수명 등)은 프로젝트 전체에서 하나만 존재해야 하는데 이 단계에서 중복이 발생하여 크러시가 나게 되었던 것이다..
그렇다면 링커(Linker)는 정확하게 무슨 일을 하나
링커는 단순하게 .o 파일과 라이브러리를 붙이는 단계는 아니고 모든 오브젝트 파일에서 심볼을 스캔한 뒤 코드에서 참조한 함수/변수/클래스가 어디서 정의 되었는지를 찾게 된다.
그 이후 각 심볼에 대해 메모리 주소를 할당하고 최종적으로 하나의 바이너리 파일을 생성하게 된다.
문제는 여기서 발생하게 되는데
만약 링커가 동일한 이름의 심볼을 발견하게 되면 어떻게 될까!!!
- 링커는 어떤 걸 써야할지 결정할 수 없다.
- 그렇기 떄문에 빌드 실패와 함께 Duplicate Symbol 에러를 발생 시킨다!
이 에러가 발생되는 구체적인 경우는 다음과 같이 있다
동일한 소스 파일이 여러 Target에 추가됨 | Tuist 등 빌드 시스템에서 sources 설정을 잘못하면 발생 |
동일 클래스를 두 번 정의함 | 복붙 실수로 같은 클래스 파일을 두 곳에 존재시키는 경우 |
동일 모듈을 두 번 링크함 | Dependency Graph가 깨져서 모듈이 중복 포함되는 경우 |
External Library 충돌 | 외부 라이브러리들이 동일 파일을 포함하고 있을 때 발생 |
Tuist를 사용할 때 Duplicate Symbol을 쉽게 접할 수 있는 이유
tuist는Target과 Dependency를 아주 쉽게 설정할 수 있도록 도와주는 툴이다.
하지만 sources 설정을 정확하게 관리하지 않으면 동일한 파일을 여러 Target에 중복 포함하게 된다.
특히 Dependency를 중복으로 걸어버리면, 동일한 모듈이 두 번 링크되게 되는데 이를 바탕으로 이런 오류를 쉽게 접할 수 있게 된다는 것..
그렇기 때문에 Tuist를 사용할수록 Dependency 설계와 Source 구성이 더욱 중요해진다!!
사진과 같이 나는 Alamofire를 외부 디팬던시로 선언하여 필요한 모듈에 사용할 수 있도록 세팅했는데 Core 내 Network 부분에서 alamofire를 사용한 뒤 App단에서 모듈을 하나로 만드는 과정에서 이 alamofire를 또 추가했던 것이었따..
이렇기 때문에 실기기에서는 이 과정을 시뮬레이터 보다 강하게 잡지 않아 잘 구현이 되었지만 시뮬레이터를 사용해서 빌드를 했을때 조금 더 내부적으로 엄격하게 디버깅이 되기때문에 시뮬레이터에서만 해당 오류가 발생하는 것이었다.!!
실기기와 시뮬레이터 환경의 차이는 뭘까
Duplicate Symbol Error는 환경에 따라 보이는 증상이 다르다. 그 이유는 시뮬레이터는 intel Mac, Apple Silicon 두 아키텍처를 지원하기 위해 멀티 아키텍처인 Fat 라이브러리를 빌드하는데 이때 두 아키텍처 모두에 대해 같은 소스가 컴파일 되고 링커가 두번 심볼을 병합하면서 중복 심볼 문제가 드러날 수 있다는 점!!
실기기는 arm64 하나만 타겟팅하기 때문에 하나의 아키텍처 내에서만 발생하고, 경우에 따라 링커가 이를 덮어쓰거나 무시하는 상황이 발생하여 에러가 나타나지 않을 수 있다.
Static Framework로 변경하게 되면 에러가 해결 되는 이유
에러를 해결 할 수 있는 방법은 여러가지가 있다 가장 쉬운 방법은 그냥 중복된 내용을 제거해주면 될 것 같다.. 그 외에 들었던 의견으로는 Static Framework로 변경하게 되면 해당 에러가 발생하지 않을 것 같다라는 의견 이었고 왜 그런 얘기가 나왔는지 한번 찾아보니
Static Library는 .a 파일로 여러 개가 링크될 경우 모든 심볼이 메인 바이너리에 포함되는 특징을 가지고 있지만 Static Framework는 내부적으로 Static Library와 리소스의 묶음 형태로 제공되기 때문에 링크될때 단일 단위로 링크되어 링커 입장에서는 재사용 가능한 하나의 독립된 블록! 이라고 볼 수 있다. 그렇기 때문에 중복 정의가 있더라도 한번만 병합 되도록 처리될 수 있다는 것!
두번째로는 Tuist는 Static Framework를 Link 단계에서 최적화 하는데 사용되지 않는 심볼을 제거하거나 중복 참조를 줄이는 방식으로 바이너리를 구성하게 되고 이는 일반 Static Library보다 중복 심볼에 덜 민감한 구조를 제공한다고 한다!
비유로 설명해보면
Static Library는 부품만 들고 와서 조립하는 DIY 가구의 형태고 같은 나사가 두번 오면 충돌이 일어난다
Static Framework는 조립된 가구 세트를 하나만 들고 오는 형태로 내부 나사가 겹쳐있어도 괜찮다
라고 할 수 있다!!
그 외에 이번 Duplicate Symbol Error를 겪으면서 단순 코드의 폴더링 아키텍처 구조 등 모듈화에만 신경썼는데프로젝트를 구성하는 환경에 대한 구조도 많은 고민과 설계가 필요하다는 점을 느낄 수 있는 트러블 슈팅이었다. 그래서 다음 시간에는 이 중복 코드 구조를 해결할 수 있도록 ProjectDescriptionHelpers를 활용해 Tuist 타겟 및 프로젝트, 그리고 여러 환경 설정에 대한 휴먼 에러를 줄일 수 있고 더욱 편리한 상태로 사용할 수 있도록 환경을 만들어주는 세팅을 갖춰보려고 한다!
그럼 오늘은 여기까지!
'◽️ Programming > iOS' 카테고리의 다른 글
AVAudioSession설정으로 인한 트러블 슈팅 과정 (0) | 2025.05.26 |
---|---|
Tuist Scaffold로 모듈 생성 자동화 하기 + 외부 Dependencies 추가하기 (0) | 2025.04.22 |
Tuist를 활용해 TMA 구조 설계하기 (0) | 2025.04.14 |
에러 핸들링과 Toast Message 활용하기 (0) | 2025.04.02 |
Static Dispatch, Dynamic Dispatch (2/2) (0) | 2025.03.04 |