← Swift & Foundation 로드맵 14 / 33

14 · Foundation이란 무엇인가

libSystem과 UIKit 사이 어디에 Foundation이 있는지부터, NS 접두사의 기원과 SE-0086의 기준, corelibs-foundation이 낳은 divergence, 2022~2026년 swift-foundation 재작성의 검증된 타임라인과 성능 수치, URL과 NSURL이 같은 타입이 아닌 이유까지.
진행률
0 / 0 완료

13장까지는 Swift 언어 자체의 규칙 — 값 의미론, 에러 처리, 제네릭, 접근 제어와 ABI — 을 다뤘다. 여기서부터는 그 언어가 40년 가까이 물려받았고, 지금 이 순간에도 발밑에서 다시 쓰이고 있는 프레임워크로 넘어간다. import Foundation 한 줄로 딸려오는 이 라이브러리는 NeXTSTEP 시절 Objective-C로 태어났지만, 2023년부터 시작된 재작성으로 그 내부 구현의 상당 부분이 Swift 네이티브 코드로 교체되는 중이다. 이 챕터는 Foundation이 플랫폼 스택에서 정확히 어디에 있는지, NS 접두사가 왜 어떤 타입엔 남고 어떤 타입엔 없는지, 리눅스용 재구현이 왜 별도 프로젝트로 시작됐다가 다시 통합됐는지, 그리고 2022~2026년의 재작성이 실제로 무엇을 바꿨는지를 다룬다. 다른 어떤 챕터보다도 "그럴듯하게 아는 것"과 "정확히 아는 것"의 격차가 큰 주제라, 성능 수치와 연도는 전부 swift.org·swift-evolution 1차 출처를 확인해 인라인으로 달았다.

Q1. Foundation은 iOS 소프트웨어 계층에서 어디에 있고 무슨 역할을 하는가?

🔑 30초 답변

Darwin 플랫폼은 대략 libSystem(커널·POSIX·BSD C API) → CoreFoundation(순수 C, Objective-C 런타임 불필요) → Foundation(NSObject 루트 클래스를 도입하는 Objective-C 표면) → UIKit·AppKit·SwiftUI(플랫폼별 UI) 순서로 쌓인다. Foundation은 그 경계에서 문자열·컬렉션·날짜·URL 같은 범용 모델 타입과, KVO·NSCoding 같은 Objective-C 런타임 계약을 함께 제공한다. 다만 2024년 가을부터는 Foundation.framework 내부 구현의 상당 부분이 swift-foundation(Swift 네이티브 코드)으로 교체됐다 — 겉의 API 표면(헤더)은 그대로지만, 안은 계속 Swift로 옮겨가는 중이다.

원리

Apple의 Cocoa Fundamentals Guide는 이 계층을 하위 계층 위에 상위 계층이 쌓이는 구조로 설명한다. CoreFoundation은 순수 C API라 Objective-C 런타임이 없는 환경에서도 링크할 수 있고, Foundation은 그 위에서 NSObject라는 런타임 접점(isKindOfClass:, respondsToSelector:, perform(_:with:), KVC/KVO, 메시지 디스패치)을 도입한다. Swift의 순수 class는 이 인프라를 자동으로 상속하지 않는다 — NSObject를 직접 상속해야만 그 계약 안으로 들어간다.

내부 동작

이 계층을 눈으로 확인하는 가장 빠른 방법은, 아주 작은 바이너리를 링크해보는 것이다. import Foundation만 있는 파일을 컴파일해 otool -L을 찍으면 실제 의존 관계가 드러난다.

Swift 6.2.1 / macOS 26 실측 — Foundation만 import한 최소 바이너리의 직접 링크 목록
$ cat stack.swift
import Foundation
print("hello")

$ swiftc stack.swift -o stack && otool -L stack
stack:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1356.0.0)
	/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 4109.1.255, weak)
	/usr/lib/swift/libswiftCore.dylib (compatibility version 0.0.0, current version 0.0.0)
	/usr/lib/swift/libswiftCoreFoundation.dylib (compatibility version 1.0.0, current version 120.100.0, weak)
	/usr/lib/swift/libswiftDispatch.dylib (compatibility version 1.0.0, current version 56.0.0, weak)
	...

Foundation.framework가 직접 걸려 있고, 그 옆에 libswiftCoreFoundation.dylib이 별도로 보인다 — Swift 쪽에서 CoreFoundation 타입을 다루는 브리지 계층이다. CoreFoundation.framework 자체는 이 목록에 별도 줄로 나타나지 않는데, 이건 CoreFoundation이 사라졌다는 뜻이 아니라 요즘 macOS의 시스템 프레임워크 다수가 디스크 위 개별 파일이 아니라 dyld 공유 캐시 안에 있어서, 이 작은 바이너리 하나의 직접 링크 목록만으로는 전체 전이(transitive) 의존성이 다 드러나지 않기 때문이다. CoreFoundation이 Foundation과 별도 계층이라는 사실 자체는 Apple의 아키텍처 문서 쪽에서 확인해야 한다.

Darwin 플랫폼 스택 — 아래로 갈수록 범용, 위로 갈수록 특수화 범용 → 특수 UIKit · AppKit · SwiftUI 플랫폼별 UI 프레임워크 Foundation NSObject 도입 · 모델 타입 · ObjC 런타임 접점 CoreFoundation 순수 C, Objective-C 런타임 불필요 libSystem 커널 · POSIX · BSD C API swift-foundation 2023~ Swift 네이티브 코어가 Foundation 내부 구현을 대체 중
Foundation은 CoreFoundation과 UIKit 사이에서 NSObject 접점과 모델 타입을 제공하며, 2023년부터는 그 내부 구현이 swift-foundation(Swift 네이티브 코드)으로 옮겨가는 중이다.

실험 · 도구

