← Swift & Foundation 로드맵 33 / 33

33 · Swift 6.3 델타

2026년 3월에 나온 Swift 6.3이 실제로 바꾼 것 — 제안 9개 전수. @c로 C에서 Swift 함수를 부르고, ModuleName::로 이름 충돌을 끊고, weak let이 드디어 허용되며, @inlinable이 사실 인라이닝이 아니라 정의 가시성에 관한 것이었다는 SE-0497의 재정의까지. 이 강의의 다른 챕터 중 무엇이 갱신되어야 하는지도 함께 짚는다.
진행률
0 / 0 완료
⚠️ 이 챕터의 출처 규칙 — 반드시 먼저 읽을 것

이 강의의 다른 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개 전수

🔑 30초 답변 스펙 기준 · 미실측

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 @cSwift 함수·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 letweaklet으로 선언 가능06장 Q5 weak/unowned · 17장 Sendable — 직접 갱신 대상
SE-0489 Codable 에러 출력EncodingError/DecodingError에 읽기 좋은 debugDescription20장 Q2 codingPath로 범인 찾기
SE-0473 Clock 기준점SuspendingClock·ContinuousClock의 기준 instant 노출22장 Date · 시간 다루기

세 항목이 "직접 갱신 대상"으로 표시됐다. 이는 이 강의의 해당 문항이 6.2.1 기준으로 설명한 제약이 6.3에서 사라졌다는 뜻이며, Q4에서 하나씩 다룬다.

실험 · 도구

이 챕터에서 측정할 수 있는 것은 "6.2.1이 이 기능들을 거부한다"다. 각 기능을 6.2.1에서 컴파일하면 어떤 진단이 나오는지 실측했다 — 이것이 곧 "이 기능이 6.3에 들어왔다"는 간접 증거다.

6.2.1 실측 — 다섯 기능이 각각 어떻게 거부되는가
① 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)는 기존 도구의 설정 화면을 열어준 것에 가깝다. 새 아이템은 적고, 대신 "이 도구를 언제 어떻게 쓸지"를 내가 세밀하게 정할 수 있게 됐다. 그리고 오래 불편했던 것 하나가 고쳐졌다 — 전에는 "이 물건은 바뀔 수 있으니 반드시 흔들리는 선반에 둬야 한다"는 규칙이 있었는데(weakvar여야 함), 이제 고정 선반에도 둘 수 있게 됐다. 비유가 깨지는 곳: 게임 업데이트는 자동으로 오지만 컴파일러 업데이트는 내가 직접 설치해야 하고, 설치하면 팀 전체가 함께 올라가야 한다.

꼬리 질문

