[iOS_CS] COW(Copy On Write) 이해하기

2022. 3. 6. 17:00Programming/iOS_Swift

  오늘은 간단히 이해할 수 있으면서도 프로그래밍을 함에 있어서 알아두면 좋을 개념을 하나 정리하고 가려고 한다!

 

값 타입과 참조 타입

  COW를 설명하기에 앞서, 하나 짚고가야할게 있다. 값 타입(Value Type)과 참조 타입(Reference Type)에 대해서인데, 알다시피 구조체는 값 타입이고 클래스는 참조 타입이다. 따라서, 아래와 같은 예시 상황에서 클래스는 인스턴스 주소를 참조하는 상황이 발생하고 구조체는 인스턴스 값을 복사하는 현상이 발생한다.

class SoldierClass {
    var name = "Ethan"
}

struct SoldierStruct {
    var name = "Park"
}

var aSolider = SoldierClass()
var bSoldier = aSolider

bSoldier.name = "Tonic"

var cSoldier = SoldierStruct()
var dSoldier = cSoldier

dSoldier.name = "Dal"

print(aSolider.name, bSoldier.name) // Tonic, Tonic
print(cSoldier.name, dSoldier.name) // Park, Dal

  조금 더 부연설명을 하자면, bSolider가 aSoldier의 인스턴스 주소를 참조하는 것이다(!! 메모리 주소를 참조하는 것이 아니다) 따라서, bSolider의 프로퍼티를 바꿔줬더니 aSolider의 프로퍼티도 바뀌어 버린 것을 확인할 수 있다. 참조하고 있기 때문에 원본에도 영향을 미친 것이다. 

  한편, 구조체의 인스턴스인 cSolider의 경우 기본적으로 일단 값 타입이기 때문에 cSolider를 dSolider에게 할당하게 되면 값에 대한 복사가 일어난다. 정리하면 cSolider와 dSoldier는 일종의 독립체다. 그 결과로 cSoldier의 프로퍼티는 유지되어 있고 dSoldier의 프로퍼티만 바뀐 것을 확인할 수 있다.

  그런데, 어쨌든 인스턴스 주소를 참조했든 안했든 네 개의 변수들은 모두 각각의 고유한 메모리 주소를 가지게 된다. 이 점을 잘 기억해두고 내려가자.

func address(o1: UnsafeRawPointer) { // 구조체용 메모리 주소 찾는 메서드
    let address = String(format: "%p", Int(bitPattern: o1))
    print(address)
}

func address<T: AnyObject>(o2: UnsafePointer<T>) { // 클래스용 메모리주소 찾는 메서드
    let address = String(format: "%p", Int(bitPattern: o2))
    print(o2.pointee) // 변수가 가르키는 타입
    print(address) // 주소값
}

address(o2: &aSolider) // 0x100db4460
address(o2: &bSoldier) // 0x100db4468
address(o1: &cSoldier) // 0x100db4470
address(o1: &dSoldier) // 0x100db4480

  네 개의 변수 모두 서로 다른 메모리 주소를 가지고 있음을 확인할 수 있다.

 

COW(Copy On Write)란?

  COW를 이해하기 위한 사전 준비가 끝났다! 먼저 COW가 무엇인지 설명하면, COW는 직역하면 '쓰여질 때 복사한다'라는 건데 즉 복사라는 말에서 느낌이 오겠지만 값 타입에서 일어나는 기능이다. 즉 실질적으로 어떤 변수에 값 타입의 인스턴스를 할당하더라도 그 순간 바로 복사가 일어나지는 않고 실질적인 수정이 일어날 때, 사용되어질때 그제서야 비로소 복사를 하겠다는 것이다. 그 전까지는 할당 받은 값 타입 인스턴스의 메모리 주소를 공유하겠다는 것이다.

 

  그렇다면, 잠깐 위의 예시에서 봤던 구조체의 인스턴스인 cSolider와 dSoldier의 메모리 주소를 확인해보자. 분명 둘의 메모리 주소는 서로 다르다. COW의 기능대로 dSolider의 프로퍼티를 변화시켜줬기 때문에 그 때 복사가 일어난게 아닐까? 라고 생각할 수 있는데 사실 변화를 안줘도 메모리 주소는 다를 것이다. 즉 할당하는 순간 복사가 일어났다. 예시로 확인해보자.

var cSoldier = SoldierStruct()
var dSoldier = cSoldier

//dSoldier.name = "Dal"

