실험하면서 과정을 기록한 글이라 정리되지 않은 글일 수 있습니다.
감안하면서 봐주세요 ㅎㅎ..
틀린점이 있다면 댓글로 알려주시면 감사하겠습니다 🙇
🧐 해당 실험을 하게 된 계기
Swift를 사용하면서, struct와 class 중 어떤 것을 사용해야할지에 대한 고민을 항상 하게되는 것 같습니다.
해당 고민은 개발에 아무리 능숙한 사람이라도 매번 하게되고 매번 고치게 된다고 하는데요.
또한, 프로젝트를 진행하면서도 struct에 mutating이 있을 때, 이를 "class의 참조기능을 활용할 필요가 없고 애플에서 struct를 권장하기 때문에 struct를 사용했다"라고 말했던 팀원이 있었어서 한번 써도될지? 쓰면안될지? 에 대해 분석해보려고합니다.
😳 mutating이 뭔데?
기존에 제가 알고 있던 지식은 struct 내부에서 내부 프로퍼티를 변경할 때 사용하는 키워드로 알고 있었는데요.
추가적으로 알게된 점은,
struct가 값타입이기 때문에 mutating 키워드를 사용하는 것입니다.
값 타입의 인스턴스 프로퍼티를 변경할 시에, 원본의 값을 변경하는 것이 아니라
인스턴스를 복사하여 새롭게 생성함으로써 기존의 인스턴스를 대체하게 해주는 방식입니다.
😀 실험 과정
1️⃣ struct 선언
struct Card {
var name: String
var number: Int
var lock: Bool
mutating func plus() {
number += 1
}
}
2️⃣ mutating을 계속해서 진행해줄 버튼 생성
3️⃣ 로직 연결
@IBAction func mutatingButtonDidTapped(_ sender: Any) {
card.plus()
print("print눌린 횟수: \(card.number)")
}
4️⃣ 중간결과..?
아무래도 struct의 크기가 너무 작아서 메모리에 인스턴스가 복사되도 영향이 없는것 같습니다.
5️⃣ struct 내부에 그나마 큰..? 데이터를 넣은 결과
한글 ㄱ이 1,000,000개면 크기가 3byte * 1,000,000 = 3,000,000 byte = 약 3MB 인데요..
인스턴스가 복사되서 메모리에 남는다면 지금 300MB가 메모리에 추가되었어야하는데 추가가 안됐네요..?
바로 Copy-On-Write 때문이었습니다.
struct Card {
var name: String
var number: Int
var lock: Bool
var arr: [String] = []
mutating func plus() {
number += 1
}
}
//...
override func viewDidLoad() {
super.viewDidLoad()
for _ in 1...1000000 {
card.arr.append("ㄱ")
}
}
6️⃣ 그렇다면, mutating이 아닌 외부에서 내부 값을 변경하는 로직이 있다면?
이런 구조는 잘 쓰이지 않을걸 알지만
ViewController가 Card의 number를 증가시키도록 해보겠습니다.
func increase() {
card.number += 1
}
@IBAction func valueChagnedButtonDidTapped(_ sender: Any) {
increase()
print("값 변경 버튼 눌린 횟수: \(card.number)")
}
이것마저 효과가 없는걸 보니,
외부에서 값을 변경해도 mutating과 같이 COW가 일어나서 참조를 하는 것 같은데요.
7️⃣ struct의 내부 값이 계속 변경되도 같은 reference를 참고할까?
계속 변경되도 같은 reference를 참고한다는 결과를 얻을 수 있었는데요.
COW로 인해 struct내부 값이 외부로 부터 변경되던가 혹은 mutating을 통해서 변경되던가 메모리를 잡아먹지 않고 계속해서 처음 주소 위치에 있는 struct의 reference를 참고한다는 것을 알 수 있었습니다.
여기부터 어쩌다보니 Copy-On-Write 실험이 되버린..😂
8️⃣ 그럼 메모리에 영향을 주는 인스턴스 복사가 일어나는 시점은 언제일까요?
흠.. 아무래도 새로운 프로퍼티에 기존 인스턴스를 할당하는 작업을 진행하면 여기까지는 COW가 동작할 것 같습니다.
그럼, 이 시점에서 복사가 완료된 객체의 주소값은 COW로 인해 복사된 객체랑 복사한 객체랑 주소값이 같을 것 같습니다.
해당 실험을 하기 위해 두 개의 버튼을 더 추가했습니다.
@IBAction func newPropertyToCopyButtonDidTapped(_ sender: Any) {
var newCard = card
print("복사된 Card객체 메모리 주소 : \(address(of: &newCard))")
}
제가 알고있는 이론상 새로운 프로퍼티에 복사를 하게 된다면,
복사한 값이 저장된 프로퍼티의 주소값은 복사된 프로퍼티의 주소값과 우선은 같아야 합니다.
우선은 그렇지 않다는 결론이 나왔는데요.
흠, 일단 Card객체의 크기가 MemoryLayout의 크기보다 크기 때문에 Malloc에 저장되는데요.
그리고 복사된 Card객체의 경우에는 현재 Card객체의 메모리 주소값만 저장하고 있으면 되기 때문에 Stack영역에 저장된것 같네요.
9️⃣ 그렇다면, 여러 프로퍼티에 인스턴스를 복사하기만 하면 메모리 사용량은 증가할까?
새로운 프로퍼티에 인스턴스를 복사하더라도 값이 바뀌지 않으면 COW가 일어난다고 알고 있었는데요.
한번 확인해볼까요?
@IBAction func propertyFiveCopyButtonDidTapped(_ sender: Any) {
var newCard1 = card
print("복사된 Card객체 메모리 주소 : \(address(of: &newCard1))")
var newCard2 = card
print("복사된 Card객체 메모리 주소 : \(address(of: &newCard2))")
var newCard3 = card
print("복사된 Card객체 메모리 주소 : \(address(of: &newCard3))")
var newCard4 = card
print("복사된 Card객체 메모리 주소 : \(address(of: &newCard4))")
var newCard5 = card
print("복사된 Card객체 메모리 주소 : \(address(of: &newCard5))")
}
버튼을 클릭하니 메모리 사용량이 오르는것을 관찰할 수 있었습니다.
(COW가 일어나지 않아서 그런걸로 착각했다는..)
이렇게 용량이 증가하는것은 Stack영역에서 기존의 Card객체를 가리키는 주소값을 갖기 위한 공간을 할당하기 위함으로 보입니다.
9️⃣ 흠... 그럼 COW로 인해 인스턴스 복사가 이루어질 때, 뭐가 복사되는거야..?
아무리 struct 자체의 주소를 확인해보고 내부 값을 확인해봐도 8번의 결과처럼만 나타나고 메모리주소는 똑같이 가리키는걸 보니, struct내부에 있는 프로퍼티의 값을 확인해봐야겠다고 생각했습니다.
메모리의 변동을 확인하기 위해, 메모리를 크게 잡아먹는것을 확인할 수 있는 arr의 값을 변경해보았습니다.
struct Card {
var name: String
var number: Int
var lock: Bool
var arr: [String] = []
mutating func plus() {
number += 1
}
mutating func append(string: String) {
arr.append(string)
}
}
이후, 3개의 프로퍼티에 card 인스턴스를 복사하고 내부의 배열값을 크게 변경하는 로직이 들어있는 버튼을 구현해봤습니다.
@IBAction func fiveInstanceValueChangeButtonDidTapped(_ sender: Any) {
print("----------------------------------------------------")
newCard1 = card
print("newCard1 메모리 주소 : \(address(of: &newCard1))")
print("newCard1.arr 메모리 주소 : \(address(of: &newCard1!.arr))")
newCard2 = card
print("newCard2 메모리 주소 : \(address(of: &newCard2))")
print("newCard2.arr 메모리 주소 : \(address(of: &newCard2!.arr))")
newCard3 = card
print("newCard3 메모리 주소 : \(address(of: &newCard3))")
print("newCard3.arr 메모리 주소 : \(address(of: &newCard3!.arr))")
print("----------------------------------------------------")
newCard1?.append(string: "ㄱ")
print("값 변경된 newCard1 메모리 주소 : \(address(of: &newCard1))")
print("값 변경된 newCard1.arr 메모리 주소 : \(address(of: &newCard1!.arr))")
newCard2?.append(string: "ㄱ")
print("값 변경된 newCard2 메모리 주소 : \(address(of: &newCard2))")
print("값 변경된 newCard2.arr 메모리 주소 : \(address(of: &newCard2!.arr))")
newCard3?.append(string: "ㄱ")
print("값 변경된 newCard3 메모리 주소 : \(address(of: &newCard3))")
print("값 변경된 newCard3.arr 메모리 주소 : \(address(of: &newCard3!.arr))")
print("----------------------------------------------------")
}
결과는 놀라웠는데요.
복사된 card 인스턴스를 갖고있는 newCard들은 아무리 값의 복사되더라도 같은 Stack주소를 계속해서 가리키고 있습니다.
그리고, 내부 프로퍼티 중 값이 바뀐 arr만 가리키는 주소가 바뀌었네요.
버튼을 누른 이후, 값의 변동폭이 커서 인스턴스가 복사되면서 메모리 사용량또한 상당히 증가한 것을 볼 수 있습니다.
여기까지 얻은 결론
Copy-On-Write는 이미 알고있었던 지식이니, 새롭게 알게된 것을 적어보겠습니다.
struct 내부에 있는 프로퍼티들 또한 각각의 메모리공간을 잡아먹고 있고, 인스턴스를 복사하고 값을 변경하면 struct의 주소가 바뀌는게 아니라 struct가 가리키고 있는 하나하나의 프로퍼티 주소가 바뀌게 되는것이다.
이런 결론을 얻었으니 이제 mutating에서도 하나하나의 프로퍼티주소를 확인해보겠습니다.
1️⃣0️⃣ mutating 시, struct 내부 프로퍼티의 주소값 확인
둘 다 Heap 영역에 저장된 걸 볼 수 있는데요.
저는 여태 mutating 시에도, Copy-On-Write가 동작하는 건줄 알았는데요.
결론은 mutating은 Copy-On-Write처럼 동작하는 것이 아니다!
mutating시에 struct는 값 타입이라서 인스턴스를 계속 복사하지만, 해당 복사가 하나의 프로퍼티에 할당된 같은 메모리주소 공간에 이루어지기 때문에 메모리주소의 변경은 없었다.
또한, 내부 프로퍼티의 메모리주소 변경도 없었다.
그리고 인스턴스가 복사된다고해서 계속해서 메모리공간을 차지하고있냐? 그것도 아니었다.
복사가 아니라 안에있는 값만 변경한다고 할 정도로 메모리 용량의 변경은 아예 없었다.
사용되는 자원은 mutating을 실행하는 버튼을 계속 클릭했을 때 CPU 자원을 사용하는 모습이 포착되었다.
결론은 이정도로 종결시킬 수 있을 것 같습니다.
struct와 class를 선택할 때, mutating 때문에 struct의 선택을 반려하는 경우는 정말정말 CPU자원의 1% 마저 아쉬울 정도의 앱을 작성해야할 것 같습니다.
엄청나게 많은 양의 mutating을 사용한다면 CPU자원이 모자랄까? 에 대한 실험도 나중에 진행해보겠습니다.
📚 Reference
- mutating을 써도 struct를 let으로 선언하면 쓰지 못하는 이유
'IOS > 실험' 카테고리의 다른 글
hitTest (0) | 2024.01.12 |
---|---|
RunLoop.main vs DispatchQueue.main (2) | 2023.10.16 |
String의 joined(separator:) DeepDive (2) | 2023.10.14 |
의존성 주입방법에 대한 고민과 DIContainer 도입과정 (2) | 2023.10.09 |
댓글