Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ARC 강한 순환 참조 (Strong Reference Cycles) #123

Closed
Youngminah opened this issue Jan 11, 2022 · 0 comments
Closed

ARC 강한 순환 참조 (Strong Reference Cycles) #123

Youngminah opened this issue Jan 11, 2022 · 0 comments

Comments

@Youngminah
Copy link
Owner

Youngminah commented Jan 11, 2022

image

아래의 글을 막힘없이 이해할 수 있다면 당신은 이미 ARC 고수..





기본적으로 ARC에 대해 알고있어야 이해 가능함

  • 까먹었다면 이전 포스팅자료 참고
  • ARC


강한 순환 참조

  • ARC에 대해 이해했다면, 기본적으로 참조횟수가 0이되면 ARC가 자동으로 메모리에서 해제시켜줌을 이해했을 것이다.
  • 하지만 절대로 ❗️ 메모리에서 해제되지 않는 경우도 있다.( RC가 0이 되지 않는 상황)
  • 강한 참조 순환이 발생하였을 때 그러함.
  • 강한 참조 순환이 발생할 수 있는 가능성은 크게 2가지 이다.
    • 클래스 인스턴스 간의 강한 참조 순환
    • 클로저에서의 강한 참조 순환
  • 차근차근 알아보고 해결법까지 알아보자



클래스 인스턴스간 강한 참조 순환

예제 1 : 참조 순환이 일어나지 않고 자동 Deinit되는 경우

class Guild {
    
    var guildName: String
    var owner: User?
    
    init(guildName: String) {
        print("Guild init")
        self.guildName = guildName
    }
    
    deinit {
        print("Guild Deinit")
    }
}

class User {
    
    var nickName: String
    var guild: Guild?
    
    init(nickName: String) {
        print("User init")
        self.nickName = nickName
    }
    
    deinit {
        print("User Deinit")
    }
}

var sesac: Guild? = Guild(guildName: "SeSAC") // Guild RC 1
sesac = nil // Guild RC 0

var nickname: User? = User(nickName: "미묘한도사") // User RC 1
nickname = nil // User RC 0



/*************** 출력 결과****************
Guild init
Guild Deinit
User init
User Deinit
*****************************************/

image

  • 위의 예제를 보면 ARC에 의해 자동 메모리에서 Deinit됨을 알 수 있다.
  • 그림 예제를 보면 좀더 쉽게 이해가 간다.
  • 참조 하고 있던 retain count가 0이 되면서 즉시 메모리에서 해제된다.

예제 2: 강한 참조 순환이 일어나는 경우

class Guild {
    
    var guildName: String
    var owner: User? 
    
    init(guildName: String) {
        print("Guild init")
        self.guildName = guildName
    }
    
    deinit {
        print("Guild Deinit")
    }
}

class User {
    
    var nickName: String
    var guild: Guild?
    
    init(nickName: String) {
        print("User init")
        self.nickName = nickName
    }
    
    deinit {
        print("User Deinit")
    }
}

var sesac: Guild? = Guild(guildName: "SeSAC")
var nickname: User? = User(nickName: "미묘한도사")

sesac?.owner = nickname // RC 2
nickname?.guild = sesac // RC 2

nickname = nil // RC 1
sesac = nil // RC 1



/*************** 출력 결과****************
Guild init
User init
*****************************************/

image

  • 그림으로 보면 왜 강한참조에서 순환 문제가 발생할 수 있는지 직관적 이해가 가능하다.
  • Retain Count (= 레퍼런스카운트)가 서로 클래스 안의 인스턴스간의 참조 때문에 0이 되지 않는다.
  • 따라서, nickname 인스턴스와 sesac이 둘다 nil이 되었음에도 불구하고 Deinit 되지 않는다.


클래스 인스턴스간의 강한 참조 순환 문제의 해결

약한 참조 Weak로 해결

class Guild {
    
    var guildName: String
    weak var owner: User? // weak로 선언하니 해제가 된다. 인스턴스 참조시 RC를 증가시키지 않음.
    
    init(guildName: String) {
        print("Guild init")
        self.guildName = guildName
    }
    
