← 홈으로
Modularization · Build · Linking

모듈화 · 빌드 · 링킹 Q&A

2026-07-07~08 학습. 모듈화가 빌드를 빠르게 하는 원리, 의존성 역전(DIP), static/dynamic framework의 링킹·중복 함정, Mach-O·헤더의 정체를 문답으로 정리한다.

핵심 3줄
  • 모듈화 빌드 이득 = 변경된 모듈만 재컴파일 + 컴파일 단위 축소 + public API 안 바뀌면 의존 모듈 재컴파일 회피 + 독립 모듈 병렬 컴파일
  • framework는 포장, 알맹이가 static/dynamic — static은 앱 바이너리에 흡수, dynamic은 번들 Frameworks/에 embed 후 dyld 로드
  • 여러 바이너리가 같은 static을 각자 링크하면 중복(3벌도 가능) → 공유할 건 dynamic으로

모듈화하면 빌드가 빨라지는 이유가 "변경된 모듈만 재컴파일"뿐인가?

그게 가장 크지만, 4가지가 함께 작용한다.

  • ① 변경된 모듈만 재컴파일(증분 빌드 최대 기여) — Network만 고치면 Network만 다시 빌드
  • ② 컴파일 단위 축소 — Swift는 타입체크·제네릭 분석·extension 검색·최적화를 넓은 범위로 함. 모듈이 작으면 한 번에 분석할 코드량↓
  • ③ public API 불변 시 의존 모듈 재컴파일 회피 — Data 내부 구현만 바뀌고 public 인터페이스가 그대로면 UI·Domain은 기존 산출물 재사용
  • ④ 독립 모듈 병렬 컴파일 — 서로 의존 없는 모듈은 동시 컴파일 → 코어 활용↑
주의: 지나치게 잘게 쪼개면 Target 관리·링킹·의존성 계산 비용이 늘어 오히려 느려질 수 있다.

병렬 컴파일은 서로 독립적일 때만 가능한가?

모듈 간에는 그렇다. 의존 엣지로 연결되면 순서가 강제된다(B를 의존하는 A는 B 먼저). 서로 의존 없는 모듈끼리만 병렬.

Core ──▶ A
     ├─▶ B     A·B·C는 서로 독립 → Core 빌드 후 셋이 병렬
     └─▶ C

보충: "모듈 간 병렬"은 독립일 때만이지만, 한 모듈 안의 파일들은 Swift가 어느 정도 병렬 컴파일한다(병렬성은 모듈 간 + 모듈 내 두 층위).

서로를 참조(순환 의존)하면 왜 안 좋은가?

  • 순환(A↔B)이면 컴파일 순서를 못 정함 → Swift 모듈은 순환 import 불허(빌드 실패, 결국 한 모듈로 합쳐야)
  • 강결합 — 한쪽만 고쳐도 상대 재컴파일 → 증분 빌드 이점 소멸
  • 병렬성·재사용·테스트성 하락 — B만 떼어 쓰거나 테스트하기 어려움

그래서 단방향(사이클 없는) 의존이 원칙.

"Domain이 Data를 의존"하게 짰다 — 잘못인가? (의존성 역전)

잘못이다. Clean Architecture는 의존성이 항상 안쪽(Domain)을 향한다. Domain은 가장 안쪽 = 아무것도 의존하지 않고, Data가 Domain을 의존해야 한다.

// Domain (컴파일 타임엔 프로토콜만 앎)
protocol UserRepository { func fetch() -> User }
struct GetUser {
    let repo: UserRepository          // 추상 타입
    func run() -> User { repo.fetch() }  // 실행 시 주입된 구현체 호출
}
// Data — 프로토콜을 구현하며 Domain을 import
struct UserRepositoryImpl: UserRepository { func fetch() -> User { /*네트워크*/ } }
// 조립(주입)
let useCase = GetUser(repo: UserRepositoryImpl())
  • 컴파일 의존: Domain은 UserRepository(추상)만 의존, Impl의 존재를 모름 → 코드 의존은 Data → Domain
  • 런타임 동작: DI로 Impl을 주입 → repo.fetch()는 동적 디스패치로 Impl.fetch 실행
  • "역전"인 이유: 실제 실행 흐름(도메인이 데이터 구현을 호출)과 소스 의존 방향(데이터→도메인)이 반대라서. Domain은 네트워크·DB가 바뀌어도 재컴파일·수정 불필요, 테스트도 가짜 구현 주입으로 쉬움

