본문 바로가기
Swift

[Swift] 스위프트 메모리 관리

by giop15 2023. 10. 26.
반응형

이번 게시글에서는 스위프트에서 메모리 관리에 대해서 알아보겠습니다.

 

메모리 관리와 ARC (Automatic Reference Counting)

스위프트에서 메모리를 관리하는 방법 중 하나는 ARC, 즉 Automatic Reference Counting입니다. 이것은 클래스 인스턴스를 더 이상 사용하지 않을 때 자동으로 메모리에서 해제해주는 기술입니다. ARC는 참조 카운트(Reference Count)를 사용하여 객체의 생존 주기를 관리합니다.

 

참조 카운트와 메모리 해제

참조 카운트는 현재 인스턴스를 참조하고 있는 개수를 나타냅니다. 참조 카운트가 0이 되면 해당 인스턴스는 더 이상 필요하지 않다는 것을 의미하며, 메모리에서 자동으로 해제됩니다. 참조 카운트는 증가하고 감소할 수 있습니다.

  • 참조 카운트가 증가할 때: 인스턴스를 새로 생성하는 경우, 기존 인스턴스를 다른 변수에 대입할 때.
  • 참조 카운트가 감소할 때: 변수가 더 이상 해당 인스턴스를 가리키지 않을 때, 변수에 nil이 지정되었을 때, 변수에 다른 값을 대입한 경우, 속해 있는 인스턴스가 메모리에서 해제될 때.

Person 클래스 객체를 사용한 예시를 보겠습니다.

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

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

reference1 = Person(name: "randy") // 참조 카운트: 1
// "randy is being initialized"

reference2 = reference1 // 참조 카운트: 2
reference3 = reference1 // 참조 카운트: 3
 
reference3 = nil // 참조 카운트: 2
reference2 = nil // 참조 카운트: 1
reference1 = nil // 참조 카운트: 0
// "randy is being deinitialized"

Person 클래스 객체를 생성하고 참조 변수들을 사용하여 참조 카운트가 어떻게 변하는지 주목하세요. 참조 변수들을 nil로 설정함으로써 메모리에서 어떻게 해제되는지 확인할 수 있습니다.

위의 예시에서는 기본적인 메모리 관리 및 참조 카운트의 작동 방식을 살펴보았습니다.

 

그러나 때로는 순환 참조 문제가 발생할 수 있으며, 이것은 메모리 누수를 초래할 수 있습니다.

순환 참조란 서로를 참조하는 두 개의 객체 사이의 참조 구조를 의미합니다.

예를 들어, 만약 두 객체가 서로를 강한 참조로 참조한다면, 이것은 순환 참조로 이어질 수 있습니다.

 

아래의 코드를 통해 이를 설명합니다

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

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

var person: Person?
var pet: Pet?

person = Person(name: "randy") // Person 참조 카운트: 1
// "randy is being initialzed"

pet = Pet(name: "puppy") // Pet 참조 카운트: 1
// "puppy is being initialized"

person!.pet = pet // Pet 참조 카운트: 2
pet!.owner = person // Person 참조 카운트: 2

person = nil // Person 참조 카운트: 1
pet = nil // Person 참조 카운트: 1

인스턴스의 변수에 nil을 지정해주었지만, 아직 참조 카운트가 남아 있어서 메모리에서 해제가 되지 않습니다. 그리고 인스턴스를 가리키는 변수들이 nil로 지정받아 메모리에 해제되어서 남아 있는 참조 카운트를 감소시킬 수도 없습니다. 필요하지 않는 데이터들을 메모리에 가지고 있어 메모리 누수 현상이 발생하고 있습니다.

 

이러한 문제를 해결하기 위해 약한 참조미소유 참조라는 방법을 사용합니다.

 

[약한 참조]

약한 참조는 자신이 참조하고 있는 인스턴스의 참조 카운트를 증가시키지 않습니다. 참조 타입의 프로퍼티나 변수 앞에 weak 키워드를 사용합니다. 그리고 자신이 참조하고 있는 인스턴스가 메모리에서 할당이 되면 nil을 할당하기 때문에 약한 참조는 무조건 변수로 선언을 해야하고 타입은 옵셔널 타입이어야 합니다.

 

