Swift

[Swift] 메모리 관리 (1) | ARC와 메모리 누수 (feat.강한순환참조)

하이D:) 2023. 10. 16. 16:06

Swift 메모리에 대해서 공부를 하다 보면 자연스레 ARC라는 단어가 익숙해지게 되죠.

메모리 관리는 필요한지, ARC 시스템은 어떤 원리인지와 메모리 누수가 일어나는 상황에 대해서 정리해 보면 어느 정도개념이 잡힐 것 같으니 정리해 봅시다!

 

[Swift] 메모리 관리 (1) | ARC와 메모리 누수 (feat.강한순환참조)

[Swift] 메모리 관리 (2) | 약한참조 WeakReference 비소유참조 UnownedReference

 

# 🥨 메모리 관리가 필요한 타입

우리가 흔히 알고 있는 타입 중에 값 형식의 인스턴스 은 메모리 관리가 필요하지 않고 참조 형식, 즉, 우리가 흔히 아는 클래스, 클로저 등의 인스턴스에 대해서는 메모리 관리가 필요하다. 왜일까?

 

내가 아는 선에서 내용을 공유하자면,

 

값 타입의 경우에는 인스턴스 생성 시 주로 스택 메모리에 할당된다!

함수가 호출되면 스택 영역에 스택프레임이 생성되는 건 아시죠? 이렇게 스택 프레임이 생성되었을 때 함수의 지역 변수가 저장되는 것처럼 값형식의 인스턴스도 스택에 저장되는 것입니다.

 

그럼 함수가 종료되었을 경우에는 어떻게 될까? 함수가 종료되면 해당 스택 프레임이 제거되며 할당된 메모리도 함께 해제된다. 즉, 값형식의 인스턴스도 메모리에서 함께 사라지는 것!

 

반면 참조 타입의 인스턴스 생성 시! 힙 영역에 동적할당으로 메모리를 위치시킨다.

그러므로 함수가 실행되고 종료될 때 스택프레임의 생성되고 종료된다고 해서 메모리 상에서 사라지진 않는 것이다. 그렇기 때문에 힙 영역에 할당되는 참조 타입의 경우에는 개발자가 따로 메모리 관리를 해주어야 하는 것!

 

근데 우리는 Swift 프로그래밍을 클래스 인스턴스와 클로저를 사용했다고 따로 메모리 관리를 해주지 않았지 않는가?? 이유는 바로 Swift에서 우리가 따로 메모리에 대해 신경 쓰지 않아도 되도록 ARC 시스템을 제공하기 때문이다.

 

 

# 🥨 Reference Counting

우선 ARC를 알기 전에 Swift에서 레퍼런스 카운팅이 어떻게 되는지에 대해 아는 게 중요하다!

 

ReferenceCounting이란 참조 타입의 인스턴스가 사용되고 있는 횟수를 계산하는 것이다. 아래와 같이 person이라는 변수에 인스턴스를 할당해서 사용하고 있다면 해당 인스턴스의 RC(Reference Counting)는 1이 된다.

class Person {
    var name = "person1"
    var age = 10
}

var person1 : Person = Person()

 

 

# 🥨 ARC란?

그렇다면 위에서 알아본 RC랑 관련 있을 것 같은 ARC란 무엇일까?

ARC는 "Automatic Reference Counting"의 약어로, 자동 참조 계수를 관리하는 메모리 관리 기술이다. ARC는 개발자가 객체의 메모리를 수동으로 할당 및 해제하는 번거로움을 줄여주며 메모리 누수를 방지하기 위해 사용된다!

 

ARC는 객체가 참조되는 횟수를 추적하고, 해당 객체를 더 이상 사용하지 않을 때 메모리에서 자동으로 해제한다. 즉, RC가 0이 되는 순간 해당 객체는 더 이상 필요하지 않다는 것을 의미하고 자동으로 메모리에서 해제되는 것이다.

 

class Person {
    var name = "person1"
    var age = 10
}

var person1 : Person? = Person()

person1 = nil

위의 예시와 같이 person1이라는 변수에 nil을 할당하면 이제 Person의 인스턴스를 바라보고 있는 게 더 이상 없기 때문에 RC가 0으로 되면서 ARC에 의해 자동적으로 메모리에서 사라진다!

 

 

# 🥨 ARC & MRC & GC

ARC라는 메모리 관리 시스템을 이해했다면 MRC와 GC는 ARC와 뭐가 다른지가 궁금할 수 있다.

 

일단 ARC 의 가장 큰 특징 두 가지를 살펴보면 다음과 같다.

  • 컴파일 타임에 실행되며,
  • RC를 카운팅 해주는 함수인 retain, release 등의 코드를 심어준다.

 

반면 MRC

  • ARC와 다르게 개발자가 직접 RC를 카운팅 해주는 함수인 retain, release 등의 코드를 사용하여 관리해주어야 한다.
  • 이 방식은 Objective-C에서 사용한다

 