".framework"면 dynamic인가? Mach-O란? static은 .a인가 .lib인가?

framework는 "포장"일 뿐, 알맹이가 static/dynamic이다.

  • .framework = 바이너리 + Headers/(ObjC 공개 선언) + Modules/(.swiftmodule·modulemap) + 리소스를 묶은 폴더
  • static framework = 안에 정적 Mach-O(아카이브) / dynamic framework = 안에 동적 Mach-O(dylib)

Mach-O = Apple 플랫폼의 바이너리 형식(Windows=PE, Linux=ELF, Apple=Mach-O). 구조 = 헤더(magic·CPU) + load commands(세그먼트·의존 라이브러리·진입점) + 세그먼트(__TEXT=코드, __DATA=데이터). object(.o)·executable·dylib·static archive가 전부 Mach-O 계열 — static도 Mach-O다(타입만 다름).

static library = .a(Unix ar 아카이브). .lib은 Windows(MSVC) 형식이라 iOS엔 안 쓴다. Headers = 공개 API 선언(구현 아님), 컴파일러가 사용하는 쪽 타입체크에 쓰고 실제 구현은 링크 때 연결("메뉴판 vs 주방").

static / dynamic 링킹 전체 과정 — 어디에 어떻게 들어가나?

StaticDynamic
컴파일 산출물.o.a(archive).o.framework(동적 바이너리)
코드 위치앱 실행 바이너리 안에 복사·흡수앱 번들 Frameworks/ 폴더에 embed
링크 시점링크 타임(빌드 때 확정)런타임(dyld가 로드)
중복단일 바이너리 링크면 1개 (⚠️여러 바이너리가 각자 링크하면 중복)번들에 1벌, 런타임 1회 로드 → 공유
앱 시작 속도빠름(로딩 없음)dyld 로딩 비용(pre-main↑)
바이너리 크기실행 파일 커짐분리됨

static은 Frameworks/안 들어가고 앱 실행 바이너리에 직접 박힌다. 링커의 dead code stripping으로 안 쓰는 심볼은 빠질 수 있다.

여러 dynamic이 같은 static을 각자 링크하면? (중복 3벌 함정)

자동 dedup이 안 되어 벌 수만큼 중복된다. 앱·B·C가 모두 static A를 정적 링크하면:

앱(실행파일)      ← Static A 흡수  (1벌)
B.framework(dyn) ← Static A 흡수  (1벌)
C.framework(dyn) ← Static A 흡수  (1벌)
→ 앱 실행 시 세 Mach-O가 한 프로세스에 로드 = A 코드 3벌
  • 왜: 앱·B·C는 각자 따로 링크되므로 서로가 A를 품는 걸 모름. static은 흡수돼 사라져 링커가 겹침을 못 봄
  • 결과: 빌드는 되지만 런타임에 objc class ... implemented in both 경고 + 싱글턴이 여러 개·캐스팅 실패 등 조용한 버그(크래시 아님)
  • 해결: A를 dynamic으로A.framework 1벌을 앱·B·C가 공유(dyld 1회 로드). 또는 A를 한 곳에서만 링크
정상 케이스: 앱 + 익스텐션처럼 서로 다른 프로세스가 각자 같은 static을 포함하는 건 충돌 아님(메모리 공간 분리).

앱 + 공유확장 + 위젯확장 = 세 개의 프로세스인가?

네, 각각 별개 프로세스다(세 개). 익스텐션은 앱 안에서 도는 게 아니라 자기 프로세스로 뜬다(공유확장은 호스트 앱 컨텍스트의 별도 프로세스, 위젯은 WidgetKit 관리 프로세스).

  • 메모리 공간이 분리 → 같은 static을 각자 포함해도 충돌 없음
  • 프로세스 간 메모리 공유 안 됨 → 데이터 공유는 App Group(공유 컨테이너/UserDefaults suite)으로