address(o1: &cSoldier) // 0x1002fc470
address(o1: &dSoldier) // 0x1002fc480

  보이다시피, 프로퍼티를 안바꿔줘도 이미 복사는 일어났다. 그러면 COW가 안되는건가? 라고 생각할 수 있는데, 사실 COW는 콜렉션 타입에서만 일어나는 기능이다. 콜렉션 타입은 Array, Set, Dictionary로 모두 구조체로 이뤄진 값 타입이다.

 

COW 적용 테스트

var aArray = [0, 1, 2, 3]
var bArray = aArray

address(o1: &aArray) // 0x600001370ea0
address(o1: &bArray) // 0x600001370ea0

  콜렉션 타입 중 하나인 Array를 통해 테스트해봤다. Array의 메모리 주소를 확인해보면 현재 값이 단지 전달만 상태이기 때문에 같은 메모리 주소를 공유하고 있다. 다음으로 COW가 정말 실행이 되는지 bArray의 값을 수정해보자.

var aArray = [0, 1, 2, 3]
var bArray = aArray

bArray[0] = 77

address(o1: &aArray) // 0x6000001f57a0
address(o1: &bArray) // 0x6000001f47e0

  bArray의 0번째 인덱스를 수정해줬더니, 그제서야 값 복사가 일어났고 메모리 주소가 달라졌음을 확인할 수 있다.

 

  다음으로 Set와 Dictionary도 테스트해보자.

var aSet: Set = [0, 1, 2]
var bSet = aSet

var aDictionary = ["0": 0]

for i in 0..<10000 {
    aDictionary.updateValue(i, forKey: "\(i)")
}
var bDictionary = aDictionary

address(o1: &aSet) // 0x10244c5a0
address(o1: &bSet) // 0x10244c5a8
address(o1: &aDictionary) // 0x10244c5b0
address(o1: &bDictionary) // 0x10244c5b8

 

  문제가 생겼다. 분명 아무 값을 수정하지도 않았음에도 메모리 주소가 다르게 나온다! 여기서 사실 정말 고민을 많이했다. COW는 되지 않는 것인가? 된다면 Array만 되는 건가? 계속 고민하고 테스트 하기를 반복하다가, 도저히 해결이 안돼서 스택오버플로우에 질문을 올렸다.

 

 

I'm trying to test COW on Collection Types. But it didn't work

I'm trying to test COW on Collection Types such as Arrays, Set and dictionaries. But it didn't work on Set and Dictionaries. Here're my code and memory addresses of them. Please help. var aArray: ...

stackoverflow.com

 

 

  나의 질문에 너무 친절히 답변을 해준 Rob씨의 말을 요약해보면,

 

1. COW 타입(콜렉션 타입을 말하는 것 같다)은 결국 값 타입이기 때문에 값이 전달되는 순간 그 즉시 새로운 메모리 주소가 할당될 가능성이 있다.(may be copied ~~~~)

  즉 그들의 backing storage만 공유가 될 뿐이라고 한다.

 

2. COW는 stdlib 실행에 있어서 특징일 뿐(단지 코드일 뿐)이다.

  즉 Swift언어에서 핵심적인 부분이 아니라고 한다. 또 stdlib 실행 관련해서 일부분은 실행이 안되기도 한단다.(이게 COW를 말하는 것 같다) 이렇게 COW가 실행이 안되다 보니 사용자가 커스텀해서 직접 COW를 구현할 수도 있다고 한다.

 

4. Array의 메모리 주소가 같게 나온 이유는 최적화 때문일 수도 있다.

  즉 Array는 일부 케이스에서 최적화가 엄청 잘돼기 때문에 같게 나온거일 수도 있다고 한다. 

 

결론

  사실 답변도 전부 모호한 표현(may, 가능성이 있다...)을 써서 알려줬기 때문에 뭐가 정답인지는 정확히 알 수가 없다. 다만 분명히 말할 수 있는 것은 COW로 인해서 메모리 주소를 공유할 수 있지만 절대 무조건적으로 되는 것이 아니다. 이정도만 알고 넘어가면 될 것 같다. 

 

느낀 점

  한 가지 놀란 점은 분명 공식 문서에서 설명하고 있는 개념일지라도 실제로는 안될 수도 있구나였다. 세상에 완벽한 언어는 없으니까(?) 아무튼 간단하게 정리하고 끝낼 수 있는 개념일 줄 알았는데 예상치 못한 난관에 봉착해서 정리하는데 시간이 좀 걸린 것 같다. 스오플에 질문도 처음 올려봤는데 생각보다 답변이 너무 친절해서 놀랐다. 앞으로 자주 이용해야할 듯.