otool -L은 프로젝트 아무 파일이나 컴파일해서 바로 재현할 수 있다. Xcode로 만든 실제 앱 바이너리에도 같은 명령을 써보면(otool -L YourApp.app/YourApp | grep Foundation) Foundation이 앱 번들 안이 아니라 시스템 프레임워크로 동적 링크된다는 걸 확인할 수 있다 — 이 사실은 15장에서 "FoundationEssentials를 써도 iOS 앱이 가벼워지지 않는다"는 이야기의 근거가 된다.

프로젝트 적용

Foundation을 습관적으로 import하기 전에, 그 코드가 정말 이 계층까지 필요한지 한 번 따져보는 습관은 실무에서 은근히 유용하다. 표준 라이브러리만으로 되는 일에 Foundation을 얹으면 그만큼 무거운 의존성(그리고 15장에서 다룰 크로스플랫폼 제약)이 딸려온다.

Foundation이 정말 필요한지 먼저 따져보기
import Foundation   // ❌ 습관적으로 추가 — 이 함수엔 전혀 필요 없다

func isBlank(_ s: String) -> Bool {
    s.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}

// ✅ 순수 Swift 표준 라이브러리만으로 충분 — Foundation 의존 자체가 없다
func isBlankPure(_ s: String) -> Bool {
    s.allSatisfy { $0.isWhitespace || $0.isNewline }
}

반대로 Foundation의 런타임 접점(NSObject 계층)이 실제로 필요한 순간도 있다. dynamic 키워드는 Objective-C 클래스 멤버에만 붙일 수 있어서, 순수 Swift class에는 컴파일 에러가 난다 — 이 계층 개념이 실제 컴파일러 에러로 드러나는 지점이다.

NSObject 상속 여부가 Foundation 런타임 계약 진입 여부를 가른다
// ❌ 순수 Swift class는 Foundation/ObjC 런타임 계약에 들어가 있지 않다
final class Counter {
    dynamic var value = 0   // 컴파일 에러: 'dynamic' only works with members of an Objective-C class
}

// ✅ NSObject를 상속해야 그 계약(런타임 인프라) 안으로 들어간다
final class ObservableCounter: NSObject {
    @objc dynamic var value = 0   // Foundation/ObjC 런타임 접점을 통해 KVO 등이 가능해진다
}
⚠️ 흔한 오해

"Foundation은 전부 Objective-C로 되어 있다"는 더 이상 정확하지 않다. 2024년 가을(iOS 18/macOS 15)부터 핵심 타입 다수가 Swift로 재작성되어 Darwin의 Foundation.framework 내부에서 실행된다 — 겉의 헤더(API 표면)는 그대로지만 구현이 바뀌었다(Q4). 반대로 "NSObject를 상속 안 하면 Swift 코드는 Foundation과 무관하다"도 틀렸다 — String, Array 같은 순수 값 타입도 필요할 때 내부적으로 Foundation/CoreFoundation과 브리징된다(16장에서 심화).

🧒 쉽게 이해하기

아파트 건물을 층층이 쌓아 올린다고 생각하면 된다. libSystem은 기초 공사(전기·수도 같은 가장 기본적인 배관)이고, CoreFoundation은 건물 골조다. Foundation은 각 세대 내부 구조와 엘리베이터·우편함 같은 공용 규칙이고, UIKit·SwiftUI는 실제로 눈에 보이는 인테리어 디자인이다. 비유가 깨지는 곳: 보통 건물은 한 번 골조를 세우면 잘 안 바꾸지만, Foundation은 겉모습(API)은 그대로 둔 채 안의 골조 자체(구현 언어)를 통째로 갈아 끼우는 공사를 지금도 진행 중이다.

꼬리 질문

CoreFoundation과 Foundation은 왜 별도 프레임워크로 나뉘어 있는가?
CoreFoundation은 순수 C API라 Objective-C 런타임이 필요 없는 환경에서도 링크할 수 있도록 설계됐고, Foundation은 그 위에 Objective-C 클래스 표면과 NSObject 런타임 계약을 얹은 계층이다. 이 이중 구조 덕분에 CFStringRefNSString처럼 캐스팅만으로 오가는 toll-free bridging이 가능해진다 — 자세한 메커니즘은 16장에서 다룬다.
쉽게 말하면 골조(CoreFoundation)와 그 위에 얹는 세대별 마감(Foundation)을 따로 관리하면, 골조는 그대로 두고 마감만 새로 할 수 있다.
libSystem 위에 Foundation 없이 바로 UIKit을 얹으면 안 되는가?
이론적으로 불가능한 건 아니지만, UIKit이 요구하는 문자열·컬렉션·URL·직렬화 같은 모델 계층 자체를 Foundation이 이미 제공하고 있어서, 사실상 UIKit은 Foundation을 전제로 설계돼 있다. 그 계층을 건너뛰려면 UIKit이 기대하는 모든 모델 타입을 처음부터 다시 만들어야 한다.
쉽게 말하면 인테리어 업체가 전기·배관 배치도(모델 계층) 없이 바로 마감 공사를 할 수는 없는 것과 같다.
Foundation.framework가 앱 바이너리 안이 아니라 시스템에 동적 링크된다는 사실은 어디로 이어지는가?
이 계층에서 Foundation이 OS와 함께 미리 로드돼 있다는 사실은, "FoundationEssentials처럼 더 작은 모듈을 import하면 iOS 앱이 가벼워진다"는 흔한 기대가 왜 Darwin에서는 대체로 틀리는지 설명하는 근거가 된다. 정적 링크가 일어나는 Linux/Windows 환경에서만 그 크기 절감이 실제로 의미를 갖는다.
쉽게 말하면 동네에 이미 있는 대형 도서관(시스템 프레임워크)에서 책을 빌려 보는 거라, 내가 굳이 그 책을 복사해서 집에 쌓아둘 필요가 없다. 자세한 이야기는 15장에서 이어진다.

Q2. `NS` 접두사는 어디서 왔고, SE-0086은 무슨 기준으로 떼고 남겼는가?

🔑 30초 답변