그럼 static B·C가 static A를 의존 + 앱도 A 의존이면? (이번엔 B·C도 static)

문제없다. 중복 여부를 가르는 기준은 딱 하나 — "그 static이 몇 개의 '최종 바이너리'에 흡수되느냐".

  • 하나의 최종 바이너리로만 흡수 → 링커가 중복 심볼을 1벌로 정리 → 안전
  • 여러 최종 바이너리(여러 dynamic 등)에 나뉘어 흡수 → 벌 수만큼 중복 → 위험
// B·C가 static이면 → 결국 전부 앱 실행 파일 하나로 흡수
static B ─┐
static C ─┼─▶ static A,  앱도 A
앱        ─┘
   ↓ 링킹(단일 최종 바이너리 = 앱 실행 파일)
앱 실행 파일 하나에 A·B·C가 모임 → 링커가 A·B·C 각 1벌로 정리 ✅

// B·C가 dynamic이면 → 별도 바이너리가 생겨 A 흡수가 갈라짐
B.framework(dyn) ← A (바이너리1)
C.framework(dyn) ← A (바이너리2)← A (바이너리3)  → 런타임에 A 3벌 ❌

왜 갈리나: 중간에 dynamic framework가 끼어 "별도 바이너리"가 생기면 그 경계에서 static 흡수가 갈라진다. static끼리만 이어지면 결국 앱 실행 파일 하나로 다 모여 링커가 1벌로 정리 → 안전.

딱 하나의 예외 — merged static library: 아래 별도 Q&A에서 상세히.

[예외 상세] merged static library — 왜 duplicate symbol 빌드 에러가 나나?

.a.o들을 묶은 아카이브("부품 상자")일 뿐, 그 자체로 링크된 게 아니다. 최종 링크 때 링커가 필요한 .o만 꺼내 앱에 넣는다. 그래서 보통은 A를 A.a로 두고 앱이 한 번만 링크 → 1벌.

merged 상황: 벤더가 static framework B를 배포할 때 B가 쓰는 A를 B 안에 미리 합쳐서 준 경우:

정상:   B.a = [B 심볼]           A.a = [A 심볼]
merged: B.a = [B 심볼 + A 심볼]     ← A가 B 안에 흡수된 채 배포

앱이 B도 링크 + A도 직접 링크하면, A 심볼 출처가 둘:
  ① B.a 안의 A 심볼   (merged라 딸려옴)
  ② A.a 의 A 심볼     (앱이 직접 링크)duplicate symbol '_OBJC_CLASS_$_AClass' → 빌드 실패

왜 에러(ODR): 링커 철칙 — 하나의 심볼(클래스·함수·전역)은 최종 바이너리에 정의가 딱 하나여야 한다. 같은 심볼 정의를 두 출처에서 발견하면 어느 게 진짜인지 정할 수 없어 duplicate symbol 에러.

static merged 중복dynamic 중복(3벌)
언제빌드 타임(링크 단계)런타임(dyld 로드)
증상duplicate symbol 에러 → 빌드 실패objc class in both 경고 + 조용한 버그
왜 이 시점앱을 한 번의 링크로 만드는데 A 정의 2개가 동시에 만나 링커가 즉시 잡음각 dynamic은 따로 링크돼 서로 A 품은 걸 모름 → dyld가 둘 다 로드하는 런타임에야 만남

→ static은 "한 자리에서 만나 즉시 에러"(발견 쉬움), dynamic은 "따로 살다 런타임에 만나 경고"(조용한 버그).

어떻게 merge되나: static은 .o 아카이브라 물리적으로 합칠 수 있다 — libtool -static -o B.a B_own.a A.a로 여러 .a를 하나로 묶거나, B를 빌드할 때 A를 흡수하게 설정. 벤더가 "고객이 A를 몰라도 B만 넣으면 되게" 편의로 말아서 배포할 때 생김.

실무 발생: 벤더 SDK가 인기 라이브러리를 번들했는데 앱도 CocoaPods/SPM으로 직접 추가 / 두 벤더 SDK가 각각 같은 A를 merged로 품어 둘 다 링크.

해결책은 "B를 dynamic"이 아니라 "A를 dynamic" 아닌가? prefix는 정확히 뭘 하나?

