[iOS/Swift] ARC - Swift의 메모리 관리 기법 그런데 이제 weak을 곁들인

ARC는 Swift의 메모리 관리기법이다.

RC 즉, Reference Counting이라는걸 이용해 자신을 참조하고 있는 수를 계산해서 메모리에서 해제할지를 결정하는 방식인데..

이번 글에서는 ARC에 대한 개념적인 내용은 생략하고, 실제 코드에서의 흐름을 조금 더 살펴볼 예정이다.

 

물론 자세한 코드는 뒤에서 볼 예정이지만, 흐름 중 다음과 같은 내용이 있었다.

"두 개의 클래스가 서로를 참조하고 있는 상황에서, 그 중 하나의 연결을 끊고, 해당 클래스를 참조하는 인스턴스에 nil을 할당해서 연결을 끊으면 모든 연결관계가 끊어져서 Deinit이 된다"

 

그런데 문득 이런 궁금증이 생겼다.

왜 서로 물려있는 연결관계중에 하나만 끊어줘도 모든 연결관계가 끊어질까?

 

이러한 의문은 흐름을 천천히 살펴보면서 해결해보겠다.


ARC 원리와 흐름

  • ARC는 클래스의 인스턴스가 더이상 필요하지 않을 때 메모리를 자동으로 해제한다
class Guild {
    var name: String
    
    init(name: String) {
        self.name = name
        print("Guild Init")
    }
}

class User {
    var nickname: String
    
    init(nickname: String) {
        self.nickname = nickname
        print("User Init")
    }
}

var myGuild: Guild? = Guild(name: "최강길드") // RC + 1
var character: User? = User(nickname: "도사") // RC + 1

  • 위 코드를 그림으로 나타내면 다음과 같음

 

  • 여기서 스택영역에 있는 `myGuild`와 `character`가 할일을 다 끝내고 스택영역에서 사라지면?(코드에선 예를 들기 위해 `nil`을 할당)
myGuild = nil
character = nil

⇒ 이렇게 인스턴스들은 힙영역에 남아있게 되는가? ❌

⇒ ARC로 인해 자동으로 해제됨

 

⇒ 자동으로 인스턴스까지 해제된 상태

  • 즉, RC가 0이되면 자동으로 메모리(힙 영역)에서 삭제가 되는것임

 

  • 다음 코드에서는 각각의 인스턴스가 참조하고있을 뿐 아니라 각 인스턴스 안에서의 프로퍼티로 인해 서로를 참조하는 상황
class Guild {
    var name: String
    var owner: User?
    
    init(name: String) {
        self.name = name
        print("Guild Init")
    }
}

class User {
    var nickname: String
    var guild: Guild?
    
    init(nickname: String) {
        self.nickname = nickname
        print("User Init")
    }
}

var myGuild: Guild? = Guild(name: "최강길드") // RC + 1
var character: User? = User(nickname: "도사") // RC + 1

myGuild?.owner = character // RC + 1
character?.guild = myGuild // RC + 1
  • 위 코드를 그림으로 나타내면 다음과 같음

 

  • 여기서 스택영역에 있는 `character`, `myGuild`에 `nil`을 할당해서 없어지게 하면 각 클래스 인스턴스끼리 참조하고 있는 애들을 없앨 수 있는 방법이 없어지게 됨!

⇒ 각 클래스의 RC는 1인 상태로 영구적으로 존재하게 돼버림

myGuild = nil
character = nil

⇒ 강한순환참조로 인한 메모리누수 발생

 

  • 그럼 스택영역의 `character`, `myGuild`에 `nil`을 할당하기 전에 클래스 인스턴스 안에서 서로 참조하는 애들부터 끊어준다면?
character?.guild = nil

  • 일단 `character.guild = nil` 의 의미는 User 인스턴스 안의 `guild`라는 프로퍼티에는 원래 Guild 인스턴스를 참조하고 있던 `myGuild`가 있었는데, 그거 대신에 `nil`을 할당함으로써 Guild를 참조하고있던 RC가 -1이 됨

⇒ 그래서 현재 상태는 Guild의 RC는 1, User의 RC는 2인 상태

 

  • 이 상태에서 `myGuild`, `character`에 `nil`을 할당함으로써 각각 인스턴스의 참조를 해제해준다면?
myGuild = nil
character = nil

  • 스택영역에 있던 `myGuild`와 `character`는 `nil`이 되면서 스택영역에서 사라짐
  • 각각 참조하고 있었던 인스턴스들의 RC가 -1씩 되면서 Guild의 RC는 0, User의 RC는 1이 됨

 

  • 이때! Guild의 RC가 0이 된다는것의 의미는!
  • Guild를 참조하고있는 애가 아무도 없다
  • ARC의 동작으로 인해 아무것도 참조하고 있지 않은 인스턴스는 메모리에서 해제한다
  • 즉, Guild인스턴스는 더이상 힙영역에 존재하지 않게 된다
  • 그말인 즉슨 Guild 인스턴스 안의 `name`, `owner` 프로퍼티들도 존재하지 않게 된다
  • `owner`에는 User 인스턴스를 참조하는 값이 들어있었는데 이친구도 사라지게 된다
  • 결국 User를 참조하고 있는 RC도 -1이 돼서 User의 RC는 0이 된다. ! !

  • User의 RC도 0이 됨으로써 User 인스턴스 역시 힙 영역에서 사라지게 된다
  • 모든 인스턴스들이 사라지면서 메모리에 아무것도 남아있지 않게 된다

 