    deinit {
        print("Guild Deinit")
    }
}

class User {
    
    var nickName: String
    var guild: Guild?
    
    init(nickName: String) {
        print("User init")
        self.nickName = nickName
    }
    
    deinit {
        print("User Deinit")
    }
}

var sesac: Guild? = Guild(guildName: "SeSAC") // Guild RC 1
var nickname: User? = User(nickName: "미묘한도사") // User RC 1

sesac?.owner = nickname // User RC 1
nickname?.guild = sesac // Guild RC 2

nickname = nil // User RC 0 Guild RC 1
sesac?.owner
sesac = nil // Guild RC 0



/*************** 출력 결과****************
Guild init
User init
User Deinit
Guild Deinit
*****************************************/

image

  • 둘 중에 하나만 약한 참조 weak로 선언해도, 둘 다 Deinit될 수 있다.
  • 이 부분에서 참고 할 수 있는 점은 Deinit되는 순서를 보자 ❗️❗️
  • Guild클래스 안의 User인스턴스 owner를 약한 참조 객체로 선언했기 때문에
  • retain count가 (여기서) 최대 1인 User, Guild보다 먼저 Deinit 된다.
  • 즉, 디테일하게 설정해보자면 , 둘중에 하나만 weak로 선언할 것이라면
  • 둘 중에 수명이 더 짧아야 하는 인스턴스를 가리키는 프로퍼티를 약한 참조로 선언하는 것이 좋다. 💡
  • 주목할점: sesac?.owner에서 에러 발생하지 않음
  • User가 메모리에서 해제된 뒤 sesac?.owner가 혹시라도 사용되더라도 문제 없다.

미소유 참조 unowned로 해결

class Guild {
    
    var guildName: String
    unowned var owner: User
    
    init(guildName: String, owner: User) {
        print("Guild init")
        self.guildName = guildName
        self.owner = owner
    }
    
    deinit {
        print("Guild Deinit")
    }
}

class User {
    
    var nickName: String
    var guild: Guild?
    
    init(nickName: String) {
        print("User init")
        self.nickName = nickName
    }
    
    deinit {
        print("User Deinit")
    }
}

var nickname: User? = User(nickName: "미묘한도사")
var sesac: Guild? = Guild(guildName: "SeSAC", owner: nickname!)

nickname!.guild = sesac 
nickname = nil 
sesac?.owner // unowned 예제 에러 발생 💥
sesac = nil




/*************** 출력 결과****************
User init
Guild init
User Deinit
에러 발생!!
*****************************************/

image

  • Weak랑 다를 것 없는 예제 잖아? 했지만 에러발생
  • 미소유 참조는 약한 참조와 다르게 참조 대상이 되는 인스턴스현재 참조 하고 있는 것과 같은 생애 주기를 갖거나 더 길다고 본다.
  • 이게 대체 무슨 외계어야 ?! 🥲 🥲
  • weak 예제와 비교해보면 쉽게 이해할 수 있음
  • 즉 여기예제에서 대입해서 보면, owner(참조하고 있는 프로퍼티)참조하고있는 대상(User)와 같은 생애 주기 또는 더 짦아야 한다는 것
  • 차 근 차 근 읽어보면 이해가 갈 수 있음
  • 왜냐면 이 글을 쓰는 '현재의 나'는 이해했기 때문. 미래의 나야 이해했니..?
  • 그런데 owner보다 생애주기가 적어도 더 길어야 하는 User가 레퍼런스 카운트가 0 이 되면서 먼저 메모리에서 해제되었기 때문에
  • owner는 없는 주소값을 가지고 있기 때문에 참조 할때 에러가 뜨는것이다.
  • 이것으로 내릴수 있는 결론.
  • unowned로 선언된 참조 프로터티는 살아있는 동안 절대❗️❗️ nil을 할당 하지 않는다.
  • 즉, unowned로 선언된 owner은 옵셔널타입으로 선언하는 것은 잘못된 것임.
  • 우와우... 대충읽으면 절대로 이해할 수 없는 개념이도다....

image

