문과 코린이의, [IOS] 기록/Swift 이론

[문과 코린이의 IT 기록장] IOS Swift - 메모리 관리 / 메모리 참조 방법 (weak / strong / unowned)

벼리네 2023. 5. 25. 11:53
반응형

[문과 코린이의 IT 기록장] IOS Swift - 메모리 관리 / 메모리 참조 방법 (weak / strong / unowned)

[문과 코린이의 IT 기록장] IOS Swift - 메모리 관리  / 메모리 참조 방법 (weak / strong / unowned)

 


1. ARC (Automatic Reference Counting = 자동 참조 계수)

- IOS에서 앱의 메모리 사용자동으로 추적 및 관리해주는 역할을 한다.

 * 코드를 작성했을 때, 해당 참조하는 인스턴스들이 더 이상 필요하지 않게 된다면 ARC가 자동으로 메모리를 해제해준다.

- 이는 클래스에서 만들어진 객체에서만 적용되며, 밸류 타입 (구조체 / Enum) 등의 객체에는 적용되지 않는다.

- 인스턴스에 대한 모든 강한 참조들이 없어진다면, ARC는 자동으로 메모리를 해제해준다.

- 즉 객체의 reference count(강한 참조 개수)에 대해 자동으로 관리해주는 역할을 한다.

 

 ex. ARC 작동 예시 1

import Foundation

class ARCTest{
    // 멤버 변수
    let name : String
    
    // 생성자
    init(name : String){
        self.name = name
        print("생성자 완료")
    }
    
    // 소멸자
    deinit{
        print("소멸자 완료")
    }
}

// (해석) ARCTest 객체를 만들었고, ref1이라는 참조를 통해서 사용하겠다는 의미
// 즉 이 객체는, ref1이라는 포인터를 통해서 소유하고 있다는 의미
// 이 과정에서 소유가 생겼기 때문에, reference Count가 0 -> 1로 증가하게 된다.
var ref1 : ARCTest? = ARCTest(name : "ARC") // ref1 객체 생성

// (해석) ARCTest 객체를, ref2라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 1 -> 2로 증가하게 된다.
var ref2 : ARCTest? = ref1 // ref1에 대한 강한 참조

// (해석) ARCTest 객체를, ref3라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 2 ->3로 증가하게 된다.
var ref3 : ARCTest? = ref1 // ref1에 대한 강한 참조

// (해석) 더 이상 ref1이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 3 -> 2로 감소된다.
ref1 = nil // 메모리 해제

// (해석) 더 이상 ref2이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 2 -> 1로 감소된다.
ref2 = nil // 메모리 해제

// 그러나 ref3이 아직 ref1을 강한 참조하고 있으므로, 메모리 해제에 대한 소멸자(deinit)은 실행되지 않는다.

ex. ARC 작동 예시 2

import Foundation

class ARCTest{
    // 멤버 변수
    let name : String
    
    // 생성자
    init(name : String){
        self.name = name
        print("생성자 완료")
    }
    
    // 소멸자
    deinit{
        print("소멸자 완료")
    }
}

// (해석) ARCTest 객체를 만들었고, ref1이라는 참조를 통해서 사용하겠다는 의미
// 즉 이 객체는, ref1이라는 포인터를 통해서 소유하고 있다는 의미
// 이 과정에서 소유가 생겼기 때문에, reference Count가 0 -> 1로 증가하게 된다.
var ref1 : ARCTest? = ARCTest(name : "ARC") // ref1 객체 생성

// (해석) ARCTest 객체를, ref2라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 1 -> 2로 증가하게 된다.
var ref2 : ARCTest? = ref1 // ref1에 대한 강한 참조

// (해석) ARCTest 객체를, ref3라는 포인터를 사용해 소유하기 시작한다.
// 이 과정에서 소유가 생겼기 때문에, reference Count가 2 ->3로 증가하게 된다.
var ref3 : ARCTest? = ref1 // ref1에 대한 강한 참조

// (해석) 더 이상 ref1이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 3 -> 2로 감소된다.
ref1 = nil // 메모리 해제

// (해석) 더 이상 ref2이라는 포인터를 이용해 ARCTest 객체를 사용하지 않는다.
// reference Count가 2 -> 1로 감소된다.
ref2 = nil // 메모리 해제

// (해석) reference Count가 0이 되었으므로 ARCTest 객체가 해제되게 된다.
// reference Count 1 -> 0으로 감소된다.
ref3 = nil
// ref3까지 메모리 해제하게 되면서, ref1 객체에 대한 강한 참조가 모두 사라지게 된다.
// 따라서 소멸자(deinit)가 실행되게 된다. (ARC가 자동으로 작동한다는 근거가 됨)

 

 


2. 메모리 참조 방법

- 강한 참조만 쓰게 된다면, 메모리가 낭비되지 않도록 메모리 해제를 위해 굉장히 많은 신경을 써야 할 것이다.