NS는 NeXTSTEP에서 유래한 접두사다(Foundation Kit → OpenStep → macOS Foundation.framework 계보). SE-0086(Swift 3, 2016)이 90개 넘는 타입에서 이를 뗐는데, 기준은 "Objective-C 런타임에 본질적으로 묶였는가", "상위 프레임워크(AppKit/UIKit) 전용 네임스페이스인가", "이미 별도 값 타입 대응물이 있는가"였다. 중요한 건 이 변화가 Swift 쪽 이름표만 바꾼 것이지, Objective-C 클래스 자체를 개명한 게 아니라는 점이다 — 제안서 원문도 "모든 변경은 Swift에만 해당하며 Objective-C는 바뀌지 않는다"고 명시한다.

원리

SE-0086은 접두사를 뗄지 말지를 가르는 규칙을 이렇게 정의한다(원문 그대로).

기준유지 대상 예시핵심 이유
Objective-C 런타임 자체에 본질적으로 묶임NSObject, NSAutoreleasePool, NSException, NSProxySwift 값 타입으로 옮길 수 없는 런타임 개념 그 자체
상위 프레임워크(AppKit/UIKit) 전용 네임스페이스NSUserNotification, NSXPCConnection, NSBackgroundActivityFoundation이 아니라 더 상위 프레임워크 네임스페이스에 맞춰야 함
이미 별도 값 타입 대응물이 존재NSArray, NSString, NSPersonNameComponentsArray/String은 이름만 바꾼 게 아니라 원래부터 별개인 stdlib 타입(SE-0069)
근시일 내 값 타입 도입 예정(전환기 한정 규칙)NSAttributedString, NSRegularExpression, NSPredicate이후 AttributedString처럼 진짜 값 타입이 별도로 생겼다
객체 전용 제네릭 컬렉션(Any 미지원)NSCache, NSMapTable, NSHashTable, NSOrderedSet구조체 전환이 함께 걸려 있어 지금도 보류 중(28장)
위 어디에도 안 걸림NSFileManagerFileManager, NSTaskProcess(개명까지)Swift 쪽 이름만 교체 — Objective-C 클래스 자체는 그대로

내부 동작

"이름만 바뀌었다"는 말을 가장 직접적으로 확인할 수 있는 지점은 런타임 클래스 이름이다. NSStringFromClass는 Swift 쪽 타입 이름이 아니라 Objective-C 런타임이 실제로 아는 이름을 돌려준다.

Swift 6.2.1 / macOS 26 실측 — Process의 런타임 클래스 이름은 여전히 NSTask다
import Foundation

print(NSStringFromClass(Process.self))       // NSTask
print(NSStringFromClass(FileManager.self))   // NSFileManager

// "값 타입 대응물이 이미 있다" 규칙으로 접두사가 남은 타입들
print(NSStringFromClass(NSCache<NSString, NSObject>.self))  // NSCache
print(NSStringFromClass(NSRegularExpression.self))            // NSRegularExpression

Process라는 Swift 이름 뒤에서 실제로 도는 Objective-C 클래스는 여전히 NSTask다. SE-0086 원문이 못 박은 그대로다 — "제안된 모든 변경은 Swift에만 해당하며, Objective-C는 바뀌지 않는다." 즉 SE-0086은 새 클래스를 만든 게 아니라, Swift 쪽에서 기존 Objective-C 클래스를 가리키는 이름표를 바꿔 단 것이다.

실험 · 도구

위 코드는 swift 파일명.swift로 바로 재현된다. 크래시 리포트나 디버거 콘솔에서 NSTask, NSFileManager 같은 옛 이름이 튀어나와도 당황할 필요 없다 — Swift 코드에서는 Process/FileManager로 부르는 바로 그 타입이다.

프로젝트 적용

이런 이름 불일치를 디버깅 편의 함수로 미리 흡수해두면, 크래시 로그와 코드 사이를 오갈 때 헷갈리지 않는다.

런타임 클래스 이름까지 함께 찍어주는 디버그 헬퍼
func debugDescribe(_ obj: AnyObject) -> String {
    let runtimeName = NSStringFromClass(object_getClass(obj)!)
    return "\(type(of: obj)) (런타임 클래스: \(runtimeName))"
}

print(debugDescribe(Process()))       // Process (런타임 클래스: NSTask)
print(debugDescribe(FileManager()))   // FileManager (런타임 클래스: NSFileManager)

NSCache, NSRegularExpression처럼 접두사가 남아 있는 타입을 보고 "왜 아직도 레거시를 써야 하지"라고 걱정할 필요는 없다 — SE-0086이 의도적으로 남긴, 지금도 정식 API다.

NSCache는 레거시가 아니라 SE-0086이 의도적으로 남긴 정식 API
// 스레드 세이프 + 메모리 압박 시 자동 축출 — Dictionary로는 못 하는 일 (28장 참고)
let cache = NSCache<NSString, NSData>()
cache.countLimit = 100
cache.setObject(Data("thumbnail".utf8) as NSData, forKey: "key-42")
⚠️ 흔한 오해

"이름이 바뀌었으니 Objective-C의 NS* 클래스와 완전히 다른 타입이다"는 상황에 따라 다르다. Process/FileManager처럼 그냥 개명만 된 경우는 진짜 같은 클래스이고 이름표만 다르다(위 실험이 그 증거다). 반면 URL/Date/Data는 SE-0069가 별도의 새 값 타입을 만든 경우라 원래 클래스(NSURL 등)와 다른 실체다 — 다만 그 원래 클래스도 사라지지 않고 여전히 존재한다(Q5).

🧒 쉽게 이해하기

결혼 후 성을 바꾼 사람과, 새로 주민등록증을 발급받은 경우를 비교하면 된다. Process는 그냥 개명이다 — 이름표만 바뀌었을 뿐 주민번호(런타임 클래스, NSTask)는 그대로다. URL은 새 신분증 발급에 가깝다 — URL이라는 완전히 새 값 타입이 생겼고, 원래 사람(NSURL)도 여전히 따로 살아있다(Q5). 비유가 깨지는 곳: 실제 개명은 서류 한 장으로 끝나지만, 여기서는 "개명"(SE-0086)과 "새 신분증 발급"(SE-0069)이 완전히 다른 두 제안서에서 각각 별개로 결정됐다.

