[iOS] Memory Management (ARC, Retain Cycle, Weak, Unowned)

sun02·6일 전

iOS

목록 보기
31/31

1. ARC (Automatic Reference Counting)

ARC는 Apple이 Xcode에서 제공하는 자동 메모리 관리 기능으로, 앱의 메모리 관리를 개발자가 직접 하지 않아도 되도록 도와준다.

ARC는 객체의 참조 횟수(reference count) 를 추적한다.

  • 객체가 생성되고 변수에 할당되면 참조 횟수가 증가한다.
  • 객체를 참조하던 변수가 해제되면 참조 횟수가 감소한다.
  • 참조 횟수가 0이 되면, ARC가 해당 객체의 메모리를 자동으로 해제(deallocate) 한다.

ARC의 주요 장점 중 하나는
retain cycle(순환 참조) 이나 dangling pointer(해제된 객체를 가리키는 포인터) 와 같은 일반적인 메모리 관리 문제를 예방할 수 있다는 점이다.

필요한 경우 개발자가 weak, unowned 참조를 사용하도록 유도함으로써,
순환 참조를 인식하고 끊을 수 있게 도와준다.

❗️ ARC가 대부분의 메모리 관리를 자동으로 처리해 주기 때문에,
strong / weak / unowned 참조가 어떻게 동작하는지 정확히 이해하는 것이 중요


2. iOS에서의 메모리 관리

Swift에서 메모리 관리는 ARC를 중심으로 이루어진다.

컴파일러는 ARC를 통해 앱에서 사용되는 메모리를 자동으로 추적하고, 더 이상 필요하지 않은 객체를 적절한 시점에 해제한다.

하지만 다음과 같은 경우에는 메모리에 대한 더 깊은 이해가 필요하다.

  • 복잡한 객체 관계
  • 클로저 사용
  • 파일, 네트워크, 타이머와 같은 리소스 관리

Strong Reference & Retain Cycle

강한 참조 순환(strong reference cycle) 또는 retain cycle은
두 개 이상의 객체가 서로를 강하게 참조하여 참조 횟수가 절대 0이 되지 않는 상황을 말한다.

이 경우 객체는 메모리에서 해제되지 않는다.

문제점

  • 메모리 누수(memory leak) 발생
  • 메모리 사용량 지속 증가-> 심한 경우 메모리 부족으로 앱 크래시 발생

예제

class Person {
    var name: String
    var car: Car?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is being deallocated")
    }
}

class Car {
    var brand: String
    var owner: Person?

    init(brand: String) {
        self.brand = brand
    }

    deinit {
        print("\(brand) is being deallocated")
    }
}

var john: Person?
var tesla: Car?

john = Person(name: "John")
tesla = Car(brand: "Tesla")


john?.car = tesla
tesla?.owner = john

위 코드에서는 Person의 john 인스턴스와
Car의 tesla 인스턴스가 서로를 강하게 참조하고 있다.

이 상태에서 john = nil, tesla = nil을 해도
서로를 잡고 있기 때문에 참조 카운트는 0이 되지 않고,
두 객체 모두 메모리에서 해제되지 않는다.

Retain Cycle 해결 방법

iOS에서는 retain cycle을 끊기 위해 weak 또는 unowned 참조를 사용한다.

- weak 참조

  • 참조 대상 객체가 해제될 수 있는 경우
  • 참조 대상이 해제되면 자동으로 nil
class Person {
    var name: String
    weak var car: Car?
}

Unowned References

unowned는 weak와 유사하지만 중요한 차이점이 있다.

  • 참조 대상 객체를 강하게 유지하지 않음
  • 참조 대상 객체가 항상 존재한다고 가정
  • 해제된 객체에 접근하면 런타임 크래시 발생

특징

  • 참조 대상 객체가 먼저 해제되지 않는 것이 확실할 때만 사용
  • unowned 키워드로 선언
  • 참조 카운트 증가하지 않음
  • 부모–자식과 같은 1:1 관계에 적합

예제

class Parent {
    var name: String
    var child: Child?

    init(name: String) {
        self.name = name
    }
}

class Child {
    var name: String
    unowned var parent: Parent

    init(name: String, parent: Parent) {
        self.name = name
        self.parent = parent
    }
}

위 예제에서는 Child가 Parent 없이 존재할 수 없기 때문에
unowned 참조를 사용하는 것이 안전하다.

❗️ 만약 참조 대상 객체가 해제될 가능성이 있다면
반드시 weak를 사용해야 한다.

순환 참조 해결 방법

weak 참조

  • 한 객체가 다른 객체를 강하게 소유할 필요 없을 때 사용
  • reference count 증가하지 않음
  • 자동으로 nil 처리

unowned 참조

  • 객체가 존재하는 동안 참조 대상 객체가 항상 존재함이 보장될 때
  • reference count 증가하지 않음
  • 객체가 nil이 될 수 없다고 가정하기 때문에 할당 해제된 객체에 접근하면 런타임 크래시 발생

Capture List

  • 클로저 내부에서 self를 강하게 캡처시 retain cycle이 발생
{ [weak self] in
    self?.doSomething()
}

weak self

클로저 내부에서 self를 약하게 참조하여 순환 참조 방지
→ 반드시 안전하게 unwrap 필요

deinit

deinit은 객체가 정상적으로 해제될 때 호출

  • 리소스 정리용
  • retain cycle 여부 확인용
    ❗️ retain cycle이 존재하면 deinit은 호출되지 않는다

iOS 메모리 관리 도구

Instruments

  • 메모리 할당 추적
  • 메모리 누수 탐지
  • 실시간 메모리 사용량 분석

Memory Graph Debugger

  • 객체 간 참조 관계 시각화
  • retain cycle 분석에 매우 유용

정적 코드 분석 (Static Analyzer)

  • 빌드 시점에 메모리 문제 사전 탐지

서드파티 도구

  • Facebook의 FBMemoryProfiler
  • Google의 LeakCanary
    → 고급 메모리 분석에 활용 가능

결론

순환 참조 방지

  • weak / unowned 사용
  • 클로저에서 [weak self]
  • capture list 적극 활용

순환 참조 확인

  • Instruments → Leaks
  • Memory Graph Debugger → 객체 관계 분석

참고 자료

0개의 댓글