본문 바로가기
iOS/Swift

[iOS/Swift] 메모리 누수가 발생하는 원인과 해결방안(Strong, weak, unowned)

by iosdevlime 2023. 7. 19.

지난 포스팅에서 예고한 [메모리 누수]에 대한 내용을 다뤄볼까 합니다.

 

다만, 이번 포스팅에서는 탈출 클로저가 아닌

일반적인 예시 코드를 통해

  • 정상적인 ARC 작동과정
  • 메모리 누수 발생 (Memory leak)
  • 해결 방법

위 3가지 순서로

미처 다루지 못한 메모리 누수의 원인과 해결과정을 살펴볼 예정입니다.

 

 

 


 

 

 

메모리 누수(Memory leak)와 3가지 참조유형

순환 참조(Retain Cycle)로 인하여 Reference Count가 0으로 수렴하지 않아 발생하는 문제

 

본론으로 들어가기에 앞서, 메모리 누수(Memory leak)에 대해 살펴볼까요?

 

그 전에,

CS 내용 중, ARC 동작 방식에 대한 사전지식이 필요하겠지요.

 

 

메모리 누수(Memory leak)란?

  • 둘 이상의 객체간의 강한 상호참조 관계인 '순환 참조'로 인해 발생하는 이슈입니다. 
    • Swift는 ARC를 통해 자동으로 메모리 할당&해제 과정을 수행합니다.
    • 하지만, 객체간의 강한 연결고리로 인해 메모리가 해제되지 않는 경우도 발생합니다.

 

어떻게 해결하나요?

  • 객체를 정의하는 레퍼런스(참조)의 유형을 변경함으로서 해결할 수 있습니다!
    • 메모리에 할당되는 객체는, 아래와 같이 크게 3가지 참조유형으로 구분됩니다. 
    • 객체를 선언할 시, 아무런 키워드가 없다면 일반적으로 strong(강한 참조)입니다.

 

1️⃣ strong (강한 참조)

- 참조하는 인스턴스의 RC를 증가시키는 일반적인 참조 유형
- RC가 0이 될 경우, 메모리 상에서 해제됨
- 순환 참조가 발생할 경우, 메모리 누수가 발생할 수 있음

 

2️⃣ weak (약한 참조)

- 객체의 소유권을 가지지 않고, 주소값만을 가진 Pointer
- 참조하는 객체의 RC를 증가시키지 않음 (메모리 상에 weak 유형만 남아있다면, 객체는 메모리를 해제)
- 참조하는 객체가 메모리가 해제될 경우, 자동으로 nil값이 할당됨 
(해당 객체는 반드시 optional 타입)

 

3️⃣ unowned (미 소유 참조)

- 객체의 소유권을 가지지 않지만, 항상 값이 있음을 가정함 (즉, optional 타입이 아님)
- 참조하는 인스턴스의 RC를 증가시키지 않음
- 참조하는 객체가 메모리에서 해제되어도 댕글링 포인터가 존재함
 (해당 포인터를 참조 시, Error 발생)

* 댕글링 포인터(Dangling pointer) : 객체가 해제되어도, 할당되지 않는 공간을 가르키는 포인터

 

  • 결과적으로, 적절한 참조 유형을 활용함으로서 순환참조에 의한 메모리 누수를 해결할 수 있습니다!
    • 일반적으로 [weak 참조 유형]이 메모리 누수 문제를 해결하기 위하여 활용됩니다.
      (unowned 참조 유형의 경우, 객체의 Life Cycle이 명확할 경우에 한하여 사용합니다)

 

 


 

 

예시와 함께 살펴보는 메모리 누수 해결 과정

weak 참조 유형을 객체와 함께 활용하여 순환참조 및 메모리 누수를 해결해보자

 

더 길어지기 전에, 메모리 누수 과정과 weak 참조 유형을 활용하는 과정을

아래 예시와 함께 다루도록 하겠습니다.

 

Canis(개, 늑대 등이 속한 종) 클래스가 있습니다.
인스턴스 클래스가 생성되고, 해제될 때 마다 RC가 변화는 상황을 살펴보고자 합니다.

 

 

 

정상적인 ARC 작동과정

  • Canis 클래스 내부에는 RC를 확인할 수 있는 정적 변수 countRC가 존재합니다.
    • init 메서드를 통해 새로운 클래스 인스턴스가 생성될 시, RC를 1만큼 증가시킵니다.
    • deinit 메서드를 통해 해당 클래스 인스턴스가 nil값으로 해제될 시, RC를 감소시킵니다.
  • ARC 과정에 따라 doggy, wolf 클래스 인스턴스는 자동으로 할당/해제됩니다.  
class Canis {

    // RC 할당&해제를 살펴보기 위한 정적 변수
    static var countRC: Int = 0