왜 릴리스 노트가 아니라 제안 저장소를 세는 것이 정확한가?
릴리스 노트는 편집된 하이라이트다. 지면과 독자의 관심을 고려해 항목을 고르므로, 특정 분야에만 중요한 변경이 빠질 수 있다. 실제로 이 챕터의 9개 중 릴리스 노트가 이름을 들어 소개한 것은 일부이고, weak let·Codable 에러 출력·Clock 기준점처럼 조용하지만 실무에 닿는 항목은 제안 저장소를 세지 않으면 놓친다. 반대로 제안 저장소만 보면 놓치는 것도 있다 — 제안 없이 들어가는 변경(버그 수정, 최적화 개선, 진단 메시지 개선, 도구 변화)은 제안 문서가 없다. 그래서 실전에서는 둘을 함께 본다. 제안 저장소로 언어 표면의 변화를 빠짐없이 세고, 릴리스 노트로 도구·플랫폼 쪽 변화를 확인하는 식이다.
쉽게 말하면 신문 1면(릴리스 노트)만 보면 중요한 기사를 놓칠 수 있으니, 목차 전체(제안 저장소)를 훑어보는 것이다. 다만 목차에 없는 소식(광고면, 부록)도 있으니 둘 다 봐야 한다.
6.2와 6.3의 성격이 다르다는 말은 어떤 근거인가?
제안 목록의 구성이 근거다. 6.2에는 표준 라이브러리에 새 타입을 추가하는 제안이 여럿 있었다 — 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개 챕터의 내용이 틀리게 되는가?
대부분은 그대로 유효하다. 이 강의가 다루는 것은 주로 메모리 레이아웃·디스패치·유니코드·Foundation의 동작이고, 이들은 릴리스가 올라도 바뀌지 않는다(바뀌면 ABI가 깨진다 — 13장). 실제로 갱신이 필요한 것은 이 챕터 Q1 표에서 "직접 갱신 대상"으로 표시한 세 곳이다 — 06장 Q5(weak는 var여야 한다는 설명), 08장 Q5(특수화를 컴파일러에게만 맡긴다는 설명), 13장 Q2(@inlinable의 의미). 나머지는 "6.3에서 더 쉬워진 방법이 추가됐다"는 보충이면 충분하다. 이것이 이 챕터를 부록이 아니라 정규 챕터로 둔 이유다 — 새 기능 소개가 아니라 기존 내용의 갱신 지도 역할을 한다.
쉽게 말하면 교과서 전체를 다시 쓸 필요는 없고, 세 페이지에 포스트잇을 붙이면 된다. 이 챕터가 그 포스트잇 목록이다.

Q2. @c(SE-0495)와 모듈 셀렉터(SE-0491)는 각각 무슨 문제를 푸는가?

🔑 30초 답변 스펙 기준 · 미실측

@c는 오래 실험 상태였던 @_cdecl정식화한다. 붙이면 Swift 전역 함수를 C에서 호출 가능하게 하고 enum을 C에 표현 가능하게 하며, 생성되는 호환성 헤더에 새 C 블록이 추가되어 C 파일에서 #include할 수 있다. @implementation과 함께 쓰면 손으로 쓴 C 헤더의 선언을 Swift로 구현하는 방향도 된다. 모듈 셀렉터는 문법 확장이다 — 선언을 참조하는 식별자 앞에 ModuleName::을 붙여 어느 모듈의 것인지 못 박는다. 두 모듈이 같은 이름을 노출해 생기던 모호성을 타입 별칭이나 래퍼 없이 해소한다.

원리

두 제안의 공통점은 "이미 하고 있던 일을 정식 문법으로 승격"이다. @_cdecl은 밑줄 접두사가 붙은 비공식 애트리뷰트였지만 널리 쓰여 왔다 — 제안서 자체가 "실험 상태였지만 광범위하게 사용되어 왔으므로 명확성을 위해 이 문서에서 언급한다"고 밝힌다. 이름 충돌 회피 역시 그동안 typealias를 두거나 한쪽 모듈만 import하는 우회로 해결해 왔다.

비공식 상태의 대가는 13장에서 본 문제 그대로다 — 밑줄 애트리뷰트는 언제든 바뀌거나 사라질 수 있고, 동작이 문서화되지 않아 코너 케이스가 불명확하다. 정식화는 그 동작을 계약으로 고정하는 작업이다.

내부 동작

@c가 하는 일은 세 갈래다.

대상효과
전역 함수C 호출 규약으로 심볼을 내보내고, 호환성 헤더의 C 블록에 선언을 출력
enumC에서 표현 가능한 형태로 내보낸다
@objc와의 관계@objc는 ObjC 호환 전역 함수에 대해 같은 동작을 제공한다
@implementation 조합전역 함수까지 지원 확대 — C 헤더에 선언, Swift로 구현

방향을 헷갈리지 않는 것이 중요하다. 16장이 다룬 것은 Swift가 ObjC/C를 부르는 방향이었고, @cC가 Swift를 부르는 반대 방향이다.