- 이 문제를 해결하기 위해 IOS에서는 weak / unowned를 이용할 수 있도록 하며 이를 사용하면, 조금 더 효율적으로 메모리를 관리 가능하다.


1) strong (강한 참조)

- 강한 참조는 객체를 꼭  소유해야할 때 표현하는 방식이다. (이 객체는 필요한 객체라는 것을 명시하는 것이다)

- 강한 참조를 하게 되면 reference count(강한 참조 개수)가 증가하게 된다.

 * reference count가 0이라면, 객체 사용이 끝이 났다는 신호이다. => ARC를 통해 소멸자 수행

- 강한 참조는, reference count를 증가시켜, ARC로 인한 메모리 해제를 피하고, 객체를 안전하게 사용하고자 할 때 사용한다.

 

(1) 객체 소유 변수

- 객체를 소유하는 참조 변수의 종류에 따라, 강한 참조에 의한 객체의 메모리가 해제 되는 시점이 다르다.

a. 지역 변수 : 함수 종료와 함께 소유권 해제

class MyApplication{
	func sayHello(){
    	var obj : MyClass!
        obj = MyClass() // reference Count + 1
    
    	print("Hello")
    } // 함수가 종료되면서 obj라는 지역변수가 사라진다. 그리고 이와 동시에 객체에 대한 소유권이 사라진다.
     // reference Count - 1
}

b. 프로퍼티 : 객체 생명 주기에 따라 소유권 해제

class MyApplication{
	var obj : MyClass! // 프로퍼티로 객체를 소유하는 방식
    
    init(){
    	obj = MyClass() // myApplication 객체를 생성할 때의 생성자임
        // 따라서 var obj : MyClass! = MyClass() 와 같은 의미.
    	// MyApplicaton이라는 객체가 존재하는 한, obj는 계속 존재하게 된다.
    }
    
    func hello(){
    	obj.howAreYou()
    }
}

c. 타입 프로퍼티 : 해당 객체에 nil을 넣어서 소유권 해제

class MyApplication{
	static var obj : MyClass!
}

MyApplication.obj = MyClass() // reference count + 1
MyApplication.obj = nil // 객체에 nil을 넣어서 소유권 해제 : reference count - 1

 

(2) 콜렉션과 소유권

- 콜렉션에 객체 저장시 : 콜렉션이 객체 소유

- 콜렉션에서 객체 삭제시 : 소유권 해제

- 콜렉션 객체 해제시 : 소유권 해제

var obj : MyClass! = MyClass() // obj 포인터로 MyClass 객체 가르킴. reference Count + 1
var array = [obj] // array 배열 내 원소로 MyClass에 대한 소유권 가지게 됨. reference Count + 1
obj = nil // 메모리 소유권 해제. reference Count - 1

// array 배열이 MyClass 객체에 대한 소유권을 가지고 있으므로, 아직 reference Count가 남아있음.
array.remove(at:0) // 배열의 소유권 삭제. reference Count - 1

// MyClass 객체는 메모리 상에서 해제됨.

 

(3) 예시

// 위의 ARC 예시와 같은 코드입니다.

import Foundation

class ARCTest{
    // 멤버 변수
    let name : String
    
    // 생성자
    init(name : String){
        self.name = name
        print("생성자 완료")
    }
    
    // 소멸자
    deinit{
        print("소멸자 완료")
    }
}

var ref1 : ARCTest? = ARCTest(name : "ARC") // ref1 객체 생성

var ref2 : ARCTest? = ref1 // ref1에 대한 강한 참조
var ref3 : ARCTest? = ref1 // ref1에 대한 강한 참조

ref1 = nil // 메모리 해제
ref2 = nil // 메모리 해제

ref3 = nil
// ref3까지 메모리 해제하게 되면서, ref1 객체에 대한 강한 참조가 모두 사라지게 된다.
// 따라서 소멸자(deinit)가 실행되게 된다.

위의 ARC예시와 같은 내용

 

 

(4) 강한 순환 참조(Strong Reference Cycles)와 메모리 누수(Memory Leak) 문제

- 객체에 대한 소유관계가 잘못된 경우에는 메모리 누수가 일어날 수 있으며, 강한 순환 참조는 2개 이상의 관계에서도 가능하다.

- 이는 서로 소유권을 가지므로 해제되지 않는 상황이다.

- 이럴 경우에는 수동으로 해제하도록 프로그래머가 작성해야 한다.

ex. 정상 해제 상황

class ClassA{
	var objB : ClassB!
    deinit { print("A 객체 해제") }
}


class ClassB{
	var objA : ClassA!
    deinit { print("B 객체 해제") }
}

var a : ClassA! = ClassA() // ClassA객체에 대한 reference Count + 1
var b : ClassB! = ClassB() // ClassB객체에 대한 reference Count + 1

