코딩캣: 코딩하는 고양이.
[번역글] Swift의 자동 레퍼런스 카운트 (2) [完]
Language/Objective-C & Swift
2021. 9. 17. 19:11

본 게시글은 Automatic Reference Counting (ARC) in Swift를 바탕으로 작성하였습니다.

Swift의 자동 레퍼런스 카운트

자동 레퍼런스 카운트(ARC: Automatic Reference Counting)는 Swift에서 메모리를 관리하는 메커니즘입니다. ARC라는 개념을 가지고 작업하는 것은 최신의 iOS 개발에서 필수적입니다. 이번 게시글에서 우리는 iOS의 메모리 관리를 위해 ARC가 어떻게 사용되는지 살펴보겠습니다.

  • ARC는 어떻게 작동되는가
  • iOS 앱에서 메모리 관리가 왜 중요한가
  • 객체의 retain 횟수가 소멸에 어떻게 영향을 주는가
  • 메모리 관리의 실전 예제
  • 순환 retain과 같은 흔한 결함
  • strong 참조 및 weak 참조를 써서 작업하기

준비 되었다면 시작하겠습니다.

 

할당과 소멸이 어떻게 작동하는가

이전 예제에서 우리는 메모리에 있는 인스턴스에 대해 strong 참조가 그 인스턴스를 retain하여 그 retainCount 값을 0보다 큰 값으로 설정한 것을 확인하였습니다.

우리는 또한 생성자로서 Car()와 같은 코드가 어떻게 객체를 만들고 메모리에 이를 보관하는지에 대해 살펴보았습니다. 이것을 “할당(allocation)”이라고 하는데 Car 클래스형 객체를 담기 위해 RAM의 일부가 확보되기 때문입니다. 그렇다면 Car 객체가 “소멸(deallocation)”하는 것은 무엇일까요?

다른 데이터를 위해 메모리는 언제 비워지고 언제 해제되는 것일까요? 게다가 이전 예제에서 우리는 명시적으로 car1car2nil로 대입해 주었는데, 이것은 여러분이 일상적으로 코딩하는 방식은 아닐 것입니다. 여러분이 사용했던 모든 참조형 변수에 대해 nil을 대입하며 마치는 것은 분명히 아닐 것입니다.

명시적으로 nil이 대입되지 않을 경우 strong 참조형 변수에게는 어떤 일이 일어날까요?

이 질문에 대한 답을 찾기 위해서는 ARC의 핵심부를 건드리게 됩니다. 좋은 소식은 이것들이 자동으로 수행되기 때문에 여러분은 이에 대해 생각할 필요가 없다는 것이고, 나쁜 소식은 이것이 자동으로 수행되기 때문에 여러분이 이에 대해 이해하기 위해서는 다소 깊게 파고 들어가야 한다는 것입니다.

다음 예제를 살펴보겠습니다.

// Swift
func rentCar(type: String) {
    var rental = Car()
    rental.type = type
    
    // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
    rental.drive()
}
// Objective-C
-(void) rentCarWithType:(NSString *)type {
    Car * rental = [[Car alloc] init];
    [rental setType: type];
    
    // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
    [rental drive];
}

 

위 함수에서 우리는 특정 유형의 렌터카를 새로 만들었습니다. 그 다음 그 차를 가지고 운전을 했고 렌탈 회사에 반납한 후 집에 돌아갑니다. 그리고 질문합니다. “그렇다면 Car 클래스형 인스턴스는 언제 소멸되는 것일까?”

 

여러분이 retainCount를 기억하고 있다면 여러분은 rentCar()의 본문이 끝나는 곳에서 Car 인스턴스가 소멸됨을 보장받을 수 있습니다. 그 이유는 다음과 같습니다.

(1) Car 인스턴스의 retainCountrental 변수이 이를 참조하면서 0에서 1로 증가합니다.

(2) type이라는 문자열은 Swift에서 값 형식(value-type)입니다. 그래서 rental 변수의 retainCount에 영향을 주지 않습니다.

(3) 적어도 우리가 본 바로는 drive() 함수는 어떤 부수적인 효과도 갖지 않기에 retainCount 값에 영향을 주지 않습니다.

(4) rentCat 메소드(함수)의 본문 끝 부분에서 rental 변수는 더 이상 필요가 없게 됩니다. 다시 말하면 함수의 스코프(scope) 존재성이 여기에서 마치게 됩니다. 때문에 rental 변수는 암시적으로 nil로 설정되면서 Car 객체는 메모리에서 소멸됩니다.

 

