본문 바로가기
iOS/iOS

[iOS] strong / weak / unowned / 순환 참조

by 0inn 2022. 9. 8.

strong (강한 참조)

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

default값이 strong이므로 지금까지 우리는 자연스럽게 인스턴스를 생성하고 사용하던 것이 전부 strong이었던 것입니다.

이러한 strong에도 문제점이 있는데 그것이 바로 순환 참조입니다.

 

순환 참조

순환 참조가 발생할 경우, 영구적으로 메모리가 해제되지 않을 수 있습니다.

예시로 순환 참조의 경우에 대해 알아봅시다.

 

class Student {
    var name: String
    var teacher: Teacher?
    
    init(name: String) {
    	self.name = name
    }
}

class Teacher {
    var name: String
    var student: Student?
    
    init(name: Int) {
    self.name = name
    }
}

// 인스턴스 생성

var student1: Student? = .init(name: "학생")
var teacher1: Teacher? = .init(name: "선생님")

 

student1이 가리키는 Student 인스턴스의 RC가 1이 되고, teacher1이 가리키는 Teacher 인스턴의 RC가 1이 됩니다.

이 때, 다음의 코드를 작성해봅시다.

 

student1?.teacher = teacher1
teacher1?.student = student1

 

이렇게 되면, Student의 인스턴스와 Teacher의 인스턴스가 서로를 가리키며 RC가 증가해 2가 됩니다.

이렇게 두 개의 객체가 서로를 참조하고 있는 형태를 순환 참조라고 합니다.

 

순환 참조의 문제점

student1 = nil
teacher1 = nil

 

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

따라서 student1, teacher1가 가리키던 변수에 nil을 할당했으니 RC가 0이 되어 각각 가리키던 Student와 Teacher 인스턴스가 힙에서 해제되어야 합니다.

하지만, 위와 같이 할 경우 각각의 인스턴스가 힙에서 사라지지 않습니다. 즉, 메모리 해제가 되지 않습니다.

 

왜일까요 ?

student1와 teacher1에 nil을 대입한 순간 각각 가리키던 인스턴스의 RC 값을 1씩 감소시킵니다.

이 때, 힙에 있는 Student와 Teacher 인스턴스들의 RC는 2에서 1이 감소된 1이 됩니다.

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

 

strong으로 선언된 변수들이 순환 참조 되었을 때 가장 큰 문제점은 서로가 서로를 참조하고 있기 때문에 RC가 0이 되지 못한다는 점입니다.

게다가 해당 인스턴스를 가리키던 변수(student1, teacher1)도 nil이 지정됐기 때문에 인스턴스에 접근할 방법도 없어 메모리 해제가 불가능합니다.

그러므로 어플이 죽기 전까지 memory leak이 발생하게 됩니다.

이렇게 strong을 사용해 순환 참조에 문제가 생긴 경우를 강한 순환 참조라고 합니다.

 

그렇다면, 이러한 강한 순환 참조를 해결할 방법은 없을까요 ?

weak과 unowned를 사용하면 됩니다 !

weak(약한 참조)

인스턴스를 참조할 시 RC를 증가시키지 않습니다.

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

여기서 weak는 무조건 옵셔널 타입의 변수여야합니다.

그 이유는 프로퍼티를 선언한 이후 나중에 nil이 할당되어야하기 때문이죠.

 

위의 예시를 그대로 들어보겠습니다.

강한 순환 참조의 문제가 서로가 서로를 참조하면서 생긴 문제였기 때문에 한 쪽을 weak로 선언하겠습니다.

프로퍼티 앞에 weak만 붙여주면 됩니다.

 

class Student {
    var name: String
    weak var teacher: Teacher?
    
    init(name: String) {
    	self.name = name
    }
}

class Teacher {
    var name: String
    var student: Student?
    
    init(name: Int) {
    self.name = name
    }
}

//

var student1: Student? = .init(name: "학생")
var teacher1: Teacher? = .init(name: "선생님")

student1?.teacher = teacher1
teacher1?.student = student1

 

 

아까는 // 아래 코드를 실행했을 때, 강한 순환 참조를 발생시켰는데요.

이렇게 weak로 선언한 경우에는 RC가 증가하지 않습니다.

그러므로 student1가 가리키는 Student 인스턴스의 RC값은 2가 되지만, teacher1이 가리키는 Teacher 인스턴스의 RC값은 1이 됩니다.

왜인지 이해가 가시나요 ?

teacher의 프로퍼티가 weak이기 때문에 Teacher 인스턴스를 참조하지만 해당 RC 값은 증가시키지 않기 때문입니다.

 