Swift 코드 내가 쓰는 쪽 C / ObjC 코드 기존 코드베이스 Swift → C 호출 · 브리징 헤더 (16장) C → Swift 호출 · @c (SE-0495) @c 가 만드는 것 ① C 호출 규약 심볼 ② 호환성 헤더의 새 C 블록 — C 파일에서 #include 가능 ③ @implementation 과 조합하면 반대로 "C 헤더에 선언 · Swift 로 구현"도 된다 @_cdecl 의 정식 후계 — 코너 케이스가 함께 정리됐다
16장이 다룬 브리징은 Swift → C 방향이다. @c는 그 반대 방향을 정식 문법으로 만든다.

모듈 셀렉터는 훨씬 단순하다. 제안서의 표현대로 "선언을 참조하기 위해 식별자가 쓰이는 모든 자리"에서 ModuleName:: 접두사를 허용한다.

모듈 셀렉터 — 스펙 기준 예시 (6.2.1에서는 구문 에러)
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에서 두 기능이 어떻게 거부되는지가 이 절의 유일한 실측이다.

6.2.1 실측 — @c
$ cat z.swift
@c func myFunc(_ x: Int32) -> Int32 { x * 2 }

$ swiftc -O -c z.swift
z.swift:1:2: error: unknown attribute 'c'
6.2.1 실측 — 모듈 셀렉터 (애트리뷰트가 아니라 구문이므로 파싱 단계에서 막힌다)
$ 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 expression

6.2.1에서 같은 목적을 달성하려면 지금도 @_cdecl을 쓸 수 있다. 다만 밑줄 애트리뷰트이므로 13장의 경고가 적용된다 — 공식 지원 대상이 아니다.

6.2.1에서의 현행 방법 — @_cdecl (비공식)
@_cdecl("my_func")
func myFunc(_ x: Int32) -> Int32 { x * 2 }
// C 쪽: int32_t my_func(int32_t);  ← 헤더를 손으로 선언해야 했다

프로젝트 적용

① @c가 실질적으로 바꾸는 것은 헤더 관리다. @_cdecl 시절에는 C 쪽 선언을 손으로 유지해야 했고, 시그니처가 어긋나면 컴파일은 통과하고 실행 중에 깨졌다29장의 언어로 말하면 "조용한 실패"다. @c는 선언을 생성된 헤더에 넣어주므로 그 불일치가 구조적으로 사라진다.

C에서 Swift를 부르는 구조 — 스펙 기준
// 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 에러를 던지면 어떻게 되는가?
던질 수 없다. C에는 Swift의 throws에 대응하는 개념이 없고, 04장 Q1에서 본 대로 Swift의 throws전용 레지스터로 에러를 전달하는 ABI이므로 C 호출 규약과 맞지 않는다. 그래서 C 경계로 내보내는 함수는 에러를 값으로 변환해야 한다 — 관례적으로 정수 반환값을 상태 코드로 쓰거나, 출력 파라미터로 에러 정보를 채운다. 04장 Q4에서 다룬 Resultthrows 변환과 같은 종류의 경계 작업이다. 구조적으로는 이렇게 된다 — 내부는 throws로 자연스럽게 쓰고, @c 함수는 그것을 감싸는 얇은 어댑터로 두어 do/catch로 상태 코드를 만든다. 그러면 에러 처리 로직이 C 경계 때문에 오염되지 않는다.
쉽게 말하면 C는 "예외"라는 말을 모르는 손님이다. 그래서 문제가 생겼을 때 소리쳐 알리는 대신 번호표에 오류 번호를 적어 건네주는 방식으로 바꿔줘야 한다.

Q3. 성능 제어 3종 — @specialized · @inline(always) · 정의 가시성은 무엇이 다른가?

🔑 30초 답변 스펙 기준 · 미실측

세 애트리뷰트가 서로 다른 층을 제어한다. 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 — 스펙 기준 형태 (6.2.1에서는 unknown attribute)
@specialized(where T == Int)
@specialized(where T == Double)
public func process<T: Numeric>(_ values: [T]) -> T {
    values.reduce(0, +)
}
// 호출 지점에서 T 를 못 봐도, 런타임에 T == Int 면 사전 특수화 버전으로 재디스패치된다

