구조체나 열거형처럼 전달할 때마다 값을 복사해서 새로운 인스턴스를 전달하는 값 타입과는 달리 참조 타입은 하나의 인스턴스가 참조를 통해 여러 곳에서 접근하기 때문에, 언제 메모리에서 해제되는지가 중요한 문제다. 만약 인스턴스가 더 이상 필요하지 않은데도 계속해서 메모리에서 해제되지 않으면 한정적인 메모리 자원을 낭비하게 되고 이는 성능의 저하로 이어진다.
Java 나 Kotlin 같은 언어들은 가비지 컬렉션이란 기법을 사용해 메모리를 관리한다. Swift는 ARC 라는 기법을 사용하는데 이것에 대해 알아보도록 하자
ARC란?
Automatic Reference Counting (ARC)는 이름에서도 알 수 있듯이 자동으로 메모리를 관리해주는 방식이다. ARC 는 더 이상 필요하지 않은 클래스의 인스턴스를 메모리에서 해제하는 방식으로 동작한다. 인스턴스를 메모리에서 해제하는 개념은 가비지 컬렉션과 동일한데, 인스턴스가 해제되는 시점을 결정하는 데 아주 큰 차이점이 존재한다.
ARC 는 인스턴스가 해제되어야 할 시점을 컴파일 타임에 결정한다. 반면 가비지 컬렉션은 런타임에 계속해서 참조를 계산하면서 인스턴스를 해제한다.
메모리 관리 기법 | ARC | 가비지 컬렉션 |
참조 카운팅 시점 | 컴파일 타임 | 런타임 |
장점 | 컴파일 타임에 인스턴스의 해제 시점이 정해져있기 때문에, 인스턴스가 언제 메모리에서 해제될 지 예측 가능 컴파일 타임에 인스턴스 해제 시점이 정해져 있기 때문에 메모리 관리를 위해 시스템 자원을 추가할 필요가 없다 |
상호 참조 상황 등의 복잡한 상황에서도 인스턴스를 해제할 수 있는 가능성이 더 높다 신경 쓸 부분이 별로 없다 |
단점 | ARC 의 작동 규칙을 모르고 사용하면 인스턴스가 영원히 해제되지 않을 수 있음 | 메모리 관리를 위한 추가 자원이 필요하므로, 성능 저하가 발생할 수 있다. 명확한 규칙이 없기 때문에, 인스턴스가 정확히 언제 해제될 지 예측 불가능하다. |
가비지 컬렉션은 비교적 프로그래머가 신경 쓸 부분이 적다. 메모리 관리를 위한 추가적인 자원이 들어가기 때문에 성능은 조금 떨어질 수 있어도, 코드에 특별한 규칙을 넣지 않아도 알아서 메모리를 관리하기 때문이다. 하지만 ARC 는 컴파일 타임에 해제 시점이 결정되기 때문에, 복잡한 상황이 런타임에 발생했을 때도 인스턴스의 해제 시점이 동적으로 변한다거나 그러지 않는다.
즉 우리가 원하는 방향으로 메모리 관리가 이루어지려면 ARC 에 인스턴스 해제 시점에 대해 명확한 정보를 전해줘야 한다. 클래스가 인스턴스를 생성할 때마다 ARC 는 그 인스턴스에 대한 정보를 저장하기 위한 메모리 공간을 따로 할당한다. 해당 메모리 공간에는 인스턴스의 타입 정보, 프로퍼티 값 등을 저장한다. 이후 인스턴스가 더 이상 필요없는 상태가 되면 ARC 는 자동으로 해당 인스턴스를 메모리에서 해제한다.
그러나 인스턴스가 더 필요한 상황에서 해제한다면 그 이후 인스턴스와 관련된 프로퍼티에 접근하거나 인스턴스 메소드를 호출할 수 없다. 만약 인스턴스에 강제로 접근하려 한다면 NPE 로 인해 프로그램이 강제로 종료될 확률도 크다. 인스턴스가 지속적으로 필요한 상황에서 ARC는 인스턴스의 참조 여부를 계속해서 추적한다. 다른 인스턴스의 프로퍼티나 변수, 상수 등에서 인스턴스를 참조한다면 ARC 가 해당 인스턴스를 해제하지 않는다. ARC에 적용되는 메모리 관리 규칙을 알아보도록 하자
강한참조
인스턴스가 계속해서 메모리에 남아있어야 하는 명분을 만들어 주는 것이 바로 강한참조(Strong Reference) 이다. 인스턴스의 참조 횟수가 0 이 되는 순간 메모리에서 해제되는데, 인스턴스를 다른 인스턴스의 프로퍼티나 변수, 상수 등에 할당할 때 참조 횟수가 1 증가한다. 만약 프로퍼티나 변수, 상수 등에 다시 nil 을 할당해주면, 참조 횟수가 다시 1 감소한다.
참조의 기본은 강한참조이므로, 클래스 타입의 프로퍼티나 변수 상수등을 선언할 대 별도의 식별자를 명시하지 않으면 강한참조를 한다.
class Person {
let name : String
init(name : String){
self.name = name
}
deinit {
print("\(name) is deinitialized")
}
}
var reference1 : Person?
var reference2 : Person?
var reference3 : Person?
reference1 = Person(name: "JH") //Person 인스턴스 할당 및 참조 횟수 1
reference2 = reference1 //Person 인스턴스 참조 횟수 2
reference3 = reference2 //3
reference1 = nil //참조 횟수 2
print("reference 1 gone")
reference2 = nil //참조 횟수 1
print("reference 2 gone")
reference3 = nil //참조 횟수 0
print("reference 3 gone")
//출력 결과
reference 1 gone
reference 2 gone
reference 3 gone
JH is deinitialized
클래스 타입인 Person 의 인스턴스를 reference1 에 할당한다. 해당 인스턴스는 새로 생성되었고 강한참조로 reference1 에 할당되었기 때문에 참조 횟수가 1이된다. 그 이후 두 개의 변수에 추가적으로 reference1 이 참조하는 Person 인스턴스를 할당했다. 따라서 참조 횟수는 2 증가해서 3이 된다. 하나의 인스턴스를 3 개의 변수가 참조하고 있기 때문에, ARC 는 절대 이 인스턴스를 해제할 수 없다.
그 다음으로 각 변수에 nil 을 할당할 때마다, 강한 참조 관계는 해제되고 인스턴스 참조 횟수가 1씩 감소한다. 0이 되는 순간 ARC 규칙에 의해 메모리에서 해제되며, deinit 코드 블록을 호출하는 것을 볼 수 있다.
함수 내 지역 변수에서 참조 횟수에 대해서 알아보자
func foo() {
let person = Person(name: "Hello World")
}
foo()
//출력 결과
Hello World is deinitialized
foo() 함수 내부에 Hello World 이름의 새로운 인스턴스를 person 이라는 상수에 할당했다. 따라서 함수 내부에서 해당 인스턴스의 참조횟수는 1이 된다. 그 이후 함수가 종료되면 지역변수가 사라지기 때문에 Hello World 인스턴스의 참조 횟수는 다시 0이 된다. 이 순간 ARC 규칙에 의해 Hello World 인스턴스는 메모리에서 사라지고 deinit 이 호출된다. 만약 지역변수에 할당한 인스턴스를 상위 스코프에서 참조하면 어떤 현상이 발생할까?
var globalVariable : Person? = nil
func foo() {
let person = Person(name: "Hello World") //Person 인스턴스 참조 횟수 1 증가!
globalVariable = person //Person 인스턴스 참조 횟수 2
}
foo()
//함수가 종료된 후 참조횟수 1
globalVariable 이란 변수가 함수 내부에서 할당한 인스턴스를 참조하고 있으므로, 함수 내부에서 할당한 인스턴스의 메모리는 해제되지 않는다. 만약 여기서 globalVariable 에 다른 인스턴스를 할당하거나 nil 을 할당한다면 Hello World 인스턴스는 메모리에서 해제될 것이다.
var globalVariable : Person? = nil
func foo() {
let person = Person(name: "Hello World")
globalVariable = person
}
foo()
globalVariable = Person(name: "another")
//globalVariable에 새로운 인스턴스가 할당되는 순간 이전 인스턴스와의 강한 참조 관계가 해제되고,
//이전 인스턴스는 메모리에서 해제됨
//출력 결과
Hello World is deinitialized
강한참조 순환 문제
그러나 복합적으로 강한참조가 일어나는 상황에서 규칙을 모르고 사용하게 되면 문제가 발생할 수 있다. 인스턴스가 서로를 강한참조할 때를 대표적인 예로 들 수 있다. 이를 강한참조 순환이라 한다.
class Person {
let name : String
var bestFriend : Person?
init(name : String){
self.name = name
print("\(name) is initialized")
}
deinit {
print("\(name) is deinitialized")
}
}
var JH : Person? = Person(name: "JH") //JH 인스턴스 참조 횟수 1
var UK : Person? = Person(name: "UK") //UK 인스턴스 참조 횟수 1
JH?.bestFriend = UK //UK 인스턴스 참조 횟수 2
UK?.bestFriend = JH //JH 인스턴스 참조 횟수 2
JH = nil //JH 인스턴스 참조 횟수 1
UK = nil //UK 인스턴스 참조 횟수 1
//두 인스턴스의 참조 횟수가 0이 되지 않았기 때문에, 영원히 메모리에서 해제되지 않는다!
Person 클래스에 Person 타입의 저장 프로퍼티 bestFriend 를 추가적으로 선언했다. 사람이 친구가 없어서 절친이 없을 수도 있기 때문에 Optional 타입으로 정의했다. JH 와 UK 이라는 두 인스턴스를 할당할 때 두 인스턴스의 참조 횟수는 1이 된다. 그 다음 각 인스턴스가 서로를 bestFriend 프로퍼티에 할당할 때 참조 횟수가 1씩 증가해서 2가 된다. 그 다음 각 인스턴스에 다시 nil 을 할당하는데, 각 인스턴스 안 프로퍼티가 서로를 가르키는 상태에서 참조 횟수가 1씩 감소해 1이 되기 때문에 영원히 인스턴스가 메모리에서 해제되지 않는다.
JH 에 nil 을 할당한 시점에서, 다행히 UK 인스턴스에서 bestFriend 프로퍼티를 통해 JH 인스턴스에 접근할 수 있는 방법이 남아있다. 따라서 위 코드에서 정상적으로 메모리를 해제하려면 다음과 같이 코드를 수정할 수 있다.
JH = nil //JH 인스턴스 참조 횟수 1
UK?.bestFriend?.bestFriend = nil //UK 인스턴스 참조 횟수 1
UK = nil //UK 인스턴스 참조횟수 0 -> 따라서 UK의 bestFriend인 JH 도 참조횟수 0 됨
//출력 결과
JH is initialized
UK is initialized
JH is deinitialized
UK is deinitialized
위 코드에서 UK 인스턴스가 메모리에서 해제될 때, UK 의 bestFriend 프로퍼티가 참조하는 JH 의 참조횟수도 0이 되는 것을 알 수 있다. 즉 인스턴스가 메모리에서 해제될 때, 자신의 프로퍼티가 강한참조를 하던 인스턴스의 참조 횟수를 1 감소시킨다는 것을 알 수 있다.
위와 같은 방법으로 인스턴스를 메모리에서 해제할 수 있지만, 코드가 너무 복잡하고 프로그래머가 까먹을 수 있는 상황이 충분히 생길 수 있다. 메모리 누수가 탐지되어 추후 문제를 해결하려 할 때 문제의 원인을 찾기가 매우 힘들어질 수도 있다. 다음에 소개할 약한참조와 미소유 참조를 통해 위 문제를 좀 더 깔끔히 해결해보자!
약한 참조
약한참조는 강한참조와 달리 자신이 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다. 참조 타입의 프로퍼티나 변수의 선언 앞에 weak 키워드를 써주면 그 프로퍼티나 변수는 자신이 참조하는 인스턴스를 약한참조한다.
약한참조를 사용하면, 해당 변수나 프로퍼티는 참조횟수를 증가시키지 않았기 때문에 인스턴스의 참조 횟수가 0이 되어 메모리에서 해제될 수 있다. 따라서 언제든 nil을 가리킬 수 있으며, 따라서 상수로 사용할 수 없고 항상 옵셔널 타입이어야 한다!
약한참조 변수나 프로퍼티는 자신이 참조 횟수를 증가시키지 않았기 때문에, 언제든 참조하는 인스턴스는 메모리에서 해제될 수 있다. 위 강한참조 순환 문제를 약한참조를 통해 해결해보자!
class Person {
let name : String
weak var bestFriend : Person? //약한 참조 프로퍼티
init(name : String){
self.name = name
print("\(name) is initialized")
}
deinit {
print("\(name) is deinitialized")
}
}
var JH : Person? = Person(name: "JH") //JH 인스턴스 참조 횟수 1
var UK : Person? = Person(name: "UK") //UK 인스턴스 참조 횟수 1
JH?.bestFriend = UK //bestFriend 프로퍼티는 UK 인스턴스를 약한 참조하기 때문에 참조 횟수 변화없음
UK?.bestFriend = JH //위와 동일
JH = nil //JH 인스턴스 참조 횟수 0
print(UK?.bestFriend?.name ?? "UK`s bestFriend is nil")
UK = nil //UK 인스턴스 참조 횟수 0
//출력 결과
JH is initialized
UK is initialized
JH is deinitialized
UK`s bestFriend is nil
UK is deinitialized
bestFriend 프로퍼티를 약한참조 프로퍼티로 변경했다. 이제 bestFriend 에 인스턴스를 할당하는 것은 참조횟수를 증가시키지 못한다. 따라서 서로가 서로를 참조하고 있더라도, JH 변수에 다시 nil 을 할당하는 순간 JH 인스턴스가 메모리에서 해제된 것을 알 수 있다. 그 다음 JH 인스턴스를 가리키던 UK 의 bestFriend 프로퍼티의 이름을 출력하게 했는데, 여기서 우리는 UK 의 약한참조 프로퍼티가 참조한 JH 인스턴스가 nil 이 되어서 UK 의 bestFriend 프로퍼티도 nil 을 가리키게 된 것을 확인할 수 있다.
미소유참조
참조횟수를 증가시키지 않고 참조할 수 있는 방법은 약한참조만 있는 것이 아니다. 약한참조와 마찬가지로 미소유참조 도 인스턴스의 참조 횟수를 증가시키지 않는다. 하지만 미소유참조는 약한참조와 다르게 자신이 소유하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 동작한다. 즉 자신이 참조하는 인스턴스가 메모리에서 해제되더라도 스스로 nil 을 할당해주지 않는다. 그렇기 때문에 미소유참조는 옵셔널이나 변수가 아니어도 된다.
하지만 미소유참조를 하면서 메모리에서 해제된 인스턴스에 접근하려 한다면 런타임 오류가 발생해 프로세스가 강제 종료된다. 따라서 미소유참조는 꼭 참조하는 동안 해당 인스턴스가 살아있다는 것을 확신할 수 있을 때 쓰도록 하자!
참조 타입의 변수나 프로퍼티의 정의 앞에 unowned 키워드를 써주면 그 변수나 프로퍼티는 자신이 참조하는 인스턴스를 미소유참조하게 된다. 그렇다면 미소유 참조는 어떤 관계에서 사용할 수 있을까? 사람과 신용카드의 관계를 생각해보자. 사람이 신용카드를 소지하지 않을 수 있지만, 신용카드는 꼭 명의자가 있어야 한다. 명의자와 신용카드는 서로를 참조해야 하는 상황이고 신용카드는 꼭 명의자가 존재한다는 확신이 있을 때, 다음과 같이 표현해볼 수 있다.
class Person {
let name : String
//카드를 소지할 수도, 소지하지 않을 수도 있다.
//카드를 한 번 가지면 잃어버리면 안되므로 강한참조를 한다.
var creditCard : CreditCard?
init(name : String ) {
self.name = name
}
deinit { print("\(name) is deinitialzed")}
}
class CreditCard {
let number: UInt
//카드는 꼭 소유자가 존재해야 한다.
unowned let owner : Person
init(number:UInt, owner : Person) {
self.number = number
self.owner = owner
}
deinit {
print("Card #\(number) is being deinitialized")
}
}
var jisoo : Person? = Person(name: "jisoo") //Person 인스턴스 참조 횟수 1
if let person : Person = jisoo {
//CreditCard 의 참조 횟수 1
person.creditCard = CreditCard(number: 1004, owner: person)
}
jisoo = nil //Person 인스턴스 참조 횟수 0
//CreditCard 인스턴스 참조 횟수 0
//출력 결과
jisoo is deinitialzed
Card #1004 is being deinitialized
Person 클래스는 CreditCard? 를 강한참조하는 프로퍼티가 존재하고, CreditCard 는 Person 타입의 인스턴스를 미소유참조하는 owner 프로퍼티가 있다. jisoo 변수에 새로운 인스턴스를 할당하면 참조 횟수는 1이 된다. 이제 jisoo 가 새로운 신용카드를 발급받았다.
새로운 신용카드의 번호는 1004고, 당연히 주인은 jisoo 가 된다. 이 때 jisoo 의 creditCard 가 CreditCard 인스턴스를 강한참조하므로,
CreditCard 인스턴스의 참조횟수는 1이 된다.
그 다음 jisoo 변수에 nil 을 할당해보자. jisoo 가 가리키는 인스턴스의 참조횟수가 0이 되기 때문에, 메모리에서 해제된다. 또한 Person 인스턴스가 해제되면서 해당 인스턴스가 강한참조를 하고 있던 CreditCard 인스턴스의 참조횟수도 0이 되기 때문에 메모리에서 해제되게 된다.
이렇게 사람이 신용카드를 소지하다가 죽으면 신용카드도 없애야 하는 상황을 unowned 키워드 하나로 표현할 수 있으며 순환참조 문제도 피해갈 수 있다.
미소유참조와 암시적 추출 옵셔널 프로퍼티
서로 참조해야 하는 프로퍼티에 값이 꼭 있어야 하면서도 한번 초기화되면 그 이후에는 nil 을 할당할 수 없는 조건을 갖추어야 하는 경우엔 어떻게 해야할까? 다음 예제를 보도록 하자
class Company {
let name : String
var ceo : CEO!
init(name : String, ceoName : String) {
self.name = name
self.ceo = CEO(name: ceoName, company: self)
}
func introduce() {
print("\(name) 의 CEO 는 \(ceo.name)입니다.")
}
}
class CEO {
let name : String
unowned let company : Company
init(name:String,company:Company) {
self.name = name
self.company = company
}
func introduce() {
print("\(name)은 \(company.name) 의 CEO 입니다.")
}
}
let company : Company = Company(name: "무한상사", ceoName: "김태호")
company.introduce()//무한상사 의 CEO 는 김태호입니다.
company.ceo.introduce() //김태호은 무한상사 의 CEO 입니다.
회사를 창립하려면 CEO 가 있어야 한다. 그리고 CEO 는 단 하나의 회사를 운영한다. 회사가 사라지면 최고경영자가 있을 의미가 없다. 즉 Company 를 초기화하면서 CEO 인스턴스가 생성되면서 프로퍼티로 할당되어야 하고, Company 가 존재하는 한 ceo 프로퍼티에는 꼭 CEO 인스턴스가 존재해야 한다. 만약 회사가 사라지면 CEO 가 있을 필요가 없기 때문에 CEO 타입의 company 는 미소유 참조를 사용한다.
Company 타입의 ceo 프로퍼티에 암시적 추출 옵셔널을 사용한 이유는, Company 타입의 인스턴스를 초기화할 때 CEO 타입의 인스턴스를 생성하는 과정에서 자기 자신을 참조하도록 보내줘야 하기 때문이다. 만약 일반 프로퍼티를 사용했다면 자신의 초기화가 끝난 경우에만 CEO(name: ceoName, company: self) 와 같은 코드를 호출할 수 있다. 아직 자신의 저장 프로퍼티인 ceo 를 저장하기 전에 self 를 호출할 수는 없기 때문이다.
그래서 모든 조건을 충족하려면 Company 의 ceo 프로퍼티는 암시적 추출 옵셔널로, CEO 의 company 프로퍼티는 미소유참조 상수를 사용하면 된다.
클로저의 강한참조 순환
이 때까지 미소유참조, 약한참조 등을 사용해서 참조 순환 문제를 해결하고, 현실의 개념에 대입해 올바른 사용법을 알아보았다. 그러나 인스턴스끼리만의 참조로 인해 참조순환문제가 일어나는 것은 아니다. 클로저가 인스턴스의 프로퍼티일 때나, 클로저의 값 획득 특성 때문에 참조 순환 문제가 일어나기도 한다.
클로저로 인한 강한참조 순환이 일어나는 이유는 클로저가 클래스와 마찬가지로 참조 타입이기 때문이다. 클로저를 클래스 인스턴스의 프로퍼티로 할당하면 클로저의 참조가 할당된다. 이 때 참조 타입과 참조 타입이 서로 강한참조를 하기 때문에 강한참조 순환 문제가 발생한다.
물론 클로저로 인한 문제는 클로저의 획득 목록이라는 것을 통해 해결할 수 있다. 그러나 클로저의 획득 목록을 통해 강한참조 순환을 해결하기 전에 클로저로 인한 순환 문제가 어떻게 발생하는지 알아보자.
class Person {
let name : String
let hobby : String?
lazy var introduce: () -> String = {
var introduction : String = "My name is \(self.name)"
guard let hobby = self.hobby else {
return introduction
}
introduction += " "
introduction += "My hobby is \(hobby)"
return introduction
}
init(name : String, hobby: String? = nil) {
self.name = name
self.hobby = hobby
}
deinit {
print("\(name) is being deinitialized")
}
}
var jongho : Person? = Person(name: "Jong Ho", hobby: "BasketBall")
print(jongho?.introduce())
jongho = nil
//출력 결과
Optional("My name is Jong Ho My hobby is BasketBall")
위 코드에서 jongho 변수에 Person 인스턴스를 할당한 뒤, 인스턴스의 클로저 프로퍼티를 호출한 후 다시 nil 을 할당해주었다. 하지만 출력 결과를 보면 알 수 있듯이 Person 인스턴스가 메모리에서 해제되지 않아 deinit 도 호출되지 않았다. Person 클래스의 introduce 프로퍼티에 클로저를 할당한 후 클로저 내부에서 self 프로퍼티를 사용할 수 있었던 이유는 introduce 가 지연 저장 프로퍼티이기 때문이다. 만약 지연 저장 프로퍼티가 아니라면 self 사용하여 접근할 수 없다.
lazy 프로퍼티가 다른 프로퍼티에 접근하려면 Person 클래스의 인스턴스가 모두 초기화되어 사용이 가능한 상태에서만 접근이 가능하다. 따라서 클로저 내부에서는 self 프로퍼티를 통해서만 다른 프로퍼티에 접근할 수 있다.
자기소개를 하려고 introduce 프로퍼티를 통해 클로저를 호출하면 그 때 클로저는 자신의 내부에 있는 참조 타입 변수 등을 획득한다. 문제는 클로저가 내부에 있는 참조들을 사용할 수 있도록 참조 횟수를 증가시켜 메모리에서 해제되는 것을 방지하는데, 이 때 자신을 프로퍼티로 갖는 인스턴스의 참조 횟수도 증가시킨다.
클로저 내부에서 self 프로퍼티를 여러 번 호출하여 접근한다고 해도 참조 횟수는 한 번만 증가한다.
획득목록
앞의 문제는 획득목록 을 통해 해결할 수 있다. 획득목록은 클로저 내부에서 참조 타입을 획득하는 규칙을 제시해줄 수 있는 기능이다. 예를 들어 클로저 내부의 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는 변경된 값을 사용하는 것을 확인할 수 있다. a 변수는 클로저가 생성됨과 동시에 획득목록 내에서 다시 a 라는 이름의 상수로 초기화된 것이다.
그러나 획득목록에 해당하는 요소가 참조 타입이라면 조금 다른 결과를 볼 수 있다.
class someClass {
var value : Int = 0
}
var x = someClass()
var y = someClass()
let closure2 = {[x] in
print(x.value,y.value)
}
x.value = 10
y.value = 10
closure2() //10, 10
변수 x 는 획득목록을 통해 값을 획득했고, y는 따로 명시하지 않았다. 그러나 x와 y의 value 를 바꿔준 뒤 클로저를 실행하면 변경된 값이 출력되는 것을 확인할 수 있다. 두 변수 모두 인스턴스를 참조하는 참조 변수이기 때문이다. 참조 타입은 획득목록에서 어떻게 참조할 것인지, 즉 강한획득을 할 것인지, 약한 획득을 할 것인지, 미소유 획득을 할 것인지를 정해줄 수 있다.
만약 약한 획득이나 미소유 획득을 하게 되면 참조 횟수는 증가되지 않지만, 약한 획득의 경우 클로저가 획득하는 상수가 옵셔널 상수로 지정된다는 것을 주의해야 한다. 클로저 내부에서 약한획득한 상수를 사용하려 할 때 이미 메모리에서 해제되 nil 을 가리킬 수도 있기 때문이다.
class SomeClass {
var value : Int = 0
deinit {
print("SomeClass with \(value) is deinitialized")
}
}
var x : SomeClass? = SomeClass()
var y : SomeClass = SomeClass()
let closure2 = {[weak x,unowned y] in
print(x?.value,y.value)
}
x = nil
y.value = 20
closure2()
//출력 결과
SomeClass with 0 is deinitialized
nil 20
위 코드에서 x 변수를 약한참조로, y 를 미소유참조로 획득하도록 지정했다. x 가 약한참조를 하게 되므로 클로저 내부에서 사용하더라도 클로저는 x가 참조하는 인스턴스의 참조 횟수를 증가시키지 않는다. 그렇게 되면 변수 x가 참조하는 인스턴스가 메모리에서 해제되어 클로저 내부에서도 더 이상 참조가 불가능한 것을 볼 수 있다. y는 미소유참조를 했기 때문에 참조 횟수를 증가시키진 않지만, 만약 메모리에서 해제된 상태에서 접근하려고 하면 다음과 같이 에러가 발생한다.
획득목록을 통한 참조 순환 해결방법을 알아보았으므로 처음 코드를 다시 해결해보도록 하자!
class Person {
let name : String
let hobby : String?
lazy var introduce: () -> String = { [unowned self] in
var introduction : String = "My name is \(self.name)"
guard let hobby = self.hobby else {
return introduction
}
introduction += " "
introduction += "My hobby is \(hobby)"
return introduction
}
init(name : String, hobby: String? = nil) {
self.name = name
self.hobby = hobby
}
deinit {
print("\(name) is being deinitialized")
}
}
var jongho : Person? = Person(name: "Jong Ho", hobby: "BasketBall")
print(jongho?.introduce())
jongho = nil
//출력결과
Optional("My name is Jong Ho My hobby is BasketBall")
Jong Ho is being deinitialized
위 코드의 출력 결과를 보면 의도한 대로 jongho 변수가 가리키는 인스턴스가 메모리에서 잘 해제된 것을 볼 수 있다. introduce 프로퍼티가 self 를 미소유참조하도록 했기 때문이다. self 프로퍼티를 미소유 참조하도록 한 것은, 해당 인스턴스가 존재하지 않으면 프로퍼티도 호출할 수 없기에 실행 중에 self 가 nil 일 때 클로저를 호출할 수 없다고 판단했기 때문이다.
그러나 만약에 self 를 미소유참조한다면 어떤 문제가 발생할까? 다음 코드를 보도록 하자
class Person {
let name : String
let hobby : String?
lazy var introduce: () -> String = { [unowned self] in
var introduction : String = "My name is \(self.name)"
guard let hobby = self.hobby else {
return introduction
}
introduction += " "
introduction += "My hobby is \(hobby)"
return introduction
}
init(name : String, hobby: String? = nil) {
self.name = name
self.hobby = hobby
}
deinit {
print("\(name) is being deinitialized")
}
}
var jongho : Person? = Person(name: "Jong Ho", hobby: "BasketBall")
var introduction = jongho?.introduce
print(jongho?.introduce())
jongho = nil
print((introduction ?? {})())
jongho 인스턴스의 프로퍼티를 변수에 할당했다. 그리고 jongho 를 nil 로 만들고, 아까 변수에 할당한 클로저를 다시 호출해보면, jongho 가 가리킨 인스턴스는 이미 메모리에서 해제되었기 때문에, nil 접근으로 프로세스가 강제종료되는 것을 볼 수 있다. 이런 문제의 발생 여지가 있기 때문에 weak 참조를 다음과 같이 사용할 수도 있다.
class Person {
let name : String
let hobby : String?
//weak 획득
lazy var introduce: () -> String = { [weak self] in
//self가 nil 인지 검사한다!
guard let self = self else {
return "self instance is nil"
}
var introduction : String = "My name is \(self.name)"
guard let hobby = self.hobby else {
return introduction
}
introduction += " "
introduction += "My hobby is \(hobby)"
return introduction
}
init(name : String, hobby: String? = nil) {
self.name = name
self.hobby = hobby
}
deinit {
print("\(name) is being deinitialized")
}
}
var jongho : Person? = Person(name: "Jong Ho", hobby: "BasketBall")
var introduction = jongho?.introduce
print(jongho?.introduce())
jongho = nil
print((introduction ?? {})())
'Swift' 카테고리의 다른 글
weak와 unowned의 차이 (ARC) (0) | 2022.11.08 |
---|---|
Swift - DispatchQueue (1) (0) | 2022.02.24 |
Swift - where 절 (0) | 2022.02.21 |
Swift - 타입 중첩 (0) | 2022.02.21 |
Swift - 제네릭 (0) | 2022.02.20 |
Comment