Swift 메모리 관리 – 클래스편

해당 포스트에서는 Apple의 메모리 관리에 대해 설명합니다. 대부분 ARC를 사용하여 자동으로 메모리를 처리한다고 하더라도 여전히 몇 가지 함정이 있습니다. 객체간의 설명하는 올바른 참조 유형을 선택하면 메모리 누수를 막을 수 있습니다.

자동 메모리 관리란?

Apple에서 구현한 자동 메모리 추적 관리 시스템인 ARC(Automatic Reference Counting)이라고 합니다.

ARC는 Objective-C와 Swift 언어에 대한 자동 참조 계산 기능을 제공하는 Clang(LLVM) 컴파일러의 메모리 관리 기능입니다. 컴파일시, 자동으로 구문을 분석해서 적절하게 레퍼런스 카운팅 횟수 코드를 삽입해주며, 런타임 중에 별로의 메모리 관리가 이루어지지 않습니다.

ARC는 런타임에 오브젝트를 비동기적으로 할당 해제하는 백그라운드 프로세스가 없다는 점에서 자바의 가비지 콜렉션 추적과 다릅니다. 가비지 컬렉션의 추적하는 것과 달리 ARC는 참조주기를 자동으로 처리하지 않습니다. 즉, 객체에 대한 강력한(Strong) 참조를 사용하여, 참조 카운트가 0이 되지않는 이상 해당 인스턴스는 해제가 되지 않습니다. 그 결과 강력한 상호 참조로 메모리의 교착상태(Deadlock)누수(Leak)가 발생할 수 있습니다.

또한, ARC는 클래스의 인스턴스에서만 적용됩니다. 구조체, 열거형 및 값 유형의 형식은 참조로 전달되지 않습니다. 이것은 Apple에서 가능한 Classes보다 Structs를 사용을 권고하는 또 하나의 이유입니다.

ARC에서 사용하는 속성은 총 3가지가 있으며, 해당 속성에 대해 더 알아보도록 합시다.

  1. 강한 참조 (Strong)
  2. 약한 참조 (Weak)
  3. 소유하지 않음 (unowned)

강한 참조 (Strong)

ARC의 참조 속성의 기본값입니다. 강한 참조는 참조 카운트를 1올리게 됩니다. 메모리에서 해제가 될 경우, 자동으로 -1를 하게됩니다. 이러한 특징을 살려 프로세스 로직을 선형(linear) 참조 플로우 형태로 구현했을 경우에 는 문제가 없습니다. 부모를 할당 해제하고 보유 수를 줄이면 부모에 포함된 모든 자식의 참조 카운트 또한 감소하게 됩니다. 다음은 강력한 참조의 예입니다.

일단 예제를 먼저 들어봅시다. 우리는 Music이라는 클래스를 만들어보겠습니다.

class Music {
  var title: String
  
  init(with title: String) {
    self.title = title
    print("Music of the title \(title) allocated")
  }

  deinit {
    print("Music of the title \(title) is being deallocated")
  }
}

해당 코드는 초기화 프로그램에 title이라는 인자값을 받고 메모리에 할당되어, print 메서드를 사용하여 메모리에 할당되었다고 출력하게 되며, 메모리에서 해제 될 시에는 deinit 클래스 전용 메서드를 통해 메모리에서 해제되었었음을 출력하는 클래스입니다.

do 문을 사용하여 코드를 내부에서 실행할 수 있도록 해당 코드를 구성해봅시다.

do {
 let getLucky = Music(with: "Get Lucky")
}

로그를 살펴보도록 할까요?

  • Music of the title Get Lucky allocated
  • Music of the title Get Lucky is being deallocated

Music 객체는 Do 내부에서 초기화와 함께 메모리에 할당되며, Do 구분 마지막에 자동으로 메모리에서 해제가 잘 되었습니다.

추가로, Singer라는 클래스를 만들어 봅시다.

class Music {
   var title: String
   var singer: Singer?

   init(with title: String) {
     self.title = title
     print("Music of the title (title) allocated")
   }

   deinit {
     print("Music of the title (title) is being deallocated")
   }
 }


 class Singer {
   var name: String
   var music: Music?

   init(with name: String) {
     self.name = name
     print("Singer (name) allocated")
   }

   deinit {
     print("Singer (name) deallocated")
   }
 }

Music 클래스 유형 또한 Singer 클래스 유형을 선택적 유형을 통해 사용할 수 있으며, 또한 Singer라는 클래스 유형에서도 Music 유형을 선택적 유형으로 사용할 수 있도록 구성하였습니다.

먼저 기존과 같이 두개의 클래스 유형을 생성하는 코드를 Do 구문을 써서 로그를 확인해 봅시다.

do {
  let getLucky = Music(with: "Get Lucky")
  let daftPunk = Singer(with: "Daft Punk")
}