실험 · 도구

6.2.1 실측 — 세 애트리뷰트 모두 거부된다
$ 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'

④의 진단이 특히 정확하다. @inline6.2.1에도 이미 존재하는 애트리뷰트다 — @inline(never)@inline(__always)(밑줄 두 개)가 있었다. SE-0496이 한 일은 밑줄 없는 정식 옵션 always를 추가한 것이며, 그래서 에러가 "unknown attribute"가 아니라 unknown option 'always' for attribute 'inline'이다.

6.2.1에서의 현행 방법 — 밑줄 버전은 이미 동작한다
@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는 "손님이 자주 주문하는 조합 몇 가지는 미리 만들어두라"는 것이다. 비유가 깨지는 곳: 사람이라면 "레시피 알려준 걸 없던 일로 하자"고 부탁할 수 있지만, 이미 배포된 프로그램에는 그런 부탁이 통하지 않는다 — 그래서 이 결정은 처음에 신중해야 한다.

꼬리 질문

정의를 노출하면 왜 "함수 정의만 다시 컴파일해 링크하는" 것이 불가능해지는가?
호출자가 그 정의의 사본을 자기 코드에 이미 넣어버렸기 때문이다. 정의가 노출되면 호출자 측 컴파일러는 그것을 인라인하거나 특수화해 자기 바이너리 안에 코드를 만든다. 그 뒤 내가 라이브러리에서 함수 본문만 고쳐 다시 링크해도, 호출자 바이너리에 박힌 사본은 옛 코드 그대로다. 그래서 변경 효과를 보려면 호출자까지 다시 컴파일해야 하고, 그것이 증분 빌드가 느려지는 이유이자 바이너리 호환성을 유지한 채 구현을 교체할 수 없게 되는 이유다. 30장 Q4에서 Span@_alwaysEmitIntoClient가 back-deploy를 가능하게 하는 대가로 "영구적 ABI 고정"을 지불한다고 한 것이 정확히 같은 메커니즘이다.
쉽게 말하면 레시피를 복사해 간 식당은 내가 원본을 고쳐도 자기 복사본을 보고 요리한다. 바뀐 걸 반영하려면 복사본을 다시 받아 가야 한다.
@inline(__always)(밑줄)가 이미 있는데 SE-0496이 왜 필요했는가?
밑줄 애트리뷰트의 문제가 그대로 적용된다 — 비공식이라 소스 호환성 약속의 대상이 아니고, 동작이 문서화되지 않아 경계 사례가 불명확하다. Q2에서 @_cdecl@c가 같은 이유로 정식화된 것과 완전히 같은 패턴이다. 정식화가 가져오는 실질적 차이는 셋이다. 첫째, 제안 문서가 동작을 명세한다 — 언제 강제되고 언제 무시되는지, 다른 애트리뷰트와 어떻게 상호작용하는지가 계약이 된다. 둘째, 진단이 정비된다. 셋째, 공개 API에 쓸 수 있다 — 밑줄 애트리뷰트를 라이브러리 공개 표면에 쓰는 것은 권장되지 않았다. 6.3 이후에는 새 코드에 밑줄 없는 쪽을 쓰고, 기존 __always는 점진적으로 옮기는 것이 맞다.
쉽게 말하면 임시로 붙여 쓰던 손글씨 라벨을 정식 인쇄 라벨로 바꾼 것이다. 기능은 같아도 이제 공식적으로 보증되고, 남에게 보여주는 제품에도 붙일 수 있다.
이 강의 08장·13장은 6.3 기준으로 무엇을 고쳐 읽어야 하는가?
두 곳이다. 08장 Q5는 "제네릭 특수화는 컴파일러가 호출 지점의 구체 타입과 함수 본문을 볼 수 있을 때 일어난다"고 설명했는데, 이 메커니즘 설명은 그대로 유효하다. 추가되는 것은 "볼 수 없을 때 함수 작성자가 @specialized로 미리 만들어둘 수 있다"는 탈출구 하나다. 13장 Q2는 더 근본적이다 — @inlinable을 "모듈 경계를 넘어 인라이닝을 허용하는 애트리뷰트"로 읽었다면, 6.3의 SE-0497 이후에는 "정의 가시성을 여는 ABI 결정이고, 인라이닝은 그 결과 중 하나"로 읽는 것이 정확하다. 이 재해석은 6.3에서 동작이 바뀌었다는 뜻이 아니다 — 원래부터 그랬던 것을 제안서가 명시적으로 정리한 것이고, 그래서 6.2.1을 쓰는 사람에게도 지금 유효한 교정이다.
쉽게 말하면 규칙이 바뀐 게 아니라 규칙의 진짜 이름이 밝혀진 것이다. "인라이닝 허가증"이라고 부르던 것이 실은 "레시피 공개 허가증"이었다.

