[Swift] COW (Copy-On-Write) 개념 완전 정복하기

2025. 6. 3. 22:52Swift, iOS Foundation

요즘은 메모리 관리와 관련된 내용을 공부 중입니다.

Swift에서 메모리와 관련된 가장 핵심 개념은 ARC (Automatic Reference Counting)라고 볼 수 있지만, 
오늘 글에서는 CS 기초 개념쪽에 더 가까운 내용을 다루게 갈 것이구요. 앞으로도 메모리 관리나 디버깅과 관련된 다소 생소(?)할 수 있는 글들이 많이 올라올 것 같습니다!
오늘은 그 메모리 개념의 가장 기초적인 가벼운 개념 COW (Copy-On-Write)부터 만나러 가보시죠!

예전 Swift 메모리와 관련된 글은 아래 링크를 들어가시면 확인할 수 있습니다.

 

얕은 복사 (Shallow Copy)는 뭐고, 깊은 복사 (Deep Copy)는 뭔데?

이 둘은 메모리에서 Swift의 데이터를 어떻게 복사하는가, 그리고 복사된 데이터가 원본 데이터와 얼마나 독립적인가에 대한 이야기입니다.

  • 얕은 복사 (Shallow Copy) : 객체의 참조만 복사, 내부 데이터는 사실 공유되고 있음 -> 클래스가 해당되는 참조 타입 (Reference Type)의 복사 전략
  • 깊은 복사 (Deep Copy) : 객체를 아예 새로 만들어서, 내부 데이터의 내용물까지 모두 복사하는 것 -> 구조체와 열거형이 해당되는 값 타입 (Value Type)의 복사 전략

왼쪽이 얕은 복사 (Shallow Copy), 오른쪽이 깊은 복사 (Deep Copy)가 해당되는 이미지

 

하지만, 사실 참조 타입에서도 깊은 복사 (Deep Copy)를 적용할 수 있습니다.

직접 깊은 복사를 수행하는 커스텀 메서드 (deepCopy() 혹은 clone())를 구현하거나
Objective-C 스타일의 NSCopying 프로토콜을 클래스에 채택하여, copy() 메서드를 구현하는 방식으로 만들 수 있죠.

아래 예시 코드를 참고하며 이해해 봅시다!

// 깊은 복사를 수행하는 커스텀 메서드로 깊은 복사 구현하기
class Person: NSCopying {
    var name: String

    init(name: String) {
        self.name = name
    }
    
    func deepCopy() -> User {
        return Person(name: self.name)
    }
}

let original = Person(name: "Mini")
let cloned = original.deepCopy()

cloned.name = "Mini2"

print(original.name) // ✅ "Mini"
print(cloned.name)   // ✅ "Mini2"
// Objective-C 스타일의 NSCopying 프로토콜로 깊은 복사 구현하기
class Person: NSCopying {
    var name: String

    init(name: String) {
        self.name = name
    }

    func copy(with zone: NSZone? = nil) -> Any {
        return Person(name: self.name)
    }
}

let original = Person(name: "Mini")
let cloned = original.copy() as! Person
cloned.name = "Mini2"

print(original.name) // ✅ "Mini"
print(cloned.name) // ✅ "Mini2"

 

 

COW (Copy-On-Write)란 무엇일까?

자 이제 원래 하고 싶었던 얘기로 들어갈 차례입니다. COW가 무엇인지 설명 들어가봅니다.

COW (Copy-On-Write)는 직역해보면 "작성 시 복사"입니다.
조금 더 자세하게 들어가면 값이 여러 곳에서 공유될 때, 수정이 발생할 때만 (= 작성 시) 복사를 수행하는 전략이라고 해석할 수 있죠.

  • 처음에는 얕은 복사 (Shallow Copy)가 수행 -> 이후 수정 (Write)하려 할 때, 원본이 다른 곳에서 공유 중이라면 그때 깊은 복사 (Deep Copy)가 발생한다는 원리
  • Swift 표준 라이브러리의 Array, Dictionary, Set과 같은 컬렉션 타입 (Collection Type)은 기본적으로 COW (Copy-On-Write)를 적용하고 있습니다.
  • 뿐만 아니라, Swift 4 이후의 String과 Data 타입도 모두 내부적으로 COW가 적용되어 복사가 진행됩니다.


아래 예시 코드를 보면서 COW (Copy-On-Write)가 무엇인지 더 직접적으로 느껴보죠.

a와 b는 Collection Type인 Array이므로, 기본적으로 COW (Copy-On-Write)가 적용되어 있습니다.
즉 b = a 코드에서는 얕은 복사 (Shallow Copy)가 이루어지는 것에서 -> 실제 b에 값이 변경되는 순간 (b.append())에 깊은 복사 (Deep Copy)로 바뀌는 상황을 확인할 수 있죠.

var a = [1, 2, 3]
var b = a  // 참조만 복사됨 (깊은 복사 같지만, 사실 이 시점에서는 "얕은 복사")

print(a) // [1, 2, 3]
print(b) // [1, 2, 3]

b.append(4) // 이 시점 (Write가 발생한 시점)에 비로소 "깊은 복사"가 이루어진 것!

print(a) // [1, 2, 3]
print(b) // [1, 2, 3, 4]

 

 

Why COW? | 왜 Copy-On-Write가 필요한거지?

너무나 당연하게도 성능이 좋아지기 때문입니다.

"오잉? 어떤 성능이 좋아진다는 것이지...?"조금 더 자세하게 들어가자면!

최대한 깊은 복사가 필요한 경우 (= Write가 이루어져, 진짜 분리가 필요해졌을 때)에만 복사가 이루어지니, 메모리 낭비를 최소화할 수 있을 겁니다.
또힌, 수정 시에 비로소 깊은 복사가 이루어지니, 원본 데이터를 최대한 보호한 상황에서 복사가 이루어질 수 있죠.
이런 Swift의 메모리 안정성 상황을 문장으로 "값 타입은 불변처럼 다뤄지면서도, 복사 성능은 참조 타입처럼 최적화된다."라고 정의할 수 있습니다.

즉 정리하자면, 얕은 복사의 빠른 속도 + 깊은 복사의 안정성을 모두 결합하는 최적의 방법이 COW (Copy-On-Write)인 것입니다!