Swift 메모리 관리에 대해 아라보자 - (2) 강한참조

Yang Si Yeon·2021년 6월 17일
1

이전 포스트 (1) ARC 편과 이어집니다...


강한참조란

전 포스팅에서 인스턴스를 메모리에 유지시키려면 ARC가 해당 인스턴스를 해제하지 않고 유지해야하는 명분을 제공해야한다고 하였다. 이 명분을 만들어 주는 것이 바로 강한참조다. 참조의 기본은 강한참조이기 때문에 별도의 식별자를 명시하지 않으면 강한참조를 한다.

인스턴스는 참조 횟수가 0이 되는 순간 메모리에서 해제되는데, 위에서 말했듯이 인스턴스를 다른 인스턴스의 프로퍼티, 변수, 상수 등에 할당할 때 강한참조를 사용하면 참조 횟수가 1 증가하고, nil을 할당해주면 참조 횟수가 1 감소한다.


자 이제 코드를 보자.
class Person {
	let name: String
    init(name: String) { 
    	self.name = name
        print("\(name) is being initialized")
    }
    deinit {
    	print("\(name) is being deinitialized")
    }
}

위 코드를 보면 Person 클래스는 인스턴스의 name 프로퍼티를 초기화하고 프린트하는 이니셜라이저와 인스턴스가 해제되었음을 알리는 디이니셜라이저를 가지고 있다.

그리고 아래에는 3개의 Person? 타입 변수가 선언되어 있으며, 해당 변수들은 옵셔널 타입이기 때문에 현재는 Person 인스턴스를 참조하고 있지 않고, nil 값으로 초기화된 상태이다.

var reference1: Person?
var reference2: Person?
var reference3: Person?

자 그리고 이제 새로운 Person 인스턴스를 만들고 3개의 변수 중 하나에 넣어보자.

reference1: Person(name: "ssionii")
// "ssionii is being initialized" 출력
// 인스턴스 참조 횟수: 1

새로운 Person 인스턴스는 처음 메모리에 생성되면 이니셜라이저가 호출되어 메세지를 출력한다.
이후 강한참조로 reference1에 할당되었기 때문에, Person 인스턴스의 참조 횟수가 1 증가한다.

reference2 = reference1
// 인스턴스 참조 횟수: 2
reference3 = reference1
// 인스턴스 참조 횟수: 3

이제 하나의 Person 인스턴스가 3개의 변수에 강한 참조로 참조되고 있으며, 참조 횟수는 3이다. 따라서 계속 메모리에 살아있게 된다.

reference1 = nil	// 인스턴스 참조 횟수: 2
reference2 = nil 	// 인스턴스 참조 횟수: 1
reference3 = nil	// 인스턴스 참조 횟수: 0
// "ssionii is being deinitialized" 출력

두개의 변수에 차례로 nil을 할당하면 참조 횟수는 감소한다. 하지만 0이 아니기 때문에 Person 인스턴스는 여전히 살아있다. 마지막으로 reference3에 nil을 할당하면 참조 횟수는 0이 되고 ARC 규칙에 의해 메모리에서 해제되며 메모리에서 해제되기 직전에 디이니셜라이저를 호출해 메세지가 출력된다.


이번엔 아래 두개의 코드를 비교해보자.

func foo() {
	let reference: Person = Person(name: "ssionii")
    // ssionii is being initialized
    // 인스턴스 참조 횟수: 1
    
    // 함수 종료 시점
    // 인스턴스 참조 횟수: 0
    // ssionii is beign deinitialized
}
foo()
var globalReference: Person?

func foo() {
	let reference: Person = Person(name: "ssionii")
    // ssionii is being initialized
    // 인스턴스 참조 횟수: 1
    
    globalReference = reference
    // 인스턴스 참조 횟수: 2
    
    // 함수 종료 시점
    // 인스턴스 참조 횟수: 1
}
foo()