그래서 서로 참조하는 애들중에 하나만 nil을 해줘도 전체가 메모리에서 해제가 되는 것이다!

 


weak의 역할

  • 이렇게 참조하는 값들을 하나하나 찾아서 `nil`을 할당해주는것은 현실적으로 불가능 ⇒ 참조가 일어날것같은 애들한테 `weak` 혹은 `unowned` 키워드를 붙여준다
    • `weak` 키워드가 붙은 애들이 다른 클래스를 참조할 때 RC가 올라가지 않게 된다
    • `weak` 키워드가 붙은 애들은 반드시 옵셔널 타입이어야 한다
class Guild {
    var name: String
    weak var owner: User?
    
    init(name: String) {
        self.name = name
        print("Guild Init")
    }
}

class User {
    var nickname: String
    var guild: Guild?
    
    init(nickname: String) {
        self.nickname = nickname
        print("User Init")
    }
}

var myGuild: Guild? = Guild(name: "최강길드") // RC + 1
var character: User? = User(nickname: "도사") // RC + 1

myGuild?.owner = character // RC 변동 X
character?.guild = myGuild // RC + 1

  • `owner`가 `weak`으로 선언되어있어서 `chracter`를 통해 User 인스턴스를 참조하고 있더라도 RC가 올라가지 않음

 

  • 이 상태에서 스택 영역에 있는 `myGuild`와 `character` 변수들에 `nil`을 할당해서 없앤다면?
myGuild = nil
character = nil

⇒ 각 인스턴스들의 RC가 -1씩 될것임

  • User 인스턴스의 RC가 0이 되었다!!
    • User를 참조하고있는 애가 아무도 없다
    • ARC의 동작으로 인해 아무것도 참조하고 있지 않은 인스턴스는 메모리에서 해제한다
    • 즉, User인스턴스는 더이상 힙영역에 존재하지 않게 된다
    • 그말인 즉슨 User 인스턴스 안의 `nickname`, `guild` 프로퍼티들도 존재하지 않게 된다
    • `guild`에는 Guild 인스턴스를 참조하는 값이 들어있었는데 이친구도 사라지게 된다
    • 결국 Guild를 참조하고 있는 RC도 -1이 돼서 Guild의 RC는 0이 된다.
    • Guild의 RC도 0이 됨으로써 Guild 인스턴스 역시 힙 영역에서 사라지게 된다

  • 모든 인스턴스들이 사라지면서 메모리에 아무것도 남아있지 않게 된다

마찬가지로 서로 참조하는 애들중에 하나에만 weak을 해줘도 전체가 해제되는 것이다..!!

 


weak과 unowned의 차이는?

  • 현재 `owner`에는 `weak` 키워드가 붙어있고, `owner`의 값인 `character`가 참조하는 User 인스턴스가 사라진 상태
    • 이렇게 되면 `weak` 키워드가 붙은 `owner`에는 자동으로 `nil`이 할당되게 됨

 

  • 반면 unowned는?
    • `owner`에 `weak`이 아닌 `unowned`키워드가 붙는다면 참조하는 값이 없어지면 자동으로 nil을 할당해주지 않고 계속 값을 찾게됨
class Guild {
    var name: String
    unowned var owner: User?
    
    // ...
}

 

⇒ 이렇게 되면 앱 동작에 문제가 생길 수도 있음

unowned로 인해 오류가 발생한 경우

 

  • 일반적으로 weak과 unowned를 구분하는 경우
  • 보통 수명이 더 짧은 인스턴스를 가리키는 애를 `weak` 해줌
    • 예를 들어 Guild가 사라지는것보다 User가 탈퇴할 가능성이 더 높다 User 인스턴스를 참조하는 Guild의 `owner`가 `nil`이 될 가능성이 더 높다 그럼 Guild의 `owner`를 `weak`으로 선언한다
  • 반면, 수명이 더 긴 인스턴스를 가리키는 애를 `unowned`해줌
    • 예를 들어 Guild가 사라지는것보다 User가 탈퇴할 가능성이 더 높다 그렇다면 만약 Guild의 `owner`에 `unowned`를 할 경우 오류가 날 가능성이 생긴다 그래서 수명이 더 긴 Guild인스턴스를 가리키는 User의 `guild`를 `unowned`해줌

※참고자료

https://babbab2.tistory.com/26

 

iOS) 메모리 관리 (1/3) - ARC(Automatic Reference Counting)

안녕하세요~~ 소들입니다 👀 오늘은 지난 시간 메모리 구조에 이어 Swift를 사용할 때 메모리 관리가 어떤 식으로 되는지에 대해 공부해볼 거예요 :) ARC 면접 단골 질문이라죠? 깔깔 iOS 개발자라

babbab2.tistory.com