Swift의 메모리 관리 시스템 (ARC, MRC)

이진욱(JIN WOOK)·2024년 12월 3일
0

제비에관하여(Swift)

목록 보기
8/10

효율적이고 빠른 프로그램을 만들기 위해서는 메모리 관리를 잘해야한다.
그렇기에 모든 컴퓨터 프로그래밍 언어에는 메모리 관리 시스템이 존재한다.
대표적으로 JAVA에는 Garbage Collector - 쓰레기 청소부가 있고
Swift에는 Reference Counting에서 시작하는
ARC - Automatic Reference Counting이 있다.

Reference Counting

인스턴스 즉, 실제데이터 = 메모리 에 할당된데이터는 반드시 하나 이상의 참조하는 값이 존재해야 메모리에 유지가 된다.

class MyClass {}

let myClass1 = MyClass() //인스턴스의 생성

현재 이 코드에서 myClass1 이라는 변수는 MyClass를 참조하고 있다.

이것을 증명하기위해
클래스내에 소멸자(Deinitialization)를 사용해서 메모리가 해제가 되는지 확인해보자

단, 소멸자는 클래스에서만 동작한다.
왜냐하면 클래스는 heap이라는 데이터 영역에 저장되고, 이 공간은 실제로 ARC라는 개념을 통해서 관리가 되기 때문이다.
(클래스의 경우 여러 참조자가(변수,상수, 클로저 등) 동일한 인스턴스를 참조할수 있다.)
구조체의 경우 값자체가 복사되니 이 값은 독립적으로 사용된다.
스택에 저장되기 때문에 사용하지 않으면 알아서 사라진다.


class MyClass {
    
    init() { //init 이라는 생성이 있다면
        print("클래스 생성")
    }
    
    deinit { //deinit이라는 소멸도 있다.
        print("클래스 소멸")
    }
}

//RC +1
var myClass1: MyClass? = MyClass() //인스턴스의 생성

//RC +2
var myClass2 = myClass1

myClass1 = nil

가설

myClass1 = nil 을 할당했으니,
myClass2 도 nil이 할당되어 
"클래스 소멸" 이라는 문장이 출력되지 않을까?

결과

결과는 이렇다.
위 가설은 틀렸다.
이미 메모리에 올라간 순간 RC는 2로 변경된 상태이다.
즉, 할당한 값과 관련없이 myClass1, myClass2 모두 MyClass()인스턴스를 참조하고 있다는것이다.
myClass1 = nil을 할당한건 인스턴스를 참조하고 있는 변수에 대해서만 nil을 할당한것이다.

이렇게 참조하고 있는 모든 변수에 nil을 할당해줘야 RC = 0이 되어
비로소 클래스가 소멸(메모리에서 해제)된다.

이렇게 인스턴스를 참조하고 있는 참조자의 갯수를 카운팅 하는것을 RC라고 한다.
이 카운팅을 기반으로 메모리할당 해제가 이뤄진다.

ARC(Automatic Reference Counting )

위처럼 RC를 자동으로 카운팅 해주는걸 ARC가 담당하고 있다.

  • 참조를 하면 RC 카운트가 증가
  • 참조가 해제되면 카운트가 감소한다.
  • RC가 0이되면 그떄서야 메모리에서 해제가 된다.

딱히 별 생각이 없어서 이게 왜 편한건데? 라고 생각했는데
스위프트가 나오기전 Objective-C에서는
Manual Reference Counting이라는 개념이 있었다고 한다.
말그대로 수동으로 관리를 하는것이다..

만약MRC를 계속 사용하고 있었다면

class MyClass {
    
    init() { //init 이라는 생성이 있다면
        print("클래스 생성")
    }
    
    deinit { //deinit이라는 소멸도 있다.
        print("클래스 소멸")
    }
}


var myClass1: MyClass? = MyClass() //인스턴스의 생성 RC 1

var myClass2 = myClass1
retain(myClass2) //RC 2

release(myClass1) //RC 1로 감소
release(myClass2) //RC 0으로 감소 -> 메모리 해제

이렇게 retain , release 메서드를 사용하면서 직접 카운트를 올리고 내려주고를 직접해야 했다고 한다..(상남자 그자체)

뭔가 자동으로 관리된다해서 마음을 편하게 먹으면 안된다.
카운트 하면서 메모리에 올리고 메모리에서 해제되는것을 자동으로 해주는것이지 이 개념을 기반으로 순환참조로 인한 메모리 누수현상과 같은것은 직접 해결해줘야한다!

순환 참조로 인한 메모리 누수현상

메모리 누수현상이란?
순환참조는 간단히 말해 두개 이상의 객체가 서로가 서로를 가리키는 상황이다.

서로를 가리키기때문에 RC상태가 1이상으로 유지가 된다.
이렇게 RC가 1이상으로 유지가 되어 메모리에서 내려가지 않는 현상을 메모리 누수현상이라고 한다.

예제코드를 확인해보자

class Person {
    var pet: Dog?
    init() {
        print("Person 클래스 생성")
    }
    deinit {
        print("Person 클래스 소멸")
    }
}

class Dog {
    var owner: Person?
    init() {
        print("Dog 클래스 생성")
    }
    deinit {
        print("Dog 클래스 소멸")
    }
}

Person은 pet(Dog)이 있다.
Dog는 owner(Person)가 있다.

실제 객체 생성

// person rc = 1
var person: Person? = Person()
// dog rc = 1
var dog: Dog? = Dog()