꼬리 질문

NSCache는 왜 아직도 NS가 붙어 있는가?
SE-0086의 "객체 전용 제네릭 컬렉션" 규칙 때문이다. NSCacheAny가 아니라 클래스 타입에만 쓸 수 있는 제네릭이라, 접두사를 떼려면 구조체로의 전환까지 함께 걸려 있는데 그 전환이 아직 이뤄지지 않았다. 28장에서 NSCache의 실제 동작(스레드 세이프, 압박 시 자동 축출)을 다룬다.
쉽게 말하면 이사(구조체 전환)를 하려면 짐(내부 구현)을 통째로 다시 싸야 해서, 개명(접두사 제거)만 먼저 할 수 없는 경우다.
URL이나 Date 클래스(NSURL, NSDate)는 왜 SE-0086의 "접두사 제거" 표에 등장하지 않는가?
SE-0086은 기존 클래스의 이름만 바꾸는 제안이었는데, NSURL/NSDate는 이름이 바뀐 게 아니라 그 옆에 완전히 새로운 값 타입(URL, Date)이 SE-0069를 통해 추가된 경우다. 그래서 "이름 제거" 표에는 안 나오고, "값 타입 대응물이 이미 있다"는 규칙으로 오히려 원래 클래스가 접두사를 유지하는 쪽에 속한다. 자세한 내용은 Q5에서 다룬다.
쉽게 말하면 명단(SE-0086 표)은 "이름만 바꾼 사람들" 목록이라, "새 사람이 하나 더 태어난" 경우(SE-0069)는 애초에 그 명단에 낄 자리가 없다.
SE-0086이 Objective-C 코드에도 영향을 주는가?
아니다. 제안서 원문이 명시적으로 "제안된 모든 변경은 Swift에만 해당하며, Objective-C는 변경되지 않는다"고 못 박는다. Objective-C 쪽에서는 여전히 NSTask, NSFileManager 그대로다 — Swift 임포터가 이를 Process, FileManager로 보여주는 매핑만 새로 생겼을 뿐이다.
쉽게 말하면 번역기가 다른 언어로 이름을 다르게 불러줄 뿐, 원래 사람(Objective-C 클래스)은 그대로인 것과 같다.

Q3. swift-corelibs-foundation은 왜 만들어졌고 어떤 구조적 문제를 낳았는가?

🔑 30초 답변

swift-corelibs-foundation은 2015년 12월 Swift 오픈소스화와 함께, Objective-C 런타임이 없는 Linux에도 Foundation API를 주려고 만들어졌다. 문제는 이것이 Darwin Foundation과 완전히 독립된 재구현이었다는 점이다 — 같은 API 이름 뒤에 서로 다른 두 구현체가 있어, 동작이 플랫폼마다 미묘하게 달랐고 버그를 두 번 고쳐야 했다. 2023년 이후 swift-foundation이 등장하며 이 구조가 뒤집혔다: 지금 swift-corelibs-foundation은 사실상 FoundationEssentials/FoundationInternationalization을 그대로 재노출(re-export)하는 얇은 레이어일 뿐이다.

원리

divergence(동작 발산) 문제는 이렇게 요약된다 — API 시그니처가 같아도 내부 구현이 다르면, 한쪽 플랫폼에서만 재현되는 버그가 생기고, 그 버그를 고치려면 두 코드베이스를 각각 수정해야 한다. swift-corelibs-foundation README 자체도 "API 변경이 불가피한 영역이 있다"고 인정한다 — 대표적으로 AutoreleasingUnsafeMutablePointer처럼 Objective-C 런타임의 오토릴리즈 인프라에 기대는 몇몇 "funnel method"는 Linux에 대응물이 없어 시그니처 자체가 갈렸다.

내부 동작

이 구조가 실제로 어떻게 뒤집혔는지는 소스에서 바로 확인할 수 있다. swift-corelibs-foundation의 Foundation 타깃은 이제 Essentials.swift 파일 두 줄로 시작한다.

2026년 현재 swift-corelibs-foundation 실제 소스(Sources/Foundation/Essentials.swift) 전문
$ curl -s https://raw.githubusercontent.com/swiftlang/swift-corelibs-foundation/main/Sources/Foundation/Essentials.swift

// Re-export FoundationEssentials and Internationalization
@_exported import FoundationEssentials
@_exported import FoundationInternationalization

즉 오늘날의 Foundation(Linux)은 독자 재구현이 아니라, swift-foundation 두 모듈을 @_exported로 그대로 재노출한 뒤 레거시 NS* 클래스 표면(DateFormatter.swift, FileManager.swift 등)만 얇게 얹은 껍데기다. "이중 구현으로 인한 divergence"라는 근본 문제가 아키텍처 레벨에서 없어진 셈이다.

2015~2023 · 독립된 두 구현 Darwin Foundation Objective-C, 비공개 소스 swift-corelibs-foundation Linux, 독자 재구현 동작 divergence — 같은 API, 다른 결과 2023~ · 공유 코어 + 얇은 레이어 swift-foundation 공유 Swift 코어 Foundation (Darwin, 얇은 ObjC 표면) corelibs-foundation (재노출 레이어)
2023년 이전엔 Darwin과 Linux가 각자 다른 구현을 가져 동작이 갈렸지만, swift-foundation 이후로는 하나의 코어를 공유하고 각 플랫폼은 얇은 레이어만 얹는다.

실험 · 도구

curl 명령은 누구나 그대로 실행해 재현할 수 있다. Sources/Foundation/ 디렉터리의 다른 파일들(DateFormatter.swift, FileManager+POSIX.swift 등)을 열어보면, 대부분 레거시 NS* API·포매터·플랫폼별 POSIX 처리만 담고 있고 URL/Data/JSONDecoder 같은 핵심 로직은 더 이상 여기 없다는 것도 직접 확인 가능하다.

