본문 바로가기
iOS/Swift

메모리 누수가 발생하는 상황 : 강한 참조 사이클 + 해결방안?

by 나리._. 2022. 1. 17.

이 포스팅을 보기전 RC가 무엇인지 모른다면 아래 포스팅을 먼저 보시면 도움이 되실거 같슴다 ^.^

https://yesiamnahee.tistory.com/168

 

힙 메모리 관리 - ARC 정리

힙 메모리를 왜 관리해야할까? 스위프트에서 값 형식은 스택에 저장이 되고 그 스택의 스코프가 종료되면 메모리에서 자동으로 제거되기 때문에 메모리 관리를 할 필요가 없다 ! 그에 반해, 

yesiamnahee.tistory.com

 

메모리 누수는 강한 참조 사이클이 있을때 발생한다.

 

강한 참조 사이클이란?

클로저와 인스턴스가 강한 참조로 서로를 가르키고 있다면 강한 참조 사이클(Strong Reference Cycle)발생.

 

 

객체간의 강한 참조 사이클 예시

lass Dog {
    var name: String
    var owner: Person?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}


class Person {
    var name: String
    var pet: Dog?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}


var bori: Dog? = Dog(name: "보리") // bori RC: 1
var gildong: Person? = Person(name: "홍길동") // gildong RC: 1


bori?.owner = gildong // gildong RC: 2
gildong?.pet = bori // bori RC: 2


// 강한 참조 사이클(Strong Reference Cycle)이 일어남
// 메모리주소에 nil을 담아도 RC가 1이기 때문에 메모리 해제가 안된다. !!!
bori = nil // bori RC: 1
gildong = nil // gildong RC: 1

 

위의 코드를 보면

객체가 서로를 참조하는 강한 참조 사이클 인해

변수의 참조에 nil 할당해도 RC가 0이 되지 않기 때문에 

메모리 해제가 되지 않는 메모리 누수(Memory Leak) 상황이 발생한다 !

 

 

그럼 이러한 메모리 누수(Memory Leak)의 해결방안은 무엇일까?

 

가장 원초적인 방법으로는..

RC를 고려하여, 참조 해제 순서를 주의해서 코드 작성 

but.. 이것은 코드가 길어지고 많아질수록 .. 신경쓸 것이 많음/실수 가능성 ⬆️..로 이어진다..

 

 

따라서 아래의 두 방법을 통해 위의 코드의 메모리 릭을 해결해보잣!

 

 - 1) Weak Reference (약한 참조)

 - 2) Unowned Reference (비소유 참조)

 

 

1) Weak Reference (약한 참조)

class Dog {
    var name: String
    weak var owner: Person?         // weak 키워드 ==> 약한 참조
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}


class Person {
    var name: String
    weak var pet: Dog?         // weak 키워드 ==> 약한 참조
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }
}


var bori: Dog? = Dog(name: "보리")
var gildong: Person? = Person(name: "홍길동")


// 상대방의 RC 올라가지 않게끔
// 강한 참조 사이클이 일어나지 않음
bori?.owner = gildong
gildong?.pet = bori

gildong = nil
bori?.owner   // gildong만 메모리 해제시켰음에도 ===> nil
bori = nil

 

위의 코드를 실행해보면

 

 

 

사진과 같이 메모리 해제가 잘되는 것을 확인할 수 있다!

약한 참조의 경우, 참조하고 있던 인스턴스가 사라지면 nil로 초기화 되어있다 !!!

 

여기서 주의!

weak 키워드는 var 키워드만 가능하고 옵셔널로 선언해야한다.

왜일까?

weak 가리키는 상대방이 없어지면 자동으로 nil 할당을 시키기 때문에 변수로 선언해야하고 옵셔널로 선언되어야한다.

 

 

 

2) Unowned Reference (비소유 참조)

class Dog {

