ARC

comdongsam·2022년 2월 14일
0

iOS Development

목록 보기
8/10

Automatic Reference Counting

직역하면 자동 참조 카운팅입니다. iOS의 메모리 관리 기법입니다. 간단하게 이야기하면 어떤 객체가 메모리에서 해제되기 위해서는 그 객체를 가리키는(= 참조하는) 객체의 갯수가 0이 되어야 합니다. Heap 영역에 저장되는 객체가 차지하는 메모리를 관리하기 위해서 사용합니다.

강한 참조와 약한 참조는 RC를 관리하는 기법입니다. 강한 참조는 RC를 증가시키는 반면 약한 참조는 RC를 증가시키지 않습니다. 말로만 설명하니까 어렵네요. 아래 코드를 함께 보면서 설명드려보겠습니다.

작동 원리

강한 참조

강한 참조는 RC를 증가시키는 참조 방법입니다. 모든 참조의 default 값은 강한 참조입니다.

예시 코드

아래 코드를 playground에서 실행해봅시다.

class SomeClass {
    
    var name = "some class"
    
    func printClassName() {
        DispatchQueue.global().async {
            sleep(3)
            print("클래스 이름: \(self.name)")
        }
    }
    
    deinit {
        print("\(name) 메모리에서 해제")
    }
}

func someFunction() {
    let someClass = SomeClass()
    someClass.printClassName()
		print("some function 실행 끝")
}

someFunction()

콘솔에 출력되는 결과는 아래와 같습니다.

"
some function 실행 끝
클래스 이름: some class
some class 메모리에서 해제
"

분명히 someFunction이 리턴되면 내부의 someClass는 메모리에서 해제되어야 하는데 return 되어도 해제되지 않고 메모리에 남아있다가 3초 뒤에 클래스 이름을 출력된 이후에야 해제되는 것을 볼 수 있습니다. 메모리에서 어떤 일이 일어나고 있었을까요?

메모리에서 일어나는 일

  1. 전역에서 실행한 someFunction이 Stack 영역에 올라갑니다.

  1. someFunction 내부에서 SomeClass의 인스턴스를 만들었습니다. Heap 영역에 객체가 생성됩니다. 또한 someFunction이 강하게 참조하고 있으므로 해당 객체의 RC는 1입니다.

  1. someFunction 내부에서 SomeClass의 메소드를 실행합니다. 해당 메소드는 글로벌 큐에 3초 쉬고 클래스 이름을 출력하는 클로저를 보내는 메소드입니다. 따라서 해당 클로저가 Heap 영역에 생성됩니다. 이 클로저는 context로 self를 캡쳐합니다. (강한 참조) 따라서 someClass 객체의 RC를 1 증가시켜 2가 됩니다.

  1. someFunction이 마지막으로 print문을 실행하면서 return 됩니다. 즉 Stack 영역에는 더 이상 someFunction이 존재하지 않게 됩니다. 즉 RC를 1 증가시키게 됩니다. 하지만 글로벌 큐에 보낸 클로저가 아직 살아있습니다. (3초 후에 실행되도록 했기 때문에) 따라서 아직 RC는 0이 아니므로 메모리에서 해제되지 않습니다.

  1. 3초뒤 글로벌 큐에서 클로저를 실행하면서 클로저가 메모리에서 해제되고 RC도 1 감소하여 0이 됩니다. 이제야 객체는 메모리에서 해제됩니다.

약한 참조

약한 참조는 RC를 증가시키지 않는 참조 방법입니다. 변수 앞에 weak라는 키워드를 붙여서 선언합니다. 클로저에서는 아래 코드처럼 캡쳐하는 대상 앞에 weak를 붙여서 사용합니다.

예시 코드

class SomeClass {
    
    var name = "some class"
    
    func printClassName() {
        DispatchQueue.global().async { [weak self] in
            sleep(3)
            print("클래스 이름: \(self?.name)")
        }
    }
    
    deinit {
        print("\(name) 메모리에서 해제")
    }
}

func someFunction() {
    let someClass = SomeClass()
    someClass.printClassName()
		print("some function 실행 끝")
}

someFunction()

콘솔에 출력되는 결과는 아래와 같습니다.

"
some function 실행 끝
some class 메모리에서 해제
클래스 이름: nil
"

이번에는 출력되는 순서도 다르고 클래스의 이름도 nil이 출력되고 있습니다. 메모리에서 일어나는 일을 살펴봅시다.