Q4. 6.3은 이 강의의 어느 문항을 갱신하는가?

🔑 30초 답변 스펙 기준 · 미실측

세 곳이다. ① 06장 Q5·17장weak let(SE-0481)이 허용되어 "weak는 반드시 var여야 한다"는 설명이 낡았다. 이 제약의 원래 목적이 아니라 Sendable 검사와의 마찰이 제거 동기였다는 점이 중요하다 — weak가 가변이어야 했기 때문에 그것을 가진 클래스·클로저를 동시성 문맥 사이에 공유하는 것이 안전하지 않았다. ② 20장 Q2DecodingError가 읽기 좋은 debugDescription을 갖게 된다(SE-0489). ③ 13장 Q2·08장 Q5 — Q3에서 다룬 @inlinable의 재정의와 @specialized. 나머지 30개 챕터의 내용은 메모리 레이아웃·디스패치·유니코드·Foundation 동작이라 릴리스와 무관하게 유효하다.

원리

weak let이 왜 금지됐었는지가 이 절의 출발점이다. 06장 Q5에서 본 대로 weak 참조는 대상 객체가 해제되면 자동으로 nil이 된다. 즉 내가 대입하지 않아도 값이 바뀌는 것처럼 보인다. Swift는 그것을 이유로 weakvar를 요구해 왔고, 6.2.1의 진단문이 그 이유를 그대로 말한다.

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내 코드가 대입할 수 있는가에 관한 것이다. 두 가지가 원래 별개였는데 그동안 묶여 있었다.

6.2 이전 — weak 는 반드시 var weak var ref: Node? · 런타임이 nil 로 바꾼다 (필요한 동작) · 내 코드도 재대입할 수 있다 (원치 않던 부작용) → 가변 저장 프로퍼티라 Sendable 검사가 막는다 6.3 — weak let 허용 weak let ref: Node? · 런타임이 nil 로 바꾼다 (그대로) · 내 코드는 재대입할 수 없다 → Sendable 마찰이 줄어든다 핵심 구분 — 두 가지는 원래 별개였다 ① "런타임이 값을 nil 로 바꾼다" = ARC 가 하는 일. weak 의 본질이며 let 이어도 그대로 일어난다. ② "내 코드가 다른 객체를 대입할 수 있다" = var 가 주는 것. weak 에 꼭 필요한 것이 아니었다. 6.2 이전에는 ①을 이유로 ②까지 강제했고, 그 부작용이 Sendable 검사에서 드러났다.
런타임이 nil로 바꾸는 것과 내 코드가 재대입하는 것은 다른 일이다. weak let은 앞의 것만 남긴다.
weak let — 스펙 기준 형태 (6.3 이상 필요)
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의 현재 상태를 측정해 두는 것이 유용하다. 제안서는 기본 설명이 "유용한 정보를 읽기 어려운 형식에 묻어둔다"고 지적하는데, 내 툴체인에서 실제로 어떤지 확인해 보면 이렇다.

