← 홈으로
Build System · Linking

컴파일 · 링킹 · 심볼 · static/dynamic · 중복

소스가 앱이 되기까지(컴파일 → 링킹 → 런타임), 심볼이 무엇이고 언제 결정되는지, static/dynamic 차이와 duplicate symbol·전역 상태 불일치가 왜 생기는지를 문답으로 정리한다. (2026-06-20 복습)

Linking Symbol duplicate symbol

0. 큰 그림 — 소스 → 앱

[컴파일] swiftc: 소스 → .o + .swiftmodule .o : 기계어 + 심볼 테이블(정의된/미해결 심볼) // 주소는 아직 미정 .swiftmodule: 타입 시그니처(인터페이스) // import하는 쪽 타입체크용 [묶기/패키징] 여러 .o → .a(static lib) / .framework [링킹] ld: 미해결 심볼 ↔ 실제 정의 매칭 + 주소 배치 static : 코드를 앱 실행 바이너리에 복사(합침) dynamic : "이 dylib에 있다"만 기록 → 런타임 바인딩 [런타임] dyld: dynamic 심볼의 최종 주소 바인딩(ASLR로 매번 다름)

1. 심볼(symbol)

심볼이란? 메서드 이름 같은 거야?

✅ 대체로 맞다. 심볼 = 링커가 "이름 있는 것"을 식별하는 고유 이름표. 함수가 대표 예. 단:

  • 함수만이 아니라 전역 변수·타입 메타데이터·witness table도 심볼
  • 사람이 쓴 이름이 아니라 mangled name(모듈·타입·시그니처 인코딩) — 오버로딩·제네릭·모듈 구분 위해 충돌 없는 고유 이름
func render<T: Drawable>(_:) in GraphicsKit ↓ name mangling $s11GraphicsKit6render...AA8DrawableRzlF // 실제 심볼 (swift demangle로 복원)

심볼은 언제 결정돼? 컴파일? 링킹?

이름은 컴파일, 주소(해결)는 링킹(또는 런타임).

단계무엇
컴파일(.o)심볼 이름 생성 + 심볼 테이블 기록 (주소 미정)
링킹(ld)미해결 심볼 매칭 + 주소 배치 (static은 확정)
런타임(dyld)dynamic 심볼의 최종 주소 바인딩

심볼 테이블엔 정의된 심볼(이 .o가 제공, 이름+위치)과 미해결 심볼(호출하는데 주소 모름, 이름만)이 있다. nm App.o로 확인 가능.

2. static vs dynamic & .app 번들

static / dynamic은 앱에 어떻게 들어가? .app 번들 구조는?

앱에 들어가는 방식
Static (.a / static framework)코드가 앱 실행 바이너리(Mach-O)에 복사·합쳐짐 — 별도 파일 아님
Dynamic (dynamic framework).app 번들 안에 별도 .framework 파일 + 런타임 dyld 로드
MyApp.app/ ├── MyApp // 실행 파일(Mach-O) — static 코드 녹아듦 ├── Info.plist ├── Frameworks/ // dynamic framework들 (별도 파일) │ ├── FrameworkA.framework │ └── FrameworkB.framework ├── Assets.car // 리소스 └── PlugIns/ // 앱 익스텐션 등

→ 실행 바이너리 + dynamic .framework + 리소스. static 코드는 바이너리에 녹아들어 별도 파일이 없다.

3. 링커는 어떻게 "한 번만" 링크하나

App과 Static2가 둘 다 Static1.log를 쓰면 중복 안 돼? 어떻게 한 번만?

링커가 "이미 정의된 심볼"을 추적하고, static lib는 "필요한 .o만 골라" 가져온다(on-demand pull).

App → log 참조 (미해결) ld: Static1.a에서 log 가진 .o 가져옴 → log "정의됨" 등록 ✅ Static2 → log 참조 ld: log는 이미 정의됨 → 다시 안 가져옴 // 한 번만!

핵심 규칙(ODR): 정의는 정확히 하나여야 하고, 참조는 여럿이어도 된다.

정의 개수결과
참조 여럿 + 정의 1개1✅ 정의 하나를 공유 (한 번만 링크)
정의 2개2❌ duplicate symbol

"의존(참조)"과 "품기(embed)"는 같은 거 아냐? Static2가 Static1을 가지고 있는 거잖아?

