Swift

[Swift] mutating 키워드와 COW(Copy On Write)

하이D:) 2023. 12. 24. 18:37

swift에서 value type(enum, struct)의 프로퍼티는 기본적으로 메서드 내에서 수정할 수 없는데 수정해야 하는 경우 앞에 mutating키워드를 붙여줘야 하는 것을 모두 알고 있을 것이다.

mutating을 선언한 메서드는 메서드 내에서 프로퍼티를 변경할 수 있고 메서드가 종료될 때 변경한 모든 내용을 원래 struct 인스턴스 메모리에 다시 기록하도록 하는 것이다.

struct Person {
    var name: String
 
    mutating func changeName() {
        name = "changedName"
    }
}

var heidi = Person(name: "heidi")
heidi.changeName()

 

 

 

그렇다면 mutating라는 키워드는 왜 필요한 것일까?

swift는 값이 변경될 때 복사를 하는 COW(Copy on Write)로 최적화를 진행한다.

구조체 인스턴스(값타입)를 하나 만들고 다른 변수에 복사를 하더라도, 바로 구조체 내의 모든 값이 복사되는 것이 아닌, 일단 둘 다 같은 인스턴스를 참조하다가, 값이 변경될 때만 복사를 진행해서 메모리나, 컴퓨테이션 파워를 최대한 아끼는 방식이다.

mutating 키워드 없이 메서드가 property를 변경하게 되면 언제 실제로 복사를 해야 하는지 알 수 없기 때문에 mutating 키워드를 통해 해당 메서드가 호출된다면 실제 복사를 해야한다고 알려준다.

 

struct Person {
    var name: String
 
    mutating func changeName() {
        name = "changedName"
    }
}

var heidi = Person(name: "heidi")
var firstcCopy = heidi
var secondCopy = heidi
//이렇게 복사된 구조체 인스턴스들이 있을 때
//인스턴스 내 프로퍼티 값이 변경되지 않았을 때에는 같은 곳을 참조하면서 바라보고있음

heidi.name = "heidi2" //이렇게나
heidi.changeName() //이렇게
//프로퍼티 값이 변경되었을 때에 비로소 복사함
//즉, 실제 복사할 시점을 알려주기위해 메서드에 mutating 키워드를 사용해야한다.

print(heidi.name)
print(firstcCopy.name)
print(secondCopy.name)

 

 

* COW(Copy On Write) 특성에 대해 좀 더 자세히 알아보자면

  • “Write가 발생했을 때 Copy를 수행한다”
  • 값 형식을 전달 시에 항상 모든 값이 복사되는 값 형식의 문제점을 보완하기위해 채택한 모델
  • 실제 수정이 이뤄질 때 복사를 하고 그 전엔 참조를 통해 swift에서 불필요한 메모리할당(불필요한 복사)을 줄여서 효율적으로 관리하기 위한 개념
  • 복사 동작을 할 때 실제 원본이나 복사본이 수정되기 전까지는 복사를 하지 않고 원본 리소스를 공유(같은 주소값 참조)하고, 원본이나 복사본에서 수정이 일어날 경우, 그때 복사하는 작업을 하는 것 (하나의 배열을 복사했을 때 그 배열이 수정되기 전까지는 같은 주소값을 가지고 있다가 값이 수정되면 새로운 메모리 할당)
  • Swift의 원시 타입과 Collection들(Int, Double, String, Array, Set, Dictionary)에는 이러한 Copy on write가 이미 구현되어 있다
var numbers = [1, 2, 3, 4, 5]
var firstCopy = numbers
var secondCopy = numbers
//Array의 여러 복사본은 복사본 중 하나를 수정할 때까지 동일한 저장소를 공유
//=> numbers, firstCopy, secondCopy가 처음에는 같은 저장소를 공유하고


// The storage for 'numbers' is copied here
// numbers의 값이 바뀔 때, 저장소가 복사되고 numbers는 복사된 새로운 저장소를 가지고 있게 된다.
numbers[0] = 100
numbers[1] = 200
numbers[2] = 300
// 'numbers' is [100, 200, 300, 4, 5]
// 'firstCopy' and 'secondCopy' are [1, 2, 3, 4, 5]

 

만약 numbers의 원소 개수가 대략 천만개라면 값을 전달할 때마다 매번 천만 개의 값을 복사해줘야 해서 시간도 오래 걸릴뿐더러 메모리를 많이 사용하게 된다.