메모리에서 일어나는 일

  1. 여기 까지는 위의 강한 참조와 동일합니다. 함수가 Stack영역에 올라가고 내부에서 선언한 클래스가 Heap 영역에 생성되고 강한 참조를 했으므로 RC가 1 올라갑니다.

  1. 하지만 위 코드에서는 클로저에서 self를 캡쳐할 때 약한 참조를 사용했습니다. 따라서 Heap에 생기는 클로저는 객체의 RC를 증가시키지 않습니다. 객체의 RC는 여전히 1입니다.

  1. Stack에 있던 함수가 리턴이 되면서 메모리에서 해제됩니다. 강한 참조가 해제되었으므로 RC를 1 감소시킵니다. 이 때 객체의 RC가 0이 되면서 메모리에서 해제됩니다.

  1. 3초 후에 클로저는 self를 참조하려고 하지만 이미 메모리에서 해제되어 없는 상태입니다. 따라서 nil이 출력되게 됩니다.

Retain Cycle

참조 사이클은 서로 강하게 참조하고 있어서 서로 메모리에서 해제되지 않는 현상을 가르킵니다. 해제되어야 할 메모리가 계속 남아있어 메모리 누수 (Memory Leak)의 주범입니다.

참조 사이클을 해결하기 위해서는 한쪽 참조를 약한 참조로 바꾸어서 해결할 수 있습니다.

초보 개발자가 자주 만드는(?) 참조 사이클

개발을 하다보면 종종 의도치않게 참조 사이클을 만들어 버리곤 합니다. 저도 자주 실수를 했었습니다. 제가 주로 실수한 부분을 여러분께 공유드려보도록 하겠습니다.

Delegate 패턴

class ListCell: UITableViewCell {
	weak var delegate: ListCellDelegate?
}

UITableView나 UICollectionView를 사용하다보면 시스템에서 구현된 delegate 패턴이 아니라 커스텀 delegate를 지정해서 사용해야할 때가 있습니다. 이 때 보통 UIViewController를 delegate로 지정합니다.

delegate를 선언할 때는 반드시 weak로 선언해주시기 바랍니다. weak로 선언하지 않을 경우 VC가 해제되어야 할 때 ListCell이 VC를 강하게 참조해서 VC가 해제되지 않습니다. 즉 VC는 Cell을 강하게 참조하고 Cell은 VC를 강하게 참조해서 retain cycle이 발생하는 것입니다.

네트워크 API의 completionHandler

class VC: UIViewController {
	func fetchData() {  
			let service = FirebaseService()      
		  service.fetchNewData { [weak self] data in
	       self.updateUI(data)
		  }
	 }
}

간단한 예시로 설명드리겠습니다. 위 코드는 네트워크 (Firebase)에서 데이터를 받아와서 UI 업데이트를 하는 간단한 코드입니다. 이 때 completionHandler로 전달하는 클로저에서는 약한 캡쳐리스트를 사용하는 것이 좋습니다.

왜냐하면 네트워크에서 데이터를 받아오는 함수가 바로 return 된다는 보장이 없기 때문입니다. 위에서 예로 들은 Firebase는 데이터를 보내주고 바로 return 하는 API도 있지만 많은 API가 옵저버 형식으로 구현되어 있습니다. 쉽게 설명하면 데이터를 보낸 이후에도 데이터가 추가되면 추가된 데이터를 보내주기 위해서 return 하지 않고 메모리에 남아 있습니다.

이 경우에 강한 참조를 쓰게되면 VC를 메모리에서 해제하려고 해도 Firebase의 API가 VC를 참조하고 있기 때문에 메모리에서 해제되지 않게 됩니다. 즉 VC가 API를 강하게 참조하고 API가 VC를 강하게 참조해서 retain cycle이 만들어진 것이죠.

마치며...

  1. 처음에 이해하기 좀 어려운 개념이었습니다. 저도 많은 블로그를 찾아보고 유튜브를 찾아보며 공부했던 기억이 있습니다.
  2. 이해하기 어렵지만 개발하면서 적용하기는 더 어렵습니다. deinit을 해보면서 혹시 retain cycle이 발생하지는 않았나 확인하고 어디서 발생한 것인지 열심히 디버깅해서 해결하곤 했습니다.
  3. 하지만 iOS 개발자라면 반드시 숙지해야하는 개념입니다. 저도 더 열공해야겠습니다 ㅎㅎ
profile
교사 출신 iOS 개발자입니다. iOS를 사랑하고 교육 관련 비지니스, 코딩 교육에 관심이 많습니다. https://github.com/SteadySlower

0개의 댓글