궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 고급문법 [ARC 메모리 관리 2 - 강한 참조 순환 문제]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 bongcando님의 게시글을 참고하여 작성되었습니다.

 

강한 참조 순환 문제

순환 참조란 두 가지 이상의 객체가 서로에 대한 강한 참조(Strong Reference) 상태를 가지고 있을 때 발생하며, 순환 참조가 발생하게 되면 서로에 대한 참조가 해제되지 않기 때문에 메모리에서 유지되며 이로 인해 메모리 릭이 발생하게 된다.

 

이런 순환 참조를 해결하기 위해 약한 참조(weak), 미소유 참조(unowned) reference를 사용한다.

단일 인스턴스에 대한 참조

인스턴스 끼리 서로가 서로를 강한 참조할 때를 대표적인 예로 들 수 있는데, 이를 강한 참조 순환이라고 한다.

 

예를 한번 보자

 

먼저 name 프로퍼티를 이니셜라이저로 가지는 Person 클래스가 있고, 옵셔널 Person? 타입을 가질 수 있는 옵셔널 변수를 정의하고 출력하는 예이다.

import UIKit

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

이것을 출력해보면

var student1: Person?
var student2: Person?
var student3: Person?

print(student1) //nil 
print(student2) //nil 
print(student3) //nil

student1, student2, student3 의 변수 들는 다음과 같은 특징을 가지게 된다는 것을 확인할 수 있다.

  • student1, student2, student3 변수는 옵셔널 Person? 타입으로 선언되며 nil 값으로 할당됨.
  • Person 인스턴스로 참조되지 않음.

그럼 sutendet1 변수를 Person인스턴스에 할당해보자면

student1 = Person(name: "Seogun") 
//Seogun is being initialized

student1Person클래스의 이니셜라이저가 호출되어 print("\(name) is being initialized")를 출력했다. 하지만 deinit 내부의 print 함수가 호출되지 않는 것으로 보아 디이니셜라이저는 호출되지 않았다는 것을 알 수 있다.

이 새로운 인스턴스가 student1에 할당 되었고, student1과 강한 참조를 가지게 되었다. Person 인스턴스가 student1에 대한 강함 참조를 가지게 되면서 ARCPerson 클래스의 메모리에 유지하고 할당해제 시키지 않게 만들게 된다.

 

만약 student1이 가지고 있는 참조를 해제하면 student1이 가지고 있었던 참조를 해제하면서 Person 인스턴스는 어떠한 참조도 가지고 있지 않게 되며 디이니셜라이저가 호출된다.

student1 = nil
//Seogun is being deinitialized 출력

그리고 하나의 인스턴스에 두 가지 이상 변수를 할당해보도록 하면

student1 = Person(name: "Seogun")
//Seogun is being initialized

student2 = student1
student3 = student1

//메모리 주소
print(Unmanaged.passUnretained(student1!).toOpaque()) //0x0000600003418a40
print(Unmanaged.passUnretained(student2!).toOpaque()) //0x0000600003418a40
print(Unmanaged.passUnretained(student3!).toOpaque()) //0x0000600003418a40

student1, student2, student3 모두 같은 메모리 주소를 참조하고 있다는 것을 알 수 있다!

 

student2, student3 변수에 Person 인스턴스를 참조하고 있는 student1을 할당하면서, Person 인스턴스에 세 개의 강한 참조가 걸리게 되며, Person 인스턴스는 자신에게 할당된 세 개의 강한 참조가 해제되기 전까지는 할당 해제되지 않는다.

 

그리고 student1student2가 참조 해제 되어도 Person 인스턴스는 여전히 student3에 대한 강한 참조를 가지고 있기 때문에 할당 해제되지 않는다.

student1 = nil
student2 = nil

print(student1) //nil
print(student3) //옵셔널 Person

마지막으로 student3의 참조를 해제하게 되면 디이니셜라이저가 호출되면서 모든 참조에서 해제 되게 된다.

student1 = nil
student2 = nil

print(student1) //nil
print(student3) //옵셔널 Person

student3 = nil
//Seogun is being deinitialized

TIP
 
 

인스턴스에 대한 참조는 여러 개가 생길 수 있고, 해당 인스턴스를 deinit 시키기 위해서는 해당 인스턴스가 가진 모든 참조를 해제시켜야 한다.

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

ARCPerson 인스턴스에 대한 참조의 개수를 트래킹 하고, Person 인스턴스가 더 이상 필요하지 않게 되어 참조가 0이 된다면 할당 해제시킨다는 것을 알 수 있었다.

 

그런데 만약 두 클래스 인스턴스가 서로에 대한 참조를 유지하게 된다면?

