본문 바로가기
Programming

Swift 메모리 관리

by 나무수피아는 지식의 가지를 뻗어가는 공간입니다. 2025. 8. 5.
반응형

메모리 관리 (Memory Management)

Swift는 메모리를 자동으로 관리하기 위해 ARC(Automatic Reference Counting)를 사용합니다. ARC는 개발자가 직접 메모리를 해제하지 않아도, 객체가 더 이상 필요하지 않을 때 자동으로 메모리에서 해제해 주는 기능입니다. 이는 iOS 및 macOS 앱 개발 시 안정성과 성능을 동시에 확보하는 데 매우 중요한 역할을 합니다. 하지만 ARC가 자동으로 처리해 주더라도, 강한 참조 순환(Retain Cycle) 문제가 발생할 수 있어 이를 이해하고 방지하는 것이 매우 중요합니다.

1. ARC (Automatic Reference Counting)

Swift에서 클래스 인스턴스는 메모리에서 해제되기 전까지 해당 인스턴스를 참조하는 강한 참조(strong reference)가 하나 이상 존재하는지 ARC가 추적합니다. 강한 참조가 모두 사라지면, ARC는 해당 인스턴스를 메모리에서 자동으로 해제합니다.

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

var person1: Person? = Person(name: "Alice")
person1 = nil  // 메모리 해제됨

위 예제에서 person1 변수는 Person 인스턴스를 강하게 참조합니다. person1nil을 할당하면, 강한 참조가 사라지고 ARC가 자동으로 메모리를 해제합니다. 이때 deinit 메서드가 호출되어 객체가 소멸되었음을 확인할 수 있습니다.

2. 강한 참조(strong), 약한 참조(weak), 미소유 참조(unowned)

ARC는 참조 타입의 메모리 관리를 위해 세 가지 참조 유형을 제공합니다. 이를 잘 이해하고 사용하는 것이 메모리 누수를 방지하는 데 필수적입니다.

  • strong: 기본 참조 방식으로, 객체의 참조 횟수를 증가시킵니다. 인스턴스가 계속 살아 있도록 유지합니다.
  • weak: 참조 횟수를 증가시키지 않으며, 객체가 해제되면 자동으로 nil로 설정됩니다. 주로 optional 타입으로 선언합니다.
  • unowned: 참조 횟수를 증가시키지 않으며, 객체가 해제되어도 nil로 설정되지 않습니다. 비옵셔널 타입에서 주로 사용하며, 해제된 참조를 접근하면 런타임 오류가 발생할 수 있습니다.

각각의 참조 방식은 상황에 따라 적절히 선택해야 하며, 특히 두 객체가 서로를 참조하는 경우 약한 참조미소유 참조를 사용해 강한 참조 순환을 피하는 것이 중요합니다.

class Owner {
    var name: String
    init(name: String) { self.name = name }
    var pet: Pet?
}

class Pet {
    let name: String
    weak var owner: Owner?  // 순환 참조 방지
    init(name: String) { self.name = name }
}

var owner: Owner? = Owner(name: "Tom")
var pet: Pet? = Pet(name: "Cat")

owner?.pet = pet
pet?.owner = owner

owner = nil  // 둘 다 메모리 해제됨
pet = nil

위 예제에서 OwnerPet은 서로를 참조합니다. 만약 둘 다 strong 참조라면, 강한 참조 순환이 발생해 메모리가 해제되지 않습니다. 하지만 Pet 클래스 내 ownerweak로 선언하여 순환 참조를 방지했습니다. 따라서 owner = nil 실행 시 둘 다 메모리에서 해제됩니다.

3. 메모리 순환 방지: 캡처 리스트 (Capture List)

클로저(Closure)는 참조 타입이며, 클로저 내부에서 self를 캡처할 때 강한 참조 순환이 발생할 수 있습니다. 특히 인스턴스가 클로저를 소유하고, 클로저가 다시 인스턴스를 참조하는 경우가 그렇습니다.

이 문제를 해결하기 위해 Swift는 캡처 리스트(Capture List)라는 기능을 제공합니다. 캡처 리스트를 통해 클로저가 selfweak 또는 unowned 참조로 캡처하도록 지정할 수 있습니다.

