지금 자료구조 스터디를 하면서 stack
을 구현해보고 있는데 사실 stack자체를 구현하는건 그렇게 어렵지 않았는데 문득 push랑 pop같은 stack의 필수연산자를 구현하다보니 mutating키워드
에 대한 근본적인 물음이 진짜 잠깐 스쳐갔고 이를 조금 해결해보기위해 내용을 정리해보려합니다.
기본적으로 mutating은 class에서는 사용하지 않고 struct
에서 사용하고, 배울때는 보통 내부의 속성값을 변화시키는 메서드일때 해당키워드를 붙여줘야한다
라고 배웁니다. 사실 처음엔 그런가보다하고 넘어갔었는데 지금와서 이 근본적인 이유가 궁금해진걸보니 그만큼 성장한건가 싶긴하네요... 어쨋든 이유를 한번 알아봅시다
본격적으로 오늘의 주제에 대해 알아보기전에 처음부터 이해를 하려면 muatating키워드를 붙이지 않았을때 어떤 오류가 발생하는지를 봐야합니다
영어는 잘못하지만 해석을 쫌쫌따리 해보면
self
가 immutable
하다고 한다, 근데 immutable한 value에서
mutating한 memeber
를 사용하지 못한다고 하네요
이게 무슨뜻일까를 생각해보면 보통 mutable과 immutable을 나누는 가장 기본적인 방법은 let과 var입니다. 당연히 let은 immutable
이고 var은 mutable
이죠. 근데 사실 구조체를 선언할때(stack구조체) 내부 변수도 var로 선언해놨고 해당구조체를 var이라는 변수안에다가 넣을수도 있는데(실제로도 그렇게 했습니다) 대체 왜 self(구조체 인스턴스)가 immutable일까를 생각해보면 구조체나 클래스의 인스턴스나 객체를 만들면 해당 인스턴스나 객체인 self는 let으로 결정됩니다
??? : 아니 그럼 클래스 내부에도 mutating키워드를 써야하는거 아닌가요...?
라고 물어본다면 클래스와 구조체의 가장 큰차이점은 value type
과 reference type
이라는 차이가 있죠
클래스의 객체를 저장할때는 주소값을 저장하기때문에 주소값 자체가 let으로 결정되는거지 해당 주소에있는 변수들의 값을 바꾼다고 해서 주소값이 바뀌지 않기 때문에 우리는 클래스를 사용할때 내부 변수의 값을바꾼다고해서 mutating키워드를 사용하지 않는것입니다. 반대로 구조체인스턴스는 값자체를 복사해서 stack영역에 저장하고 이때 let으로 저장하기때문에(immutable로저장하기때문에)내부 변수들을 var로 선언하더라도 속성을 바꾸면 인스턴스자체가 immutable하기 때문에 변경이 불가능하게 되는거죠
근데 왜 self는 immutable이 default일까를 개인적으로 생각을 해보면 사실 mutable하다는건 어떤일이 발생할지 모른다는 의미고 이건 불확실해진다는의미와도 일맥상통하기도 해서 mutable하게 쓸수있는 방법을 마련했기때문에(mutating키워드) immutable을 default로 설정해놓은게 아닐까?
어쨋든 요약을 해보면 우리가 인스턴스나 객체를 생성하게되면 기본적으로 immutable한데(=let으로 선언된다) class는 reference type이라 프로퍼티변경을 할 수있지만 struct는 value type이라 프로퍼티를 변경할 수 없기 때문에 해당 명령을 수행하기 위한 방법이 필요하고 그 방법이 mutating키워드
인거죠
이에 대한 내용을 정리하게 된 가장 큰 이유는 mutating관련해서 공부를하다보면 진짜 5개의 블로그에서 한글자도 안빠지고 똑같이 결론을 내리시더라고요
mutating 키워드는 해당 메서드가 호출된다면 실제 복사를 해야한다고 알려주는 역할입니다.
이게무슨소리야...진짜 저 제목 그대로 구글에 복붙해보면 거의 모든 블로그에서 저 문장을 그대로 말하는걸 볼 수 있답니다...
사실 저는 이 문장이 이해가 안되기도 해서 조금더 공부를 해봤습니다. 이를 이해하기 위해서는
swift의 COW(Copy on Write)
에 대해 알아야합니다.
우선 해당 내용은 메모리주소에 관한 내용이니 메모리 주소를 알려주는 함수를 하나 가져와보겠습니다
func address(of object: UnsafeRawPointer) -> String{
let address = Int(bitPattern: object)
return String(format: "%p", address)
}
COW에 대해서 간단히 설명하면 실제 원본이나 복사본이 수정되기 전까지는 복사를 하지 않고 원본을 공유하고 원본이나 복사본에서 수정이 일어날 경우, 그때 복사하는 작업을 하는걸 의미합니다. 이건 사실 컴퓨터에서의 COW이고 Swift에서 COW는 collection type
에서 사용하는 언어라고 생각하시면되고 collection type은 dictionary나 array등이 있습니다. 오늘은 array에 대해서 알아볼 예정입니다.
기본적으로 swift에서 array는 구조체죠?
그렇다는 말은 array를 새로 생성할때마다 값들이 stack영역에 계속 쌓인다는 말이 됩니다. 근데 이게 좀 그런게 있어요 우리가 코딩을 하다보면 같은 array를 여러군데서 복사해서 사용할때가 있는데 그럴때마다 똑같은 array를 복사해서 사용하면 조금 비효율적이잖아요? 그래서 array자체를 다른 stored property에다가 저장하면 값을 복사를 안하고 같은 주소를 참조하게 합니다 그리고 변경이 일어나면 그때서야 새로운 주소에 변경된결과의 array를 저장합니다
즉 write할때(=변경이일어날때) copy한다 라는 의미에서 Copy On Write가 되는겁니다
그럼한번 실제로 그런가 볼까요?
var testArray1 = [1,2,3]
var testArray2 = testArray1
위의 코드를 보면 우리가 원래알던대로면 array는 value type인 struct고 그 값들을 두개의 변수에 저장했으니 두개의 array가 똑같이 복사되어서 저장될거라고 예상할수있습니다(주소가 다르겠죠 아무래도?)
그런데 저 둘의 메모리주소를 찍어보면
똑같네요,,,,?? 분명 value type인데 reference type처럼 동작하는 모습을 볼수가 있습니다. 그러면 우리 testArray2에 4라는 값을 추가해보고 다시 메모리주소를 찍어봅시다
다시 빌드를 해보니 두 array의 메모리 주소가 다릅니다. 이제서야 우리가 알고있는 value type처럼 작동을 하게됩니다.
그러면 여기서 약간 궁금한점이 있습니다
mutating을 통해 변화시킨 구조체는 write를 한거나 다름없으니까 copy가되는걸까...? 어떻게 될까?
struct Stack<T> {
var stack: [T] = []
mutating func push(_ element: T) {
self.stack.append(element)
}
mutating func pop() -> T? {
self.stack.popLast()
}
}
위의 코드를 사용할거고 muatating키워드
가 붙어있는 push
를 사용해보겠습니다
사실처음엔 mutating으로 내부 변수를 바꿔주면 애초에 구조체자체가 바뀌는거라고 생각해서 새로운 주소에 할당될거라고 예상을했었는데 실제로 왜 그렇지 않은지를 알아보니 mutating키워드의 두번째 역할은 메서드가 종료될 때 변경한 모든 내용을 원래 struct에 다시 기록
이라고 하네요
자, 그럼 아직까지 mutating 키워드는 해당 메서드가 호출된다면 실제 복사를 해야한다고 알려주는 역할입니다.
에 대한 궁금증은 해결되지 않았는데요. 제가 고민을 열심히 해본 결론을 설명해드리겠습니다.
우선 mutating키워드를 사용해 메서드를 사용한다는건 어떤 속성값
을 바꿔준다는 의미가 되겠죠? 그렇다는말은 어떤 속성값
이 write가 된다는 뜻이되고 이는 어떤 속성값
이 실제복사 즉 write에 의한 copy를 하게된다는 뜻이 됩니다.
COW가 일어난다는 말은 적어도 해당 프로퍼티의 메모리 주소가 바뀐다는 뜻이 되기도합니다. 그렇다면 우리 Stack이라는 구조체안에 stack이라는 array의 메모리 주소를 찍어봅시다
실제로도 mutating키워드가 붙은 push라는 메서드가 호출되면 stack이라는 변수(array타입의)가 write된다는걸 알려줘서 복사(이걸 실제복사라고 표현한듯합니다)하게됩니다.
1.구조체나 클래스나 모두 인스턴스와 객체는 생성될때 let이 default지만 reference type과 value type의 차이로 인해 구조체의 인스턴스의 경우 내부 변수를 변경할수가 없어서 이를 가능하게 해주는 키워드가 mutating이다.
2.mutating을 통해 구조체가 변화한다고 해도 메모리주소를 새로 할당받는것이아니라 호출이 끝나면 원래 메모리주소에 새로운 구조체를 덮어씌워주는 역할도 한다.
3.mutating키워드의 메서드는 메서드를 통해 변화시키는 값이 write된다는 것을 알려주는 역할을 하고 이를 통해 COW로 copy할수있게된다.
정리가 잘 된 글이네요. 도움이 됐습니다.