    init() {
        Canis.countRC += 1 // RC 증가
        print("메모리에 할당되었습니다. 현재 RC는? : \(Canis.countARC)")
    }

    deinit {
        Canis.countRC -= 1 // RC 감소
        print("메모리에서 해제되었습니다. 현재 RC는? :  \(Canis.countARC)")
    }
}

var doggy: Canis? = Canis() // 메모리에 할당되었습니다. 현재 RC는? : 1
var wolf: Canis? = Canis() // 메모리에 할당되었습니다. 현재 RC는? : 2

doggy = nil // 메모리에서 해제되었습니다. 현재 RC는? :  1
wolf = nil // 메모리에서 해제되었습니다. 현재 RC는? :  0

 

 


 

 

메모리 누수(Memory leak) 발생

  • 이번엔, species란 Canis 클래스 타입의 프로퍼티를 내부에 선언합니다.
    • 이후, doggy, wolf란 명칭의 클래스 인스턴스를 선언합니다.
    • 2개의 클래스 인스턴스를 생성했으므로, 메모리 상 RC가 +2 증가합니다.
  • 각각의 클래스 인스턴스의 species 프로퍼티의 값으로 교차된 클래스 인스턴스를 할당합니다.
    • 할당한 후, 각각의 클래스 인스턴스에 nil을 할당하여 메모리를 해제하고자 합니다.
    • 하지만, deinit 로그가 출력되지 않습니다! (메모리 누수)
class Canis {

    // RC 할당&해제를 살펴보기 위한 정적 변수
    static var countRC: Int = 0

    var species: Canis?

    init() {
        Canis.countRC += 1 // RC 증가
        print("메모리에 할당되었습니다. \(Canis.countARC)")
    }

    deinit {
        Canis.countRC -= 1 // RC 증가
        print("메모리에서 해제되었습니다. \(Canis.countARC)")
    }
}

var doggy: Canis? = Canis() // Doggy 인스턴스가 메모리에 할당되었습니다. 1
var wolf: Canis? = Canis() // Doggy 인스턴스가 메모리에 할당되었습니다. 2

doggy?.species = wolf // 메모리에 할당되었습니다. 1
wolf?.species = doggy // 메모리에 할당되었습니다. 2


// 여기서, 각각의 인스턴스 객체에 nil을 할당하였지만, 해제 로그가 발생하지 않음
doggy = nil // ?
wolf = nil // ?

 

  • 이유는 즉, doggy와 wolf의 프로퍼티인 species가 상호 참조하고 있기 때문입니다.
    • 클래스 인스턴스는 nil값이 되었으나, Heap에 남아있는 프로퍼티간의 참조는 존재합니다.
    • 결과적으로 RC값이 0이 되질 않는, 순환 참조로 인한 메모리 누수 문제가 발생합니다.

doggy, wolf 인스턴스에 nil이 할당됨으로서, 참조값에 대한 접근이 불가능함
doggy, wolf 인스턴스에 nil이 할당됨으로서, 참조값에 대한 접근이 불가능함

 

 

 


 

 

 

해결방법(with. weak 참조유형)

  • 메모리 누수는 결국 객체(프로퍼티)간의 강한 참조로 인해 발생합니다.
    • 참조하는 객체가 메모리 상 해제될 때 RC값을 감소시켜야 하므로,
      3가지 유형 중, weak 참조유형을 활용합니다.
    • Canis 클래스 내부에 위치한 species 변수 앞에 weak 키워드를 작성합니다.
  • 그 결과, 정상적으로 메모리 해제 과정이 진행됩니다.
class Canis {

    // RC 할당&해제를 살펴보기 위한 정적 변수
    static var countRC: Int = 0

    weak var species: Canis?

    init() {
        Canis.countRC += 1 // RC 증가
        print("메모리에 할당되었습니다. \(Canis.countARC)")
    }

    deinit {
        Canis.countRC -= 1 // RC 증가
        print("메모리에서 해제되었습니다. \(Canis.countARC)")
    }
}

var doggy: Canis? = Canis() // Doggy 인스턴스가 메모리에 할당되었습니다. 1
var wolf: Canis? = Canis() // Doggy 인스턴스가 메모리에 할당되었습니다. 2

doggy?.species = wolf // 메모리에 할당되었습니다. 1
wolf?.species = doggy // 메모리에 할당되었습니다. 2


// 해제 로그 또한 정상적으로 작동한다.
doggy = nil // 메모리에서 해제되었습니다. 1
wolf = nil // 메모리에서 해제되었습니다. 0

 


 

순환 참조, 메모리 누수에 이어

이를 해결하는 방안의 일환으로 살펴본 3가지 객체 참조 유형까지 살펴보았습니다.

 

다음 포스팅에서는 실제 프로젝트를 통해

클로저를 사용할때 발생하는 메모리 누수를 다뤄보도록 하겠습니다.

 

댓글