iOS Development Guide

copy-on-write 개념

copy-on-write(CoW)는 "읽기 단계에서는 공유"하고, "쓰기 시점에만 복사"하는 전략이다. Swift 표준 값 타입 컬렉션이 값 의미를 유지하면서 성능을 확보하는 핵심 메커니즘이다.

Value semantics Deferred copy Knowledge item #17
학습 날짜

기록 없음

왜 CoW가 필요한가

  • 값 타입은 대입/전달에서 독립 상태를 보장해야 한다.
  • 큰 컬렉션을 매번 즉시 복사하면 비용이 과도하다.
  • CoW는 읽기 중심 경로를 싸게 만들고, 실제 변경이 있을 때만 비용을 지불한다.

동작 순서

  • 복사 직후에는 내부 저장소를 공유할 수 있다.
  • 수정 요청 시 저장소가 단독 소유인지 확인한다.
  • 공유 중이면 새 저장소를 만든 뒤 분리해서 수정한다.

가장 기본 예시

var a = [1, 2, 3]
var b = a

// 이 시점에는 내부 버퍼를 공유할 수 있다.
b.append(4)

// b를 수정하는 순간 분리 복사 발생
print(a) // [1, 2, 3]
print(b) // [1, 2, 3, 4]
외부에서 관찰되는 결과는 항상 값 타입 규칙을 따른다. CoW는 그 규칙을 깨지 않고 성능 최적화를 제공하는 내부 전략이다.

CoW 오해 정리

  • "대입하면 즉시 전체 복사"는 CoW에서는 틀릴 수 있다.
  • "내부 공유하니 참조 타입"도 틀리다. 외부 의미는 값 타입이다.
  • "읽기만 해도 복사"가 아니라, 보통 mutation에서 분리 복사가 일어난다.

실무에서 비용이 커지는 순간

  • 큰 컬렉션을 여러 복사본에서 각각 자주 수정할 때
  • 핫 루프 내부에서 append/remove를 반복할 때
  • 브리징(Foundation 변환)과 함께 메모리 재할당/복사가 겹칠 때

질문에서 나온 핵심 오해 정리

질문 정리
`as`는 수정인가? 아니다. `as`/`as?`/`as!`는 타입 변환(브리징)이며 mutation은 `append`/`remove` 같은 메서드에서 발생한다.
브리징 비용은 항상 큰가? 아니다. 항상 \"아무 일도 없음\"도 아니고 항상 \"큰 복사\"도 아니다. 작은 검사/래핑 비용은 있고, 큰 복사/변환은 케이스 의존적이다.
배열이 크면 왜 위험한가? 필요 시 복사/재할당해야 하는 데이터 양이 커지기 때문이다. 특히 큰 배열 + 반복 mutation + 반복 브리징 조합이 누적 비용을 키운다.
`isKnownUniquelyReferenced`는 동시성 보장 도구인가? 아니다. 목적은 \"복사 필요 여부 판단\"이다. 동시성 안전은 별도(락, actor, 직렬화)로 보장해야 한다.
왜 `if !isKnownUniquelyReferenced { copy }`를 쓰나? 공유 저장소를 그대로 수정하면 값 의미가 깨지기 때문이다. 수정 직전에 분리(copy-before-write)해서 \"내 값만 변경\"되게 만든다.

브리징 + mutation이 겹치는 패턴

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
    }
}
문제 포인트는 `as` 자체가 mutation이라서가 아니다. hot loop에서 mutation 비용과 브리징 비용이 같이 반복되어 누적되는 점이 핵심이다.

직접 CoW 타입을 만들 때 핵심

커스텀 값 타입에서 참조 저장소를 쓸 때는 "수정 전 유일 참조 확인"이 핵심이다.

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 }
}
`isKnownUniquelyReferenced`는 CoW에서 "지금 복사가 필요한가"를 판별하는 도구다. 동시성 안전성 자체를 보장하지는 않으므로, 필요하면 락/actor/직렬화로 보호해야 한다.

언제 신경 써야 하나

상황 체크 포인트 권장 액션
대용량 컬렉션 전달이 많다 읽기 중심인지 수정 중심인지 읽기 중심이면 CoW 이점을 활용하고, 수정 중심이면 구조 재검토
성능 이슈 제보가 있다 실제 복사 발생 지점 Instruments로 allocation/retain/release를 측정
커스텀 값 타입 설계 중 값 의미 보장 여부 쓰기 전에 저장소 분리 로직을 명시적으로 구현

실무 체크리스트

  • "값 의미 유지"가 성능보다 우선이라는 원칙을 지켰는가?
  • 핫패스에서 mutation이 과도한지 측정으로 확인했는가?
  • 필요 시 `reserveCapacity` 등 재할당 완화 전략을 적용했는가?
  • CoW를 추측으로 설명하지 않고 코드/측정 결과로 설명하는가?

한 줄 결론

CoW는 값 타입의 정합성을 지키면서 복사 비용을 지연시키는 타협점이다.

그래서 질문의 핵심은 "복사가 일어나는가"가 아니라, "언제 실제 복사가 터지는가"를 예측하고 측정할 수 있는가다.