일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 프래그먼트
- async
- 테스크
- 옵셔널
- rx
- View
- 구조체
- 연산자
- 차이
- 해시
- 안드로이드
- concurrency
- 서브스크립트
- 스위프트
- 클로저
- 알고리즘
- 생명주기
- Subject
- observable
- Swift
- Self
- 리스트뷰
- RxSwift
- ios
- weak
- 풀이
- 이스케이핑
- 자바
- 백준
- 프로그래머스
- Today
- Total
study record
[swift] ARC 본문
*이 글은 책 “스위프트 프로그래밍”을 읽고 작성한 글입니다.
매번 전달할 때마다 값을 복사해 전달하는 값 타입과는 다르게 참조 타입은 하나의 인스턴스가 참조를 통해 여러 곳에서 접근하기 때문에 언제 메모리에서 해제되는지가 중요한 문제이다. 인스턴스가 적절한 시점에 메모리에서 해제되지 않으면 한정적인 메모리 자원을 낭비하게 되며, 성능의 저하로 이어지게 된다. 스위프트는 프로그램의 메모리 사용을 관리하기 위해 메모리 관리 기법인 ARC를 사용한다.
ARC가 관리해주는 참조 횟수 계산은 참조타입인 클래스의 인스턴스에만 적용된다. 구조체나 열거형은 값 타입이므로 참조 횟수 계산과 무관하다. 즉, 구조체나 열거형은 다른 곳에서 참조하지 않으므로 ARC로 관리할 필요가 없다.
ARC란
ARC 기능은 이름에서 알 수 있듯이 자동으로 메모리를 관리해주는 방식이다. 프로그래머가 메모리 관리에 신경을 덜 쓸 수 있기에 편리하다. ARC는 더이상 필요하지 않은 클래스의 인스턴스를 메모리에서 해제하는 방식으로 동작한다.
자바 등 다른 언어에서 사용되는 메모리 관리 기법인 가비지 컬렉션과는 어떤 차이가 있는가?
ARC와 가바지 컬렉션의 가장 큰 차이는 참조를 계산하는 시점이다. ARC는 인스턴스가 언제 메모리에서 해제되어야 할지를 컴파일과 동시에 결정한다. 가비지 컬렉션은 프로그램 동작 중에 참조 카운팅을 한다.
ARC
- 참조 카운팅 시점 : 컴파일 시
- 장점 :
- 컴파일 당시 이미 인스턴스의 해제 시점이 정해져 있어서 인스턴스가 언제 메모리에서 해제될지 예측할 수 있다.
- 컴파일 당시 이미 인스턴스의 해제 시점이 정해져 있어서 메모리 관리를 위한 시스템 자원을 추가할 필요가 없다.
- 단점:
- ARC의 작동 규칙을 모르고 사용하면 인스턴스가 메모리에서 영원히 해제되지 않을 가능성이 있다.
가비지 컬렉션
- 참조 카운팅 시점 : 프로그램 동작 중
- 장점 :
- 상호 참조 상황 등 복잡한 상황에서도 인스턴스를 해제할 수 있는 가능성이 더 높다.
- 특별히 규칙에 신경쓸 필요가 없다.
- 단점:
- 프로그램 동작 외에 메모리 감시를 위한 추가 자원이 필요하므로 한정적인 자원 환경에서는 성능 저하가 발생할 수 있다.
- 명확한 규칙이 없으므로 인스턴스가 정확히 언제 해제될지 예측하기 어렵다.
ARC를 이용해 자동으로 메모리 관리를 받기 위해서는 규칙을 알아야 한다. ARC는 컴파일과 동시에 인스턴스를 메모리에서 해제하는 시점이 결정하기 때문이다. 원하는 방향으로 메모리 관리가 이루어지려면 ARC에 명확한 힌트를 주어야 한다.
클래스의 인스턴스를 생성할 때마다 ARC는 인스턴스에 대한 정보를 저장하기 위한 메모리 공간을 따로 할당한다. 그 메모리 공간에는 인스턴스의 타입 정보와 함께 그 인스턴스와 관련된 저장 프로퍼티의 값 등을 저장한다. 이후 인스턴스가 더 이상 필요하지 않는 상태가 되면 인스턴스가 차지하던 메모리 공간을 다른 용도로 활용할 수 있도록 ARC가 메모리에서 인스턴스를 없앤다.
만약 아직 더 사용해야 하는 인스턴스를 메모리에서 해제시킨다면 인스턴스와 관련된 프로퍼티에 접근하거나 인스턴스의 메서드를 호출할 수 없다. 게다가 인스턴스에 강제로 접근하고자 하면 잘못된 메모리 접근으로 프로그램이 강제 종료될 수 있다.
인스턴스가 지속해서 필요한 상황에서 ARC는 인스턴스가 메모리에서 해제되지 않도록 인스턴스 참조 여부를 계속 추적한다. 다른 인스턴스의 프로퍼티나 변수 등 에서 인스턴스를 참조한다면 ARC가 해당 인스턴스를 해제하지 않고 유지해야 한다. 인스턴스를 메모리에 유지시키려면 이런 명분을 ARC에게 제공해야 한다.
강한 참조
인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어 주는 것이 바로 강한참조이다. 인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제되는데, 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당할 때 강한 참조를 사용하면 참조횟수가 1 증가한다. 또 강한 참조를 사용하는 프로퍼티, 변수, 상수 등에 nil을 할당하면 원래 자신에게 할당되어 있던 인스턴스의 참조 횟수가 1 감소한다.
참조의 기본은 강한 참조이므로 클래스 타입의 프로퍼티, 변수, 상수 등을 선언할 때 별도의 식별자를 명시하지 않으면 강한참조를 한다. 이제까지 모두 강한참조로 선언해주었던 것이다.
클래스 타입의 인스턴스가 생성되면 참조 횟수가 1증가한다. 이 인스턴스가 다른 변수에 할당되면 또 1이 증가한다. 이 변수에서 nil을 선언하면 1이 감소한다. 이렇게 참조횟수가 0이 되면 인스턴스는 ARC 규칙에 의해 메모리에서 해제되며 메모리에서 해제되기 직전에 디이니셜라이저를 호출한다.
강한참조 순환 문제
인스턴스끼리 서로가 서로를 강한참조 할 때를 강한참조 순환이라고 한다.
class Person {
let name: String
init(name: String) {
self.name = name
}
var room: Room?
deinit {
print("\(name) is being deinitialized")
}
}
class Room {
let number: String
init(number: String) {
self.number = number
}
var host: Person?
deinit {
print("Room \(number) is being deinitialized")
}
}
var yagom: Person? = Person(name: "yagom") // Person 인스턴스 참조 횟수 : 1
var room: Room? = Room(number: "505") // Room 인스턴스의 참조 횟수 : 1
room?.host = yagom // Person 인스턴스 참조 횟수 : 2
yagom?.room = room // Room 인스턴스 참조 횟수 : 2
yagom = nil // Person 인스턴스 참조 횟수 : 1
room = nil // Room 인스턴스 참조 횟수 : 1
// Person 인스턴스 참조할 방법 상실 - 메모리에 잔존
// Room 인스턴스 참조할 방법 상실 - 메모리에 잔존
이 예제 코드를 실행할 때 디이니셜라이저는 영원히 호출되지 않는다.
yagom 변수가 참조하던 Person 클래스의 인스턴스에 접근할 방법도, room 변수가 참조하던 Room 클래스의 인스턴스에 접근할 방법도 사라졌다. 참조 횟수가 0이 아니므로 ARC가 인스턴스를 메모리에서 해제시키지 않으므로 메모리에 계속 남아있게 된다.
// 강한 참조 순환 문제를 수동으로 해결
var yagom: Person? = Person(name: "yagom")
var room: Room? = Room(nunmber: "505")
room?.host = yagom
yagom?.room = room
yagom?.room = nil
yagom = nil
room?.host = nil
room = nil
이렇게 인스턴스를 메모리에서 해시킬 수 있을지도 모르지만 해제해야할 프로퍼티가 너무 많거나 깜빡할 수 있다. 이를 위해서 약한참조와 미소유참조를 통해 더 명확하게 해결할 수 있다.
약한참조
약한 참조는 강한 참조와 달리 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다. 참조 타입의 프로퍼티나 변수의 선언 앞에 weak 키워드를 써주면 그 프로퍼티나 변수는 자신이 참조하는 인스턴스를 약한참조한다.
약한참조를 사용한다면 자신이 참조하는 인스턴스가 메모리에서 해제될 수도 있다는 것을 예상해야한다. 참조횟수를 증가시키지 않았기 때문에 참조하던 프로퍼티나 변수에서 참조 횟수를 감소시켜 0으로 만들면 참조하던 인스턴스가 메모리에서 해제되기 때문이다.
약한 참조는 상수에서 쓰일 수 없다. 만약 자신이 참조하던 인스턴스가 메모리에서 해제되면 nil이 할당될 수 있어야 하기 때문이다. 그래서 약한참조를 할 때는 자신의 값을 변경할 수 있는 변수로 선언해야 한다. 더불어 nil이 할당될 수 있어야 하므로 약한참조는 항상 옵셔널이어야 한다.
class Room {
let number: String
init(number: String) {
self.number = number
}
weak var host: Person?
}
var yagom: PErson? = Person(name: "yagom") // Person 인스턴스 참조 횟수 : 1
var room: Room? = Room(number: "505") // Room 인스턴스 참조 횟수 : 1
room?.host = yagom // Person 참조 횟수 : 1
yagom?.room = room // Room 인스턴스 참조 횟수 : 2
yagom = nil // Person 인스턴스 참조 횟수 : 0, Room 인스턴스 참조 횟수 : 1
강한참조 순환 문제를 해결하기 위하여 Room 클래스의 host 프로퍼티가 약한참조를 하도록 weak키워드를 추가했다. room 변수가 참조하는 인스턴스의 host 프로퍼티가 약한참조를 하므로 참조횟수가 증가하지 않는다.
또한 인스턴스가 메모리에서 해제될 때, 자신의 프로퍼티가 강한참조를 하던 인스턴스의 참조 횟수를 1 감소시킨다. 자신이 참조하던 인스턴스가 메모리에서 해제되면 자동으로 nil을 할당한다.
미소유참조
약한참조와 마찬가지로 미소유참조는 인스턴스의 참조 횟수를 증가시키지 않는다. 미소유참조는 약한참조와 다르게 자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반한다. 즉, 자신이 참조하는 인스턴스가 메모리에서 해제되더라도 스스로 nil을 할당하지 않는다. 따라서 미소유참조를 하는 변수나 프로퍼티는 옵셔널이나 변수가 아니어도 된다.
미소유참조를 하면서 메모리에서 해제된 인스턴스에 접근하려 한다면 잘못된 메모리 접근으로 런타임 오류가 발생해 프로세스가 강제로 종료된다. 따라서 미소유참조는 참조하는 동안 해당 인스턴스가 메모리에서 해제되지 않을 것이 확신될 때 사용해야 한다.
참조 타입의 변수나 프로퍼티의 정의 앞에 unowned 키워드를 써주면 자신이 참조하는 인스턴스를 미소유참조하게 된다.
class Person{
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
}
class CreditCard {
let number: UInt
unowned let owner: Person
init(number: UInt, owner: Person) {
self.number = number
self.owner = owner
}
}
var jisoo: Person? = Person(name: "jisoo")
if let person: Person = jisoo {
person.card = CreditCard(number: 1004, owner: person)
// CreditCard 참조횟수 : 1
// Person 참조횟수 : 1
}
jisoo = nil
// Person 참조횟수 : 0
// CreditCard 참조횟수 : 0
CreditCard의 이니셜라이저에서 owner 프로퍼티에 미소유참조되는 Person 인스턴스는 참조 횟수가 증가하지 않는다.
클로저의 강한참조 순환
강한참조 순환 문제는 두 인스턴스끼리의 참조일 때만 발생하는 것 외에 클로저가 인스턴스의 프로퍼티일 때나, 클로저의 값 획득 특성 때문에 발생한다. 클로저 내부에서 인스턴스의 프로퍼티에 접근할 때나 클로저 내부에서 인스턴스의 메서드를 호출할 때 값 획득이 발생할 수 있는데, 모두 클로저가 self 를 획득하므로 강한참조 순환이 발생한다.
강한참조 순환이 발생하는 이유는 클로저가 클래스와 같은 참조타입이기 때문이다. 클로저를 클래스 인스턴스의 프로퍼티로 할당하면 클로저의 참조가 할당된다. 이때 참조 타입과 참조 타입이 서로 강한참조를 하기 때문에 강한참조 순환 문제가 발생한다.
클로저는 자신의 내부에 있는 참조 타입 변수를 획득한다. 클로저는 자신이 호출되면 언제든지 자신 내부의 참조들을 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지하는데, 이때 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시킨다. 이렇게 강한참조 순환이 발생하면 자신을 강한참조 프로퍼티로 갖는 인스턴스가 메모리에서 해제될 수 없다.
획득목록
이 문제를 획득목록을 통해 해결할 수 있다. 획득목록은 클로저 내부에서 참조 타입을 획득하는 규칙을 제시해줄 수 있는 기능이다. 예를 들어 클로저 내부의 self 참조를 약한참조로, 강한참조로 지정할 수 있다는 것이다. 획득목록을 사용하면 참조방식을 클로저에 제안할 수 있다.
획득목록은 클로저 내부의 매개변수 목록 이전 위치에 작성해준다. 획득목록은 참조 방식과 참조할 대상을 대괄호([ ])로 둘러싼 목록 형식으로 작성하며 획득 목록 뒤에는 in 키워드를 써준다. 획득목록에 명시한 요소가 참조 타입이 아니라면 해당 요소들은 클로저가 생성될 때 초기화된다.
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
b = 20
}
a = 10
b = 10
closure() // 0 10
print(b) // 20
a는 클로저의 획득목록을 통해 클로저가 생설될 대 값 0을 획득했지만 b는 따로 값을 획득하지 않았다. 차후에 a와 b 값을 변경한 후 클로저를 실행하면 a는 클로저가 생성되었을 때 획득한 값을 갖지만 b는 변경된 값을 사용하는 것을 확인할 수 있다.
그러나 획득목록에 해당하는 요소가 참조 타입이라면 다른 결과가 생긴다. 획득목록에 명시했더라도 값이 변화한다. 명시한 변수가 참조 타입의 인스턴스이기 때문이다. 따라서 이런 경우에 참조 타입은 획득 목록에서 어떤 방식으로 참조할 것인지, 즉 강한획득을 할 것인지, 약한획득을 할 것 인지, 미소유획득을 할 것인지를 정해줄 수 있다.
class SimpleClass {
var value: Int = 0
}
var x: SimpleClass? = SimpleClass()
var y = SimpleClass()
let closure = { [weak x, unowned y] in
print(x?.value, y.value)
}
x = nil
y.value = 10
closure() // nil 10
'Swift > 스위프트 프로그래밍' 카테고리의 다른 글
[Swift] 메모리 안전 (0) | 2022.02.07 |
---|---|
[Swift] 오류 처리 (0) | 2022.02.06 |
[Swift] where절 (0) | 2022.01.24 |
[Swift] 패턴 (0) | 2022.01.20 |
[Swift] 타입 중첩 (0) | 2022.01.19 |