a = nil // ClassA()객체에 대한 reference Count - 1
b = nil // ClassB()객체에 대한 reference Count - 1

ex . 강한 순환 참조 발생

class ClassA{
	var objB : ClassB!
    deinit { print("A 객체 해제") }
}


class ClassB{
	var objA : ClassA!
    deinit { print("B 객체 해제") }
}

var a : ClassA! = ClassA() // ClassA객체에 대한 reference Count + 1
var b : ClassB! = ClassB() // ClassB객체에 대한 reference Count + 1

a.objB = b // ClassB객체에 대한 reference Count + 1
b.objA = a // ClassA객체에 대한 reference Count + 1

a = nil // ClassA객체에 대한 reference Count - 1
b = nil // ClassB객체에 대한 reference Count - 1

// reference Count가 각각 1개씩 남아있음.
// 서로에 대한 프로퍼티에 대해 상호 소유하고 있기 때문에 메모리 누수 발생


2) weak (약한 참조)

- 객체를 참조하지만, reference count(강한 참조 개수)는 변화하지 않는다. * 객체를 소유하지 않는 방법

- 약한 참조는 강한 순환 참조를 해결하기 위해 사용되는 보편적인 방법이다. 

- 주로 인스턴스의 생명주기가 짧을 때 사용한다. * 상호 독립적으로 존재하는 객체에 사용한다

  ex. 사용자와 스마트폰 / 운전자와 자동차

- 약한 참조는 순환 참조에 의한 메모리 누수 문제를 막기 위해 사용한다.

- weak는 참조하던 객체가 해제되면 nil이 된다. 따라서 nil값을 가질 수 있으므로 이는 옵셔널 타입이다.

import Foundation

class StrongRef1{
    weak var strongRef : StrongRef2? = nil // 약한 참조
}

class StrongRef2{
    var strongRef : StrongRef1? = nil // 약한 참조
}

var strongRef1 : StrongRef1? = StrongRef() // strongRef1 객체 변수 생성
var strongRef2 : StrongRef2? = StrongRef() // strongRef2 객체 변수 생성

strongRef1?.strongRef2 = strongRef2
strongRef2?.strongRef1 = strongRef1

// 두 객체의 메모리 해제
strongRef1 = nil
strongRef2 = nil

// 두 객체 변수는 nil을 넣어서 메모리가 해제되었다.

결과적으로 strongRef1 Instance와 storngRef2 Instance 둘 다, 소멸자가 작동한다.

 


3) unowned (미소유 참조)

- 객체를 참조하며, reference count(강한 참조 개수)는 변화하지 않는다. * 객체를 소유하지 않는 방법

- 이 또한 약한 참조와 마찬가지로, 인스턴스가 참조하는 것을 강하게 유지하지 않는다.

- 그러나 이는 다른 인스턴스와 같은 생명주기를 가지거나 더 긴 생명주기를 가질 때 사용한다.

- 미소유 참조는, Optional는 사용하지 못한다. 따라서 nil값을 가질 수 없고 항상 값을 지니고 있어야 한다.

 * 따라서 완전히 종속적인 경우에 사용한다.   ex. 신용카드와 사용자 / 국가와 수도

- 이는 참조하던 객체가 해제되도 nil로 변화하지 않는다. 따라서 Dangling pointer 위험이 존재한다.

- 미소유 참조는, 객체의 생명주기가 명확하고, 개발자에 의해 제어 가능이 명확한 경우, weak Optional 타입 대신해 사용한다.

 

ex. 문제 발생

class Contury{
	var capital : Capital!
}	

class Capital{
	unowned var country : Country // unowned는 옵셔널로 선언할 수 없음
    init(country : Country){ // 따라서 초기화가 필요함 (생성자에서 초기화)
    	self.country = country
    }
}


var korea : Country! = Country() // Country 객체에 대한 reference Count + 1
var seoul : Capital! = Capital(country : korea) // Capital 객체에 대한 reference Count = 1
// country에 대한 reference Count는 증가하지 않음.

korea.capital = seoul // korea의 captital 변수에 seoul 주소값 넣기

// 객체 해제 (오류 발생)
korea = nil;



ex. 정상 장동

class Contury{
	var capital : Capital!
}	

class Capital{
	unowned var country : Country // unowned는 옵셔널로 선언할 수 없음
    init(country : Country){ // 따라서 초기화가 필요함 (생성자에서 초기화)
    	self.country = country
    }
}


var korea : Country! = Country() // Country 객체에 대한 reference Count + 1
var seoul : Capital! = Capital(country : korea) // Capital 객체에 대한 reference Count = 1
// country에 대한 reference Count는 증가하지 않음.

korea.capital = seoul // korea의 captital 변수에 seoul 주소값 넣기

// 객체 해제 확인
seoul = nil korea = nil;

반응형