iOS+/Swift

ARC(Automatic Reference Counting)

hot6 2025. 1. 5. 16:30

개요

이 글에서는 Swift에서 사용하는 메모리 관리 기법인 ARC(Automatic Reference Counting)에 대해 알아본다

우선, 다른 언어에서 많이 사용되는 GC(Garbage Collection)를 이용한 메모리 관리와 ARC 방식은 어떤 차이가 존재하는지 확인하고,

Swift에서 강한 순환 참조 문제를 해결하기 위해 사용되는 다음 키워드들에 대해 알아본다

 

  • weak
  • unowned
  • Capture List

 

GC와 ARC

C++의 스마트 포인터는 참조 카운팅을 통해 객체가 더 이상 사용되지 않을 때 자동으로 메모리를 해제시킨다..

 

Swift는 Heap 영역 관리에 ARC 방식을 사용하는데, ARC 방식을 이용하면 언어 차원에서 스마트 포인터처럼 객체 카운팅을 수행하고 관리한다

GC(Garbage Collection)

  • GC는 런타임에 메모리를 관리하는 방식
  • "stop-world"라는 과정을 포함, 이는 GC가 동작하는 동안 다른 작업이 일시 중지되는 것을 의미
  • GC는 객체 그래프를 순회하면서 더 이상 접근할 수 없는 객체를 식별하여 메모리를 해제
  • 메모리 관리에 대해 신경 쓸 필요가 없지만, GC가 실행되는 동안 애플리케이션이 일시 중지될 수 있음

ARC(Automatic Reference Counting)

  • ARC는 컴파일 타임에 메모리를 관리하는 방식
  • 순환 참조를 해결해야 하며, 이로 인해 컴파일 타임이 길어질 수 있음
  • ARC는 각 객체가 몇 번 참조되는지 참조 카운트를 유지하며, 카운트가 0이 되면 메모리를 해제
  • 순환 참조를 해결하지 못하면 메모리 누수가 발생할 수 있음

 

강한 순환 참조 문제

Swift에서 Heap 영역에는 래퍼런스 타입class, closure 등이 보관된다

 

강한 순환 참조 문제는 두 개 이상의 참조형 타입의 데이터가 서로 인스턴스를 강한 참조할 때 발생하는데, 참고로 강한 참조의 키워드는 strong이며, 생략 시 기본으로 적용된다

 

ARC를 이용한 메모리 관리에선 참조 개수가 0이 되어야 할당 해제가 되는데, 서로 계속 참조하고 있기 때문에 할당 해제가 이뤄지지 않는다

이러한 강한 순환 참조 문제는 참조 횟수를 증가시키지 않고 인스턴스를 약한 참조 이용하면 해결할 수 있다

 

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

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

var a: A? = A()
var b: B? = B()

a?.b = b
b?.a = a

/// deinit이 출력되지 않음
a = nil
b = nil

약한 참조

일반적으로 객체에 대한 참조를 객체 내부에 가지면서 발생하는 강한 순환 참조 문제는 다음과 같은 키워드로 해결할 수 있다

weak

  • 참조 횟수를 증가시키지 않는 키워드
  • 옵셔널 → 접근 시 매번 reference 체크 비용이 발생
class A {
    var b: B?
    deinit { print("A is being deinitialized") }
}

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

var a: A? = A()
var b: B? = B()

a?.b = b
b?.a = a

/// deinit이 출력!
a = nil
b = nil

unowned

  • 참조 횟수를 증가시키지 않는 키워드
  • 존재보장을 가정하여 사용 → nil 접근 시 오류가 발생
  • 대신 접근 시 reference 체크 비용 없다(weak에서는 체크 비용 발생)
class A {
    var b: B?
    deinit { print("A is being deinitialized") }
}

class B {
    unowned var a: A  // optional이 아닌 약한 참조
    
    init(a: A) {
		    self.a = a // 옵셔널이 아니므로 생성 시, 설정
    }
    
    deinit { print("B is being deinitialized") }
}

var a: A? = A()
var b: B? = B(a: a!)

a?.b = b

a = nil
// Fatal error!
b?.a 

Capture List

클로저는 자신이 참조하는 외부 변수 또는 인스턴스를 캡처할 수 있기 때문에, 클로저 또한 강한 참조 문제를 발생시킬 수 있다

 

Capture List를 이용하면 클로저가 생성될 때 참조하는 인스턴스의 참조 방법을 정의하여, 이런 문제를 해결할 수 있다

-> [weak self][unowned self] 처럼 약한 참조 키워드를 같이 사용한다

 

class MyClass {
    var value = 0
    lazy var myClosure: () -> Void = { [weak self] in
        self?.value += 1
    }
}

 

마무리

ARC는 Swift에서 메모리를 효율적으로 관리하는 핵심 메커니즘이다

하지만 모든 문제를 자동으로 해결해주지는 않으며, 강한 순환 참조와 같은 문제는 개발자가 직접 해결해야 한다

 

이 글에서 다룬 weak, unowned, 그리고 클로저의 Capture List는 이러한 문제를 해결하는 데 중요한 도구다

이들을 적절히 활용하면 메모리 관리와 관련된 문제를 대부분 예방할 수 있으며, 안정적인 코드 작성이 가능하다

 

특히, 기존 비동기 처리 방식에서는 콜백 함수를 클로저 형태로 전달했기 때문에, 클래스 내부에서 정의된 코드라면 해당 클로저와 클래스를 둘러싼 순환 참조 문제가 발생할 수 있었다

이런 문제를 해결하기 위해 Capture List를 사용해 순환 참조를 방지해야 했지만, Swift Concurrency를 사용하면 이러한 문제를 크게 걱정하지 않아도 되어, 더 간결하고 안전한 비동기 코드 작성이 가능해진 점도 인지해두자