class HTMLElement {
    let name: String
    let text: String?

    lazy var asHTML: () -> String = { [unowned self] in
        return "<\(self.name)>\(self.text ?? "")</\(self.name)>"
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) 태그가 메모리에서 해제됨")
    }
}

var heading: HTMLElement? = HTMLElement(name: "h1", text: "Welcome")
print(heading?.asHTML() ?? "")
heading = nil  // 메모리 정상 해제됨

위 예제에서 클로저는 [unowned self]로 선언되어 self를 강한 참조하지 않습니다. 이를 통해 인스턴스와 클로저 간 강한 참조 순환을 방지하며, 인스턴스가 해제될 때 클로저도 안전하게 메모리에서 해제됩니다.

4. 강한 참조 순환(Retain Cycle)의 이해와 해결 방법

강한 참조 순환은 두 개 이상의 객체가 서로를 강한 참조 하여 참조 카운트가 0이 되지 않아 메모리가 해제되지 않는 상태를 말합니다. 이는 메모리 누수로 이어지며 앱 성능 저하나 크래시를 유발할 수 있습니다.

다음은 강한 참조 순환의 대표적인 예시입니다:

class A {
    var b: B?
    deinit { print("A deinitialized") }
}

class B {
    var a: A?
    deinit { print("B deinitialized") }
}

var objectA: A? = A()
var objectB: B? = B()

objectA?.b = objectB
objectB?.a = objectA

objectA = nil
objectB = nil
// 둘 다 메모리 해제되지 않음! 강한 참조 순환 발생

위 코드에서 AB 인스턴스가 서로를 strong으로 참조하기 때문에, objectAobjectBnil로 할당되어도 메모리가 해제되지 않습니다.

이를 해결하려면 한쪽 참조를 weak 또는 unowned로 변경해야 합니다:

class A {
    var b: B?
    deinit { print("A deinitialized") }
}

class B {
    weak var a: A?  // 약한 참조로 변경
    deinit { print("B deinitialized") }
}

var objectA: A? = A()
var objectB: B? = B()

objectA?.b = objectB
objectB?.a = objectA

objectA = nil
objectB = nil
// 둘 다 정상적으로 메모리 해제됨

5. 클로저 내에서 self 참조에 따른 순환 참조 문제

클로저는 참조 타입으로, 클래스 내 프로퍼티로 클로저를 선언하고 클로저 내부에서 self를 참조하면 강한 참조 순환이 발생할 수 있습니다.

클로저 내 self 참조는 기본적으로 강한 참조입니다. 따라서 인스턴스와 클로저가 서로 강하게 참조하는 상황이 되어 메모리 해제가 불가능해집니다.

이를 방지하려면 캡처 리스트를 사용하여 selfweak 또는 unowned로 캡처해야 합니다.

class ViewController {
    var name = "Swift"
    lazy var closure: () -> Void = { [weak self] in
        guard let self = self else { return }
        print("Hello, \(self.name)")
    }
    deinit {
        print("ViewController deinitialized")
    }
}

var vc: ViewController? = ViewController()
vc?.closure()
vc = nil  // 메모리 해제 정상 처리

6. 요약 및 권장사항

  • Swift는 ARC를 통해 객체 메모리를 자동으로 관리합니다.
  • 강한 참조가 남아있으면 객체가 해제되지 않으므로 강한 참조 순환(Retain Cycle)에 주의해야 합니다.
  • 두 객체가 서로 참조할 경우, 한쪽 참조를 weak 또는 unowned로 선언하여 순환 참조를 방지하세요.
  • 클로저 내에서 self를 캡처할 때는 반드시 캡처 리스트를 사용하여 weak 또는 unowned로 참조하세요.
  • 메모리 누수가 의심되면 Xcode의 Instruments - Leaks 도구로 확인하는 습관을 들이세요.
반응형

'Programming' 카테고리의 다른 글

C 기본 문법  (43) 2025.08.07
C 역사와 특징  (57) 2025.08.06
Swift 고급 타입  (56) 2025.08.04
Swift 오류 처리  (46) 2025.08.03
Swift 확장과 접근 제어  (47) 2025.08.02