새벽의 기록

[SOPT] 문법 스터디 #7 초기화 해제 본문

SOPT/문법 스터디

[SOPT] 문법 스터디 #7 초기화 해제

OneTen 2024. 11. 17. 02:02
초기화 해제
    - deinit 메서드에 대해 알아보고, 메모리 누수를 방지하기 위한 deinit 활용 예시를 작성해봅시다 !
    - ARC와 순환 참조에 대해 알아봅시다 (생각과제보다 더 딥하게 들어가주세요)
    - 강한 순환 참조를 방지하기 위해서는 어떻게 해야 할까요?
    - 클로저와 초기화 해제의 관계를 클로저 캡처를 통한 메모리 누수 방지 중점으로 알아봅시다 (1주차 클로저 참고)

 

초기화 해제

1. deinit 메서드에 대해 알아보고, 메모리 누수를 방지하기 위한 deinit활용 예시를 작성해봅시다 !

deinit 메서드는 클래스 인스턴스가 메모리에서 해제될 때 호출되는 소멸자 메서드.
클래스 인스턴스가 더 이상 필요하지 않을 때 호출되며, 보통 파일이나 네트워크 연결 해제, 타이머 제거, 관찰자 제거 등의 작업을 수행하여 메모리 누수를 방지할 수 있다.
Swift는 자동으로 메모리 관리를 해주지만, deinit을 활용해 추가적인 정리 작업을 하면 메모리 관리가 더 안전해진다.
 
 
특징

  • 클래스에서만 사용할 수 있다.
  • 객체가 해제될 때 자동으로 호출된다.
  • 명시적으로 호출할 수 없으며, Swift의 자동 참조 카운트(ARC)에 의해 호출.
class MyClass {
    init() {
        print("MyClass initialized")
    }
    
    deinit {
        print("MyClass is being deinitialized")
    }
}

var instance: MyClass? = MyClass()  // "MyClass initialized" 출력
instance = nil                       // "MyClass is being deinitialized" 출력

 

2. ARC와 순환 참조에 대해 알아봅시다 (생각과제보다 더 딥하게 들어가주세요)

Swift는 ARC(Automatic Reference Counting)라는 메모리 관리 시스템을 사용하여 클래스 인스턴스의 수명을 관리한다.
ARC는 각 클래스 인스턴스에 대해 몇 개의 참조가 있는지 추적하여, 참조 카운트가 0이 되면 해당 인스턴스의 메모리를 해제한다.
 
즉, 참조 카운트가 1 이상이라면 메모리에서 할당 해제되지 않는다.
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/

Documentation

docs.swift.org

 

 

강한 참조(Strong Reference)와 순환 참조(Circular Reference)

 
강한 참조(Strong Reference)
ARC에서 참조 카운트를 증가시키는 것은 강한 참조라고 하며, 직접 카운트를 감소(nil)시켜주지 않으면 카운트가 내려가지 않는다.
기본적으로 변수는 클래스 인스턴스를 강하게 참조한다.
 
즉, 해당 변수가 존재하는 한 인스턴스의 참조 카운트는 증가하며, ARC는 그 인스턴스를 해제하지 않는다.

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

var person1: Person? = Person(name: "Kim") // 참조 카운트 1
var person2 = person1  // 참조 카운트 증가 - 참조 카운트 2
person1 = nil          // person1이 nil이 되어 참조 카운트는 1
person2 = nil          // 참조 카운트가 0이 되며, 메모리에서 해제

 
 
 
순환 참조(Circular Reference)
클래스 인스턴스 간에 서로 강한 참조를 할 경우, 참조 카운트가 0이 되지 않아 메모리 누수가 발생할 수 있는데,
이를 순환 참조(Circular Reference)라고 한다.
 
순환 참조는 두 개 이상의 인스턴스가 서로를 참조하면서 해제되지 않는 상태가 되는 상황이다.

class Person {
    var name: String
    var apartment: Apartment?
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\\(name) is being deinitialized")
    }
}

class Apartment {
    var unit: String
    var tenant: Person?
    init(unit: String) {
        self.unit = unit
    }
    deinit {
        print("Apartment \\(unit) is being deinitialized")
    }
}

var kim: Person? = Person(name: "Kim") // person 참조 카운트 1
var apt: Apartment? = Apartment(unit: "1A") // apartment 참조 카운트 1

kim?.apartment = apt // apartment 참조 카운트 2
apt?.tenant = kim  // 순환 참조 발생 - person 참조 카운트 2