그렇다면 다시 student1와 teacher1 변수에 nil을 대입해보겠습니다.

student1 = nil
teacher1 = nil

 

 

  1. student1와 teacher1 변수는 nil이 할당된 순간 각각이 가리키던 Student와 Teacher에 대한 RC를 1씩 감소시킵니다.
  2. RC가 0이 된 Teacher 인스턴스가 메모리에서 해제됩니다.
  3. Teacher 인스턴스가 메모리에서 해제됨에 따라 해당 인스턴스의 프로퍼티인 student가 가리키던 Student 인스턴스의 RC가 1 감소합니다.
  4. weak로 선언된 teacher이 참조하던 인스턴스가 메모리에서 해제되었으므로 teacher의 값이 nil로 할당됩니다.
  5. Student 인스턴스의 RC 값이 0이 되었으니 메모리에서 해제합니다.

이렇듯 순환 참조이지만 weak로 선언되어 RC 값을 올리지 않는 것을 약한 순환 참조라고 합니다.

여기서 의문점 !

그렇다면 둘 중 아무거나 weak로 선언해도 상관없을까요 ?

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

unowned(미소유 참조)

강한 순환 참조를 해결할 수 있고, RC값을 증가시키지 않는 것까지는 weak과 동일합니다.

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

참조하던 인스턴스가 만약 메모리에서 해제된 경우, nil을 할당받지 못하고 해제된 메모리 주소값을 계속 들고 있게 됩니다.

이렇게 되면 unowned으로 선언된 변수가 가리키던 인스턴스가 메모리에서 먼저 해제된 경우, 접근하려 하면 에러가 발생하게 됩니다.

 

위의 예시를 또 다시 들어보겠습니다.

 

class Student {
    var name: String
    var teacher: Teacher?
    
    init(name: String) {
    	self.name = name
    }
}

class Teacher {
    var name: String
    unowned var student: Student?
    
    init(name: Int) {
    self.name = name
    }
}

//

var student1: Student? = .init(name: "학생")
var teacher1: Teacher? = .init(name: "선생님")

student1?.teacher = teacher1
teacher1?.student = student1

 

student1의 수명이 teacher1보다 더 길다는 가정하에 teacher1이 가리키는 Teacher 인스턴스의 student 프로퍼티를 unowned로 선언했습니다.

메모리에 올라가고 해제하는 등의 전체적인 동작은 weak과 동일합니다.

이 때, 중요한 점은 unowned가 붙은 teacher1의 student가 가리키는 student1(Student) 인스턴스는 teacher1(Teacher) 인스턴스가 메모리에서 해제되기 전까지 절대 먼저 해제되어서는 안됩니다.

 

만약 student1의 인스턴스가 teacher1의 인스턴스보다 먼저 메모리에서 해제되면 어떻게 될까요 ?

weak의 경우엔 자동으로 student의 값이 nil로 지정되겠지만, unowned의 경우 이미 해제된 메모리 주소값을 계속 들고 있습니다.

이 때, teacher1.student 로 접근하려하면 이미 메모리에서 해제된 포인터 값에 접근하려 해서 에러가 발생합니다.

unowned는 에러를 발생시킬 위험이 있어서 weak를 사용하는 것을 권장합니다.

 

여기서 의문점 !

그렇다면 둘 중 아무거나 unowned로 선언해도 상관없을까요 ?

가능하지만 weak과 반대로 둘 중에 수명이 더 긴 인스턴스를 가리키는 애를 미소유 참조로 선언합니다.

 

+ unowned는 nil을 할당받지 않으므로 무조건 Non-Optional Type으로 선언해야 했지만,

   Swift 5.0부터는 Optional Type으로도 선언 가능하다고 합니다.

 

strong vs. weak vs. unowned

  strong weak unowned
RC O X X
사용 시점 default 강한 순환 참조 발생할 경우 - 강한 순환 참조 발생할 경우
- 참조하는 인스턴스가 먼저 메모 리에서 해제될 가능성이 없는 경우
특징 강한 순환 참조로 인해
Memory leak 발생 가능
참조하던 인스턴스가 해제되면
자동으로 nil 할당
참조하던 인스턴스가 먼저 메모리에서 해제되면, 해제된 주소값을 들고 있음 (에러 가능성 높음)

 


참고

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

 

 

'iOS > iOS' 카테고리의 다른 글

[iOS] UIView.frame과 UIView.bounds  (0) 2023.09.04
[iOS] URLSession  (0) 2022.09.13
[iOS] ARC (Automatic Reference Counting)  (0) 2022.09.08