프로젝트 적용

서버사이드 Swift(Vapor 등)나 크로스플랫폼 패키지를 짤 때, 오늘날은 "Darwin과 Linux가 같은 swift-foundation 코어로 동작한다"는 전제를 갖고 작성해도 대체로 안전하다.

2024년 이후엔 이 코드가 macOS와 Linux에서 같은 swift-foundation 코어로 동작한다
import Foundation

struct Ping: Codable { let ok: Bool }

func decodePing(_ raw: String) throws -> Ping {
    try JSONDecoder().decode(Ping.self, from: Data(raw.utf8))
}

다만 divergence가 완전히 사라진 건 아니다. ObjC 런타임 인프라에 기대는 소수 API는 여전히 플랫폼별로 갈릴 수 있어서, 그런 경로엔 조건부 컴파일로 안전망을 두고 CI에서 실제로 Linux 빌드를 돌려 확인해야 한다 — Darwin 통과가 Linux 동작을 보장하지 않는다.

남은 divergence 영역은 조건부 컴파일로 표시해두고 실제 Linux CI로 검증한다
#if canImport(Glibc)
// Linux엔 ObjC 런타임의 오토릴리즈 포인터 인프라가 없다 —
// 그 인프라에 기대는 소수 API는 여전히 플랫폼별로 시그니처가 갈릴 수 있다
import Glibc
#endif
⚠️ 흔한 오해

"Linux Foundation은 여전히 Apple Foundation의 부분집합/열화판이다"는 2023년 이전엔 맞는 말이었지만, 지금은 대체로 틀렸다. 핵심 로직은 100% 같은 코드(swift-foundation)를 공유하고, 남은 차이는 Objective-C 런타임 의존 API 소수로 좁혀졌다. "swift-corelibs-foundation 저장소는 이제 쓸모없다"도 틀렸다 — 레거시 NS* 클래스 표면과 플랫폼별 POSIX 처리는 여전히 이 저장소가 담당한다.

🧒 쉽게 이해하기

예전엔 서울 지점과 뉴욕 지점이 같은 매뉴얼 제목 아래 각자 다른 내용의 매뉴얼을 따로 썼다(두 팀이 독립적으로 재구현). 지금은 본사가 매뉴얼을 하나로 통일해서 두 지점이 그 원본을 그대로 쓰고, 각 지점은 현지 사정에 맞는 아주 짧은 부록만 따로 붙인다. 비유가 깨지는 곳: 회사 매뉴얼은 한 번 통일하면 그걸로 끝이지만, Foundation의 통합은 여전히 진행 중인 공사라 부록(레거시 호환 레이어)이 언제 완전히 사라질지는 정해져 있지 않다.

꼬리 질문

지금도 swift-corelibs-foundation 저장소가 남아있는 이유는?
핵심 로직은 swift-foundation으로 옮겨갔지만, Darwin에는 필요 없는 레거시 NS* 클래스 표면(예전 DateFormatter의 세부 동작, 플랫폼별 POSIX/Win32 파일 처리)이 이 저장소에 남아 있다. 완전히 사라진 게 아니라 역할이 "전체 재구현"에서 "얇은 호환 레이어"로 축소된 것이다.
쉽게 말하면 이사 가면서 대부분의 짐(핵심 로직)은 새 집(swift-foundation)으로 옮겼지만, 옛 집 주소로만 오는 우편물(레거시 API)을 받아줄 관리인은 여전히 필요한 것과 같다.
왜 굳이 swift-foundation과 swift-corelibs-foundation 두 저장소로 나눠서 유지하는가? 합치면 안 되나?
swift-foundation은 플랫폼 무관 핵심 로직을 담은 패키지이고, swift-corelibs-foundation은 Darwin의 Foundation.framework가 필요 없는 환경(Linux/Windows)에서 레거시 NS* API 표면을 흉내 내는 역할이다. 성격이 다른 관심사를 분리해두면, Darwin은 swift-foundation을 자기 프레임워크 내부에서만 쓰고 별도 호환 레이어 없이 곧장 소비할 수 있다.
쉽게 말하면 핵심 재료(swift-foundation)를 만드는 공장과, 그 재료를 특정 지역 규격에 맞게 포장하는 공장을 나눠 놓은 것과 같다.
Windows는 이 divergence 문제에서 자유로운가?
아니다. 오히려 2026년 1월 Windows Workgroup 신설 자체가 Windows 쪽에 아직 좁혀야 할 격차가 남아있다는 신호다. Linux는 핵심 로직을 오래 공유해와 격차가 좁지만, Windows는 상대적으로 뒤처져 있다 — 자세한 내용은 15장에서 다룬다.
쉽게 말하면 서울·뉴욕 지점은 오래전에 매뉴얼을 통일했지만, 새로 연 도쿄 지점은 아직 통일 작업이 진행 중인 것과 같다.

Q4. swift-foundation 재작성은 무엇을 어떻게 바꿨는가? — 타임라인과 공개된 성능 수치

🔑 30초 답변

"Foundation 재작성"은 한 번의 릴리스가 아니라 최소 4년짜리 마이그레이션이다. 2022년 12월 "The Future of Foundation"에서 계획을 발표했고, 2023년 4월 첫 프리뷰 패키지와 함께 검증 가능한 성능 수치가 처음 공개됐다. 2023년 가을(iOS 17/macOS 14)엔 Predicate·FormatStyle 같은 신규 API가 먼저 실렸고, JSONDecoder·URL·Calendar·FileManager 같은 기존 핵심 타입이 실제로 통째로 교체된 건 2024년 가을(iOS 18/macOS 15) — 이때 WWDC24 세션 10136이 밝혔듯 Objective-C로 짠 기존 앱들도 코드 수정 없이 그 개선의 혜택을 받았다.

원리

