왜 CoW가 필요한가
- 값 타입은 대입/전달에서 독립 상태를 보장해야 한다.
- 큰 컬렉션을 매번 즉시 복사하면 비용이 과도하다.
- CoW는 읽기 중심 경로를 싸게 만들고, 실제 변경이 있을 때만 비용을 지불한다.
copy-on-write(CoW)는 "읽기 단계에서는 공유"하고, "쓰기 시점에만 복사"하는 전략이다. Swift 표준 값 타입 컬렉션이 값 의미를 유지하면서 성능을 확보하는 핵심 메커니즘이다.
기록 없음
var a = [1, 2, 3]
var b = a
// 이 시점에는 내부 버퍼를 공유할 수 있다.
b.append(4)
// b를 수정하는 순간 분리 복사 발생
print(a) // [1, 2, 3]
print(b) // [1, 2, 3, 4]| 질문 | 정리 |
|---|---|
| `as`는 수정인가? | 아니다. `as`/`as?`/`as!`는 타입 변환(브리징)이며 mutation은 `append`/`remove` 같은 메서드에서 발생한다. |
| 브리징 비용은 항상 큰가? | 아니다. 항상 \"아무 일도 없음\"도 아니고 항상 \"큰 복사\"도 아니다. 작은 검사/래핑 비용은 있고, 큰 복사/변환은 케이스 의존적이다. |
| 배열이 크면 왜 위험한가? | 필요 시 복사/재할당해야 하는 데이터 양이 커지기 때문이다. 특히 큰 배열 + 반복 mutation + 반복 브리징 조합이 누적 비용을 키운다. |
| `isKnownUniquelyReferenced`는 동시성 보장 도구인가? | 아니다. 목적은 \"복사 필요 여부 판단\"이다. 동시성 안전은 별도(락, actor, 직렬화)로 보장해야 한다. |
| 왜 `if !isKnownUniquelyReferenced { copy }`를 쓰나? | 공유 저장소를 그대로 수정하면 값 의미가 깨지기 때문이다. 수정 직전에 분리(copy-before-write)해서 \"내 값만 변경\"되게 만든다. |
import Foundation
func hotLoopBridgeAndMutate() {
var swift: [NSNumber] = []
for i in 0..<10_000 {
swift.append(NSNumber(value: i)) // mutation
if i % 5 == 0, !swift.isEmpty {
swift.removeLast() // mutation
}
let ns: NSArray = swift as NSArray // 브리징 (mutation 아님)
_ = ns.count
}
}커스텀 값 타입에서 참조 저장소를 쓸 때는 "수정 전 유일 참조 확인"이 핵심이다.
final class Storage {
var items: [Int]
init(items: [Int]) { self.items = items }
func copy() -> Storage { Storage(items: items) }
}
struct IntBuffer {
private var storage: Storage
init(_ items: [Int]) { storage = Storage(items: items) }
mutating func append(_ value: Int) {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy()
}
storage.items.append(value)
}
var values: [Int] { storage.items }
}| 상황 | 체크 포인트 | 권장 액션 |
|---|---|---|
| 대용량 컬렉션 전달이 많다 | 읽기 중심인지 수정 중심인지 | 읽기 중심이면 CoW 이점을 활용하고, 수정 중심이면 구조 재검토 |
| 성능 이슈 제보가 있다 | 실제 복사 발생 지점 | Instruments로 allocation/retain/release를 측정 |
| 커스텀 값 타입 설계 중 | 값 의미 보장 여부 | 쓰기 전에 저장소 분리 로직을 명시적으로 구현 |
CoW는 값 타입의 정합성을 지키면서 복사 비용을 지연시키는 타협점이다.
그래서 질문의 핵심은 "복사가 일어나는가"가 아니라, "언제 실제 복사가 터지는가"를 예측하고 측정할 수 있는가다.