effective-swift

Item 8. finalizer와 cleaner 사용을 피하라

Java의 객체 소멸자에 대해서 간단하게 소개한 후 Swift의 디이니셜라이저를 활용한 예제를 소개합니다.

Java의 객체 소멸자

finalize() 메서드는 클래스의 인스턴스가 더이상 참조되지 않을 때 가비지 컬렉터(Garbage Collector)가 힙에서 객체를 제거하기 전에 자동으로 호출 됩니다.

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

Cleaner 는 해당 객체가 phantom reachable이 되었을 때를 통지 받은 후 실행되도록 Cleaning actions을 등록합니다. GC에 의해 수거될 객체들은 register() 메서드를 사용하여서 cleaner 객체에 등록되어야 합니다.

 public class CleaningExample implements AutoCloseable {
        // A cleaner, preferably one shared within a library
        private static final Cleaner cleaner = <cleaner>;

        static class State implements Runnable {

            State(...) {
                // initialize State needed for cleaning action
            }

            public void run() {
                // cleanup action accessing State, executed at most once
            }
        }

        private final State;
        private final Cleaner.Cleanable cleanable

        public CleaningExample() {
            this.state = new State(...);
            this.cleanable = cleaner.register(this, state); // 등록
        }

        public void close() {
            cleanable.clean();
        }
    }

Java의 finalizer와 cleaner 특징

Java의 객체 소멸자인 finalizer와 cleaner는 다음과 같은 특징을 가지고 있습니다.

Swift에서는

Swift에서는 클래스의 인스턴스 레퍼런스 카운트가 0이 되면 메모리에서 할당 해제합니다. 그리고 클래스의 인스턴스가 소멸되기 직전에 deinit이 호출됩니다. 즉, Java에서와 달리 개발자가 인스턴스의 소멸 시점과 deinit이 불릴 시점을 예측할 수 있습니다.

그렇다면 deinit에서는 어떤 일을 할 수 있을까요?

책 본문에 나온 C++의 내용과 같이 Swift도 인스턴스가 소멸될 때 deinit을 통해 비메모리 자원 회수 용도(reclaim other nonmemory resources)로 사용할 수 있습니다.

deinit 때 처리해줄 일의 예시(NotificationCenter, FileHandle, DBConnection(SQLite))로는 item9에서 설명하고 있습니다. 이번 아이템에서는 추가적인 예시로 RxSwift의 Dispose() 메서드와 DisposeBag에 대해서 설명하겠습니다. 아래에 예시는 다양한 구현방법 중 하나입니다.

RxSwift

Dispose() 메서드와 DisposeBag을 소개하기 앞서 두 가지 필요한 내용을 정리하겠습니다.

RxSwift의 Dispose()

RxSwift 에서 Observable 을 subscribe(구독) 하면 항상 Disposable 을 반환합니다. 이 Disposable 들을 dispose 해주지 않으면 메모리에 계속 남아 메모리 누수가 발생합니다. 따라서 구독한 Disposable 들을 명시적으로 dispose 해줘야합니다.

final class MyViewController: UIViewController {
    var subscription: Disposable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        subscription = theObservable().subscribe(onNext: {
            // handle your subscription
        })
    }
    
    deinit {
        subscription?.dispose()
    }
}

위와 같은 방법을 사용하면 일일이 Dispoable 프로토콜에 구현되어있는 dispose() 를 호출하여 없애줘야 합니다. 하지만 RxSwift 가이드 문서의 Disposing 에서 권장하는 방법 중 하나는 dispose() 를 직접 호출하지 않고 DisposeBag을 사용하는 것입니다. DisposeBag 이 할당 해제될 때, 각 dispoable에 dispose 메서드가 호출됩니다.

RxSwift의 DisposeBag

DisposeBag 의 내부구현 중 일부를 살펴보면

public final class DisposeBag: DisposeBase {
  
  private var disposables = [Disposable]()
  
    private func dispose() {
    let oldDisposables = self._dispose()

    for disposable in oldDisposables {
        disposable.dispose()
    }
  }
  
  private func _dispose() -> [Disposable] {
    self.lock.performLocked {
        let disposables = self.disposables
            
        self.disposables.removeAll(keepingCapacity: false)
        self.isDisposed = true
            
        return disposables
        }
    }

  deinit {
    self.dispose()
  }
}

이렇게 DisposeBagDisposable 타입의 배열에 담긴 Disposable 들을 dispose 하는 메서드가 구현되어 있습니다. 그리고 DisposeBag이 할당 해제될 때(deinit 에서) dispose() 메서드를 호출해 DisposeBag이 담고있는 Disposable들을 dispose해 각 disposable Observer는 관찰하고 있던 것에서 자동으로 구독해지시킵니다.

아래 예제 코드는 dispose() 메서드를 사용한 예제를 DisposeBag을 사용하는 방법으로 바꾼 것입니다.

final class MyViewController: UIViewController {
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
      
      let parsedObject = theObservable
        .map { [parser] json in
        return parser.parse(json)
       }
        parsedObject.subscribe(onNext: {
          // handle your subscription
        })
        .disposed(by: disposeBag)
    }
}

참고

Java

  1. finalize - docs.oracle

  2. Class Cleaner - docs.oracle

  3. Phantom Reachable - docs.oracle

Swift

  1. Deinitialization - Swift.org

  2. Memory management in RxSwift – DisposeBag

  3. RxSwift

  4. RxSwift - DisposeBag

  5. [Question-Archive](https://github.com/TTOzzi/Question-Archive)

  6. Getting Started With RxSwift and RxCocoa