swift.org의 "The Future of Foundation"은 재작성 이유를 세 가지로 든다. 첫째 성능 — "Swift 네이티브 구현은 더 이상 C와 Swift 사이 변환 비용을 치르지 않는다." 둘째 기여 접근성 — C 코드를 감싸는 대신 Swift로 짜여 있어 개발자가 직접 읽고 기여하기 쉽다. 셋째, 그리고 가장 근본적인 이유 — "어떤 API든 구현이 여러 개 있으면 플랫폼을 넘나들 때 동작 발산(divergent behavior)과 결국 버그로 이어진다"는 것. 이 세 번째가 Q3의 divergence 문제를 정면으로 겨냥한 진술이다.

내부 동작

2023년 4월 26일 프리뷰 발표가 처음 공개한 네 가지 성능 수치는 다음과 같다 — 이 챕터에서 인용 가능한 수치는 이 넷뿐이다.

영역swift.org 공식 수치(원문 표현)
Locale 고정 조회"Swift 클라이언트 기준 자릿수 단위(order of magnitude)로 빠름"
Calendar 계산"일부 벤치마크에서 20% 이상 개선"
Date FormatStyle 포매팅"150% 개선"
JSON 디코딩"200%에서 거의 500%까지 개선"

그로부터 1년 뒤, Swift 6 발표 블로그는 "JSONDecoder, URL, Calendar, FileManager, ProcessInfo 등 핵심 타입이 Swift로 완전히 재구현됐다"며 "이 타입들은 macOS 15 / iOS 18과 구현을 공유한다"고 명시한다. 즉 2023년 발표는 계획과 신규 API 공개였고, 기존 핵심 타입의 실제 교체는 정확히 한 사이클 뒤인 2024년 가을에 일어났다 — 이 시차를 뭉개고 "2023년에 다 바뀌었다"고 말하면 부정확하다.

"재작성"은 한 릴리스가 아니라 4년 넘게 이어지는 마이그레이션이다 2015-12 corelibs-foundation 2016 SE-0086 (NS 제거) 2022-12 재작성 발표 2023-04 프리뷰+성능수치 2023 가을 신규 API(iOS17) 2024 가을(Swift 6) 핵심 타입 실제 교체 JSONDecoder·URL·Calendar 2026-01 Windows Workgroup
2022년 발표부터 2024년 가을 핵심 타입 실제 교체, 2026년 Windows Workgroup 신설까지 — Foundation 재작성은 여전히 진행 중인 다년 마이그레이션이다.

실험 · 도구

지금 설치된 Swift/Foundation이 실제로 이 재작성된 구현을 쓰고 있는지는 NSFoundationVersionNumber로 대략 가늠할 수 있고, JSONDecoder의 체감 처리량은 직접 벤치마크해볼 수 있다.

Swift 6.2.1 / macOS 26 실측 — 5,000개 객체 배열을 200회 디코딩
import Foundation

struct Item: Codable { let id: Int; let name: String; let price: Double; let inStock: Bool }

let items = (0..<5000).map { Item(id: $0, name: "product-\($0)", price: Double($0) * 1.5, inStock: $0 % 2 == 0) }
let data = try! JSONEncoder().encode(items)
print("payload bytes:", data.count)                    // 314540

let decoder = JSONDecoder()
let start = Date()
for _ in 0..<200 { _ = try! decoder.decode([Item].self, from: data) }
let elapsed = Date().timeIntervalSince(start)
print(elapsed, elapsed / 200 * 1000)                    // 1.0089...초 / 1회 평균 5.04ms

이 머신에서는 약 307KB(314,540바이트) payload를 200회 디코딩하는 데 총 1.01초, 1회 평균 약 5.0ms가 걸렸다. 다만 이건 "재작성 전 대비 몇 % 개선"을 재현하는 벤치마크가 아니다 — 지금 이 자리에서 실행되는 게 이미 swift-foundation 구현이라, 비교할 "이전" 버전이 이 머신엔 없다. 공식 개선폭(200~500%)을 직접 검증하려면 구형 corelibs-foundation 툴체인이 남아있는 예전 Swift 버전과 나란히 돌려야 하는데, 그 비교는 20장에서 다시 다룬다.

프로젝트 적용

2023년 가을에 새로 생긴 API(Predicate, FormatStyle 등)는 그만큼의 최소 배포 타깃을 요구한다는 걸 @available로 명시해야 한다.

2023년 가을 신규 API는 iOS17/macOS14 이상을 요구한다
import Foundation

@available(iOS 17, macOS 14, *)
func formattedShort(_ date: Date) -> String {
    date.formatted(.dateTime.year().month(.abbreviated).day())
}

반대로 기존 핵심 타입(JSONDecoder 등)은 코드를 한 줄도 안 바꿔도, 앱의 배포 최소 버전을 iOS 18/macOS 15 이상으로 올리는 것만으로 재작성의 성능 이득을 그대로 받는다 — 이건 실무에서 배포 타깃 상향을 검토할 실질적인 이유가 된다.

배포 최소 버전을 올리는 것 자체가 (코드 변경 없이) 통합 구현의 혜택을 받는 방법이다
// Package.swift
let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v18), .macOS(.v15)],
    targets: [.target(name: "MyApp")]
)
⚠️ 흔한 오해

"2023년 발표 = 그해 바로 다 바뀜"은 틀렸다. 신규 API(Predicate 등)는 2023년 가을에 실렸지만, URL/Calendar/JSONDecoder 같은 기존 핵심 타입의 완전 교체는 2024년 가을(iOS 18/macOS 15)이다. "swift-foundation은 Linux 전용 프로젝트다"도 틀렸다 — 2024년 가을부터는 Darwin의 Foundation.framework 자체가 이 코드를 쓴다. "Calendar가 1.5~18배 빨라졌다" 같은 수치는 2차 출처에서만 도는 검증 불가 주장이라 이 챕터에서는 쓰지 않았다.

🧒 쉽게 이해하기