맞다. 공유되는 A는 "A 자체를 dynamic으로" 떼어내야 한다. ("B를 dynamic으로"는 근본 해결이 아님 — B가 A를 품은 채 dynamic이 되면 B 안의 A + 앱의 A가 여전히 런타임 2벌)

// A를 dynamic framework로 분리하면
Frameworks/A.framework  ← 1벌만
     ▲        ▲
    앱        B          둘 다 이 하나를 참조(dyld 1회 로드) → 싱글턴도 1개 ✅

심볼 prefix는 "공유"가 아니라 "충돌 회피"다. 두 벌의 A가 서로 다른 이름을 갖게 해서 링커/dyld가 별개 심볼로 보게 만든다 — 반드시 한쪽(B가 품은 A)만 rename해야 의미가 있다.

// B가 품은 A만 rename → 앱의 원본 A와 이름이 달라짐
B 안의 A   → BVendor_AClass, BVendor_A_foo …   (prefix)
앱이 쓰는 A → AClass, A_foo …                   (원본)
→ 이름이 다르니 "완전히 다른 심볼" → 충돌 없음 (단 A가 메모리에 2벌 공존)
  • 만약 B도 앱도 같은 prefixed 이름을 참조하면 → 이름이 같아져 여전히 중복. prefix는 "한쪽만 rename"해야 함
  • prefix 방식은 A가 2벌 공존(공유 아님) → 크기 낭비 + 싱글턴 각자. 공유가 목적이면 dynamic, 이름 충돌만 급히 피하려면 prefix
방법A가 몇 벌언제
A를 dynamic으로1벌(공유)A를 진짜 공유하고 싶을 때 = 근본 해결
앱에서 A 직접 링크 제거1벌(B 것 사용)B가 A 제공, 앱이 A 직접 안 써도 될 때
심볼 prefix(한쪽만 rename)2벌(독립)공유 불가/불필요, 충돌만 피할 때 차선책

dynamic은 왜 duplicate symbol 빌드 에러가 안 나나? (별도 링크 단계)

빌드 때 ld(정적 링커)가 두 번 따로 실행된다. 앱과 dynamic B는 서로 다른 링크 단위다.