다르다. 참조는 코드를 안 품고, embed는 코드를 복사해 품는다.

참조(reference)품기(embed)
"그 심볼 쓸 거야" 이름만코드를 자기 안에 복사
코드 복사?❌ 없음✅ 있음
static lib 기본이게 기본명시적으로 합칠 때만
// 참조 (정의 1개 → 정상) Static1.a = [ log.o ] // log 코드 여기 하나 Static2.a = [ track.o ] // log 코드 없음! track.o는 log를 참조만 // embed (정의 2개 → duplicate) SDKA.a = [ featureA.o, xCompute.o ] // OpenSourceX embed SDKB.a = [ featureB.o, xCompute.o ] // 또 embed → xCompute 정의 2개

static .a는 자기 .o 아카이브일 뿐, 의존하는 다른 .a 코드를 자동으로 합치지 않는다 → 기본은 참조. embed는 "의존성 포함 fat lib" 만들 때(libtool 병합 등).

import만 하고 안 쓰면 문제없어?

대체로 그렇다. import는 타입체크용이고, 링크는 실제 사용하는 심볼만 끌어온다.

  • 순수 Swift static lib(lazy): import만 하고 안 쓰면 코드 안 끌려옴 → 중복도 안 생김 ✅
  • ⚠️ 예외: dynamic framework(통째 포함) / -ObjC·-force_load·-all_load(전체 강제 포함) / ObjC 클래스 등록 → 안 써도 포함

4. duplicate symbol — 왜 안 되나 & 코드 예시

왜 같은 심볼이 둘이면 안 돼?

심볼 = 이름 → 실체(주소) 매핑. 같은 이름에 실체가 둘이면 모호해진다.

  • 함수 중복: foo가 0x1000·0x5000 둘 → 어느 구현을 부를지 모호
  • 전역 변수 중복: 저장 공간이 둘 → 한쪽 수정이 다른 쪽에 안 보임(상태 불일치)
counter 심볼 → 0x2000 (A 복사본) counter 심볼 → 0x6000 (B 복사본) A.increment() // 0x2000 +1 → A는 1 B.read() // 0x6000 읽음 → B는 0 ← 안 보임!

비유: 전화번호부에 "김철수"가 두 번(번호 각각) → "김철수한테 전화/메시지"가 모호·안 닿음. 이름당 하나여야 한다.

Swift 코드로 — 정상(A) vs duplicate(B) vs 런타임(C)

A. 정의 1개 + 참조 여럿 → 정상

// Static1 public func log(_ msg: String) { print("[LOG] \(msg)") } // 정의 하나 // Static2 (import Static1) public func track(_ e: String) { log("track: \(e)") } // 참조 // App (Static1, Static2 import) — log 참조 2곳 → 정의 1개 공유 → ✅ 빌드 성공

B. 정의 2개(같은 모듈 embed) → duplicate symbol 빌드 에러

// OpenSourceX (static lib) public func xCompute(_ a: Int) -> Int { a * 2 } // SDKA (OpenSourceX를 static link해서 embed), SDKB도 embed // App → SDKA, SDKB 둘 다 link duplicate symbol '$s11OpenSourceX8xComputeySiF' in: libSDKA.a(OpenSourceX.o) libSDKB.a(OpenSourceX.o) // 같은 모듈 복제 → 맹글 이름 동일 → 충돌

C. 정의 2개여도 dynamic으로 갈라지면 → 빌드 통과·런타임 경고

// @objc class를 두 dynamic framework가 각자 embed objc[xxxx]: Class _TtC11OpenSourceX8MyLogger is implemented in both FrameworkA and FrameworkB. One of the two will be used. Which one is undefined.
왜 다른 모듈은 충돌 안 하나: Swift mangled name에 모듈명이 포함($s7ModuleA3foo... vs $s7ModuleB3foo...) → 다른 모듈이면 이름이 달라 안 겹침. duplicate는 같은 모듈을 두 곳에 복제할 때만.

빌드타임 에러 vs 런타임 경고 — 무엇이 가르나

구성같은 바이너리?언제 터지나
두 dynamic framework가 static lib 각각 embed❌ 별도 바이너리런타임 경고·상태 불일치
한 앱 바이너리에 같은 코드 두 벌 입력✅ 같은 바이너리빌드타임 duplicate symbol
한 앱 바이너리, static lib 한 번만 링크에러 없음