미소유 참조와 암시적 옵셔널 프로퍼티 언래핑

  • 클래스 인스턴스에서 약한 참조, 미소유 참조는 해당 참조가 nil이 될 수 있느냐 없느냐로 구분할 수 있다.
  • 하지만 이 경우를 제외한 제 3의 경우도 발생할 수 있다.
  • 두 프로퍼티가 절대 nil이 되지 않는 경우임
  • 예제를 보자
class Country {
    let name: String
    var capitalCity: City!
    init(name: String, capitalName: String) {
        self.name = name
        self.capitalCity = City(name: capitalName, country: self)
    }
}

class City {
    let name: String
    unowned let country: Country
    init(name: String, country: Country) {
        self.name = name
        self.country = country
    }
}

var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
  • 모든 나라는 반드시 수도가 있어야 하고, 모든 수도는 반드시 나라에 포함되어야 한다.
  • 즉, 둘다 nil일 가능성 없음
  • 하지만 서로를 참조해야 하는 상황때문에 강한 참조 순환의 위험성이 생긴다.
  • 이러한 경우 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 언래핑으로 참조 문제 해결
  • 클래스 간의 상호의존을 위해 Countryinit 안에서 capitalCity(City 인스턴스)를 생성한다
  • 이 구조에서는 항상 Country를 먼저 만들어야 City를 만들 수 있다
  • 이니셜라이저 챕터에서 설명한 것처럼 capitalCity의 이니셜라이저에 넘길 self는,
  • 자기자신의 1단계 초기화가 끝날때까지(속성에 값이 다 들어갈 때까지) 유효하지 않다
  • 따라서 capitalCity를 암시적 언랩핑 옵셔널(!)로 설정하여
    • (1) 초기화 1단계 시점에서 capitalCitynil이어도 괜찮도록 하고
    • (2) 추후 capitalCity에 접근할 때 언랩핑할 필요가 없게 만들고
    • (3) 결과적으로 클래스 간의 상호의존성을 설정하면서
    • (4) strong 참조 사이클도 방지할 수 있다

image

  • 머리 터지기 전에 정리




클래스 인스턴스간의 강한 참조 순환 문제의 해결 정리 ⭐️

  • [Case1] 서로 nil이 될 수 있을 때 -> weak 참조 이용
  • [Case2] 한 쪽만 nil이 될 수 있을 때 -> unowned 참조 이용 (사라질수있는 클래스에서 상대방을 unowned 로 참조)
  • [Case3] 그럼 둘 다 nil이 될 수 없는 경우는 -> unowned 이용하되, 암시적 언래핑 옵셔널 프로퍼티



클로저에서의 강한 참조 순환

  • 클로저에서 강한 참조 순환은 캡쳐랑 관련 있음.
  • 아니 캡쳐가 뭔데 ?! 너무 불친절 하자나!!
  • 변수를 클로저 또는 중첩 함수에서 사용하게 될 경우 , 외부 변수를 내부적으로 저장해서 사용하게 되는데 이를 캡쳐라 부름
  • ❗️ 중요❗️ 클로저는 값을 캡쳐할 때, 클로저 자체가 클래스와 마찬가지로 참조타입이기 때문에
  • 캡쳐할 값이 값타입 이등 참조타입이든 관계없이 항상 참조캡쳐를 한다.
  • Reference Capture이기 때문에 변수는 참조 관계가 되고, 참조이기 때문에 클로저 내부의 값, 외부의값 변경하면 서로서로 영향을 받는다.
  • 여기까지 이해 했나욘?
  • 강한 참조 순환은 변수 뿐만 아니라 클로저와의 관계에서 발생할 수도 있다. 왜냐하면 클로저에서는 self를 캡쳐하기 때문이다.
  • 이 문제를 해결하기 위해 클로저 캡쳐 리스트를 이용한다.

예제 : 캡쳐 리스트는 무엇인가.. 부글 부글...🔥

  • 참조 관계를 벗어나기 위한 값 타입으로써 이용하고자 할 때 캡쳐리스트를 사용한다.
  • 캡쳐리스트로 캡쳐된 대상은 값타입 뿐만아니라 상수 let으로만 이용할 수 있다.