[ld 실행 #1] B.o + A.a → B.framework   (B에 A 흡수, A 1벌 → OK)
[ld 실행 #2] 앱.o + A.a → 앱 실행파일    (앱에 A 흡수, A 1벌 → OK)
             └ dynamic B는 "동적 참조"로만 연결 — 앱 ld는 B가 export한
               심볼만 볼 뿐, B 내부의 A를 "정의"로 취급 안 함
[런타임]     dyld가 앱 + B.framework 로드 → A 클래스 2개 발견 → 경고
  • duplicate symbol 에러는 하나의 ld 링크 단계에서 같은 심볼 정의 2개를 볼 때만 난다
  • dynamic은 B 링크·앱 링크가 분리돼 각자 A가 1벌씩 → 링커가 중복을 볼 기회가 없음 → 빌드 통과
  • static merged는 앱 하나의 링크B 속 A + A.a가 동시 등장 → 즉시 에러

런타임 중복이 크래시가 아니라 경고인 이유 + 치명성: dyld는 같은 ObjC 클래스가 두 이미지에 등록되면 크래시시키지 않고 which one is undefined 경고 후 하나를 골라 진행한다(링커의 ODR 강제와 달리 관대). 그래서 오히려 더 위험 — duplicate symbol 빌드 에러는 즉시 발견·수정되지만, 런타임 중복은 빌드·QA를 통과하고 두 A가 다른 버전이면 싱글턴 분리·상태 불일치 같은 재현 어려운 버그로 프로덕션에서 터진다. "크래시로 안 죽어 덜 시끄러운" 게 함정. (ObjC 클래스는 경고+택1, Swift metadata 중복은 더 미묘하거나 크래시 가능)

duplicate symbol은 merge+앱참조 "그때만"? 그리고 "심볼 정의"란?

"그때만"이 아니다. 본질은 하나의 ld 링크 단계에 "같은 심볼의 정의"가 2개 이상 모일 때. merge+앱참조는 대표적 한 케이스일 뿐.

케이스왜 2벌
static1이 static2 merge + 앱이 static2도 직접 링크①static1 경유 ②직접 = 2벌
static1·static3가 각각 static2 merge + 앱이 둘 링크양쪽에 = 2벌 (앱이 static2 직접참조 안 해도!)
같은 static lib를 링크에 두 번 지정정의 2벌
전역변수를 헤더에 extern 없이 정의여러 .o에 복사

반대로 static1만 static2를 merge하고 앱이 static2를 직접 링크 안 하면 → 정의 1벌 → 에러 없음. 핵심은 "누가 참조하느냐"가 아니라 "정의가 몇 벌 모이느냐".

심볼(symbol) = 함수·전역변수·클래스에 붙는 "이름표"..o/바이너리의 심볼 테이블엔 두 종류가 적힌다:

구분의미
정의(defined)그 심볼의 실체가 여기 있다(함수 본문, 변수 실제 메모리)
참조(undefined)이 심볼을 쓰는데 실체는 다른 데 있음 → 링크 때 채워줘
  • 주인님 표현 "어디에 어떤 메서드·변수가 정의됐는지 적어놓은 것" = 심볼 테이블의 정의 부분이 맞다(+ 필요한 외부 심볼 "참조"도 함께 기록)
  • 링커는 참조를 정의에 연결(resolve): 정의 0개 → undefined symbol, 2개↑ → duplicate symbol, 정확히 1개 → 정상
  • Swift/C++는 오버로딩·제네릭 때문에 이름을 맹글링(mangling) → 시그니처 다르면 다른 심볼

링크 = 내가 제공하는 것(defined) ↔ 제공받아야 하는 것(undefined)을 잇는 것?

맞다. 내가 정의한 건 심볼 테이블에 정의(defined), 외부 methodK를 쓰면 참조(undefined)로 기록되고, 이 둘을 다른 모듈과 이어주는 과정이 링크.

// MyFramework 심볼 테이블 (nm 으로 확인 가능)
T _MyClass.foo          ← T = 내가 정의(실체 있음), 남이 쓸 수 있음
U _ExternalLib.methodK  ← U = undefined, 실체는 밖에 → 링크 때 연결
  • 링크 = U(참조)를 T(정의)에 연결(resolve): 정의 0개 → undefined symbol, 2개↑ → duplicate symbol, 1개 → 정상
  • 두 시점: static link=ld가 빌드 타임에 최종 바이너리로 합쳐 주소 확정 / dynamic link=빌드 땐 "외부 dylib에 있음" 표시만, 런타임 dyld가 바인딩
  • 노출 범위: public(external symbol)만 다른 프레임워크가 참조 가능. private/internal은 local symbol이라 밖에서 안 보임 → public 안 붙이면 그 심볼 참조조차 불가

"모듈명+클래스명이 겹치면 안 된다"는 무슨 내용인가?

Swift는 모듈이 네임스페이스다. 타입의 진짜 정체성 = 모듈명.타입명이고, 이게 심볼 맹글링에 들어간다.

// FrameworkA / FrameworkB 둘 다 User가 있어도
FrameworkA.User → _$s10FrameworkA4UserC
FrameworkB.User → _$s10FrameworkB4UserC   ← 모듈명이 달라 심볼이 다름 → 공존 OK

두 층위 구분:

  • 클래스명만 겹침 → 문제 아님: let u = User()는 모호(ambiguous)일 뿐, FrameworkA.User()로 명시하면 해결
  • 모듈명+클래스명 둘 다 겹침 → 진짜 충돌: 모듈명.타입명이 같아지면 맹글링 심볼이 동일 → duplicate symbol/타입 정체성 혼란. (서드파티가 우연히 같은 모듈명, 빌드설정 실수, 앱 타겟명=모듈명 충돌 등)

ObjC는 더 엄격 — 네임스페이스가 없다(flat). ObjC 클래스는 전역이라 클래스명만 겹쳐도 런타임 duplicate class. 그래서 NS·UI·회사약자 prefix 관례. @objc Swift 클래스도 ObjC 런타임엔 모듈명.클래스명 맹글링 이름으로 등록되어 모듈명이 유일성을 보장.

실무 규칙: 모듈명을 전역적으로 유니크하게(회사/제품 prefix). Core·Utils·Network 같은 흔한 이름은 다른 SDK와 모듈명 충돌 위험 → MyAppCore처럼. 같은 모듈명 프레임워크 두 개를 한 앱에 넣지 마라.

맹글링은 정의를 말하나? 모듈=import 단위? "같은 모듈명+클래스명+@objc = 런타임 중복 클래스" 뜻은?

① 맹글링 = 심볼 "이름을 만드는 규칙(인코딩)". 결과물(맹글링된 이름)이 심볼 테이블에 적힌다 — 정의만이 아니라 참조에도 똑같이 쓰인다. 오버로딩·제네릭·네임스페이스로 같은 이름이 여러 개 가능하니 모듈명+타입명+시그니처를 담아 유일하게 만든다.

T _$s10FrameworkA4UserC       ← 정의도 맹글링된 이름
U _$s10FrameworkB6methodyyF   ← 참조도 맹글링된 이름

② 모듈 = import 단위 맞다. 동시에 세 가지: import 단위 / 컴파일 단위(.swiftmodule 생성) / 네임스페이스 단위. 보통 1프레임워크=1모듈, 앱 타겟도 1모듈.

③ "같은 모듈명+클래스명 = 중복 클래스": @objc는 필요조건이 아니다. 근본 원인은 모듈명.타입명이 같으면 Swift 심볼(맹글링)이 겹치는 것.

Swift 심볼(맹글링) 충돌ObjC 런타임 duplicate class
순수 Swift✅ 발생 (static=빌드에러 duplicate symbol / dynamic=런타임 타입충돌)❌ (ObjC 런타임 안 올라감)
@objc/NSObject✅ 발생추가로 발생
  • 순수 Swift도 @objc 없이 이미 충돌 — 맹글링 심볼이 모듈명.타입명 기반이라 둘 다 같으면 심볼 동일
  • @objc/NSObject면 ObjC 런타임에 모듈명.클래스명(_TtC6Shared9Analytics)으로 등록 → 추가로 duplicate class definition 경고. ObjC 런타임은 flat(전역)
  • 모듈명이 다르면(SharedA.Analytics vs SharedB.Analytics) 심볼도 등록이름도 달라 충돌 안 함 → 같은 모듈명을 갖는 이상 상황에서만 발생
주의 — @objc 붙어도 "클래스명만" 같은 건 기본적으로 OK: @objc만 붙인 Swift 클래스는 ObjC 등록 이름에 모듈명이 자동 포함(_TtC10FrameworkA9Analytics)되어 모듈명 다르면 충돌 안 함. 클래스명만으로 충돌하는 건@objc(고정이름)으로 이름을 명시해 모듈명이 빠질 때 ②순수 ObjC 클래스(네임스페이스 없음 → prefix 필요)뿐이다.
종류ObjC 등록 이름클래스명만 같음(모듈명 다름)
@objc Swift (기본)모듈명.클래스명 맹글링OK
@objc(고정이름)지정 이름 그대로충돌
순수 ObjC 클래스클래스명 그대로충돌 → prefix

XCFramework는 prebuilt 컨테이너 — 컴파일 생략, 링크만? (맞다)

맞다. XCFramework = 이미 컴파일된 바이너리를 담는 컨테이너 → 소스 재컴파일 생략, 링크 단계에서 연결.

진짜 핵심 = 멀티 플랫폼/아키 slice 컨테이너. "static/dynamic 담는다"보다 본질은 플랫폼별 slice를 담아 빌드 시 맞는 slice 자동 선택.

MyLib.xcframework/
├── ios-arm64/                     ← iOS 실기기
├── ios-arm64_x86_64-simulator/    ← iOS 시뮬레이터
└── macos-arm64_x86_64/            ← macOS
static XCFrameworkdynamic XCFramework
소스 컴파일생략(prebuilt)생략(prebuilt)
사용측 타입체크인터페이스로인터페이스로
링크·배치링크 타임 → 앱 실행 파일 흡수Embed & SignFrameworks/
로드(이미 앱 안)런타임 dyld 로드

dynamic은 Embed & Sign 필요 — 안 하면 런타임에 못 찾아 크래시. static은 이미 앱에 흡수돼 별도 embed 없음.

"실기기 arm64 vs 시뮬레이터 arm64 충돌"이 무슨 말? .swiftmodule/헤더는?

① 예전엔 fat framework 하나로 다 썼다. lipo로 여러 아키를 한 바이너리에 합침 — 당시 실기기=arm64, 시뮬레이터=x86_64(인텔 맥)로 아키가 달라 공존 가능했다.

예전 fat framework = [ arm64(실기기) + x86_64(시뮬레이터) ]  ← 아키 달라 OK

M1(Apple Silicon) 이후 충돌: 시뮬레이터도 arm64가 됨 → 실기기 arm64와 시뮬 arm64가 둘 다 "arm64". lipo는 아키 이름으로만 구분해서 한 바이너리에 못 담음(duplicate architecture arm64). CPU는 같아도 target이 다름(arm64-apple-ios vs arm64-apple-ios-simulator).

XCFramework 해결: 아키가 아니라 폴더(slice)로 플랫폼 구분ios-arm64ios-arm64_x86_64-simulator가 둘 다 arm64여도 폴더가 달라 공존.

② 인터페이스 파일:

파일용도언어
.swiftmoduleSwift 타입체크용 선언(시그니처). 구현 본문 없음. 컴파일 때 생성Swift
.swiftinterface위의 텍스트·버전안정 버전(module stability)Swift
.h 헤더ObjC용 선언. 순수 Swift면 Module-Swift.h(Swift→ObjC 노출)ObjC
바이너리(Mach-O)실제 구현 코드링크/실행

주인님 이해대로 .swiftmodule은 "어떤 메서드가 있는지(시그니처) 적어놓은 파일"이 맞다 — 단 선언만, 구현은 바이너리에. 헤더는 ObjC용이 맞다.

[심화] 앱+dynamic B가 같은 static A 의존 → 런타임 duplicate 에러? (two-level namespace)

duplicate symbol 에러(빌드/런타임 크래시)는 안 난다. 진짜 문제는 A가 2벌 로드되어 싱글턴/전역 상태가 별개가 되는 것(상태 불일치). ObjC 클래스면 duplicate class 경고는 뜬다(크래시 아님).

앱(executable)    ← static A 흡수 (A 1벌)
B.framework(dylib) ← static A 흡수 (A 1벌)
→ 런타임에 A 코드가 앱 + B 이미지에 각각 = 2벌

왜 에러가 안 나나 — two-level namespace: dyld는 심볼을 (Level 1: 소속 이미지, Level 2: 심볼 이름) 두 단계로 바인딩한다. flat namespace(이름만)와 달리 "어느 이미지의 심볼인지"까지 못박는다.

맹글링 이름 (Level 2)소속 이미지 (Level 1)
static A가 에 흡수_$s1A6AClassC (모듈 A)앱(executable)
static A가 B에 흡수_$s1A6AClassC (모듈 A)B.framework(dylib)
  • 맹글링 이름(Level 2)은 같다 — 심볼은 정의된 모듈 A 기준으로 맹글링되므로 앱/B 어디 흡수돼도 동일(겹침)
  • 소속 이미지(Level 1)가 다르다 — 앱 vs B.framework → two-level이 이걸 구분해 각자 자기 것 참조 → 충돌 없이 2벌
  • 주의: "맹글링 모듈명"(정의된 모듈 A)과 "소속 이미지"(앱/B)는 별개 층. 안 겹치게 만드는 건 맹글링이 아니라 two-level의 소속 이미지
static은 이미지로 안 남는다: static A는 A.dylib 같은 별개 이미지가 아니라 앱/B에 각각 흡수된다. 그래서 A 심볼의 소속 이미지가 "앱" vs "B"로 갈려 Level 1이 달라진다. A를 dynamic(별개 dylib 이미지)으로 만들면 앱·B 모두 A.dylib를 Level 1로 참조 → 동일 → 1벌 공유(이게 "공유하려면 dynamic으로"의 원리). flat namespace였다면 이름만으로 전역 검색해 충돌했겠지만, Apple 기본 two-level이라 안 난다.

관련 문서