kim = nil  // apt로 인한 참조 카운트때문에 Person이 해제되지 않음 - person 참조 카운트 1
apt = nil   // kim으로 인한 참조 카운트때문에 Apartment도 해제되지 않음 - apartment 참조 카운트 1

이 경우, Kim과 apt는 서로를 강하게 참조하고 있어 메모리에서 해제되지 않는다. 이러면 더 이상 필요하지 않은 데이터를 유지해야 하기 때문에 메모리 누수가 발생한다.

 

3. 강한 순환 참조를 방지하기 위해서는 어떻게 해야 할까요?

 
 
약한 참조(weak)
참조 카운트에 영향을 주지 않는 참조 방식. 참조 대상이 메모리에서 해제되면 자동으로 nil이 된다.

class Person {
    var name: String
    var apartment: Apartment?
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\\(name) is being deinitialized")
    }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // 약한 참조로 순환 참조 방지
    init(unit: String) {
        self.unit = unit
    }
    deinit {
        print("Apartment \\(unit) is being deinitialized")
    }
}

var kim: Person? = Person(name: "Kim") // Person reference count : 1
var apt: Apartment? = Apartment(unit: "1A") // Apartment reference count : 1

kim?.apartment = apt // Apartment reference count : 2
apt?.tenant = kim // Person reference count : 1

kim = nil  // Person 해제 - Apartment reference count : 1
apt = nil   // Apartment 해제

이렇게 약한 참조를 사용하면 순환 참조 문제를 해결할 수 있다.

 

4. 클로저와 초기화 해제의 관계를 클로저 캡처를 통한 메모리 누수 방지 중점으로 알아봅시다 (1주차 클로저 참고)

class Human {
    var name = ""
    lazy var getName: () -> String = {
        return self.name
    }
    
    init(name: String) {
        self.name = name
    }
 
    deinit {
        print("Human Deinit!")
    }
}

먼저, Human이란 클래스를 만들고 name을 얻을 수 있는 Lazy 프로퍼티를 클로저를 통해 초기화 했다.
 
 
그리고 다음과 같이

var hanyeol: Human? = .init(name: "Kim-Hanyeol")
print(hanyeol!.getName())

hanyeol이란 인스턴스를 만들고, 클로저로 작성되어 있는 getName이란 지연 저장 프로퍼티를 호출
 
 
그리고 나서 더이상 hanyeol이란 인스턴스가 필요 없어서

hanyeol = nil

이렇게 인스턴스에 nil을 할당했다.
 
 
그럼 인스턴스에 nil이 할당 되었고, 나는 이 인스턴스를 다른 변수에 대입한 적 없으니 인스턴스의 RC가 0이 되어 deinit이 호출되어야 하는데

// print: Kim-Hanyeol

… 안 된다.
이게 클로저의 강한 순환 참조때문인데
먼저 클로저는 참조 타입으로, Heap에 살고 있다.
 
따라서, 내가 생성한 human이란 인스턴스

print(hanyeol!.getName())

getName을 호출하는 순간 getName이란 클로저가 Heap에 할당되며, 이 클로저를 참조한다.
(지연 저장 프로퍼티니 인스턴스 생성 직후가 아닌, 호출되는 순간에 메모리에 올라감)
 
 
근데, getName이란 클로저를 보면

class Human {
    lazy var getName: () -> String = {
        return self.name
    }
}

이렇게 self를 통해 Human이란 인스턴스의 프로퍼티에 접근하고 있다.
 
클로저는 Reference 값을 캡쳐할 때 기본적으로 "strong"으로 캡쳐를 함.
→ 따라서, 이때 Human이란 인스턴스의 Reference Count가 증가
 
즉,
Human 인스턴스는 클로저를 참조하고, 클로저는 Human 인스턴스(의 변수)를 참조하기 때문에
서로가 서로를 참조하고 있어서 둘 다 메모리에서 해제되지 않는 강한 순환 참조가 발생해 버린 것.
→ 여기서 필요한 게 weak, unowned
저번 주 과제였던 캡쳐 리스트를 사용해서 강한 순환 참조를 방지해야 한다.

class Human {
    lazy var getName: () -> String? = { [weak self] in
        return self?.name
    }
}

이런 식으로 weak, unowned로 Reference Capture를 하면 클로저 리스트를 통해 강한 순환 참조를 해결해 줄 수 있다.
 
그러면

// print: Kim-Hanyeol
// print: Human Deinit!

deinit이 정상적으로 실행된다.

Comments