그리고 Java, C#등에서 사용되는 GC(Garbage Collection)  

  • ARC와 다르게 런타임에 실행하며,
  • 런타임에 참조를 추적하는 리소스가 추가적으로 계속 사용되어 성능저하가 발생할 순 있으나 ARC에 비해 치명적인 메모리 누수를 막을 수 있다는 장점이 있다.

 

 

# 🥨 메모리 누수 Memory leak

이렇게 Swift가 똑똑하게 ARC 시스템으로 메모리 관리를 해주는데 메모리 누수가 일어나는 이유는 무엇일까?

 

위에서 본 것처럼 Swift에서는 대부분의 경우 ARC를 자동으로 처리하므로 개발자가 명시적으로 메모리 관리를 신경 쓸 필요가 없다. 그러나 강한 순환 참조와 같이 특별한 상황에서는 메모리 누수가 발생할 수 있는 것이다!

 

그렇다면 강한 순환참조란 무엇이고 이렇게 메모리 누수가 일어날 수 있는 상황을 방지하려면 어떻게 해야 할까? 아래 두 개의 타입을 예시로 어떻게 했을 때 메모리에서 해제가 되지 않는 메모리 누수 상황이 일어나는지 알아보자.

class Person {
    var name : String
    var age : Int
    
    var pet : Dog?
    
    init(name : String, age : Int) {
        self.name = name
        self.age = age
    }
    
    deinit {
        print("Person \(name) 메모리 해제")
    }
}

class Dog {
    var name : String
    var age : Int
    
    var owner : Person?
    
    init(name : String, age : Int) {
        self.name = name
        self.age = age
    }
    
    deinit {
        print("Dog \(name) 메모리 해제")
    }
}

 

우리가 알고 있는 Reference Counting 개념으로 생각해 보면 아래 코드는 문제없이 메모리에 올라갔다가 해제되는 것을 볼 수 있다. (각각 인스턴스에 nil을 할당하는 순간 소멸자가 호출되게 되고 메모리 상에서 완벽히 사라지는 것이다)

var person1 : Person? = Person(name: "person1", age: 20) // RC = 1
var dog1 : Dog? = Dog(name: "Dog1", age: 1) // RC = 1

print(person1?.name)
print(dog1?.name)

person1 = nil
dog1 = nil

 

하지만 두 개 이상의 객체가 서로를 참조하는 상황이 되면 강한 순환 참조로 인해 메모리 누수의 원인이 되는데 아래에서 자세히 알아보자!

 

 

# 🥨  강한 순환 참조

강한 순환 참조란 객체가 서로를 참조하는 것을 의미하며, 아래 코드에서 보는 것과 같이 인스턴스 참조 개수 RC가 각각 2가 된다.

var person1 : Person? = Person(name: "person1", age: 20) //RC : 1
var dog1 : Dog? = Dog(name: "Dog1", age: 1) //RC : 1
person1?.pet = dog1 //RC : 2
dog1?.owner = person1  //RC : 2

print(person1?.name)
print(dog1?.name)

person1 = nil
dog1 = nil

 

그림으로 표현하자면 이렇게 간략하게 표현할 수도 있을 것 같다.. (이해를 위해 아주 대략적인 메모리 구조를 표현했을 뿐…ㅎㅎ)

 

그림에서도 볼 수 있듯이 객체가 서로를 참조하고 있는 “강한 순환 참조” 상태라는 것을 알 수 있다.

 

이 경우, 인스턴스에 nil을 할당해도 RC가 0이 되지 않는 상황이 발생한다. 즉, 소멸자가 호출되지 않고 메모리에서 해제되지 않아 메모리 누수가 생기는 것이다.

 

위의 예시를 보면 인스턴스는 nil이 되었기 때문에 내부의 메모리에 접근할 수도, 해제할 수도 없다. 즉, 순환참조가 유지되며 발생하는 메모리 누수를 해결할 방법이 없다는 것이다.

var person1 : Person? = Person(name: "person1", age: 20) //RC : 1
var dog1 : Dog? = Dog(name: "Dog1", age: 1) //RC : 1
person1?.pet = dog1 //RC : 2
dog1?.owner = person1  //RC : 2

print(person1?.name)
print(dog1?.name)

person1 = nil
dog1 = nil

person1?.pet = nil //의미 없음
dog1?.owner = nil //의미 없음

 


이번 글에서는 Swift에서의 메모리 관리 시스템인 ARC의 개념과 ARC 시스템이 있음에도 불구하고 메모리 누수가 일어날 수 있는 상황인 강한 순환 참조에 관련해서 알아보았다. 다음 글에서는 이러한 메모리 누수가 일어날 수 있는 상황을 어떻게 방지할 수 있는지 그 방법에 대해 정리해보려 한다!