4T2F / ThinkBig2

🌟씽크빅 2팀 스터디 🌟
2 stars 0 forks source link

iOS에서 자동 참조 카운팅(ARC)과 가비지 컬렉션(Garbage Collection)의 차이점에 대해 설명해주세요. #6

Open Phangg opened 6 months ago

Phangg commented 6 months ago

iOS에서 자동 참조 카운팅(ARC)과 가비지 컬렉션(Garbage Collection)의 차이점에 대해 설명해주세요.

Phangg commented 6 months ago

ARC - Auto Reference Counting

Swift 에서 메모리 관리를 자동으로 해주는 것으로 참조 카운트 (RC) 를 관리해주는 기술 참조 값이 사용되지 않을 때, RC를 자동으로 감소시켜주고 0이 되면 자동으로 매모리 해제 시킴 retain cycle ( 순환 참조 ) 을 주의해야 함 ARC 동작은 run time 이 아니라, compile time 에 실행 돰 과거 Objective-C 에서는 MRC 라고.. RC 를 수동으로 관리해주었음

MRC 와 ARC

스크린샷 2024-04-03 오전 10 52 39

순환참조 - retain cycle

메모리 누수 ( Memory Leak ) 가 발생 -> Weak, Unowned 을 사용하여 해결 동적으로 관리해준다고 했는데 run time 이 아닌, compile time 에서 어떻게 다 이루어지지? compile 시점에서 코드를 분석하여, retain 과 release 를 코드 내부에 적절한 위치에 삽입 이후 run time 에 해당 retain, release 가 실행되면서 RC 를 관리해줄 수 있음

retain : 참조 카운트를 증가 release : 참조 카운트를 감소

Retain Cycle ?

메모리가 해제되지 않고 메모리 누수 ( Memory Leak ) 가 생기는 현상. 참조가 순환되고 있는 상태.

class Person {
    let name: String
    var home: Home?

    init(name: String) {
        self.name = name
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

class Home {
    let homeType: String
    var tenant: Person?

    init(homeType: String) {
        self.homeType = homeType
        print("\(self.homeType) - Init")
    }
    deinit {
        print("\(homeType) - Deinit")
    }
}

// Person retain
var lee: Person? = Person(name: "Lee")      // Lee - Init & rc = 1
// Home retain
var home: Home? = Home(homeType: "APT")     // APT - Init & rc = 1

// Home retain
lee?.home = home        // rc = 2
// Person retain
home?.tenant = lee      // rc = 2

// Person release
lee = nil               // rc = 1
// Home release
home = nil              // rc = 1

// 두 인스턴스가 서로 강한 참조를 통해, 순환 참조가 이루어짐
// rc 가 0 이 될 수 없는 상태, memory leak

해결을 위해, Weak Reference 사용 ( default : Strong ) Unowned Reference 사용

Weak Reference : 약한 참조

class Person {
    let name: String
    var home: Home?

    init(name: String) {
        self.name = name
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

class Home {
    let homeType: String
    weak var tenant: Person?

    init(homeType: String) {
        self.homeType = homeType
        print("\(self.homeType) - Init")
    }
    deinit {
        print("\(homeType) - Deinit")
    }
}

// Person retain
var lee: Person? = Person(name: "Lee")      // Lee - Init & rc = 1
// Home retain
var home: Home? = Home(homeType: "APT")     // APT - Init & rc = 1

// retain X (weak)
lee?.home = home        // rc = 1
home?.tenant = lee      // rc = 1

// Person release
lee = nil               // Lee - Deinit & rc = 0
// Home release
home = nil              // APT - Deinit & rc = 0

weak 키워드는 한 쪽에만 붙여줘도 된다. 이때, 해당 프로퍼티의 타입이 항상 Optional 이어야 한다. ( 당연히, var ) -> 참조하는 인스턴스가 메모리에서 해제 되었을 경우, 자동으로 nil 을 할당하기 때문

Unowned Reference : 미소유 참조

class Owner {
    let name: String
    var company: Company?

    init(name: String) {
        self.name = name
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

class Company {
    let name: String
    unowned let owner: Owner

    init(name: String, owner: Owner) {
        self.name = name
        self.owner = owner
        print("\(self.name) - Init")
    }
    deinit {
        print("\(name) - Deinit")
    }
}

var jun: Owner? = Owner(name: "jun")                // jun - Init
jun?.company = Company(name: "xxx", owner: jun!)    // xxx - Init 

jun = nil                                           // jun - Deinit & xxx - Deinit

jun 이 nil 이 될 때, 참조하고 있던 company 도 같이 사라짐

참조 카운트가 0이 되지 않았는데 원본이 사라지면 Crash 에러가 난다. 원본의 주소를 그대로 가리키고 있기 때문. 따라서, life cycle 을 고려해야 한다.

약한 참조는 참조된 인스턴스의 수명이 더 짧을 때 항상 사용되고, 소유되지 않은 참조는 참조된 인스턴스의 수명이 같거나 길 때 사용된다.


GC - Garbage Collection

메모리 관리를 프로그램 실행중에 동적으로 관리해주는 기술 감시하고 있다가, 사용을 하지 않는 상황에 메모리를 삭제 해주는 것 ( 어떤 변수도 가르키지 않게 된 메모리 ) run time 에 메모리가 관리 됨 ( 메모리사용, CPU 점유가 생길 수 밖에 없음 ) ARC 에 비해, 인스턴스가 해제 될 가능성이 더 높음 ( 메모리를 지속적으로 감시하고 있기 때문 ) 어떤 메모리를 해제해야 할 지 결정할 때 사용되는 비용이 많이 들게 됨 ( GC 의 알고리즘을 통해, 메모리 해제 시점을 찾아야하기 때문 ) 또한, GC 가 실행되는 시간이나 타이밍을 제대로 알기 어려움 ( 할당 해제 타이밍을 알 수 없음 )


iOS에서 가비지 컬렉션을 사용하지 않는 이유와 ARC를 선택한 배경..?

GC 의 경우, 런타임에서 작동하게 되는데 이때, 메모리와 CPU 를 사용하게 됨 기기의 메모리와 CPU 가 제한적인 모바일 기기에서 사용하기에 성능이 더 좋은 ARC 를 사용하지 않을까 생각 함

kmh5038 commented 6 months ago

순환참조가 아닐경우에는 강한 참조여도 괜찮은가요?

Hminchae commented 6 months ago

개발자의 실수에 의해 순환참조가 발생할 수 있는데, 이를 예방하기 위한 작업들에는 무엇이 있을까요?

Phangg commented 6 months ago

순환참조가 아닐경우에는 강한 참조여도 괜찮은가요?

네! default 가 강한참조인데, 사이클이 생기지 않는다면 ARC 에서 자동으로 관리되기 때문에 그냥 두어도 괜찮습니다.

Phangg commented 6 months ago

개발자의 실수에 의해 순환참조가 발생할 수 있는데, 이를 예방하기 위한 작업들에는 무엇이 있을까요?

제가 알기로는 실수를 할 수 있다는게 ARC 의 단점입니다.. 그래서 개발자는 직접 weak , unowned 를 잘써서 메모리를 관리해야합니다.

동호님의 이슈를 보며 알게 된 부분이지만, Autorelease Pool 을 이용하는 것도 하나의 방법일 수 있다고 생각합니다.

개발자의 실수? 라기 보다는 메모리가 해제되지 않을 수 있는 많은 데이터를 요구하는 로직에서 사용됨이 더 맞을 것 같아요!
사실 상, Objective-C 에서 사용되던 레거시 코드에 가깝다고 하네요. 현재 Swfit 의 언어 특성상, 구조체를 많이 사용하고 ARC 를 활용하기에 직접 사용할 일이 많지 않지만 알아두는게 맞는 것 같습니다 ㅎㅎ

아래는 Autorelease Pool 에 대한 글을 모아봤으니 읽어봐도 좋을 것 같아요!

Autorelease Pool 란? - 간단한 설명 Autorelease Pool 성능 테스트 Autorelease Pool 의 역사와 성능 테스트 등 - 영문 Autorelease Pool 의 역사와 성능 테스트 등 - 위 블로그 해석 및 요약 블로그