dynamic framework 경계가 심볼을 캡슐화하므로 빌드 땐 충돌이 안 보이고 런타임으로 미뤄진다. 같은 바이너리로 합쳐질 때만 빌드 단계에서 잡힌다.

해결: static lib를 dynamic framework로 만들어 .app/Frameworks/에 하나만 두고 공유 / 또는 의존성을 한 곳에서만 link.

4.5 참조 vs embed vs 인라인 · static framework · .o 단위

참조 / embed / 인라인 — 정확히 뭐가 달라?

무엇언제호출부 모습
참조이름만, 링킹 때 주소 연결 (코드는 원본 한 벌)컴파일+링킹call foo (→ 모듈3 주소)
embed모듈3의 .o를 모듈1의 .a에 별도 포함아카이브 단계(ar/libtool)call foo (→ 같은 .a 안 모듈3.o)
인라인호출부에 foo 본문을 펼쳐넣음컴파일 최적화foo 코드가 그 자리에 (call 사라짐)

"호출부에 붙여넣기"는 embed가 아니라 인라인. embed는 모듈3 .o를 라이브러리 아카이브에 통째로 같이 묶는 것(코드는 별도 오브젝트, 호출부는 여전히 call foo).

소금 비유: 참조=옆집 소금 빌려 씀(요리 때 한 번) / embed=옆집 소금통을 내 찬장(.a)에 갖다둠(둘이 갖다두면 중복) / 인라인=레시피에 소금 만드는 법을 베껴 씀(호출 사라짐).

.swift 파일 하나 = .o 하나야?

아니다(보장 아님). Swift는 C와 달리 모듈 단위로 컴파일한다.

빌드 모드.o 산출
Incremental(디버그 기본)대략 파일별 .o 비슷 (바뀐 파일만 재컴파일하려고)
WMO(릴리즈)모듈 전체를 하나(또는 병렬용 몇 개)의 .o로

C/ObjC는 "1 소스 → 1 .o"지만, Swift는 모듈이 컴파일 단위라 .o 개수가 모드·스레드 설정에 따라 달라진다.

static framework가 다른 static framework를 쓰면 어디에 포함돼?

static framework는 본질적으로 .a(자기 바이너리 없음). 다른 static framework Y를 참조만 한다(자기 안에 포함 X).

StaticFW_X.a = [ x.o ] // Y 코드 없음, Y를 참조만 StaticFW_Y.a = [ y.o ] // Y 코드 여기 앱 링크: X.a + Y.a → 앱 실행 바이너리에 x.o + y.o (각각 한 번) X의 Y 호출 → 앱 바이너리 안 y에 연결

→ Y는 X.a 안이 아니라 앱 실행 바이너리에 한 번 포함되어 연결. (static lib엔 실행파일이 없다)

static framework를 dynamic framework와 앱이 동시에 쓰면 런타임 문제?

그렇다. static framework는 자기 바이너리가 없어 쓰는 쪽에 코드가 복사된다.

DynamicFW.framework ← StaticFW static link → StaticFW 코드 복사 ┐ App 실행 바이너리 ← StaticFW 직접 static link → StaticFW 코드 복사 ┘ 양쪽에! → StaticFW가 두 별도 바이너리(DynamicFW + 앱)에 각각 복제 → 빌드는 통과(별도 바이너리라 캡슐화) → 런타임에 중복 경고·상태 불일치

참조만 하면 되는데 왜 embed하는 상황이 생겨?

이상적으론 참조만 하면 중복이 없다. embed는 다른 목적으로 생긴다:

이유설명
자족적 배포SDK를 하나의 .a/.framework로 배포하려 의존 오픈소스를 통째 품음(사용자가 별도로 안 받게)
의존성 숨김제공자가 내부 의존성을 감춤
빌드 설정 실수-force_load·static framework 잘못 구성
과거 관행옛 static lib 배포 시 의존성 포함이 흔함

→ 두 SDK가 같은 오픈소스를 각자 품으면 충돌. 이상적 해결: 공통 의존성을 별도 모듈(특히 dynamic framework)로 분리해 모두가 참조.

5. 관련 문서