로그를 살펴보도록 합시다.

  • Music of the title Get Lucky allocated
  • Singer Daft Punk allocated
  • Singer Daft Punk deallocated
  • Music of the title Get Lucky is being deallocated

서로 별개로 동작하는 경우, 메모리의 순환에 문제가 없습니다.

그런다면 서로간의 유형을 참조할 경우 어떤일이 벌어질가요? 한번 해봅시다.

do {
  let getLucky = Music(with: "Get Lucky")
  let daftPunk = Singer(with: "Daft Punk")
  getLuck.singer = daftPunk
  daftPunk.music = getLuck
}

로그를 살펴보도록 합시다.

  • Music of the title Get Lucky allocated
  • Singer Daft Punk allocated

메모리에서 해제가 되질 않네요? 왜그럴까요? 그이유는 위에서 설명한 참조 카운트가 0이 되면 메모리에 해제가 된다고 하였습니다. 결국 해당 로직은 메모리에서 해제가 되질 않았네요. 조금더 자세히 살펴볼까요?

해당 메모리 참조횟수는 서로 생성이 될 때 각각 +1을하게 됩니다. 그리고 서로 속성을 참조하는 부분에서 해당 참조 속성이 strong이므로 또한 각각 +1을 올리게 됩니다. Do 구문이 끝났습니다. 그리고 두 클래스 유형에 참조 카운트는 -1로 내려, 현재 두 클래스 유형의 참조 횟수는 1이 됩니다. 따라서 메모리에서 사라지지 않았습니다. 이를 해결하려면 서로 참조하는 부분을 Strong 말고 다른 속성을 사용해서 해결해야 합니다.


약한 참조: Weak

위의 예제에서 발생한 서로간의 참조문제에 대해 약한 참조를 사용하여 해결해보도록 하겠습니다.

강한 참조와 달리 약한 참조는 메모리 카운트를 +1증가 시키지 않습니다. 그래서 약한 참조 속성으로 선언된 클래스 유형은 ARC에 의해 할당 해제되는 것을 보호하지 않습니다. 할당 해제 시에 약한 참조는 자동으로 nil이 됩니다. 따라서 약한 참조의 유형은 반드시 선택유형을 사용해야하는 이유 또한 같습니다. 또한 상수로 정의한 유형은 약한 참조 유형으로 사용할 수 없습니다.

class Music {
/// ....

  var singer: Singer?
  weak var singer: Singer?

/// ....
}

class Singer {
/// ....

  var music: Music?
  weak var music: Music?

/// ....
}

두개의 선택 유형을 약한 참조로 바꾸고 다시 한번더 Do 구문을 실행 시켜봅시다.

do {
  let getLucky = Music(with: "Get Lucky")
  let daftPunk = Singer(with: "Daft Punk")
  getLuck.singer = daftPunk
  daftPunk.music = getLuck
}

로그를 살펴봅시다.

  • Music of the title Get Lucky allocated
  • Singer Daft Punk allocated
  • Singer Daft Punk deallocated
  • Music of the title Get Lucky is being deallocated

결과는 성공적이네요. 메모리 순환주기를 올바르게 지키게 되었습니다.

소유하지 않음 (unowned)

소유하지 않음은, 약한 참조와 동일하게 메모리의 참조 카운트를 +1하지 않습니다. 하지만, 두 속성간의 차이점은 이 속성은 선택적 유형을 사용하면 안됩니다. 이는 메모리 할당 해제시 nil로 자동으로 되지 않습니다. 일단 객체가 설정되면 객체가 결코 사라지지 않을 것이라고 확신할때에만 반드시 사용하시길 바랍니다.

Apple에 따르면 참조와 참조 된 코드가 동시에 할당 해제 될 경우에 사용하는것이 가장 좋다고 합니다.


맺음말

Apple의 ARC 시스템으로 인해 수동으로 관리하던 시절보다는 보다 편리하게 메모리를 관리하게 될 수 있었습니다. 하지만 비선형 메모리 관리 프로세스에서 강한 참조 주기로 인해 메모리 카운트가 0이 될 수 가 없는 경우는, weak(약한참조)와 unowned(소유하지 않는 참조)를 사용하여 메모리관련 문제를 방지 또는 해결하고 앱에 한정된 메모리를 균형적으로 사용하시길 바랍니다.

0 Shares:
You May Also Like
Read More

Swift 메모리 관리 – 클로저 편

스위프트의 캡쳐 목록은 클로저 코드 내부에 폐쇄된 공간의 매개 변수 목록 앞에 표시되며, 메모리 참조환경에서 강한참조(strong), 약한참조(weak), 미소유(unowned) 참조 메모리값을 캡쳐합니다.
Read More

클래스 vs 구조체

클래스와 구조체의 차이점은 무엇이 있을까요? Swift에서는 두 타입간에 매우 비슷합니다. 자 그럼 두 타입을 비교분석해보도록 합시다.