// dog rc = 2
person?.pet = dog
//실제 객체를 할당하는 과정

// person rc = 2
dog?.owner = person

// person rc = 1
person = nil
// dog rc = 1
dog = nil

이렇게 각각 인스턴스를 참조하고 있는 변수에(소유자, person 과 dog) nil을 할당하면 RC은 줄어들어 1로 변경되지만 deinit은 실행되지 않는다.
소유자 변수는 더이상 인스턴스를 소유하고 있지 않지만
내부적으로
Person클래스는 pet을 통해서 Dog를 소유하고
Dog클래스는 owner를 통해서 Person을 소유하고 있음으로 RC가 내려가지 않았다. (Person ↔ Dog)

더이상 사용하지 않음에도 불구하고 메모리에 계속 남아있는 현상이 발생한다. 이렇게 내부적으로 서로를 참조해버리면 해제 시킬 방법도 없다.
이게 바로 메모리 누수 현상 이다.

메모리 누수현상의 해결 Weak Reference, Unowned Reference

Weak Reference (약한 참조)

위에서 참조를 하면서 RC를 올리는것을 강한 참조라고 한다.

약한참조 방식을 사용하면 참조는 하지만 RC를 올리지 않고 참조를 할수 있다.

//Person클래스
 weak var pet: Dog?
 //Dog클래스
 weak var owner: Person?

이렇게 변수 앞에 weak 키워드를 선언하면 참조는 하지만 RC를 올리지 않는 약한 참조를 할수 있다.

더 짧은 생명주기를 가진 인스턴스를 참조하는쪽에 붙이면 된다.
반려동물 보다 반려인이 더 오래사는 경우를 기본 조건으로 생각한다면

//Person클래스
weak var pet: Dog?

Person클래스의 pet에 붙여주면 된다.
약한 참조의 경우, 참조하고 있던 인스턴스가 사라지면, nil로 초기화 되어있다.

참조하려는 대상이 사라졌으니 당연하다.

비소유 참조 (Unowned Reference)

이 방식도 마찬가지로 RC를 올라가지 않게 하는 키워드다.
하지만 이 키워드는 소유자보다 인스턴스의 생명주기가 더 길거나 같아야 하는게 반드시 보장되어야 한다.
즉, Dog가 Person을 참조하고 있는 상황일때 Person은 무슨일이 있어도 먼저 메모리에서 해제가 되면 안된다. 이 경우 에러가 발생한다.
(반려동물보다 반려인이 먼저 죽는 상황은 절대로 일어나지 않는다고 생각하자ㅠ 생각만 해도 끔찍하다)

이게 바로 weak 키워드와 다른점이다.
참조하고 있던 인스턴스가 사라지면 nil로 초기화되는게 아니라
존재하지 않는 인스턴스를 계속찾아다닌다.

그렇기에 반드시 nil로 재설정을 해줘야 오류가 발생하지 않는다.

클로저의 캡처와 캡처리스트 (클로저의 메모리 관리)

리마인드

  • 클로저는 외부변수를 캡쳐하는 캡쳐현상이 존재한다.
  • 클로저의 실제 동작은 스택프레임 영역에서 저장될수 있고, 생명주기가 길어지면 heap영역으로 이동한다.
  • 그렇기에 캡쳐를 하게되면 외부변수도 heap영역에 올라감으로써 클로저는 지속적으로 외부변수를 참조할수있다.

클로저도 참조타입이다. 그렇기 떄문에 참조에 대해서 동일하게 메모리 누수현상이 생길수 있다.

class Person {
    init() {
        print("클래스 생성")
    }
    var name = "홍길동"
    var introduceMySelf: (() -> ())?
    func sayMyName() {
        introduceMySelf = {
            print("제 이름은 \(self.name) 입니다.") //RC +1
        }
    }
    deinit {
        print("메모리 해제")
    }
}

var person: Person? = Person() //RC +2
person?.sayMyName()
person?.introduceMySelf?()

Person은 introduceMySelf를 참조한다. (self.name이 있으니)
introduceMySelf라는 클로저는 Person을 참조한다.

이렇게 그냥 선언하면 기본적으로 강한 참조가 이뤄진다.

클로저의 캡처리스트

위 코드중 sayMyName()쪽을 캡쳐리스트를 이용해 이렇게 변경할수도 있다.

 func sayMyName() {
        introduceMySelf = {[self] in
            print("제 이름은 \(name) 입니다.") //RC +1
        }
    }

캡쳐할 목록을 담는다 해서 캡쳐리스트이다.

그러먼 이걸 기반으로 똑같이 캡쳐리스트 내에 존재하는 캡쳐 대상을
약하게 참조한다는 키워드인 weak, unowned를 붙여주면된다.

 func sayMyName() {
        introduceMySelf = {[weak self] in
            print("제 이름은 \(self?.name) 입니다.") //RC +1
        }
    }

다만, weak self은 nil의 가능성이 존재하기 때문에
강한 참조와는 다르게 바로 name호출이 불가능하다.

다른 객체가 self를 소유하고 있지 않으면 RC = 0으로
메모리에 존재하지 않는 상태의 가능성이 존재하기에 클로저가 참조를 못하는 상황이 일어날수도 있다.

그렇기 때문에 옵셔널값을 벗겨내는 방법을 사용하는게 좋다.

profile
기술로부터 소외 되는 사람이 없도록 우리 모두를 위한 서비스를 만들고 싶습니다.

0개의 댓글

관련 채용 정보