6.2.1 실측 — 키가 없을 때 DecodingError의 현재 출력
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)
}
실행 결과 — 6.2.1
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 플랫폼에서 ErrorNSError로 브리징되는 경로 — 04장 Q5·16장). 그래서 SE-0489를 "읽을 수 없던 것이 읽을 수 있게 됐다"고 소개하는 것은 내 실측과 맞지 않는다. 정확한 표현은 "전용 debugDescription이 추가되어 형식이 정돈된다"는 점진적 개선이다. 20장 Q2에서 다룬 codingPath를 직접 파고드는 기법은 6.3 이후에도 여전히 유효하다 — 중첩이 깊은 실제 JSON에서는 여전히 그쪽이 정밀하다.

프로젝트 적용

① weak let은 Swift 6 마이그레이션 중이라면 즉시 값을 만든다. 특히 델리게이트·부모 참조를 가진 타입을 Sendable로 만들려다 막혔던 자리가 대상이다.

6.2.1에서 이 마찰을 우회하는 현행 방법들
// ① @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/unownedweakvar여야 한다weak let 가능 (SE-0481)
17장 Foundation과 Sendable가변 weakSendable을 막는다그 마찰이 줄어든다
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 } 패턴(약한 참조를 한 번 언랩해 지역 강한 상수로 붙잡는 것)은 varlet으로 바뀌어도 그대로 필요하다.
쉽게 말하면 볼펜으로 썼다는 건 내가 못 고친다는 뜻일 뿐, 그 칸의 친구가 이사 가지 않는다는 보장은 아니다. 그래서 볼 때마다 "아직 있나?"를 확인해야 하는 건 똑같다.
왜 이 여섯 문항 외에는 릴리스가 올라도 내용이 유효한가?
이 강의가 다루는 것의 성질 때문이다. 대부분의 문항은 메모리 레이아웃(01·03·09장), 디스패치 메커니즘(07·08장), 유니코드 규칙(12·24장), Foundation API의 동작(14~28장)을 다루는데, 이들은 바뀌면 ABI나 표준이 깨지는 종류다. 13장에서 본 라이브러리 진화 제약이 바로 이것을 보장한다 — @frozen 타입의 레이아웃이나 이미 공개된 함수의 심볼은 릴리스가 올라도 유지되어야 한다. 유니코드 쪽은 표준(UAX) 자체가 바뀌지 않으면 동작이 같다. 반대로 바뀔 수 있는 것은 새 문법·새 애트리뷰트·새 타입, 그리고 진단 메시지다 — 이 강의가 진단 문구를 인용한 곳들은 릴리스가 올라가면 문구가 다듬어질 수 있다(의미는 유지된다). 그래서 실측 기반 강의를 유지하는 규율은 이렇다. 수치와 레이아웃은 오래 유효하고, 진단 문구와 문법은 버전을 명시해 둔다.
쉽게 말하면 "건물의 구조와 치수"는 리모델링해도 그대로지만, "안내판 문구"와 "새로 생긴 편의시설"은 바뀐다. 이 강의 대부분은 구조와 치수를 다룬다.
6.3 툴체인을 설치해 직접 확인하려면 어떻게 하는가?
Xcode에 딸려오는 툴체인과 별도로 설치해 병행하는 것이 안전하다. swift.org가 배포하는 툴체인을 받아 설치하면 Xcode의 Toolchains 메뉴에서 선택할 수 있고, 커맨드라인에서는 TOOLCHAINS 환경 변수나 xcrun --toolchain으로 지정한다. 실무에서 주의할 점 셋이다. 첫째, 팀 전체가 같은 툴체인을 써야 한다 — 한 사람만 올리면 컴파일되던 코드가 남에게서 안 되는 상황이 생긴다. 둘째, CI를 함께 올린다. 셋째, 30장 Q4의 구분을 기억한다 — 툴체인을 올려도 배포 타깃은 그대로이므로, 새 문법을 쓸 수 있게 되는 것과 새 API를 쓸 수 있게 되는 것은 별개다. 이 챕터의 내용을 직접 실측해 보고 싶다면, 이 챕터의 Q1 실험 코드를 6.3 툴체인으로 다시 컴파일해 에러가 사라지는지 확인하는 것이 가장 빠른 검증이다.
쉽게 말하면 새 버전을 기존 것을 지우지 않고 옆에 하나 더 설치해서, 필요할 때만 골라 쓰는 방식이다. 그리고 팀원과 CI도 같이 올려야 혼자만 되는 상황을 피할 수 있다.