코드 내 다른 부분 어디에서도 rental에 대해 strong 참조를 하는 부분은 없습니다. 그리고 이 변수에 참조되는 인스턴스는 rentCar 메소드(함수)를 벗어나지 않습니다, 달리 표현하면 메소드(함수) 바깥으로 전달이 된다거나 반환이 된다거나 하지 않는다는 것입니다. 그 결과 Car 클래스형 인스턴스는 메모리에서 해제됩니다.

 

그렇다면 rental이 클래스의 프로퍼티로 있을 때는 어떨까요? 모든 것이 바뀝니다. 이를 확인해 보겠습니다.

// Swift
class RentalCompany {
    var rental: Car?
    
    func rentCar(type: String) {
        rental = Car()
        rental.type = type
        
        // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
        rental.drive()
    }
}

let company = RentalCompany()
company.rentCar(...)
// Objective-C
@interface RentalCompany
@property Car * rental;
@end
@implement RentalCompany
@synthesize rental;
-(void) rentCarWithType:(NSString *)type {
    [self setRental: [[Car alloc] init]];
    [[self rental] setType:type];
    
    // 차를 운전하고, 돌려줍니다. 그리고 아무것도 안 합니다.
    [rental drive];
}
@end

 

rentCar() 메소드의 본문이 끝나더라도 rental 프로퍼티는 여전히 새로 만들어진 Car 클래스형 인스턴스를 참조하고 있습니다.

RentalCompany 인스턴스가 rental 프로퍼티를 통해 해당 Car 인스턴스 참조를 유지하고 있기 때문에 retainCount 값이 0보다 큽니다. 다시 표현하면 RentalCompany 클래스에 의해 해당 인스턴스가 여전히 사용중이기에 strong 참조는 이 인스턴스를 메모리에 유지시키고 있습니다.

그렇다면 이렇게 짜여진 코드에서 Car 인스턴스는 메모리로부터 언제 제거될 것일까요? RentalCompany 클래스가 소멸될 때 함께 소멸됩니다. RentalCompany 인스턴스가 더 이상 필요가 없게 되어 retainCount0이 되었을 때, 여기에 딸린 Car 인스턴스도 더 이상 필요가 없게 되어서 두 인스턴스가 메모리에서 함꼐 소멸됩니다.

 

ARC를 사용할 때 흔히 발생하는 문제

여러분이 객체들의 참조를 주의깊게 추적해가는 동안 다른 누군가는 너무 어렵고 복잡해 할 것입니다. 천만다행인 것은 이것은 (거의) 자동으로 수행되는 메커니즘이라는 것입니다.

이전 장에서 우리는 짤막하게 strong 참조에 대해 살펴보았습니다. 이런 형태의 변수는 참조하고 있는 인스턴스의 retainCount를 증가시킵니다. strong 참조의 반대 개념에는 두 가지가 있는데 그 중 하나는 weak 참조입니다. weak 참조는 인스턴스의 retainCount를 증가시키지 않습니다.

이에 근거하여 여러분은 ARC를 사용할 때 두 가지의 흔한 문제를 실수로 일으킬 수 있습니다.

(1) strong 참조를 해야하는 곳에서 weak 참조를 사용하여 여러분이 나중에 해당 인스턴스를 사용하려 할 때 그 인스턴스가 이미 메모리에서 사라지고 없을 수 있습니다.

(2) weak 참조를 해야하는 곳에서 strong 참조를 사용하여 retain의 순환을 일으킬 수 있습니다.

 

특히 retain이 순환하는 것은 골치아픈 문제입니다. 이는 간단하게 이해하면 두 개의 객체가 서로를 strong으로 참조하고 있는데 이렇게 하면 두 객체는 항상 retainCount0보다 크게 되어 둘 다 메모리에서 제거될 수 없습니다. 결국 메모리 누수를 야기하는데, 이는 우리가 만든 앱이 운영체제에서 “모범 시민”으로서 행동하기 위해 피해야 할 첫 번째 불량입니다.

retain 순환 참조는 여러분이 어느 한 참조를 weak 참조로 지정하는 것으로써 해결될 수 있습니다. retain 순환은 옵셔널에도 영향을 주는데 예를 들어 클로저에서 [weak self]라든가 unowned 같은 것이 이에 해당합니다. 클로저에 대해 배울 때 함께 알아보겠습니다.

'Language/Objective-C & Swift' 카테고리의 다른 글
더 보기...
태그 : 
댓글