위 코드는 인스턴스가 foo() 함수 내부에서 생성된 후 강한 참조로 reference 상수에 참조된 것은 같다. 하지만 아래 코드는 인스턴스가 전역변수 globarReference에 강한참조 되면서 참조 횟수가 1 더 증가하여 2가 되었고, 이 상태에서 함수가 종료되어도 참조 횟수 1이 남아있어 메모리에서 인스턴스가 해제되지 않는다.


강한참조 순환 문제

위 예시에서 ARC가 클래스 인스턴스에 대한 참조 횟수를 계산하고, 해당 인스턴스가 더 이상 필요하지 않을때 인스턴스를 할당 해제하는 것을 보았다.

그런데 복합적으로 강한참조가 일어나는 상황에서 강한참조의 규칙을 모르고 사용하게 되면 문제가 발생할 수 있다. 여기서 말하는 복합적이라는 말은 인스턴스끼리 서로가 서로를 강한참조할 때를 말한다. 이 문제를 강한참조 순환 (Strong Reference Cycle)이라고 하는데, 코드를 통해 살펴보자.

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("\(name) is being deinitialized")
    }
}    

위 코드에서 Person 클래스는 강한참조를 하는 Room? 타입의 저장 프로퍼티 room을 가지며, Room 클래스는 강한참조를 하는 Person? 타입의 저장 프로퍼티 host를 가진다. 어떤 사람은 방을 가지고 있지 않을수도 있고, 어떤 방은 공실일 수도 있으므로 Optional 타입으로 정의되어 있다.

자 이제 사람에게 방을 주고, 방에게 호스트를 줘보자.

var ssionii: Person? = Person(name: "ssionii")	// Person 인스턴스 참조 횟수: 1
var room: Room? = Room(number: "1004")		// Room 인스턴스 참조 횟수: 1

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

위 코드에서 Person 클래스의 인스턴스와 Room 클래스의 인스턴스가 서로 강한참조를 하고 있다.

서로 강한참조를 하고 있는 상태에서 ssionii 변수에 nil을 할당하면 ssionii가 참조하는 인스턴스 (Person 인스턴스)의 참조 횟수는 1 감소하여 1이 된다. 이제 Person 인스턴스를 참조할 방법은 변수 room이 참조하는 인스턴스(Room 인스턴스)의 host 프로퍼티로 접근하는 방법밖에 없다.

하지만 개발자가 실수로 ssionii와 room 변수를 동시에 nil로 초기화 한다면?

ssionii = nil	// Person 인스턴스 참조 횟수: 1
room = nil	// Room 인스턴스 참조 횟수: 1

Person 인스턴스와 Room 인스턴스의 참조 횟수가 1로 남아 있지만, 해당 인스턴스들에 접근할 방법이 없다. ARC의 규칙대로라면 참조 횟수가 0이 되지 않으면 인스턴스를 메모리에서 해제시키지 않기 때문에 두 인스턴스 모두 메모리에 좀비처럼 남아있게 된다. 디이니셜라이저가 호출되지 않은 것을 보면 메모리에서 해제되지 않고 계속 남아있다는 것을 알 수 있다. 이렇게 메모리 누수가 발생된다.

해결 방법

물론 ssionii와 room 변수에 동시에 nil을 할당하지 않고, 아래와 같은 방법으로 nil을 넣어주면 인스턴스를 메모리에서 해제시킬 수 있긴하다.

var ssionii: Person? = Person(name: "ssionii")	// Person 인스턴스 참조 횟수: 1
var room: Room? = Room(number: "1004")		// Room 인스턴스 참조 횟수: 1

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

ssionii?.room = nil				// Room 인스턴스 참조 횟수: 1
ssionii = nil					// Person 인스턴스 참조 횟수: 1

room?.host = nil				// Person 인스턴스 참조 횟수: 0
room = nil						// Room 인스턴스 참조 횟수: 0

하지만 프로퍼티가 너무 많거나 복잡하게 얽혀있으면, 이러한 해결방법은 귀찮고 어렵다.

그럼 어떻게 해결해야할까?

다음편에 계속 ..


참고

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

스위프트프로그래밍 3판 - 야곰

profile
가장 젊은 지금, 내가 성장하는 데에 쓰자

0개의 댓글