Q5. Swift 6.4는 무엇을 준비하고 있는가?

🔑 30초 답변 스펙 기준 · 미실측 · 프리릴리스

2026년 8월 현재 Swift 6.4는 스냅샷 단계이고, 제안 저장소에는 이미 Implemented (Swift 6.4)24개 올라와 있다. Part C와 직접 이어지는 것이 여럿이다 — SE-052429장 Q4withUnsafeTemporaryAllocationOutputSpan 버전을 더하고, SE-052530장 Q6RawSpan안전한 로딩 API를 주고, SE-050705장 Q1_read/_modify를 정식 borrow/mutate 접근자로 승격시킨다. SE-0518 ~SendableSE-0519 Ref/MutableRef도 예정돼 있다. 프리릴리스이므로 목록과 내용은 변할 수 있다.

원리

제안의 StatusImplemented (Swift 6.4)라는 것은 "main 브랜치에 구현이 들어갔고 다음 릴리스에 실릴 예정"이라는 뜻이다. 릴리스가 확정된 것이 아니므로 두 가지 유보가 붙는다 — 릴리스 시점에 빠질 수 있고, 세부 문법이 다듬어질 수 있다. 30장 Q5에서 실측한 @lifetime@_lifetime, LifetimeDependenceLifetimes 이름 변경이 그 실제 사례다.

내부 동작

24개 중 이 강의에 닿는 것을 골라 챕터와 연결한다.

제안무엇이 강의의 연결점
SE-0524withTemporaryAllocationOutput(Raw)Span 버전29장 Q4 — unsafe 포인터 없이 임시 버퍼
SE-0525RawSpan안전한 로딩 API30장 Q6unsafeLoad의 안전한 짝
SE-0507borrow / mutate 접근자05장 Q1_read/_modify의 정식화
SE-0518~Sendable — 명시적으로 non-Sendable 표시17장 Sendable
SE-0519Ref / MutableRef — 안전한 일급 참조30장 Span 계열의 이웃
SE-0499표준 프로토콜에 ~Copyable·~Escapable 지원30장 Q3Collection 미채택 문제
SE-0527UniqueArray30장 · 01장 CoW
SE-0493defer 본문에서 async 호출04장 Q6 defer의 함정
SE-0521opaque·existential 옵셔널 문법 개선08장 some/any
SE-0506고급 Observation 추적27장 Q5 @Observable
SE-0502private 초기화 프로퍼티를 멤버와이즈 init에서 제외02장 초기화
SE-0522컴파일러 경고를 소스 수준에서 제어29장 Q5·32장 Q4 진단 관리
SE-0530Result의 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에는 쓰지 않고 내부에만
InlineArrayCollection 아님SE-0499 방향.span 경유 습관을 유지
defer에서 await 불가SE-0493지금은 Task로 감싸거나 구조 변경

② "곧 온다"가 기다림의 이유가 되지 않게 한다. 프리릴리스 항목은 빠질 수 있고, 나온다 해도 30장 Q4대로 배포 타깃이 올라가야 쓸 수 있는 경우가 있다. 그래서 실무 원칙은 "지금 되는 방법으로 만들고, 교체 지점을 표시해 둔다"다 — 29장 Q5unsafe 표기가 정확히 그 표시 역할을 한다.