아래는 약한 참조를 사용하는 예시입니다.

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

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

var person: Person?
var pet: Pet?

person = Person(name: "randy") // Person 참조 카운트: 1
// "randy is being initialzed"

pet = Pet(name: "puppy") // Pet 참조 카운트: 1
// "puppy is being initialized"

person!.pet = pet // Pet 참조 카운트: 1, pet 프로퍼티가 약한 참조로 사용이 되기 때문에 Pet 참조 카운트가 증가하지 않음
pet!.owner = person // Person 참조 카운트: 2

pet = nil // Pet 참조 카운트: 0, Person 참조 카운트: 1
// puppy is being deinitialized

person = nil // Person 참조 카운트: 0
// randy is being deinitialized

위 코드에서 Person 객체에 pet 프로퍼티를 weak를 사용하여 약한 참조하였습니다. 그래서 pet 프로퍼티에 인스턴스를 대입해도 Pet 참조 카운트가 증가하지 않고, Pet 인스턴스 변수에 nil을 지정해주면 메모리에서 해제가 되는것을 알 수 있습니다.

 

[미소유 참조]

미소유 참조는 변수 앞에 unowned 키워드를 사용하고 약한 참조와 마찬가지로 인스턴스의 참조 카운트를 증가시키지 않습니다. 하지만 약한 참조와는 다르게 자신이 참조하는 인스턴스가 항상 메모리에 존재할 것이라는 전제를 기반으로 동작합니다. 자신이 참조하는 인스턴스가 메모리에 해제가 되더라고 nil을 할당하지 않습니다. 그래서 미소유 참조를 하는 변수나 프로퍼티는 옵셔널 타입이 아니어도 됩니다.

 

아래는 미소유 참조에 대한 예시입니다.

class Person {
    let name: String
    
    var pet: Pet? // 사람은 pet이 있을 수도 있고, 없을 수 도 있다
    
    init(name: String) {
        self.name = name
        print("\\(name) is being initialized")
    }
    
    deinit {
        print("\\(name) is being deinitialized")
    }
}

class Pet {
    let name: String
    unowned var owner: Person // pet은 주인이 있을 것으로 가정한다. 이 코드는 owner가 항상 존재한다고 가정한다.
    
    init(name: String, owner: Person) {
        self.name = name
        self.owner = owner
        print("\\(name) is being initialized")
    }
    
    deinit {
        print("\\(name) is being deinitialized")
    }
}

var person: Person?
person = Person(name: "randy") // Person 참조 카운트: 1
// "randy is being initialzed"

if let person = person {
    person.pet = Pet(name: "puppy", owner: person) // Pet 참조 카운트: 1, Person 참조 카운트: 1 / owner 프로퍼티가 미소유 참조로 사용되었기 때문에 Person 참조 카운트가 증가되지 않는다
}
// "puppy is being initialized"

person = nil // Pet 참조 카운트: 0, Person 참조 카운트: 0

// randy is being deinitialized
// puppy is being deinitialized

위 코드에서 Pet 객체의 owner 프로퍼티를 unowned로 선언하여, 펫과 주인 사이에 강력한 연결이 있음을 나타냈습니다. 이는 주인이 해제되면 펫도 자동으로 해제된다는 것을 의미합니다.

코드 상에서 person에 nil을 할당하여 메모리에서 해제하면, 이로 인해 Pet의 참조 카운트도 0이 되어 메모리에서 해제됩니다.

 

지금까지 Swift에서 메모리 관리에 대한 내용을 정리해보았습니다.

반응형

'Swift' 카테고리의 다른 글

[Swift] deinitializer 활용  (1) 2023.10.26
[Swift] 클로저의 Escaping과 Capture List  (0) 2023.10.24
[Swift] 상수와 변수  (0) 2023.10.23