위에서 인스턴스 끼리 서로가 서로를 강한 참조할 때, 이를 강한 참조 순환이라 한다고 했었다. 강한 순환 참조가 발생하는 경우에 대해서 알아보도록 하자!

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!")
    }
}
  • Person 클래스는 String 타입의 name 프로퍼티와 nil로 초기화된 옵셔널 Room? 타입의 room 프로퍼티가 있다.
  • Room 클래스는 String 타입의 number 프로퍼티와 nil로 초기화된 옵셔널 Person? 타입의 host 프로퍼티가 있다. 

여기에 각각 Person?Room? 옵셔널 타입을 가지는 변수를 정의하고 Person, Room 인스턴스를 할당해보자!

var seogun: Person? = Person(name: "서근") //Person 의 인스턴스 참조 횟수 : 1
var myRoom: Room? = Room(number: "50") //Room 의 인스턴스 참조 횟수 : 1

이제 Personroom을, Room에는 host를 할당해보면

seogun?.room = myRoom // Room 인스턴스 참조 횟수 : 2
myRoom?.host = seogun // Person 인스턴스 참조 횟수 : 2

이렇게 각 인스턴스의 room, host 프로퍼티에 값을 저장했고, 인스턴스 간의 참조는 다음과 같다.

Person의 인스턴스는 Room인스턴스에 대해 강한 참조를 가지게 되었고, Room의 인스턴스는 Person인스턴스에 대해 강한 참조를 가지게 되어 두 인스턴스 사이에 강한 참조 사이클이 생성됐다.

 

각 변수에 할당된 참조횟수는 다음과 같다.

  1. seogun (총 2회 참조)
    • Person의 인스턴스를 강한 참조 하며 reference count 증가 ( 참조 횟수 : 1)
    • Person의 인스턴스가 Room 인스턴스에 대해 강한 참조를 가지게 되며 reference count 증가 ( 참조 횟수 : 2)
  2. myRoom (총 2회 참조)
    • Room의 인스턴스를 강한 참조 하며 reference count 증가 ( 참조 횟수 : 1)
    • Room의 인스턴스가 Person 인스턴스에 대해 강한 참조를 가지게 되며 reference count 증가 ( 참조 횟수 : 2)

이 두 번수에 nil을 할당해 참조에서 해제해보게 된다면

seogun = nil // Person 인스턴스의 참조 횟수 : 1
myRoom = nil // Room 인스턴스의 참조 횟수 : 1
  • seogun을 완전히 참조에서 해제하려고 했지만, Room 인스턴스가 seogun에 대한 강한 참조를 유지하고 있으므로 메모리에서 사라지지 않고 Person의 인스턴스 참조 횟수가 1로 낮아진다.
  • myRoom을 완전히 참조에서 해제하려고 했지만, Person 인스턴스가 myRoom에 대한 강한 참조를 유지하고 있으므로 메모리에서 사라지지 않고 Room의 인스턴스 참조 횟수가 1로 낮아진다.

위와 같이 서로에 대한 강한 참조로 인해 인스턴스를 제대로 해지할 수 없는 상태가 강한 순환 참조(strong retain cycle) 상태이다.

 

그럼 메모리에서 완전히 해제 하려면 어떻게 해야 할까? 

 

이럴 때 강한 참조 순환 문제를 수동으로 해결 가능하다. (비추천 이유는 밑에서)

 

seogun?.roomRoom 인스턴스에 대해 강한 참조를 유지 중이고, myRoom?.hostPerson의 인스턴스에 대해 강한 참조를 유지 중이니까 이 부분을 먼저 nil 해주고, 그 뒤에 seogunmyRoomnil을 할당해주면 디이니셜라이저가 호출된다.

var seogun: Person? = Person(name: "서근") // Person 인스턴스의 참조 횟수 : 1
var myRoom: Room? = Room(number: "50") // Room 인스턴스의 참조 횟수 : 1

seogun?.room = myRoom // Room 인스턴스의 참조 횟수 : 2
myRoom?.host = seogun // Person 인스턴스의 참조 횟수 : 2

seogun?.room = nil // Room 인스턴스의 참조 횟수 : 1
myRoom?.host = nil // Person 인스턴스의 참조 횟수 : 1

seogun = nil // Person 인스턴스의 참조 횟수 : 0
// 서근 is being deinitialized!

myRoom = nil // Room 인스턴스의 참조 횟수 : 0
// Room 50 is being deinitialized!

변수 또는 프로퍼티에 nil을 할당하면 참조 횟수가 감소한다는 규식을 생각해보면 위와 같은 방법으로 해결할 수 있을지도 모른다.

 

하지만 만약 실수로 코드를 빼먹거나 아니면 해제해야할 프로퍼티가 너무 많거나 귀찮다면???

 

이럴 때!!! 약한 참조미소유 참조를 통해 더 명확한 해결책을 찾을 수 있다.

 

ARC는 이러한 강한 순환 참조에 대한 메모리 관리를 해주지 않기 때문에 약한 참조(weak reference), 미소유 참조(unowned reference)를 사용해서 해결해야 한다.

 

 

읽어주셔서 감사합니다 🤟

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.