낡은 건물을 통째로 부수지 않고, 겉모습(파사드)은 그대로 둔 채 안의 배관과 전기를 층별로 순서대로 새로 까는 리모델링 공사에 비유할 수 있다. 2022년 겨울 공사 계획을 발표하고, 2023년 봄 자재(패키지 프리뷰)가 들어오고, 2023년 가을엔 새로 생긴 방(신규 API)부터 먼저 문을 열었고, 2024년 가을엔 기존 방들(URL·Calendar·JSONDecoder)의 배관을 실제로 다 갈아 끼웠다. 비유가 깨지는 곳: 진짜 리모델링은 공사 기간에 그 층을 못 쓰지만, 이 공사는 사용자가 앱을 그냥 쓰는 동안 다운타임 없이 조용히 진행됐다.

꼬리 질문

2023년 발표와 2024년 실제 반영 사이에 왜 시차가 있었는가?
새 API(Predicate, FormatStyle)는 기존 동작을 건드리지 않는 순수 추가라 빠르게 낼 수 있었지만, 기존 핵심 타입(JSONDecoder, Calendar 등) 전체를 새 구현으로 바꾸는 건 수십 년치 기존 앱과의 동작 호환성을 검증해야 하는 훨씬 큰 작업이라 한 사이클(약 1년)이 더 필요했다.
쉽게 말하면 새 방을 하나 짓는 것과, 이미 사람이 살고 있는 방의 배관을 통째로 바꾸는 것 중 어느 쪽이 더 조심스러운 공사인지 생각하면 된다.
이 재작성이 Objective-C로 작성된 기존 앱에도 영향을 주는가?
그렇다. WWDC24 세션 10136이 "Objective-C 앱들도 이 개선의 혜택을 받았다"고 명시한다. Foundation.framework의 API 표면(헤더)은 그대로 유지되고 내부 구현만 바뀌었기 때문에, Objective-C 코드는 재컴파일만으로(때로는 그것조차 없이, OS 업데이트만으로) 개선된 구현을 그대로 쓰게 된다.
쉽게 말하면 건물 겉모습과 문 위치는 그대로 두고 안의 배관만 바꿨으니, 그 건물에 살던 세입자(Objective-C 앱)는 이사할 필요 없이 그냥 더 좋아진 수도를 쓰게 되는 것과 같다.
Windows Workgroup이 2026년 1월에 신설됐다는 게 Windows 지원이 부족하다는 뜻인가?
대체로 그렇게 읽는 게 정확하다. 워크그룹의 공식 임무 중 하나가 "Foundation과 Dispatch를 Windows 관용구에 맞게 개선하도록 권고하는 것"이라, 이 워크그룹의 존재 자체가 "아직 개선할 게 남아있다"는 신호로 봐야 한다. Linux처럼 완성됐다고 단정할 단계는 아니다 — 자세한 내용은 15장에서 다룬다.
쉽게 말하면 이미 잘 굴러가는 지점에는 따로 개선팀을 안 만들지만, 아직 손볼 게 많은 지점엔 전담팀을 새로 꾸리는 것과 같다.

Q5. `URL`과 `NSURL`은 같은 타입인가? SE-0069의 값 타입화가 바꾼 것은?

🔑 30초 답변

아니다, 같은 타입이 아니다 — URL은 Swift 값 타입(struct)이고 NSURL은 지금도 살아있는 별도의 Objective-C 참조 타입(class)이다. 둘은 SE-0069가 도입한 브리징 관계로, as 캐스팅을 통해 성공이 보장된 방식으로 오간다. SE-0069 이전엔 NSURL 하나만 있었는데, 이 제안이 URL·Date·Data·IndexPath 등 다수 타입에 대해 "참조 타입 하나만 있는" 상태를 "값 타입(신규) + 참조 타입(기존, 그대로 유지)" 둘로 갈라놨다.

원리

SE-0069의 동기는 이렇게 요약된다 — "Swift 표준 라이브러리의 공개 API는 거의 전부 값 타입으로 구성돼 있다(Swift 2.2 기준 struct 109개, class 6개)"인데, Foundation은 반대로 참조 타입 일색이라 let/var 구분이 무의미해지는 부조화가 있었다. 이 제안으로 AffineTransform, CharacterSet, Date, DateComponents, Data, IndexSet, IndexPath, Notification, PersonNameComponents, URL, URLComponents, URLQueryItem, UUID 13개 타입이 새 struct로 도입됐고, 각각 기존 NSURL/NSDate/NSData 등과 브리징 관계를 맺었다.

내부 동작

"같은 타입인가"를 코드로 직접 물어보면 답이 바로 나온다.

Swift 6.2.1 / macOS 26 실측 — URL(struct)과 NSURL(class)의 실제 동작 차이
import Foundation

let url = URL(string: "https://swift.org")!
print(type(of: url))            // URL

let nsurl = url as NSURL
print(type(of: nsurl))          // NSURL
print(NSStringFromClass(object_getClass(nsurl)!))   // NSURL

// URL은 struct — 재할당해도 서로 영향 없음
var a = url
var b = a
b = URL(string: "https://apple.com")!
print(a.absoluteString, b.absoluteString)   // https://swift.org https://apple.com

// NSURL은 class — 두 변수가 같은 인스턴스를 계속 공유
let nsA: NSURL = nsurl
let nsB: NSURL = nsA
print(nsA === nsB)               // true

// 왕복 캐스팅은 값 동등성을 보존한다
print(url == (nsurl as URL))     // true
print(MemoryLayout<URL>.size, MemoryLayout<URL>.stride)   // 16 16

type(of:)URLNSURL을 각각 다른 타입으로 돌려준다는 것부터가 이 둘이 별개 타입이라는 직접 증거다. as NSURL 캐스팅은 as?/as!가 필요 없는, 성공이 보장된 브리징이지만 — 이 보장은 "같은 타입"이라서가 아니라 _ObjectiveCBridgeable이 값을 새 NSURL 인스턴스로 포장해주기 때문에 성립한다.