    var name: String
    unowned var owner: Person?    // Swift 5.3 이전버전에서는 비소유참조의 경우, 옵셔널 타입 선언이 안되었음
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) 메모리 해제")
    }

}



class Person {

    var name: String
    unowned var pet: Dog?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) 메모리 해제")
    }
}


var bori: Dog? = Dog(name: "보리")
var gildong: Person? = Person(name: "홍길동")


// 강한 참조 사이클이 일어나지 않음
bori?.owner = gildong
gildong?.pet = bori



// 1) 에러발생하는 케이스
// nil로 설정하고 접근하면 ===> 에러 발생

gildong = nil

// bori?.owner   // nil로 초기화 되지 않음 -> 에러!



// 2) 에러가 발생하지 않게 하려면

bori?.owner = nil      // 에러 발생하지 않게 하려면, nil로 재설정 필요 ⭐️

bori?.owner

 

 

Weak Reference (약한 참조) / Unowned Reference (비소유 참조)

두 방식의 차이점은 뭘까?

 

위의 코드를 보았으면 알겠지만,

비소유 참조의 경우, 참조하고 있던 인스턴스가 사라지면 nil로 초기화 되지 않고

약한 참조의 경우, 참조하고 있던 인스턴스가 사라지면 nil로 초기화 된다.

 

 

그럼 비소유 참조는 언제 사용하는것이 좋을까?

참조하고 있던 인스턴스가 사라지면 nil로 초기화 되지 않기 때문에~

nil 초기화되는 인스턴스보다 훨씬  짧게 존재할것이 보장이 된다면 unowned 사용하면 된다.

 

 

그럼 클로저에서는 어떻게 강한 참조 사이클을 해결할까?

 

클로저도 마찬가지로 weak, unowned 키워드 이용!

클로저에서는 해당 키워드를 어떻게 사용할까?

 

클로저의 강한 참조 사이클 예시

class Dog {
    var name = "초코"
   
    var run: (() -> Void)?
    
    func walk() {
        print("\(self.name)가 걷는다.")
    }
    
    func saveClosure() {
        // 클로저를 인스턴스의 변수에 저장
        run = {
            print("\(self.name)가 뛴다.")
        }
    }
    
    deinit {
        print("\(self.name) 메모리 해제")
    }
}

func doSomething() {
    let choco: Dog? = Dog()
    choco?.saveClosure()       // 메모리해제 되지않음 
}

doSomething()

 

이렇게 코드를 작성하면

run 변수에 클로저를 담았기 때문에 클로저를 가르키고 있다 (클로저 RC:1)

클로저는 힙에 저장되어 외부 변수(self)를 사용하기 위해 가르키고 있다. (choco RC:1)

doSomething() 함수 실행 (choco RC:2)

 

-> doSomething 함수가 종료되도 RC:1이기 때문에 메모리가 해제되지않음.

-> 강한참조사이클 발생!

 

 

클로저의 강한 참조 해결: 캡처 리스트 + 약한/비소유 참조 선언

class Dog {
    var name = "초코"
    
    var run: (() -> Void)?
    
    func walk() {
        print("\(self.name)가 걷는다.")
    }
    
    func saveClosure() {
        // 클로저를 인스턴스의 변수에 저장
        run = { [weak self] in
            print("\(self?.name)가 뛴다.")
        }
    }
    
    deinit {
        print("\(self.name) 메모리 해제")
    }
}


func doSomething() {
    let choco: Dog? = Dog()
    choco?.saveClosure() 
}


doSomething()

doSomething 함수가 종료되면 choco 인스턴스 메모리가 해제되고

해당 인스턴스가 해제되면 클로저의 RC 또한 0 되기 때문에 클로저도 사라진다. 

 

위의 코드를 실행해보면 아래와 같이 메모리 해제가 잘되는 것을 확인할 수 있다.

 

 

 

여기까지 강한 참조 사이클 + 해결방안을 정리했다 ~~!