티스토리 뷰
야곰 아카데미에서 프로젝트를 진행하다가 "OO는 왜 구조체로 구현했나요?"라는 질문을 받았는데, 그 질문에 대한 답변이 바로 떠오르지 않았다. 왜냐하면... 특별한 이유를 생각해보지 않고 구현했기 때문이었다^^;;; 굳이굳이 이유를 찾아보자면, 아직 프로젝트 요구사항이 전부 밝혀지지 않아서 굳이 class로 작성할 이유를 찾지 못했기 때문이라고 할 수 있겠다. (애플 공식문서에서 클래스에서만 지원하는 기능이 필요한 경우가 아니면 기본적으로 구조체를 쓰라고 했기 때문에)
그런데 연결 리스트를 구현해보는 과정에서 Node
는 아무 생각 없이 클래스로 구현했었던 것이 생각나서, (나를 포함한 다른 사람들도) 왜 노드는 클래스로 구현했지?라는 의문을 갖게 되었다.
Doubly Linked List의 노드는 일반적으로 아래와 같이, 노드의 값 그리고 앞뒤로 연결된 노드에 대한 reference를 프로퍼티로 갖는다.
class Node {
var value: Int
var prev: Node?
var next: Node?
}
이 노드를 아래와 같이 구조체로 구현해보면, 오류가 발생한다.
struct Node {
var value: Int
var prev: Node? // 컴파일 오류 발생
var next: Node? // 컴파일 오류 발생
}
발생하는 컴파일 오류: Node
는 값 타입이기 때문에, 재귀적으로 자신을 포함하는 저장 프로퍼티를 가질 수 없다고 한다.
왜죠..? WHY....?
결론을 요약하자면 구조체는 값 타입이므로 컴파일 타임에 스택에 공간을 할당해줘야 하는데, 컴파일 타임에 이 Node
의 크기를 알 수 없기 때문이다. Node
의 크기를 알 수 없는 이유는, 이 Node
가 재귀적으로 계속 새로운 Node
에 대한 정보를 가지고 있는 구조이기 때문에 크기를 특정할 수 없어 컴파일 타임에 알 수가 없다는 것이다.
제대로 이해하기 위해서는 Swift의 메모리 할당 방식에 대해서 알아야 한다.
분명히 몇주 전 활동학습 주제로 공부했었는데, 명확하게 이해가 안 되는 점이 있어 다시 공부하기로 함^^!
Swift의 메모리 할당/해제 방식 (힙, 스택)
프로그램을 위해 할당된 메모리 공간은 총 4가지 영역으로 나뉘어져 있다 (코드 영역, 데이터 영역, 힙 영역, 스택 영역). 그 중에서 힙이랑 스택만 정리해보자.
힙 영역
런타임 전까지는 크기를 알 수 없는 데이터를 가지고 있기 위한 메모리 덩어리
힙으로 할당된 메모리는 ARC에 의해서 해제된다
- Swift는 어떤 메모리에 대한 새로운 참조가 생겼을 때, 그 메모리의 reference count를 +1 한다
- 참조가 해제되면 그 메모리의 reference count를 -1 한다.
- reference count가 0이 되면, 메모리가 해제된다.
- 전통적인 garbage collection은 힙 전체를 돌아다니면서 더 이상 참조되지 않는 메모리를 찾아 해제하는 방식이기 때문에, Swift의 ARC가 더 효율적이다.
스택 영역
함수를 호출할 때 지역 변수, 매개 변수, 반환값(return value) 등이 스택에 저장된다.
- 하나의 함수를 호출하는 데 필요한 메모리 덩어리를 묶어서 스택 프레임(Stack Frame)이라고 부른다.
- 아래 디버그 화면에서 함수 단위 하나하나가 스택 프레임이고, 이 전체는 Call Stack이라고 부른다.
- 아래 디버그 화면에서 함수 단위 하나하나가 스택 프레임이고, 이 전체는 Call Stack이라고 부른다.
- 어떤 함수가 호출되면, CPU가 스택 포인터(스택의 가장 위를 가리키고 있는 것)을 위로 옮겨서 스택에 메모리를 할당한다. (몇 바이트의 공간이 필요한지와 관계 없이)
- 함수가 종료되면 CPU가 스택 포인터를 아래로 옮겨서, 위의 것들(스택 프레임)을 메모리에서 해제시킨다.
→ LIFO(Last In First Out) 구조 - 스택에 할당된 변수들은 실제로는 스택에서 보관하는 경우보다 CPU 레지스터에서 보관하는 경우가 더 많다
→ 이 변수들을 조작하기 위해 메모리 액세스를 요구하지 않기 때문에 더 빠르게 조작할 수 있다.
→ 이런 경우, 스택에 할당된 메모리를 해제할 때 레지스테이 보관된 변수를 별도로 clean up 하지 않아도 된다 (언젠가 이 변수들이 overwritten 될 것이기 때문에)
(구조상) 메모리의 가장 상단에 위치한다 (메모리 주소값이 높은 값 → 낮은 값 으로 써나감).
컴파일 타임에 크기를 알 수 있는 데이터에 주로 사용된다.
- 많은 언어에서 Integer는 스택에 할당되고, String은 그렇지 않다
- 어떤 숫자를 표현하기 위해 필요한 메모리는 항상 알 수 있지만(정해져있음), String은 길이가 수정될 수 있기 때문에 메모리가 얼마나 필요한지 알 수 없기 때문이다.
- Swift에서 String을 어떻게 할당하는지는 밑에서 알아볼 예정
Swift에서 int
, float
, struct
같이 값 타입으로 구현되어 있는 것들은 스택에 할당되고, class
, closure
참조 타입은 힙에 할당된다.
- 정확히는 참조 타입의 경우 참조 타입의 주소값은 스택 영역에, 참조 타입의 실체는 힙에 할당된다.
- 값 타입도 무조건 스택 영역에 할당되는 것은 아니다(힙 영역에 할당되는 경우도 존재한다).
힙 vs 스택
데이터를 스택에 할당하는 것이 힙에 할당하는 것보다 (일반적으로) 훨씬 효율적이다.
- 힙에서 빈 공간을 찾는 데 시간이 걸리고
- 그 빈 공간에 데이터를 할당할 때 얼마만큼의 공간을 쓸 것인지와 이 메모리가 사용되고 있다는 표시를 남기기 위해 저장하려는 데이터 외에 추가로 자리를 차지하기 때문이다.
- 스택은 CPU가 스택 포인터를 위로 움직이라고 지시한 후 그 메모리에 할당하기만 하면 되니까 상대적으로 작업 속도가 빠르다.
Swift는 클래스, 클로저 뿐 아니라 프로그램이 실행될 수 있는 동안 크기가 커질 수 있는 데이터를 힙에 저장한다
- 예: 크기가 정해지지 않은 배열, 문자열 등
→ 분명히!! 값 타입(구조체, integer, 배열, 문자열 등)은 스택에 저장한다고 했는데 이게 무슨말일까?
힙 영역과 스택 영역에 메모리가 할당되는 예시
struct Cat {
let name: String
var age: Int
}
class Today {
let weather: String = "맑음"
let condition: String = "졸림"
init() {
petCondition()
}
func petCondition() {
let petCondition: String = "귀여움"
}
}
class Summer {
let name: String = "써머"
let pet: Cat = Cat(name: "또킨", age: 2)
let today: Today = Today()
init() {
copy()
}
func copy() {
let copyCat: Cat = Cat(name: "치킨", age: 0)
let copyTtokin = pet
let copyToday = today
}
}
let summer: Summer = Summer()
위의 코드를 실행하면,
- 생성된
summer
인스턴스는 상수이기 때문에 스택 영역에 할당된다. Summer
는 참조 타입이기 때문에, 힙 영역에Summer
를 할당하기 위한 공간을 계산한다.- 위 예시에서 총 필요한 영역은 우선 4개이다.
name
1개,pet
2개(pet.name
,pet.age
),today
1개 - 할당하기 위한 공간(영역)을 먼저 잡은 후, 한 줄씩 실행되면서 값이 할당된다.
name = "써머"
,pet.name = "또킨"
,pet.age = 2
,today = Today()
- 위 예시에서 총 필요한 영역은 우선 4개이다.
today
에Today()
클래스를 할당해주기 위해, 힙 영역에Today
클래스를 위한 영역을 할당해준다.today.weather = "맑음"
,today.condition = "졸림"
- 힙 영역에
Today
가 할당되고 나면,Summer
의today
에Today()
가 생성되었기 때문에Today()
의init()
메서드를 호출하고,init()
메서드 안의petCondition()
메서드가 호출된다.Today.init()
과Today.petCondition()
은 스택 영역에 할당된다(함수의 호출이기 때문에).petCondition()
안의petCondition = "귀여움"
도 스택 영역에 할당된다(메서드의 지역변수니까).- 여기까지 실행되고 나면,
Today.petCondition()
과Today.init()
의 호출이 종료되면서petCondition = "귀여움"
->Today.petCondition()
->Test.init()
의 순서로 스택 영역에서 pop 되고,Today
클래스의 호출이 종료된다.
Today
클래스의 호출이 종료되면, 힙 영역에 있는summer
의today
변수에 할당된Today
클래스의 주소가 저장되고,today
는 힙 영역의Today
영역을 바라보게 된다 (retain).- 이제
summer
의 함수(init()
,copy()
)를 호출한다.init()
이 처음으로 호출되고,init()
안에서copy()
가 호출되면서copy()
의 지역 변수copyCat
과copyTtokin
,copyToday
가 차지할 영역이 스택에 할당된 후, 한 줄씩 실행되면서 초기값이 세팅된다.- 이 때
copyCat
,copyTtokin
,copyToday
는 서로 메모리에 할당되는 과정이 다르다.copyCat
의 경우name = "치킨"
,age = 0
을 하나씩 새로 할당해주고copyTtokin
의 경우pet
을 복사해와서 할당해주고copyToday
는 클래스이기 때문에, 힙 영역에 있는Today
를 바라보게 된다.
- 여기까지 모든 함수의 실행이 끝났으므로..
copy()
메서드와copy()
메서드의 지역변수들을 pop한다.- 이 때,
copyToday
가 pop하면서Today
의 retain count 가 -1된다.
- 이 때,
- 그리고
summer.init()
이 종료되어 pop된다. - 그리고
Summer
클래스에 대한 호출이 종료되면서,summer
변수에 할당되어 있는Summer
의 주소가 저장된다. - 메인 함수의 호출이 종료되면
summer
와main()
이 pop되면서,summer
가Summer
를 바라보던 게 끊어지고(release),Summer
의 retain count는 0이 되어 ARC에 의해 메모리에서 해제된다.
![](https://i.imgur.com/G5NcxTi.png)
- 그리고
Summer
가 힙 영역에서 해제되면서,today
가Today
를 더이상 바라보지 않기 때문에Today
의 retain count도 0이 되어 ARC에 의해 해제된다. - 모든 게 종료되면, 메모리에 아무것도 존재하지 않는 상태가 된다.
Swift는 Array, Dictionary, String 등을 어떻게 저장하는가?
Array, Dictionary, String 등은 그 길이를 컴파일 타임에 미리 알 수 없는데(길이가 변경될 수 있으니까) Swift에는 구조체
로 구현되어 있다. 그런데 스택에 할당하기 위해서는 컴파일 타임에 길이를 알 수 있어야 한다고 했다.
하지만 아래의 예시는 오류가 발생하지 않는다. 왜일까?
struct Node {
var value = Int
var arr: [Node] // 오류 발생하지 않음
}
한줄요약: Swift는 Copy On Write를 사용하기 때문이다.
Swift에서 Array, Dictionary, String 등은 인스턴스가 변경되었을(mutated) 때, 변경된 실제 카피는 힙 메모리에만 만들어진다.
- 값 타입으로서 스택 영역 안에 존재하는 배열이 담고 있는 것은, 사실 저 힙 메모리의 주소값이다.
- 실제로 String이나 Array가 차지하고 있는 메모리의 크기를 확인해보면, String의 길이를 늘리거나 Array의 원소를 넣고 빼도 스택 영역에서 차지하고 있는 메모리 크기는 동일하다.
(https://sujinnaljin.medium.com/ios-swift의-type과-메모리-저장-공간-25555c69ccff)
그리고 값 타입의 인스턴스를 복사해서 사용할 때, 복사본(copy)의 값이 변경되기 전까지는 실제 데이터의 복사본을 안 만든다. 복사본에서는 원본 값을 가리키는 주소값을 가지고 있다가 복사본의 데이터를 변경해주면 (복사한 배열에 원소를 추가한다던가), 그 때 데이터의 복사본을 만들어서 걔를 변경하고, 그 새로운 복사본을 가리키는 주소값을 새로 가리키게 된다.
그리고 Swift의 배열이 재미있는점.. 현재 할당되어 있는 크기가 꽉 찼는데 새로운 원소를 append 하려고 하면, 배열의 크기를 현재의 2배로 늘린다(multiple).
(https://github.com/apple/swift/blob/main/stdlib/public/core/Array.swift)
출처
https://medium.com/@leandromperez/bidirectional-associations-using-value-types-in-swift-548840734047
https://medium.com/@itchyankles/memory-management-in-rust-and-swift-8ecda3cdf5b7#.vxuxwzr2l
https://www.mikeash.com/pyblog/friday-qa-2015-04-17-lets-build-swiftarray.html
https://sujinnaljin.medium.com/ios-swift의-type과-메모리-저장-공간-25555c69ccff
https://www.freecodecamp.org/news/the-story-of-one-mother-two-sons-value-type-vs-reference-type-in-swift-6e125af2d5d0/
https://github.com/apple/swift/blob/main/stdlib/public/core/Array.swift
'Swift-iOS > Swift' 카테고리의 다른 글
URLComponents, URLQueryItem (0) | 2022.11.17 |
---|
- Total
- Today
- Yesterday
- URLQueryItem
- Endpoint
- IOS
- 참조 타입
- 네트워크
- multipart/form-data
- 부트캠프
- SWIFT
- 스택
- copy on write
- ssh-agent
- Cow
- SSH
- URLComponents
- URL
- 값 타입
- OSI
- ssh-add
- 어플
- 메모리 구조
- HTTP message
- HTTP Methods
- TCP
- 코딩
- 앱개발
- Github
- 커리어스타터캠프
- JSON
- 야곰아카데미
- ssh-configure
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |