ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 메모리관리(2/3) - strong, weak
    컴퓨터상식/운영체제(OS) 2022. 3. 4. 03:22

    1. strong (강한 참조)

    ARC는 참조 횟수를 계산하여 0이 되는 시점에 힙에서 자동으로 해제하는 것

    복습)

    class Human {
        var name: String?
        var age: Int?
        
        init(name: String?, age: Int?) {
            self.name = name
            self.age = age
        }
    }
     
    let sodeul = Human(name: "Sodeul", age: 26)

    이렇게 sodeul 이라는 변수에 인스턴스를 생성하면

    (포스팅을 위해 예제에선 sodeul이 전역 변수가 됐지만,

    보통 우리가 개발할 때 그렇듯, 그냥 어디 클래스에서 생성된 지역 변수라고 생각)

     

    이런 식으로 인스턴스는 힙 영역에 할당되고, 

    sodeul이란 지역 변수가 Human 인스턴스의 주소값을 할당받기 때문에 Human 인스턴스의 RC가 +1 

     

    ARC를 배웠으니 이는 당연해 보이지만,  당연한 것이 바로 strong, 강한 참조

     

    인스턴스의 주소값이 변수에 할당될 때, RC가 증가하면 강한 참조(strong) 인 것

     

    우리가 지금껏 자연스럽게 인스턴스를 생성하고 사용하던 것이 다 strong이자 강한 참조였던 것 (default 값이 strong)

     

    따라서 위에서 sodeul로 생성한 인스턴스는 default인 strong으로 선언되어 '강한 참조' 객체인 것

     

    하지만 이 strong 에는 순환참조의 문제가 존재

     


    2. 순환 참조

     

    2-1. 순환참조란?

    ARC의 단점

    class Man {
        var name: String
        var girlfriend: Woman?
        
        init(name: String) {
            self.name = name
        }
        deinit { print("Man Deinit!") }
    }
     
    class Woman {
        var name: String
        var boyfriend: Man?
        
        init(name: String) {
            self.name = name
        }
        deinit { print("Woman Deinit!") }
    }
     

    Man에게는 Woman 타입의 프로퍼티(girlfriend)가 있고,

    Woman에게는 Man 타입의 프로퍼티(boyfriend)가 존재

     

    var chelosu: Man? = .init(name: "철수")
    var yeonghee: Woman? = .init(name: "영희")

    이렇게 생성한 경우,

    메모리에는 다음과 같이 할당

     

     

    여기까진 문제 X

    근데 만약 철수와 영희가 사귀기로 했을 경우 다음 코드를 작성하면

    chelosu?.girlfriend = yeonghee
    yeonghee?.boyfriend = chelosu

    메모리는 다음과 같은 모양

    Man 인스턴스와 Woman 인스턴스의 RC가 각각 1씩 증가 

     

    boyFriend 프로퍼티엔 Man 인스턴스의 주소값을 할당하고,

    girlFriend 프로퍼티엔 Woman 인스턴스의 주소값을 할당한 것

     

    또한 girlFriend, boyFriend 프로퍼티는 기본 값 strong이기 때문에 서로의 RC 값이 1씩 증가한 것

     

     

    철수 인스턴스의 프로퍼티는 영희를 참조하고, 영희 인스턴스의 프로퍼티는 철수를 참조하는 것 처럼

    두 개의 객체가 서로가 서로를 참조하고 있는 형태를  순환 참조 

     

     

    2-1. 순환 참조의 문제점 

    철수와 영희를 kill 하기로 결정, nil을 지정

    chelosu = nil
    yeonghee = nil

    그러면 저번 포스팅에서 배웠던 대로

    인스턴스를 가리키던 변수에 nil을 대입했으니 RC가 1만큼 감소하고 0일 경우 메모리에서 해제 되어야 함

     

    따라서 우리가 원하는 시나리오는

    철수, 영희의 인스턴스를 다른 변수에 대입한 적도 없고,

    철수, 영희를 가리키던 변수에 nil을 할당 했으니, Reference Count가 0이 되어

    철수와 영희가 가리키던 인스턴스가 힙에서 각각 해제되어야 하는 시나리오

     

    but!!

    위 경우에 Man, Woman 클래스의 deinit 메서드가 호출되지 않는다

    (deinit 함수는 인스턴스가 메모리에서 해제될 때 실행되는 메서드)

     

    즉, 철수와 영희가 가리키던 인스턴스가 힙에서 사라지지 않고 계속 메모리를 먹고 있다는 뜻

     

     

     

    이유를 알기 위해 철수와 영희 변수에 nil을 대입했을 시점의 메모리 상태를 보면

    먼저 철수와 영희 변수에 nil을 대입한 순간, 각각 가리키던 인스턴스의 RC 값을 1씩 감소

    그런데 힙에 있는 Man & Woman 인스턴스들의  RC는 0이 아니고 1

     

    순환참조 때문에 아까 서로의 RC가 1씩 증가 됐기 때문!

    따라서 RC가 0이 아니기 때문에 해제되지 않고 계속 힙에 남아 있는 것

     

     

    이렇듯, strong으로 선언된 변수들이 순환참조 됐을 시 큰 문제점은

    1) 서로가 서로를 참조하고 있어서 RC가 0이 되지 못한다는 것

    2) 심지어 해당 인스턴스를 가리키던 변수(철수, 영희)도 nil이 지정됐기 때문에  인스턴스에 접근 할 수 있는 방법도 없어 메모리 해제도 못함

     

    따라서 어플이 죽기 전까진... memory leak이 계속 발생

     

    이렇게 strong을 사용해서 순환참조에 문제가 생긴 경우강한 순환 참조 라고 명명

     

     

     

    그럼 해결은 어떻게 할까?

     


     

    3. weak, unowned

    강한 순환 참조를 해결하기 위해선 weak와 unowned를 사용

     

    3-1. weak (약한 참조)

    약한참조란,

    1) 인스턴스를 참조할 시 RC를 증가시키지 않는다

    2) 참조하던 인스턴스가 메모리에서 해제된 경우, 자동으로 nil이 할당되어 메모리가 해제된다

    이 두가지가 핵심

     

    프로퍼티를 선언한 이후, 나중에 nil이 할당된다는 관점으로 보아 weak는 무조건 옵셔널 타입의 변수 여야함

     

     

    강한 순환 참조는 서로가 서로를 강하게 참조하고 있던 것이 문제였으니 둘 중 한 쪽을 weak로 선언해 주는 것

    (지금은 철수와 영희의 수명이 동일하기 때문에 아무나 weak로 선언해 주지만, 수명이 다를 경우 어떤 쪽을 weak로 선언해야 하는지는 추후에)

    class Man {
        var name: String
        weak var girlfriend: Woman?
        
        init(name: String) {
            self.name = name
        }
        deinit { print("Man Deinit!") }
    }
     
    class Woman {
        var name: String
        var boyfriend: Man?
        
        init(name: String) {
            self.name = name
        }
        deinit { print("Woman Deinit!") }
    }
     

    weak를 선언하는 방법은 순환 참조를 일으키는 프로퍼티 앞에 weak를 붙이기

     

    var chelosu: Man? = .init(name: "철수")
    var yeonghee: Woman? = .init(name: "영희")

    chelosu?.girlfriend = yeonghee
    yeonghee?.boyfriend = chelosu

    이렇게 weak로 선언한 경우, RC 가 증가하지 않는다

     

    girlfriend의 프로퍼티가 weak이기 때문에

    Woman Instance의 주소값을 할당받지만(참조하지만), Woman Instance의 RC의 값은 건들지 않음

     

     

    그럼, 이게 어떻게 순환 참조를 해결해줄까?

    chelosu = nil
    yeonghee = nil

     ① 철수, 영희 변수는 nil이 할당된 순간 각각의 인스턴스에 대한 RC를 1씩 감소한다 

     ② RC가 0이 된 Woman Instance가 메모리에서 해제된다 

    이때, RC Count Down 방식에 따라 boyfriend의 RC도 -1 감소

    어떤 인스턴스(Woman Instance)의 프로퍼티(boyfriend)다른 인스턴스(Man Instance)를 가리키고 있을 때,

    그 프로퍼티(boyfriend)가 속한 인스턴스(Woman Instance)가 메모리에서 해제되면

    그 프로퍼티(boyfriend)가 가리키고 있던 인스턴스(Man Instance)의 RC가 -1 감소한다)

     

    만약 이 부분이 이해가 안 간다면,

    이전 포스팅 ARC에 대해 다시 공부하고 오세욥 :))

     

     

     ③ weak로 선언된 girlfriend가 참조하던 인스턴스가 메모리에서 해제 되었으니, girlfriend의 값이 nil로 할당된다 

    (weak의 특징! 가리키던 인스턴스가 메모리에서 해제될 경우 nil이 할당된다!)

     

     ④ Man Instance의 RC 값도 0이 되었으니 메모리에서 해제한다 

     ⑤ deinit 함수도 정상 작동 

    이렇게 순환 참조이지만 weak로 선언되어 RC 값을 올리지 않는 것약한 순환 참조

     

     

    🔸 그럼 철수와 영희 중에 누구든 weak로 선언해도 상관 없나!?

    -> 예제에선 철수와 영희의 수명이 "동일"하기 때문에 아무나 weak로 선언 했지만 강한 순환 참조가 난 경우,

    둘 중에 수명이 더 짧은 인스턴스를 가리키는 애를 약한 참조로 선언

     

    철수가 먼저 죽는다 -> 영희의 boyfriend가 nil이 될 수 있다 -> 영희의 boyfriend를 weak로 선언한다

    영희가 먼저 죽는다 -> 철수의 girlfriend가 nil이 될 수 있다 -> 철수의 girlfriend를 weak로 선언한다

     

     

    3-2. unowned (미소유 참조)

     

    여러분... unowned은 weak 처럼 자세하게 설명하지 않을 것임....

    힘들어 끵.. 그리고 weak랑 아주 약간만? 달라서 크게 어렵지 않음

     

    먼저 unowned와 weak는 공통점은 1) 강한 순환 참조를 해결  2) RC 값을 증가시키지 X

     

    차이점은 unowned은  인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하는 것

     

    따라서 참조하던 인스턴스가 만약 메모리에서 해제된 경우,

    nil을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있음

     

     

    따라서 unowned으로 선언된 변수가 가리키던 인스턴스가 메모리에서 먼저 해제된 경우, 접근하려 하면 에러를 발생

     

    class Man {
        var name: String
        unowned var girlfriend: Woman?
        
        init(name: String) {
            self.name = name
        }
        deinit { print("Man Deinit!") }
    }
     
    class Woman {
        var name: String
        var boyfriend: Man?
        
        init(name: String) {
            self.name = name
        }
        deinit { print("Woman Deinit!") }
    }
     

    철수보다 "영희가 더 오래 산다는 가정"하에 철수 인스턴스의 girlfriend 프로퍼티를 unowned로 선언

    이렇게 선언한 메모리에 올라가는 방식은 weak와 동일

     

    마찬가지로 RC를 건들지 않고 참조된다.  이 외에 메모리 해제하고 하는 등 전체적인 동작이 weak와 동일

     

    But!!

    unowned가 붙은 철수의 grilfriend가 가리키는 영희(Woman) 인스턴스는,

    철수(Man) 인스턴스가 메모리에서 해제되기 전까진 절대 절대 먼저 해제되어선 안된다

    (철수가 먼저 죽고 난 후에 영희가 죽어야 함)

     

    이 부분이 weak와 다른 부분!

    만약 영희의 인스턴스가 철수의 인스턴스보다 먼저 메모리에서 해제될 경우

    yeonghee = nil

     

    weak의 경우엔 자동으로 girlfriend의 값이 nil로 지정 되겠지만,

    unowned의 경우 nil을 할당받지 못해 이미 해제된 메모리 주소값을 계속 들고 있음

     

     

    따라서 weak 경우, 철수의 girlfriend 프로퍼티에 접근하면

    위와 같이 nil이 지정되어 있으나..

    unowned의 경우엔

    이미 메모리에서 해제된 포인터 값에 접근하려 해서 에러가 발생

    이것이 바로  weak와 unowned의 차이점

     

    unowned는 에러를 발생시킬 위험이 있어서

    웬만해선 weak를 사용하는 것을 권장

     

     

     

    🔸 그럼 철수와 영희 중에 누구든 unowned로 선언해도 상관 없나!?

    -> 아무나 할 순 있지만, 강한 순환 참조가 난 경우 weak와 반대로

    둘 중에 수명이 더 긴 인스턴스를 가리키는 애를 미소유 참조로 선언

     

    철수가 먼저 죽는다 -> 영희의 수명이 더 길다 -> 수명이 더 긴 영희를 가리키는 철수의 girlfriend를 unowned로 선언한다

    영희가 먼저 죽는다 -> 철수의 수명이 더 길다 -> 수명이 더 긴 철수를 가리키는 영희의 boyfriend를 unowned로 선언한다

     

    누가 더 먼저 죽을지 모르는 경우에는..  unowned를 사용하는 게 위험

     

    unowned의 경우 내가 가리키는 인스턴스가

    먼저 메모리에서 해제될 일은 일어나지 않는다!!!(나보다 수명이 길거나 같다!!)

    라고 판단되는 경우에 쓰면 되는데....

     

    unowned를 사용하는 경우는 ARC와 클로저 편에 더 추가 했으니, (2021. 03. 17)

    이 포스팅을 참조(만) 하시길....... (완벽한 내용이라 자신은 못함..)

     

     

     

    + 원래 unowned는 가리키던 메모리가 해제돼도 nil을 할당받지 않아서

    Non-Optional Type으로 선언해야 했지만(따라서 let도 가능했..),

    Swift 5.0부턴 Optional Type으로도 선언 가능 

     

     

     

     

    3. 정리

      strong weak unowned
    Reference Counting O X X
    사용 시점 ▪ Default ▪ 강한 순환 참조가 발생할 경우 ▪ 강한 순환 참조가 발생할 경우

    ▪  참조하는 인스턴스가 
    먼저 메모리에서 해제될 
    가능성이 없는 경우
    특징 ▪ 강한 순환 참조로 인해
    Memory leak 이 발생할 수 있음
    ▪ 참조하던 인스턴스가 해제되면
    자동으로 nil을 할당
    ▪  참조하던 인스턴스가
    먼저 메모리에서 해제되면,
    해제된 주소값을 계속 들고 있음
    (에러로 이어질 가능성 높음)

     

    출처: https://babbab2.tistory.com/27?category=831129

     

     

    추가로 읽어볼것

    https://hyunndyblog.tistory.com/154

     

    [Swift] weak, unowned, Retain cycle 톺아보기

    개요 코드를 작성하다보면 이젠 정말 무의식적으로 클로저안에 [weak self] , [unowned self] 를 사용하는데요. 처음 이 키워드들에 대해 이해했을 때 메모리 사이클에서 순환 참조가..메모리릭이...하

    hyunndyblog.tistory.com

     

    댓글

Designed by Tistory.