이 강의의 다른 32개 챕터는 Swift 6.2.1에서 직접 컴파일·실행한 실측값으로 쓰였다. 그런데 Swift 6.3의 동작은 그 툴체인으로 측정할 수 없다. 그래서 이 챕터만 출처 규칙이 다르다.
| 표시 | 의미 |
|---|---|
| 스펙 기준 · 미실측 | Swift Evolution 제안서와 릴리스 노트에 근거함. 필자가 실행해 확인한 것이 아니다. |
| 6.2.1 실측 | 로컬 Swift 6.2.1에서 직접 컴파일한 결과. 대개 "6.3 이전에는 이것이 거부된다"를 보이는 데 쓴다. |
즉 이 챕터에서 측정할 수 있는 것은 "6.2.1이 이 기능을 거부한다"까지이고, "6.3에서 어떻게 동작한다"는 스펙 인용이다. 두 종류를 섞지 않으려고 문항마다 배지를 붙였다. 실측이 필요하면 6.3 이상 툴체인을 설치해 직접 확인할 것을 권한다.
Part C의 마지막 챕터다. Swift 6.3은 2026년 3월 24일에 릴리스됐고, 이 강의의 나머지가 기준으로 삼은 6.2.1보다 한 세대 위다. 그래서 이 챕터의 목적은 둘이다 — ① 6.3이 실제로 무엇을 바꿨는지 제안 단위로 빠짐없이 정리하고, ② 그중 무엇이 이 강의의 다른 챕터를 갱신해야 하는지 짚는 것이다. 후자가 특히 중요하다 — 새 기능 목록은 릴리스 노트에도 있지만, "내가 알고 있던 것 중 무엇이 낡았는가"는 아무도 알려주지 않는다.
Q1. Swift 6.3은 무엇을 바꿨는가? — 언어 제안 9개 전수
swift-evolution 저장소에서 Implemented (Swift 6.3)으로 표시된 제안은 정확히 9개다. 성격별로 셋으로 묶인다 — C 상호운용·이름 해소(SE-0495 @c, SE-0491 모듈 셀렉터), 성능·코드 생성 제어(SE-0460 @specialized, SE-0496 @inline(always), SE-0497 정의 가시성, SE-0492 @section/@used), 일상적 불편 해소(SE-0481 weak let, SE-0489 Codable 에러 출력 개선, SE-0473 Clock 기준점). 6.2가 Span·InlineArray처럼 새 타입을 들여온 릴리스였다면, 6.3은 기존 것을 더 정밀하게 제어하게 만든 릴리스다.
원리
Swift의 릴리스는 제안(Swift Evolution proposal) 단위로 추적할 수 있다. 각 제안 문서의 머리에 Status: 필드가 있고, 구현되면 어느 버전에 들어갔는지가 그 자리에 기록된다. 그래서 "이 릴리스가 무엇을 바꿨나"를 정확히 알고 싶으면 릴리스 노트가 아니라 제안 저장소를 세는 것이 낫다 — 릴리스 노트는 편집자가 고른 하이라이트라 빠지는 항목이 있다.
$ gh api repos/swiftlang/swift-evolution/tarball/main > se.tar.gz
$ tar xzf se.tar.gz && cd swiftlang-swift-evolution-*/proposals
$ ls *.md | wc -l
546
$ grep -l "Implemented (Swift 6.3)" *.md내부 동작
9개를 성격별로 묶고, 각각이 이 강의의 어느 챕터와 연결되는지 함께 표시한다.
| 제안 | 무엇 | 이 강의의 연결점 |
|---|---|---|
SE-0495 @c | Swift 함수·enum을 C에서 호출 가능하게 | 16장 ObjC 브리징의 C 쪽 짝 |
| SE-0491 모듈 셀렉터 | ModuleName::name으로 이름 충돌 해소 | 15장 모듈 분할 · 13장 접근제어 |
SE-0460 @specialized | 제네릭 함수의 사전 특수화를 명시 | 08장 Q5 제네릭 특수화 — 직접 갱신 대상 |
SE-0496 @inline(always) | 인라이닝 휴리스틱을 무시하고 강제 | 13장 Q2 @inlinable과의 구분 |
| SE-0497 정의 가시성 제어 | 심볼 생성과 정의 노출을 분리 | 13장 Q2 — 개념 자체를 다시 씀 (Q3) |
SE-0492 @section · @used | 전역 변수를 특정 바이너리 섹션에 | 임베디드·런타임 메타데이터 발견 |
SE-0481 weak let | weak를 let으로 선언 가능 | 06장 Q5 weak/unowned · 17장 Sendable — 직접 갱신 대상 |
| SE-0489 Codable 에러 출력 | EncodingError/DecodingError에 읽기 좋은 debugDescription | 20장 Q2 codingPath로 범인 찾기 |
| SE-0473 Clock 기준점 | SuspendingClock·ContinuousClock의 기준 instant 노출 | 22장 Date · 시간 다루기 |
세 항목이 "직접 갱신 대상"으로 표시됐다. 이는 이 강의의 해당 문항이 6.2.1 기준으로 설명한 제약이 6.3에서 사라졌다는 뜻이며, Q4에서 하나씩 다룬다.
실험 · 도구
이 챕터에서 측정할 수 있는 것은 "6.2.1이 이 기능들을 거부한다"다. 각 기능을 6.2.1에서 컴파일하면 어떤 진단이 나오는지 실측했다 — 이것이 곧 "이 기능이 6.3에 들어왔다"는 간접 증거다.
① weak let (SE-0481)
class Holder { weak let ref: Node? }
→ error: 'weak' must be a mutable variable, because it may change at runtime
② @c (SE-0495)
@c func myFunc(_ x: Int32) -> Int32 { x * 2 }
→ error: unknown attribute 'c'
③ @specialized (SE-0460)
@specialized(where T == Int) func f<T: Numeric>(_ x: T) -> T { x + x }
→ error: unknown attribute 'specialized'
④ @inline(always) (SE-0496)
@inline(always) func g(_ x: Int) -> Int { x + 1 }
→ error: unknown option 'always' for attribute 'inline'
⑤ @section / @used (SE-0492)
@section("__DATA,mysection") @used let marker: Int = 42
→ error: unknown attribute 'section'
→ error: unknown attribute 'used'
⑥ 모듈 셀렉터 (SE-0491)
let x = Swift::print
→ error: consecutive statements on a line must be separated by ';'
→ error: expected expression①의 진단문이 특히 정보량이 많다. 'weak' must be a mutable variable, because it may change at runtime — SE-0481이 없앤 제약의 원래 이유가 에러 메시지에 그대로 적혀 있다. Q4에서 이 이유가 왜 더 이상 유효하지 않은지 다룬다.
⑥의 진단이 "unknown attribute"가 아니라 구문 에러인 것도 의미가 있다. ::는 애트리뷰트가 아니라 문법(grammar) 확장이므로, 옛 컴파일러는 그것을 두 개의 별개 문장으로 해석하려 한다.
프로젝트 적용
① 업그레이드 판단은 "이 9개 중 내가 지금 아픈 것이 있는가"로 한다. 팀별로 답이 갈린다.
| 상황 | 6.3이 주는 것 | 급한가 |
|---|---|---|
| Swift 6 동시성 마이그레이션 중 | weak let으로 Sendable 위반이 줄어든다 | 높음 — 실질적 이득 |
| C/C++ 코드베이스와 혼용 | @c가 @_cdecl을 정식화 | 높음 — 실험 애트리뷰트 탈출 |
| Codable 디버깅이 잦음 | 에러 출력이 읽기 좋아진다 | 중간 — 편의 개선 |
| SDK·라이브러리 저자 | SE-0497로 정의 노출을 정밀 제어 | 높음 — ABI 결정에 직결 |
| 성능 병목이 제네릭 디스패치 | @specialized로 사전 특수화 | 중간 — 측정이 먼저 |
| 임베디드·시스템 프로그래밍 | @section/@used, Embedded 개선 | 높음 |
| 일반 앱 개발 | 대부분 조용한 개선 | 낮음 — 서둘 이유 없음 |
② 6.3은 언어 기능 외에도 도구 쪽이 넓게 움직였다. 릴리스 노트가 언급하는 것 중 실무에 닿는 항목은 이렇다 — Swift Testing의 새 기능(경고 수준 이슈 severity: .warning, Test.cancel(), 이미지 첨부), 공식 Android SDK, Embedded Swift용 EmbeddedRestrictions 진단 그룹, DocC의 마크다운 출력, SwiftPM의 swift package show-traits. 언어 제안 9개보다 이쪽이 팀에 더 큰 변화일 수 있다.
③ 업그레이드 절차는 이 강의의 다른 챕터와 같은 규율을 따른다 — 30장 Q4에서 본 것처럼 툴체인 버전과 배포 타깃을 분리해 확인하고, 29장 Q5처럼 새 진단은 모듈 단위로 점진 도입한다.
게임에 큰 업데이트가 두 번 왔다고 생각하자. 이전 업데이트(6.2)는 새 아이템을 준 것이었다 — 전에 없던 도구(Span, InlineArray)가 생겼다. 이번 업데이트(6.3)는 기존 도구의 설정 화면을 열어준 것에 가깝다. 새 아이템은 적고, 대신 "이 도구를 언제 어떻게 쓸지"를 내가 세밀하게 정할 수 있게 됐다. 그리고 오래 불편했던 것 하나가 고쳐졌다 — 전에는 "이 물건은 바뀔 수 있으니 반드시 흔들리는 선반에 둬야 한다"는 규칙이 있었는데(weak는 var여야 함), 이제 고정 선반에도 둘 수 있게 됐다. 비유가 깨지는 곳: 게임 업데이트는 자동으로 오지만 컴파일러 업데이트는 내가 직접 설치해야 하고, 설치하면 팀 전체가 함께 올라가야 한다.
꼬리 질문
왜 릴리스 노트가 아니라 제안 저장소를 세는 것이 정확한가?
weak let·Codable 에러 출력·Clock 기준점처럼 조용하지만 실무에 닿는 항목은 제안 저장소를 세지 않으면 놓친다. 반대로 제안 저장소만 보면 놓치는 것도 있다 — 제안 없이 들어가는 변경(버그 수정, 최적화 개선, 진단 메시지 개선, 도구 변화)은 제안 문서가 없다. 그래서 실전에서는 둘을 함께 본다. 제안 저장소로 언어 표면의 변화를 빠짐없이 세고, 릴리스 노트로 도구·플랫폼 쪽 변화를 확인하는 식이다.6.2와 6.3의 성격이 다르다는 말은 어떤 근거인가?
Span(SE-0447), InlineArray(SE-0453), Span 제공 프로퍼티(SE-0456), nonescapable 원시 타입(SE-0465), 그리고 새 검사 모드인 strict memory safety(SE-0458). 즉 쓸 수 있는 것이 늘어난 릴리스다. 6.3의 9개는 성격이 다르다 — 새 타입은 하나도 없고, 애트리뷰트 넷(@c·@specialized·@inline(always)·@section/@used)과 문법 하나(모듈 셀렉터), 제약 완화 하나(weak let), 품질 개선 둘(Codable 에러, Clock 기준점)이다. 표현력보다 제어권을 늘린 릴리스라고 읽는 것이 정확하다. 실무적 함의는 이렇다 — 6.2는 안 올리면 못 쓰는 것이 생기지만, 6.3은 안 올려도 대개 덜 편한 정도다(weak let과 @c가 예외).이 강의는 6.2.1 기준인데, 6.3으로 올리면 기존 33개 챕터의 내용이 틀리게 되는가?
@inlinable의 의미). 나머지는 "6.3에서 더 쉬워진 방법이 추가됐다"는 보충이면 충분하다. 이것이 이 챕터를 부록이 아니라 정규 챕터로 둔 이유다 — 새 기능 소개가 아니라 기존 내용의 갱신 지도 역할을 한다.Q2. @c(SE-0495)와 모듈 셀렉터(SE-0491)는 각각 무슨 문제를 푸는가?
@c는 오래 실험 상태였던 @_cdecl을 정식화한다. 붙이면 Swift 전역 함수를 C에서 호출 가능하게 하고 enum을 C에 표현 가능하게 하며, 생성되는 호환성 헤더에 새 C 블록이 추가되어 C 파일에서 #include할 수 있다. @implementation과 함께 쓰면 손으로 쓴 C 헤더의 선언을 Swift로 구현하는 방향도 된다. 모듈 셀렉터는 문법 확장이다 — 선언을 참조하는 식별자 앞에 ModuleName::을 붙여 어느 모듈의 것인지 못 박는다. 두 모듈이 같은 이름을 노출해 생기던 모호성을 타입 별칭이나 래퍼 없이 해소한다.
원리
두 제안의 공통점은 "이미 하고 있던 일을 정식 문법으로 승격"이다. @_cdecl은 밑줄 접두사가 붙은 비공식 애트리뷰트였지만 널리 쓰여 왔다 — 제안서 자체가 "실험 상태였지만 광범위하게 사용되어 왔으므로 명확성을 위해 이 문서에서 언급한다"고 밝힌다. 이름 충돌 회피 역시 그동안 typealias를 두거나 한쪽 모듈만 import하는 우회로 해결해 왔다.
비공식 상태의 대가는 13장에서 본 문제 그대로다 — 밑줄 애트리뷰트는 언제든 바뀌거나 사라질 수 있고, 동작이 문서화되지 않아 코너 케이스가 불명확하다. 정식화는 그 동작을 계약으로 고정하는 작업이다.
내부 동작
@c가 하는 일은 세 갈래다.
| 대상 | 효과 |
|---|---|
| 전역 함수 | C 호출 규약으로 심볼을 내보내고, 호환성 헤더의 C 블록에 선언을 출력 |
| enum | C에서 표현 가능한 형태로 내보낸다 |
@objc와의 관계 | @objc는 ObjC 호환 전역 함수에 대해 같은 동작을 제공한다 |
@implementation 조합 | 전역 함수까지 지원 확대 — C 헤더에 선언, Swift로 구현 |
방향을 헷갈리지 않는 것이 중요하다. 16장이 다룬 것은 Swift가 ObjC/C를 부르는 방향이었고, @c는 C가 Swift를 부르는 반대 방향이다.
@c는 그 반대 방향을 정식 문법으로 만든다.모듈 셀렉터는 훨씬 단순하다. 제안서의 표현대로 "선언을 참조하기 위해 식별자가 쓰이는 모든 자리"에서 ModuleName:: 접두사를 허용한다.
import LibraryA // Widget 을 노출한다
import LibraryB // Widget 을 노출한다
// 6.2 이전: 모호해서 컴파일 에러 → typealias 나 한쪽만 import 로 우회
// let w = Widget()
// 6.3: 어느 모듈의 것인지 못 박는다
let a = LibraryA::Widget()
let b = LibraryB::Widget()실험 · 도구
6.2.1에서 두 기능이 어떻게 거부되는지가 이 절의 유일한 실측이다.
$ cat z.swift
@c func myFunc(_ x: Int32) -> Int32 { x * 2 }
$ swiftc -O -c z.swift
z.swift:1:2: error: unknown attribute 'c'$ cat ms.swift
import Foundation
let x = Swift::print
$ swiftc -O -c ms.swift
ms.swift:2:14: error: consecutive statements on a line must be separated by ';'
ms.swift:2:14: error: expected expression6.2.1에서 같은 목적을 달성하려면 지금도 @_cdecl을 쓸 수 있다. 다만 밑줄 애트리뷰트이므로 13장의 경고가 적용된다 — 공식 지원 대상이 아니다.
@_cdecl("my_func")
func myFunc(_ x: Int32) -> Int32 { x * 2 }
// C 쪽: int32_t my_func(int32_t); ← 헤더를 손으로 선언해야 했다프로젝트 적용
① @c가 실질적으로 바꾸는 것은 헤더 관리다. @_cdecl 시절에는 C 쪽 선언을 손으로 유지해야 했고, 시그니처가 어긋나면 컴파일은 통과하고 실행 중에 깨졌다 — 29장의 언어로 말하면 "조용한 실패"다. @c는 선언을 생성된 헤더에 넣어주므로 그 불일치가 구조적으로 사라진다.
// Swift 쪽
@c func compute_checksum(_ bytes: UnsafePointer<UInt8>, _ count: Int32) -> UInt8 {
var x: UInt8 = 0
for i in 0..<Int(count) { x ^= bytes[i] }
return x
}
// C 쪽 — 생성된 호환성 헤더를 include 한다 (손으로 선언하지 않는다)
// #include "MyModule-Swift.h"
// uint8_t r = compute_checksum(buf, len);다만 위 시그니처는 29장의 계약을 그대로 짊어진다 — UnsafePointer를 받으므로 C 쪽이 유효한 수명의 포인터를 넘길 책임이 있고, Swift 쪽은 그 포인터를 함수 밖으로 내보내면 안 된다. @c 경계는 Span(30장)이 통하지 않는 자리이므로 -strict-memory-safety로 그 지점을 표시해두는 편이 좋다.
② 모듈 셀렉터는 "쓸 일이 없기를 바라는" 기능이다. 이름 충돌은 대개 설계 신호다 — 두 모듈이 같은 이름을 노출한다면 그중 하나를 감싸거나 이름을 바꾸는 것이 근본 해결이다. 셀렉터가 값을 만드는 자리는 내가 고칠 수 없는 의존성 두 개가 충돌할 때다(서드파티 SDK 두 개, 또는 모듈 이름과 타입 이름이 같은 경우).
// ✅ 고칠 수 없는 서드파티 두 개가 충돌
let client = VendorA::HTTPClient()
// ✅ 모듈 이름과 타입 이름이 같아 생기는 고전적 모호성
let v = MyLib::MyLib()
// ❌ 내 모듈끼리 충돌한다면 셀렉터가 아니라 이름을 고친다
// let x = MyFeature::Model() → Model 을 UserModel 로 개명하는 것이 낫다③ 도입 순서는 @c가 먼저다. @_cdecl을 쓰고 있는 코드가 있다면 6.3 업그레이드와 함께 @c로 옮기는 것이 기술 부채 정리에 해당한다 — 비공식 애트리뷰트 의존을 하나 없애고, 헤더 수동 관리도 함께 없앤다.
"@c를 붙이면 아무 Swift 함수나 C에서 부를 수 있다"는 틀렸다 — 전역 함수가 대상이고, 시그니처의 타입들이 C에 표현 가능해야 한다. Swift의 String·Array·제네릭·throws 같은 것은 그대로 넘길 수 없다. 또 "@c는 @objc를 대체한다"도 아니다 — 제안서가 밝히듯 @objc는 ObjC 호환 전역 함수에 같은 동작을 제공하며, 둘은 대상 언어가 다르다. 모듈 셀렉터 쪽에서는 "::가 C++처럼 네임스페이스 접근 연산자"라는 오해가 있다. Swift에는 네임스페이스 개념이 없고, 이것은 모듈 이름으로 모호성을 해소하는 문법일 뿐이다.
@c는 내 가게에 영어 간판을 추가로 붙이는 일이다. 원래는 한국어 손님(Swift)만 찾아왔는데, 영어 간판을 달면 영어 손님(C)도 들어올 수 있다. 전에도 임시로 손글씨 간판을 걸 수는 있었는데(@_cdecl), 그건 비공식이라 언제 떼어질지 몰랐고 간판 내용을 내가 직접 손으로 적어야 했다. 이제는 가게에서 정식 간판을 자동으로 만들어준다. 모듈 셀렉터는 다른 이야기다 — 같은 이름의 가게가 두 곳 있어서 "김밥천국 말고 강남점 김밥천국"이라고 지점을 붙여 말하는 것과 같다. 비유가 깨지는 곳: 지점을 붙여야 하는 상황 자체가 이름을 잘못 지은 결과일 때가 많아서, 되도록 이름을 고치는 게 낫다.
꼬리 질문
@_cdecl을 이미 쓰고 있다면 6.3에서 무엇이 달라지는가?
@c는 제안을 통과한 언어 기능이므로 소스 호환성 약속의 대상이다. 둘째, 헤더가 자동 생성된다 — 호환성 헤더에 C 블록이 추가되어 C 쪽 선언을 손으로 유지할 필요가 없고, 그에 따라 시그니처 불일치로 인한 런타임 깨짐이 사라진다. 셋째, 제안서가 밝히듯 여러 코너 케이스가 정리됐다 — 실험 애트리뷰트 시절 불명확했던 동작들이 명세로 고정됐다. 마이그레이션은 대개 애트리뷰트 이름을 바꾸고, 손으로 관리하던 헤더 선언을 생성된 헤더로 교체하는 작업이다.모듈 셀렉터가 있으면 typealias 우회는 이제 필요 없는가?
typealias에는 다른 값도 있다. 이름이 길거나 제네릭 인자가 복잡한 타입에 짧은 도메인 이름을 주는 용도, 그리고 나중에 실제 타입을 한 곳에서 교체할 수 있게 하는 간접 층 역할이다. 후자는 셀렉터로 대체되지 않는다 — 셀렉터는 사용 지점마다 모듈을 못 박으므로 오히려 결합을 늘린다. 정리하면 충돌 해소는 셀렉터, 추상화·개명은 typealias다.typealias)는 목적이 다르다. 헷갈릴 때만 지점을 붙이고, 부르기 편하려면 별명을 쓴다.@c로 내보낸 함수에서 Swift 에러를 던지면 어떻게 되는가?
throws에 대응하는 개념이 없고, 04장 Q1에서 본 대로 Swift의 throws는 전용 레지스터로 에러를 전달하는 ABI이므로 C 호출 규약과 맞지 않는다. 그래서 C 경계로 내보내는 함수는 에러를 값으로 변환해야 한다 — 관례적으로 정수 반환값을 상태 코드로 쓰거나, 출력 파라미터로 에러 정보를 채운다. 04장 Q4에서 다룬 Result ↔ throws 변환과 같은 종류의 경계 작업이다. 구조적으로는 이렇게 된다 — 내부는 throws로 자연스럽게 쓰고, @c 함수는 그것을 감싸는 얇은 어댑터로 두어 do/catch로 상태 코드를 만든다. 그러면 에러 처리 로직이 C 경계 때문에 오염되지 않는다.Q3. 성능 제어 3종 — @specialized · @inline(always) · 정의 가시성은 무엇이 다른가?
세 애트리뷰트가 서로 다른 층을 제어한다. SE-0460 @specialized는 제네릭 함수의 사전 특수화 버전을 미리 생성해두고, 호출 지점에서 타입이 안 보일 때도 그 버전으로 재디스패치하게 한다. SE-0496 @inline(always)는 컴파일러의 인라이닝 휴리스틱을 무시하고 강제한다. SE-0497은 가장 개념적으로 중요한데, 13장 Q2의 @inlinable을 다시 정의한다 — 제안서가 명시적으로 말한다. "@inlinable은 인라이닝 자체와는 거의 상관이 없다. 그것은 정의의 가시성에 관한 것이다." SE-0497은 그 가시성과 심볼 생성 여부를 각각 따로 제어하게 만든다.
원리
왜 세 개가 필요한가. 최적화가 일어나려면 세 조건이 각각 성립해야 하고, 그동안 그 조건들을 개별적으로 제어할 방법이 없었다.
| 조건 | 없으면 무엇이 막히나 | 6.3의 제어 수단 |
|---|---|---|
| 호출자가 정의(본문)를 볼 수 있다 | 특수화·인라이닝·분석이 전부 불가 | SE-0497 정의 가시성 |
| 호출자가 구체 타입을 안다 | 제네릭 특수화 불가 | SE-0460 @specialized |
| 컴파일러가 인라이닝이 이득이라 판단한다 | 휴리스틱이 코드 크기를 이유로 거절 | SE-0496 @inline(always) |
첫 줄이 나머지의 전제다. 제안서 SE-0497의 설명이 이 구조를 정확히 짚는다 — 호출자가 정의를 볼 수 있으면 특수화·인라이닝뿐 아니라 분석도 가능해진다. 예로 "객체를 함수에 넘기는데 그 함수가 객체를 공유하지도 파괴하지도 않는다"는 것을 알면 호출자가 그 객체를 힙 대신 스택에 할당할 수 있다.
내부 동작
SE-0497이 이 강의의 기존 설명을 어떻게 정밀화하는지가 이 절의 핵심이다. 13장 Q2는 @inlinable·@usableFromInline·@frozen과 라이브러리 진화의 관계를 다뤘다. 6.3의 제안서는 그 개념을 두 축으로 분해한다.
@inlinable이 실제로 하는 두 가지 SE-0497 원문제안서에 따르면 @inlinable은 서로 독립적인 두 일을 한꺼번에 한다.
① 함수 정의를 모듈 밖 호출자가 볼 수 있게 만든다 → 특수화·인라이닝·분석이 가능해진다. ② 그와 동시에 정의를 모듈 바이너리에도 컴파일해 넣어, 호출자가 사본을 만들지 않고 직접 호출할 수도 있게 한다.
문제는 이 둘이 묶여 있었다는 것이다. 정의를 노출하면 대가가 따른다 — 제안서의 표현대로, "함수 정의만 다시 컴파일해 링크해서 그 변경의 효과를 보는 일이 더 이상 불가능해진다." 증분 빌드가 느려지고, Library Evolution이 켜진 경우 바이너리 호환성을 유지하며 할 수 있는 변경의 종류가 제한된다. SE-0497은 ①과 ②를 따로 켜고 끌 수 있게 만들어, 라이브러리 저자가 "최적화 가능성"과 "나중에 고칠 자유" 사이를 정밀하게 고르게 한다.
실무적 결론 하나가 여기서 나온다. @inlinable은 "빠르게 해주는 애트리뷰트"가 아니다. 그것은 ABI 결정이며, 30장 Q4에서 Span의 back-deploy가 "미래에 바꿀 권리를 지불하고 사는 것"이라고 했던 것과 정확히 같은 종류의 거래다.
@specialized는 다른 문제를 푼다. Part A 08장 Q5는 특수화의 조건을 이렇게 정리했다 — "컴파일러가 함수 본문과 호출부의 구체 타입을 동시에 볼 수 있을 때만" 일어나고, 그래서 모듈 경계를 넘으면 @inlinable로 본문을 노출해야 크로스모듈 특수화가 가능해진다. 여기서 본문 가시성이 조건에 이미 들어 있다는 점이 SE-0497과 곧바로 이어진다. 그런데 그 조건을 만족시킬 수 없는 경우가 실무에 남는다 — 본문을 공개하고 싶지 않거나(ABI 자유를 지키려고), 실존 타입(any)을 거쳐 호출되는 경우다. @specialized는 그때를 위해 함수 작성자가 미리 특정 타입용 버전을 만들어두게 한다.
@specialized(where T == Int)
@specialized(where T == Double)
public func process<T: Numeric>(_ values: [T]) -> T {
values.reduce(0, +)
}
// 호출 지점에서 T 를 못 봐도, 런타임에 T == Int 면 사전 특수화 버전으로 재디스패치된다실험 · 도구
$ swiftc -O -c z.swift
z.swift:1:2: error: unknown attribute 'specialized'
z.swift:1:2: error: unknown option 'always' for attribute 'inline'
z.swift:1:31: error: unknown attribute 'used'
z.swift:1:2: error: unknown attribute 'section'④의 진단이 특히 정확하다. @inline은 6.2.1에도 이미 존재하는 애트리뷰트다 — @inline(never)와 @inline(__always)(밑줄 두 개)가 있었다. SE-0496이 한 일은 밑줄 없는 정식 옵션 always를 추가한 것이며, 그래서 에러가 "unknown attribute"가 아니라 unknown option 'always' for attribute 'inline'이다.
@inline(never) func a(_ x: Int) -> Int { x + 1 } // 정식 — 6.2.1 OK
@inline(__always) func b(_ x: Int) -> Int { x + 1 } // 비공식 — 6.2.1 OK
@inline(always) func c(_ x: Int) -> Int { x + 1 } // 6.3부터프로젝트 적용
① 세 애트리뷰트 모두 "측정 후에" 쓴다. 이 강의가 반복해온 규율이며, 특히 여기서 중요하다 — 세 개 다 코드 크기와 빌드 시간을 대가로 하기 때문이다.
| 애트리뷰트 | 얻는 것 | 대가 | 먼저 확인할 것 |
|---|---|---|---|
@inline(always) | 호출 오버헤드 제거 + 문맥 최적화 | 코드 크기 증가 · 캐시 압박 | 이 함수가 정말 뜨거운 경로인가 |
@specialized | 모듈 경계 넘어서도 특수화 | 바이너리에 버전마다 코드 | 특수화가 실제로 실패하고 있는가 |
| SE-0497 정의 노출 | 호출자 측 최적화 전부 가능 | 증분 빌드 · ABI 자유 상실 | 이 함수를 앞으로 고칠 일이 없는가 |
② 순서가 있다. 정의 가시성이 최적화의 전제이므로, "왜 특수화가 안 되지?"라는 질문의 답은 대개 "호출자가 정의를 못 본다"다. 그러니 @specialized·@inline(always)를 뿌리기 전에 모듈 구조와 가시성을 먼저 본다 — 08장 Q5·13장 Q2가 그 배경이다.
# 1) 특수화가 실제로 일어나는지 SIL 에서 확인한다
swiftc -O -emit-sil file.swift | grep -E "^sil .*specialized|function_ref.*specialized"
# 2) 같은 모듈로 옮기거나 WMO 를 켜서 해결되는지 본다 (07장 Q3)
swiftc -O -whole-module-optimization *.swift
# 3) 그래도 안 되면 @specialized / SE-0497 을 검토한다③ 라이브러리 저자에게는 SE-0497이 세 개 중 가장 중요하다. @inlinable을 "성능을 위해 붙이는 것"으로 이해하고 공개 API에 넓게 붙였다면, 그것은 되돌릴 수 없는 ABI 약속을 광범위하게 한 것이다. 6.3의 분리된 제어는 그 결정을 함수 단위로 다시 저울질할 기회를 준다 — 다만 이미 배포된 @inlinable은 되돌릴 수 없으므로, 이 이야기는 앞으로 추가할 API에 적용된다.
"@inlinable은 인라이닝을 시키는 애트리뷰트"라는 것이 이 절이 교정하는 핵심 오해다 — SE-0497 제안서가 직접 부정한다. 실제 역할은 정의를 모듈 밖에 노출하는 것이고, 인라이닝은 그 결과로 가능해지는 여러 최적화 중 하나일 뿐이다. 또 "@inline(always)를 붙이면 반드시 빨라진다"도 틀렸다 — 코드 크기가 늘면 명령어 캐시 적중률이 떨어져 느려질 수 있다. 마지막으로 "@specialized가 있으면 제네릭 성능 걱정을 안 해도 된다"는 과신이다. 사전 특수화는 내가 나열한 타입에 대해서만 만들어지므로, 그 목록에 없는 타입은 여전히 비특수화 경로를 탄다.
요리 레시피를 다른 식당에 넘기는 상황이다. 정의 가시성(SE-0497)은 "레시피 전문을 알려줄까, 아니면 완성된 요리만 보내줄까"의 선택이다. 전문을 알려주면 상대 식당이 자기 주방에 맞게 개선할 수 있어 결과가 더 좋아진다. 그런데 레시피를 한번 알려주면 되돌릴 수 없다 — 나중에 내가 레시피를 고쳐도 상대는 옛 레시피로 계속 요리한다. @inline(always)는 다른 이야기다 — "이 요리는 따로 접시에 담지 말고 본 요리에 섞어라"고 지시하는 것이다. @specialized는 "손님이 자주 주문하는 조합 몇 가지는 미리 만들어두라"는 것이다. 비유가 깨지는 곳: 사람이라면 "레시피 알려준 걸 없던 일로 하자"고 부탁할 수 있지만, 이미 배포된 프로그램에는 그런 부탁이 통하지 않는다 — 그래서 이 결정은 처음에 신중해야 한다.
꼬리 질문
정의를 노출하면 왜 "함수 정의만 다시 컴파일해 링크하는" 것이 불가능해지는가?
Span의 @_alwaysEmitIntoClient가 back-deploy를 가능하게 하는 대가로 "영구적 ABI 고정"을 지불한다고 한 것이 정확히 같은 메커니즘이다.@inline(__always)(밑줄)가 이미 있는데 SE-0496이 왜 필요했는가?
@_cdecl → @c가 같은 이유로 정식화된 것과 완전히 같은 패턴이다. 정식화가 가져오는 실질적 차이는 셋이다. 첫째, 제안 문서가 동작을 명세한다 — 언제 강제되고 언제 무시되는지, 다른 애트리뷰트와 어떻게 상호작용하는지가 계약이 된다. 둘째, 진단이 정비된다. 셋째, 공개 API에 쓸 수 있다 — 밑줄 애트리뷰트를 라이브러리 공개 표면에 쓰는 것은 권장되지 않았다. 6.3 이후에는 새 코드에 밑줄 없는 쪽을 쓰고, 기존 __always는 점진적으로 옮기는 것이 맞다.이 강의 08장·13장은 6.3 기준으로 무엇을 고쳐 읽어야 하는가?
@specialized로 미리 만들어둘 수 있다"는 탈출구 하나다. 13장 Q2는 더 근본적이다 — @inlinable을 "모듈 경계를 넘어 인라이닝을 허용하는 애트리뷰트"로 읽었다면, 6.3의 SE-0497 이후에는 "정의 가시성을 여는 ABI 결정이고, 인라이닝은 그 결과 중 하나"로 읽는 것이 정확하다. 이 재해석은 6.3에서 동작이 바뀌었다는 뜻이 아니다 — 원래부터 그랬던 것을 제안서가 명시적으로 정리한 것이고, 그래서 6.2.1을 쓰는 사람에게도 지금 유효한 교정이다.Q4. 6.3은 이 강의의 어느 문항을 갱신하는가?
세 곳이다. ① 06장 Q5·17장 — weak let(SE-0481)이 허용되어 "weak는 반드시 var여야 한다"는 설명이 낡았다. 이 제약의 원래 목적이 아니라 Sendable 검사와의 마찰이 제거 동기였다는 점이 중요하다 — weak가 가변이어야 했기 때문에 그것을 가진 클래스·클로저를 동시성 문맥 사이에 공유하는 것이 안전하지 않았다. ② 20장 Q2 — DecodingError가 읽기 좋은 debugDescription을 갖게 된다(SE-0489). ③ 13장 Q2·08장 Q5 — Q3에서 다룬 @inlinable의 재정의와 @specialized. 나머지 30개 챕터의 내용은 메모리 레이아웃·디스패치·유니코드·Foundation 동작이라 릴리스와 무관하게 유효하다.
원리
weak let이 왜 금지됐었는지가 이 절의 출발점이다. 06장 Q5에서 본 대로 weak 참조는 대상 객체가 해제되면 자동으로 nil이 된다. 즉 내가 대입하지 않아도 값이 바뀌는 것처럼 보인다. Swift는 그것을 이유로 weak에 var를 요구해 왔고, 6.2.1의 진단문이 그 이유를 그대로 말한다.
$ cat z.swift
class Node {}
class Holder { weak let ref: Node?; init(_ n: Node) { ref = n } }
$ swiftc -O -c z.swift
z.swift:2:25: error: 'weak' must be a mutable variable,
because it may change at runtime그런데 SE-0481은 이 규칙이 다른 곳에서 대가를 만들었다고 지적한다 — 제안서의 표현대로, weak가 반드시 가변이어야 하므로 그런 참조를 가진 클래스와 클로저는 동시성 문맥 사이에 공유하기 안전하지 않았다. Sendable 검사(17장)는 가변 저장 프로퍼티를 위험으로 보기 때문이다. 즉 "런타임에 nil이 될 수 있다"는 성질 때문에 붙인 문법 제약이, Swift 6 동시성에서 훨씬 큰 마찰로 자라난 것이다.
내부 동작
여기서 개념 구분이 하나 필요하다. weak 참조가 nil이 되는 것은 런타임 시스템이 하는 일이고, let/var는 내 코드가 대입할 수 있는가에 관한 것이다. 두 가지가 원래 별개였는데 그동안 묶여 있었다.
nil로 바꾸는 것과 내 코드가 재대입하는 것은 다른 일이다. weak let은 앞의 것만 남긴다.final class Coordinator: Sendable {
// 6.3: 재대입 불가 + 런타임 nil 은 그대로 → Sendable 검사가 통과하기 쉬워진다
weak let parent: Node?
init(parent: Node?) { self.parent = parent }
}
// 6.2.1 에서 같은 코드는:
// error: 'weak' must be a mutable variable, because it may change at runtime실험 · 도구
두 번째 갱신 대상인 Codable 에러 출력(SE-0489)은 6.2.1의 현재 상태를 측정해 두는 것이 유용하다. 제안서는 기본 설명이 "유용한 정보를 읽기 어려운 형식에 묻어둔다"고 지적하는데, 내 툴체인에서 실제로 어떤지 확인해 보면 이렇다.
import Foundation
struct S: Decodable { let a: Int }
do { _ = try JSONDecoder().decode(S.self, from: Data("{}".utf8)) }
catch {
print(String(describing: error))
print(error.localizedDescription)
print(error is CustomDebugStringConvertible)
}DecodingError.keyNotFound: Key 'a' not found in keyed decoding container.
Debug description: No value associated with key
CodingKeys(stringValue: "a", intValue: nil) ("a").
The data couldn’t be read because it is missing.
true실측 결과가 이미 상당히 읽을 만하다. String(describing:)이 에러 종류·키 이름·설명을 다 담고 있고, CustomDebugStringConvertible 준수도 이미 true다(Apple 플랫폼에서 Error가 NSError로 브리징되는 경로 — 04장 Q5·16장). 그래서 SE-0489를 "읽을 수 없던 것이 읽을 수 있게 됐다"고 소개하는 것은 내 실측과 맞지 않는다. 정확한 표현은 "전용 debugDescription이 추가되어 형식이 정돈된다"는 점진적 개선이다. 20장 Q2에서 다룬 codingPath를 직접 파고드는 기법은 6.3 이후에도 여전히 유효하다 — 중첩이 깊은 실제 JSON에서는 여전히 그쪽이 정밀하다.
프로젝트 적용
① weak let은 Swift 6 마이그레이션 중이라면 즉시 값을 만든다. 특히 델리게이트·부모 참조를 가진 타입을 Sendable로 만들려다 막혔던 자리가 대상이다.
// ① @unchecked Sendable — 17장 Q2 의 경고가 적용된다 (상속 계층에서 위험)
final class A: @unchecked Sendable { weak var parent: Node? }
// ② 약한 참조를 actor 안으로 옮긴다
actor B { weak var parent: Node? }
// ③ 참조를 아예 들지 않고 필요할 때 조회한다 (가장 안전하지만 구조 변경)
final class C: Sendable { let parentID: UUID }
// 6.3: weak let 으로 ①의 @unchecked 없이 해결되는 경우가 생긴다② 갱신 지도를 표로 남겨둔다. 이 강의를 읽는 순서와 무관하게, 6.3 이상 툴체인을 쓰는 독자가 어디를 고쳐 읽어야 하는지가 이 표다.
| 이 강의의 문항 | 6.2.1 기준 설명 | 6.3에서 |
|---|---|---|
| 06장 Q5 weak/unowned | weak는 var여야 한다 | weak let 가능 (SE-0481) |
| 17장 Foundation과 Sendable | 가변 weak가 Sendable을 막는다 | 그 마찰이 줄어든다 |
13장 Q2 @inlinable | 모듈 경계 인라이닝 허용 | 정의 가시성으로 재정의 (SE-0497) |
| 08장 Q5 제네릭 특수화 | 컴파일러가 타입을 볼 때만 | @specialized로 사전 생성 (SE-0460) |
| 20장 Q2 Codable 진단 | codingPath로 범인 찾기 | 에러 출력 형식 개선 (SE-0489) — 기법은 그대로 유효 |
| 22장 시간 다루기 | Date·Calendar 중심 | Clock 기준점 추가 (SE-0473) |
| 나머지 27개 챕터 | 메모리 레이아웃·디스패치·유니코드·Foundation 동작 — 변화 없음 | |
③ 이 표를 만드는 습관 자체가 이 절의 진짜 교훈이다. 릴리스마다 "새 기능"은 널리 소개되지만 "내가 알던 것 중 무엇이 낡았는가"는 스스로 대조해야 한다. 방법은 Q1의 절차 그대로다 — 제안 목록을 뽑고, 내 코드베이스·내 지식에서 그것이 닿는 지점을 표로 만든다.
"weak let이 되면 참조가 nil로 바뀌지 않는다"는 심각한 오해다 — nil이 되는 동작은 그대로이며, 그것은 ARC가 하는 일이고 let/var와 무관하다. 바뀐 것은 내 코드가 재대입할 수 있는지뿐이다. 또 "weak let이면 자동으로 Sendable이 된다"도 틀렸다 — 마찰이 줄어드는 것이고, 다른 가변 상태가 있으면 여전히 막힌다. 마지막으로 "6.3으로 올리면 이 강의를 다시 읽어야 한다"는 과한 걱정이다. 위 표대로 여섯 문항이 보충 대상이고 나머지는 그대로다.
weak 참조는 "친구가 이사 가면 자동으로 지워지는 주소록 칸"이다. 지워지는 건 우체국이 알아서 하는 일이다. 그런데 예전 규칙은 "이 칸은 내용이 바뀔 수 있으니 연필로만 써야 한다"(var)였다. 문제는 연필로 쓴 칸은 누가 몰래 고쳐 쓸 수도 있다는 뜻이어서, 이 주소록을 여러 사람이 같이 보게 하려면(동시성) 위험하다고 판단됐다. 6.3에서는 볼펜으로 써도 된다(let)고 바뀌었다 — 나는 못 고치지만 우체국은 여전히 지울 수 있다. 그러면 "아무도 몰래 고칠 수 없는 주소록"이 되어 같이 봐도 안전해진다. 비유가 깨지는 곳: 볼펜으로 쓴 걸 우체국이 어떻게 지우냐고 물을 수 있는데, 여기서 우체국은 규칙 위에 있는 존재(런타임)라서 가능하다.
꼬리 질문
weak let인데 런타임이 nil로 바꾸는 것이 let의 의미를 위반하지 않는가?
let이 보장하는 것은 "내 코드가 이 저장 위치에 다른 값을 대입할 수 없다"이며, "이 위치의 값이 관측 시점마다 동일하다"는 더 강한 보장이 아니다. weak는 애초에 참조 대상의 생존 여부에 따라 값이 달라지는 특수한 저장 방식이고, 그 성질은 let이 통제하는 범위 밖에 있다. 비슷한 예가 있다 — 지연 초기화나 계산 프로퍼티도 "선언 형태"와 "관측되는 값의 변화"가 일치하지 않는다. 실무적으로 중요한 것은 이렇다. weak let도 읽을 때마다 nil 검사를 해야 하고, 한 표현식 안에서 두 번 읽으면 그 사이에 해제되어 결과가 달라질 수 있다 — 06장 Q5가 다룬 guard let self else { return } 패턴(약한 참조를 한 번 언랩해 지역 강한 상수로 붙잡는 것)은 var가 let으로 바뀌어도 그대로 필요하다.왜 이 여섯 문항 외에는 릴리스가 올라도 내용이 유효한가?
@frozen 타입의 레이아웃이나 이미 공개된 함수의 심볼은 릴리스가 올라도 유지되어야 한다. 유니코드 쪽은 표준(UAX) 자체가 바뀌지 않으면 동작이 같다. 반대로 바뀔 수 있는 것은 새 문법·새 애트리뷰트·새 타입, 그리고 진단 메시지다 — 이 강의가 진단 문구를 인용한 곳들은 릴리스가 올라가면 문구가 다듬어질 수 있다(의미는 유지된다). 그래서 실측 기반 강의를 유지하는 규율은 이렇다. 수치와 레이아웃은 오래 유효하고, 진단 문구와 문법은 버전을 명시해 둔다.6.3 툴체인을 설치해 직접 확인하려면 어떻게 하는가?
TOOLCHAINS 환경 변수나 xcrun --toolchain으로 지정한다. 실무에서 주의할 점 셋이다. 첫째, 팀 전체가 같은 툴체인을 써야 한다 — 한 사람만 올리면 컴파일되던 코드가 남에게서 안 되는 상황이 생긴다. 둘째, CI를 함께 올린다. 셋째, 30장 Q4의 구분을 기억한다 — 툴체인을 올려도 배포 타깃은 그대로이므로, 새 문법을 쓸 수 있게 되는 것과 새 API를 쓸 수 있게 되는 것은 별개다. 이 챕터의 내용을 직접 실측해 보고 싶다면, 이 챕터의 Q1 실험 코드를 6.3 툴체인으로 다시 컴파일해 에러가 사라지는지 확인하는 것이 가장 빠른 검증이다.Q5. Swift 6.4는 무엇을 준비하고 있는가?
2026년 8월 현재 Swift 6.4는 스냅샷 단계이고, 제안 저장소에는 이미 Implemented (Swift 6.4)가 24개 올라와 있다. Part C와 직접 이어지는 것이 여럿이다 — SE-0524는 29장 Q4의 withUnsafeTemporaryAllocation에 OutputSpan 버전을 더하고, SE-0525는 30장 Q6의 RawSpan에 안전한 로딩 API를 주고, SE-0507은 05장 Q1의 _read/_modify를 정식 borrow/mutate 접근자로 승격시킨다. SE-0518 ~Sendable과 SE-0519 Ref/MutableRef도 예정돼 있다. 프리릴리스이므로 목록과 내용은 변할 수 있다.
원리
제안의 Status가 Implemented (Swift 6.4)라는 것은 "main 브랜치에 구현이 들어갔고 다음 릴리스에 실릴 예정"이라는 뜻이다. 릴리스가 확정된 것이 아니므로 두 가지 유보가 붙는다 — 릴리스 시점에 빠질 수 있고, 세부 문법이 다듬어질 수 있다. 30장 Q5에서 실측한 @lifetime → @_lifetime, LifetimeDependence → Lifetimes 이름 변경이 그 실제 사례다.
내부 동작
24개 중 이 강의에 닿는 것을 골라 챕터와 연결한다.
| 제안 | 무엇 | 이 강의의 연결점 |
|---|---|---|
| SE-0524 | withTemporaryAllocation에 Output(Raw)Span 버전 | 29장 Q4 — unsafe 포인터 없이 임시 버퍼 |
| SE-0525 | RawSpan의 안전한 로딩 API | 30장 Q6 — unsafeLoad의 안전한 짝 |
| SE-0507 | borrow / mutate 접근자 | 05장 Q1 — _read/_modify의 정식화 |
| SE-0518 | ~Sendable — 명시적으로 non-Sendable 표시 | 17장 Sendable |
| SE-0519 | Ref / MutableRef — 안전한 일급 참조 | 30장 Span 계열의 이웃 |
| SE-0499 | 표준 프로토콜에 ~Copyable·~Escapable 지원 | 30장 Q3 — Collection 미채택 문제 |
| SE-0527 | UniqueArray | 30장 · 01장 CoW |
| SE-0493 | defer 본문에서 async 호출 | 04장 Q6 defer의 함정 |
| SE-0521 | opaque·existential 옵셔널 문법 개선 | 08장 some/any |
| SE-0506 | 고급 Observation 추적 | 27장 Q5 @Observable |
| SE-0502 | private 초기화 프로퍼티를 멤버와이즈 init에서 제외 | 02장 초기화 |
| SE-0522 | 컴파일러 경고를 소스 수준에서 제어 | 29장 Q5·32장 Q4 진단 관리 |
| SE-0530 | Result의 async 지원 | 04장 Q4 Result ↔ throws |
흐름이 하나 보인다. Part C가 다룬 문제들의 후속이 6.4에 몰려 있다 — Span 계열의 안전한 API(SE-0525), ~Copyable 원소를 담는 컨테이너와 표준 프로토콜의 화해(SE-0499), _read/_modify의 정식화(SE-0507). 30장 Q5에서 "수명 의존성이 정식화되면 지금 클로저로 짜야 하는 API가 자연스러워진다"고 했던 방향이 실제로 진행되고 있다.
실험 · 도구
이 목록은 지금 재현 가능하다. 제안 저장소만 있으면 된다.
$ gh api repos/swiftlang/swift-evolution/tarball/main > se.tar.gz
$ tar xzf se.tar.gz && cd swiftlang-swift-evolution-*/proposals
# 다음 릴리스 예정
$ grep -l "Implemented (Swift 6.4)" *.md | wc -l
24
# 심사 중인 것 — 그다음 릴리스의 후보
$ grep -l "Active Review" *.md
# 특정 주제의 진행 상황
$ grep -l "Span\|Escapable" *.md | xargs grep -H "^\* Status" | sort -t: -k3이 절차를 분기마다 한 번 돌리는 것이 이 강의가 권하는 습관이다. Q4 ③에서 말한 "갱신 지도를 스스로 만드는 것"의 앞단이다.
프로젝트 적용
① 프리릴리스 목록으로 코드를 쓰지 않는다. 쓸 곳은 설계 판단이다 — 지금 우회로를 만들지, 아니면 곧 오는 정식 기능을 기다릴지 결정할 때 근거가 된다.
| 지금 겪는 불편 | 6.4가 해결하는가 | 지금 할 일 |
|---|---|---|
RawSpan에서 unsafeLoad를 써야 함 | SE-0525 안전한 로딩 | 정렬 검증을 헬퍼로 감싸두고 나중에 교체 |
| 임시 버퍼에 포인터 API가 필요 | SE-0524 OutputSpan 버전 | 29장 Q4 형태로 쓰되 unsafe 표기 |
_read/_modify가 비공식 | SE-0507 정식 접근자 | 공개 API에는 쓰지 않고 내부에만 |
InlineArray가 Collection 아님 | SE-0499 방향 | .span 경유 습관을 유지 |
defer에서 await 불가 | SE-0493 | 지금은 Task로 감싸거나 구조 변경 |
② "곧 온다"가 기다림의 이유가 되지 않게 한다. 프리릴리스 항목은 빠질 수 있고, 나온다 해도 30장 Q4대로 배포 타깃이 올라가야 쓸 수 있는 경우가 있다. 그래서 실무 원칙은 "지금 되는 방법으로 만들고, 교체 지점을 표시해 둔다"다 — 29장 Q5의 unsafe 표기가 정확히 그 표시 역할을 한다.
③ 이 강의를 계속 쓸모 있게 유지하는 방법으로 이 챕터를 닫는다. 33개 챕터의 대부분은 릴리스와 무관하게 유효하지만(Q4 꼬리 질문), 이 챕터만은 주기적으로 다시 써야 하는 챕터다. 절차는 세 줄이다 — ① 제안 저장소에서 새 릴리스의 목록을 센다 ② 각 항목이 이 강의의 어느 문항에 닿는지 표로 만든다 ③ 그 문항에 보충을 넣고, 실측이 필요한 것은 해당 툴체인에서 다시 측정한다.
29장부터 33장까지가 서로 다른 다섯 주제를 다뤘지만 결론이 같은 곳으로 모인다. Swift는 "조용한 실패"를 "컴파일 에러"로 옮기는 방향으로 진화하고 있다. 포인터 탈출이 exit 0으로 통과했던 것(29장)을 Span의 ~Escapable이 컴파일 에러로 바꿨고(30장), 가변 weak가 Sendable 검사와 부딪히던 마찰을 weak let이 없앴고(33장), @inlinable이 감추고 있던 ABI 결정을 SE-0497이 명시적 선택으로 드러냈다. 31장의 NaN과 32장의 타입 검사 폭발은 아직 그 여정이 끝나지 않은 자리다 — 여전히 sorted()는 조용히 정렬을 포기하고, 열 토큰짜리 표현식은 갑자기 컴파일에 실패한다. 그래서 이 강의의 결론은 API 목록이 아니라 태도다 — 감춰진 비용을 드러내는 도구를 켜고(-strict-memory-safety, isFinite, -warn-long-expression-type-checking), 주장을 실제로 측정하고, 릴리스마다 내 지식을 대조하는 것.
"Implemented (Swift 6.4)이니 6.4가 나오면 반드시 들어간다"는 보장이 아니다 — 릴리스 브랜치 컷 이후 되돌려질 수 있고, 실제로 그런 사례가 있었다. 또 "스냅샷 툴체인으로 미리 써도 된다"는 프로덕션 판단으로는 위험하다. 30장 Q5에서 실측한 애트리뷰트 이름 변경이 정확히 그 위험의 사례다. 마지막으로 "6.4 목록이 24개니 6.3(9개)보다 큰 릴리스"라는 추론도 이르다 — 아직 집계 중이고, 제안 개수가 릴리스의 영향 크기와 비례하지도 않는다.
다음 학기 시간표가 가안으로 붙은 상태다. 대부분 그대로 열리지만 몇 과목은 취소되거나 시간이 바뀔 수 있다. 그래서 가안을 보고 "이 과목 열리니까 지금 공부 안 해도 돼"라고 결정하면 안 된다. 대신 가안이 쓸모 있는 방식이 있다 — "어차피 다음 학기에 정식으로 배울 내용이니, 지금은 임시로 대충 해두고 나중에 제대로 고치자"는 계획을 세울 때다. 그리고 그렇게 임시로 해둔 자리에는 "여기 나중에 고칠 것" 표시를 남겨두는 게 중요하다. 비유가 깨지는 곳: 학교 시간표는 누가 정해서 알려주지만, 여기서는 내가 직접 목록을 세어 확인해야 한다 — 그 방법이 위의 grep 세 줄이다.
꼬리 질문
제안 상태가 Implemented (Swift 6.4)인데 실제로 빠지는 일이 있는가?
Implemented는 "main 브랜치에 구현이 병합됐다"는 뜻이고, 릴리스에 실리는 것은 릴리스 브랜치가 언제 갈라졌는지와 그 뒤 발견된 문제에 달려 있다. 구현에 심각한 결함이 발견되면 릴리스 브랜치에서 비활성화되거나 되돌려질 수 있고, 그 경우 제안 상태가 나중에 수정된다. 그래서 실무에서 신뢰할 수 있는 순서는 이렇다. ① 릴리스된 버전의 릴리스 노트 — 가장 확실하다. ② 그 버전의 툴체인으로 직접 컴파일 — 이 챕터가 6.2.1에 대해 한 것이 그것이다. ③ 제안 저장소의 Status — 방향을 아는 데는 좋지만 확정은 아니다. 프로덕션 코드의 근거로는 ②만 쓰는 것이 안전하다.SE-0499가 30장 Q3의 문제를 정말 해결하는가?
Collection의 요구사항이 원소를 복사해 꺼내는 것을 전제하므로 ~Copyable 원소를 담는 InlineArray는 채택할 수 없다"였다. SE-0499의 제목이 "표준 라이브러리의 단순한 프로토콜에서 ~Copyable·~Escapable 지원"인 것에서 알 수 있듯, 먼저 손대는 것은 단순한(simple) 프로토콜들이다 — Equatable·Comparable 같은. Collection·Sequence는 연관 타입과 Iterator가 얽혀 훨씬 어려운 문제이고, 그래서 Iterable 같은 별도 후속 제안이 논의되고 있다(30장 Q3 꼬리 질문에서 언급). 정리하면 "일부 프로토콜에서 먼저 풀리고, Collection은 더 걸린다"가 정확한 예상이다. 그러니 InlineArray에 map/filter를 쓰려면 당장은 .span 경유 습관을 유지하는 것이 맞다.Collection)은 아직 열리지 않았다.