URL(struct)은 독립 복사, NSURL(class)은 인스턴스 공유 var a = url; var b = a b = URL(string: "...apple.com")! a swift.org b apple.com 서로 다른 메모리 — b를 재할당해도 a는 그대로 let nsA = url as NSURL; let nsB = nsA nsA nsB NSURL 인스턴스 (heap) swift.org nsA === nsB → true
URL(구조체)은 재할당할 때마다 완전히 독립된 값이 되지만, NSURL(클래스)로 캐스팅한 두 변수는 여전히 힙의 같은 인스턴스 하나를 공유한다.

실험 · 도구

위 코드는 swift 파일명.swift로 그대로 재현된다. MemoryLayout<URL>.size가 16인 것도 직접 확인할 수 있다 — struct 내부에 문자열 저장소를 가리키는 포인터류 필드 2개(값 자체 또는 CoW 버퍼 참조) 정도로 구성된 작은 값 타입이라는 뜻이다.

프로젝트 적용

대부분의 Swift/최신 Foundation API는 URL만으로 충분하다. 옛 Objective-C 스타일 API(델리게이트, NSItemProvider 등)와 맞물릴 때만 NSURL로 브리징한다.

기본은 URL, ObjC 상호운용이 필요할 때만 NSURL로 브리징
import Foundation

func loadConfig(from url: URL) throws -> Data {
    try Data(contentsOf: url)
}

// 옛 Objective-C 스타일 API와 맞물릴 때만 필요 — 항상 성공하는 브리징 캐스트
func legacyHandOff(_ url: URL) -> NSURL {
    url as NSURL
}

URL이 struct라는 사실은 실무에서 실수를 줄여준다. 여러 곳에 넘겨도 각자 독립적으로 복사되므로, NSURL 참조를 여기저기 공유하던 시절처럼 "누가 이 URL을 몰래 바꿨지" 하는 버그가 구조적으로 생기지 않는다.

URL을 struct 프로퍼티로 들고 있으면 어디로 넘겨도 독립적으로 복사된다
struct DownloadTask {
    let source: URL
    var destination: URL
}

func renamed(_ task: DownloadTask) -> DownloadTask {
    var copy = task
    copy.destination = copy.destination.appendingPathComponent("renamed")
    return copy   // task.destination은 전혀 영향받지 않는다
}
⚠️ 흔한 오해

"URL은 그냥 NSURL의 새 이름일 뿐이다"는 틀렸다 — NSURL은 지금도 별도 클래스로 존재하고, URL은 SE-0069가 새로 만든 독립된 값 타입이다(Q2). 반대로 "URL과 NSURL은 서로 무관한 타입이다"도 틀렸다 — as 캐스팅이 항상 성공하는, 값 동등성을 보존하는 브리징 관계를 갖는다. 다만 이건 CFString↔NSString 같은 toll-free bridging(포인터만 재해석)과는 다른 방식이라는 점도 16장에서 구분해서 짚는다.

🧒 쉽게 이해하기

URL은 내가 손으로 적어서 갖고 다니는 주소 메모(각자 자기 메모를 갖고, 하나를 고쳐도 다른 사람 메모는 안 바뀐다)이고, NSURL은 사무실 게시판에 붙은 공식 주소 안내판(다들 같은 안내판을 보고, 누가 그걸 떼서 고치면 그 안내판을 보던 모두에게 영향을 준다)이다. 둘 사이를 오갈 땐 "안내판 내용을 그대로 베껴 내 메모로 만들기"(as URL)나 "내 메모를 공식 안내판 형식으로 다시 써주기"(as NSURL)를 하는데, 이 복사는 실패하지 않는다. 비유가 깨지는 곳: 진짜 안내판은 하나만 존재하지만, NSURL도 여러 변수가 그 "하나의 안내판" 자체를 얼마든지 같이 가리킬 수 있다(참조 공유) — 반면 내 메모(URL)는 대입할 때마다 항상 새 메모가 된다.

꼬리 질문

`url as NSURL`은 항상 성공하는가, 실패할 수도 있는가?
URLNSURL 방향은 _ObjectiveCBridgeable이 보장하는 무조건 성공하는 캐스트라 as만으로 충분하다(as? 불필요). 반대 방향은 상황에 따라 다르다 — NSURLURL도 보장된 브리징이지만, 임의의 NSObjectURL로 캐스팅하는 것(예: anyObject as? URL)은 그 객체가 실제로 URL을 표현하지 않으면 실패할 수 있다.
쉽게 말하면 "내 메모를 안내판 형식으로 바꾸기"는 항상 되지만, "아무 종이나 안내판인 척 읽어보기"는 그 종이가 실제로 주소가 아니면 실패한다.
URLComponents는 어떤 타입이고 URL과 무슨 관계인가?
URLComponents도 SE-0069가 함께 도입한 struct다. URL을 scheme/host/path/query 같은 구성요소로 쪼개거나, 반대로 그 구성요소들을 조립해 URL을 만드는 용도로 쓴다. NSURLComponents 클래스도 여전히 존재하며 같은 브리징 관계를 맺는다. 21장에서 실제 조립·해체 패턴을 자세히 다룬다.
쉽게 말하면 URL이 완성된 편지봉투라면, URLComponents는 "받는 사람/보내는 사람/우편번호"를 각각 따로 적었다가 나중에 봉투로 조립하는 메모지다.
NSURL 인스턴스 두 개가 값은 같은데 서로 다른 인스턴스라면 `===`는 무엇을 반환하나?
false다. ===는 정체성(같은 메모리 인스턴스인가)을 묻는 연산자라, 값이 같아도 힙에서 서로 다른 객체면 false를 준다. 다만 NSURLisEqual(_:)을 오버라이드해 내용을 비교하므로, Swift의 ==(내부적으로 isEqual을 씀)로는 true가 나온다 — 01장에서 다룬 =====의 구분이 여기서도 그대로 적용된다.
쉽게 말하면 내용이 똑같이 인쇄된 안내판을 두 개 따로 만들었다면, "내용이 같냐"고 물으면 그렇다고 하겠지만 "이게 그 안내판이냐(바로 그 물건이냐)"고 물으면 아니라고 답하는 것과 같다.

출처 · 참고자료