③ 이 강의를 계속 쓸모 있게 유지하는 방법으로 이 챕터를 닫는다. 33개 챕터의 대부분은 릴리스와 무관하게 유효하지만(Q4 꼬리 질문), 이 챕터만은 주기적으로 다시 써야 하는 챕터다. 절차는 세 줄이다 — 제안 저장소에서 새 릴리스의 목록을 센다 각 항목이 이 강의의 어느 문항에 닿는지 표로 만든다 그 문항에 보충을 넣고, 실측이 필요한 것은 해당 툴체인에서 다시 측정한다.

🟣 한 걸음 더 — Part C가 남긴 한 문장 종합

29장부터 33장까지가 서로 다른 다섯 주제를 다뤘지만 결론이 같은 곳으로 모인다. Swift는 "조용한 실패"를 "컴파일 에러"로 옮기는 방향으로 진화하고 있다. 포인터 탈출이 exit 0으로 통과했던 것(29장)을 Span~Escapable이 컴파일 에러로 바꿨고(30장), 가변 weakSendable 검사와 부딪히던 마찰을 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의 문제를 정말 해결하는가?
방향은 맞지만 완전한 해결이라고 단정할 수는 없다. 30장 Q3에서 본 문제는 "Collection의 요구사항이 원소를 복사해 꺼내는 것을 전제하므로 ~Copyable 원소를 담는 InlineArray는 채택할 수 없다"였다. SE-0499의 제목이 "표준 라이브러리의 단순한 프로토콜에서 ~Copyable·~Escapable 지원"인 것에서 알 수 있듯, 먼저 손대는 것은 단순한(simple) 프로토콜들이다 — Equatable·Comparable 같은. Collection·Sequence는 연관 타입과 Iterator가 얽혀 훨씬 어려운 문제이고, 그래서 Iterable 같은 별도 후속 제안이 논의되고 있다(30장 Q3 꼬리 질문에서 언급). 정리하면 "일부 프로토콜에서 먼저 풀리고, Collection은 더 걸린다"가 정확한 예상이다. 그러니 InlineArraymap/filter를 쓰려면 당장은 .span 경유 습관을 유지하는 것이 맞다.
쉽게 말하면 문이 여러 개 잠겨 있는데 쉬운 문부터 열고 있는 중이다. 가장 크고 복잡한 문(Collection)은 아직 열리지 않았다.
이 강의를 6.3이나 6.4 기준으로 전면 개정하는 것이 나은가, 이 챕터로 델타만 관리하는 것이 나은가?
델타 챕터 방식이 실용적이다. 이유가 셋이다. 첫째, Q4 꼬리 질문에서 본 대로 대부분의 내용이 버전과 무관하다 — 메모리 레이아웃·디스패치·유니코드·Foundation 동작이 그렇다. 전면 개정은 바뀌지 않은 90%를 다시 검증하는 비용을 치른다. 둘째, 이 강의의 자산은 "직접 측정한 수치"인데, 전면 개정하면 모든 수치를 새 툴체인에서 재측정해야 하고 그 과정에서 측정하지 않은 것을 측정한 것처럼 쓸 위험이 생긴다(이 챕터가 배지를 붙인 이유). 셋째, 독자에게 "무엇이 바뀌었는가"가 별도 챕터로 있는 것이 오히려 유용하다 — 6.2 시절 지식을 가진 사람이 가장 알고 싶은 것이 그것이다. 다만 델타가 쌓이면 본문의 설명과 어긋나는 곳이 늘어나므로, Q4의 갱신 표가 커지면 그때 해당 문항 본문을 손보는 절충이 현실적이다.
쉽게 말하면 교과서를 통째로 다시 쓰는 대신 맨 뒤에 "바뀐 것 목록"을 붙이는 방식이다. 목록이 너무 길어지면 그때 본문을 고친다.

출처 · 참고자료