func firstClosure() {
    
    var number = 20
    print("1: \(number)")
   
    // number를 내부적으로 저장(캡쳐)하고 있음 = 복사하고있음.
    // 구조체, 값, 복사 -> 클래스처럼 참조가 되는 형태로 캡쳐가 되고 있다.
    // 왜? -> 클로저: 클로저는 캡처를 무조건 참조타입으로 하게된다. => 클로저는 Reference Capture를 한다.
    // [number] 이런식으로 사용할 변수를 괄호안에 주면 해당 변수는 값타입으로 복사 가능 , 상수로 캡처가 됨.
    // 그래서 number는 let형태가 되므로 클로저 안에서 변경 불가능하다.
    let closure: () -> Void = { [number] in
        //number = 50 // 오류 발생 💥
        print("closure: \(number)")
    }
    
    number = 100
    print("2: \(number)")
    
    closure()
}

firstClosure()


/*************** 출력 결과****************
1: 20
2: 100
closure: 20
*****************************************/
  • 왜 클로저 안의 number 값은 바뀐 100이 아닌 20이 나왔을까?
  • 값타입으로 이용하려고 캡쳐리스트를 이용하여 캡쳐하였기 때문.
  • 즉, number는 캡쳐리스트의 캡쳐로 변경할 수 없는 let의 값타입으로 이용가능함.

클로저에서의 강한 참조 순환 예제

class HTML {
    let name: String
    let text: String?
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    deinit {
        print("HTML Deinit")
    }
}
var paragraph: HTML? = HTML(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil


/*************** 출력 결과****************
<p>hello, world</p>
*****************************************/

image

  • self는 자기 자신 클래스를 가리키는건 상식이니 알쥬?
  • 클로저에서는 self를 참조 타입으로 캡쳐하기 때문에 참조타입 간의 강한 참조 순환이 이루어 진다.
  • 출력 결과에서 알수 있듯이 HTML 클래스가 Deinit되지 않고 있음.



클로저에서의 강한 참조 순환 문제의 해결

class HTML {
    let name: String
    let text: String?
    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    deinit {
        print("HTML Deinit")
    }
}
var paragraph: HTML? = HTML(name: "p", text: "hello, world")
print(paragraph!.asHTML())
paragraph = nil

/*************** 출력 결과****************
<p>hello, world</p>
HTML Deinit
*****************************************/

image

  • 이러한 경우 캡쳐리스트를 이용하여 self를 값타입으로 복사하되
  • 메모리 참조 방법을 weak, unowned 중에 하나로 명시해주면된다.
  • asHTML 클로져를 weak, unowned로는 안되나용?
  • 메모리 참조 방법(strong, weak, unowned)는 클래스에서만 쓰일 수 있다.

캡쳐리스트 약한 참조와 미소유 참조 구별법

  • unowned 참조
    • 클로저와 클로저가 캡쳐할 인스턴스(self) nil이 될 일이 없을 때 -> unowned 참조를 사용.
    • 즉, 항상 동시에 해제된다. (두 개의 라이프사이클이 동일한 경우이다)
  • weak 참조
    • 클로저가 캡쳐할 인스턴스가 어느 미래 시점에 nil이 될 수 있을 때
    • optional 타입일 때, 인스턴스가 해제되면 nil이 할당된다.
    • 따라서 클로저 바디 안에서 nil 체크를 하여 해제된 인스턴스에는 접근하지 않을 수 있다.



클로저에서의 강한 참조 순환 약한 참조 vs 미소유 참조 정리 ⭐️

  • 클로저에서 self가 nil이 될 수 있다면 [weak self]를 사용해라.
  • 클로저에서 self가 절대 nil이 아닌 경우 [unowner self]를 사용해라.



그래도 언제 써야 할지 모르겠어? 이 표를 활용해봐

image




참고자료




@Youngminah Youngminah changed the title 강한 순환 참조 (Strong Reference Cycles) ARC 강한 순환 참조 (Strong Reference Cycles) Jan 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant