내용정리
오늘은 최종 프로젝트를 진행하며
Instrument를 사용하여 메모리릭을 발견하고 해결한 과정을 간단히 작성해 보려고 한다.
Memory Leak이란 더이상 필요하지 않은 메모리를 해제하지 않고 계속 유지하는 상황을 말한다.
이는 앱의 메모리 사용량이 지속적으로 증가하고, 성능 저하 및 크래시로 이어질 수 있기 때문에 반드시 해결해야 하는 문제이다.
메모리릭의 주 발생 원인은 강한 순환 참조(Strong Retain Cycle)이며, 약한 참조로 변경하여 해결할 수 있다.
Instrument는 Xcode에 내장된 성능 분석 및 디버깅 도구로, 앱의 메모리 사용량, CPU 성능, 디스크 I/O, 네트워크 활동 등을 실시간으로 분석하여 성능 저하나 메모리 릭 등의 문제를 탐지하고 개선할 수 있도록 도와주는 도구이다.

위 사진처럼 시각적으로 메모리릭의 발생 여부를 확인할 수 있기 때문에 메모리릭의 발생 여부를 확인하기 좋고, 어떤 부분에서 발생하는지 확인하기 좋다.
최종 프로젝트를 진행하며 앱스토어 배포 전 메모리릭을 체크하기 위해 Xcode의 Instrument를 사용해 보았다.
지금까지는 이 도구를 사용해도 한 번도 메모리릭을 발견하지 못했었기 때문에 이번에도 별 생각 없이 사용하고 있었는데, 갑자기 붉은 마크가 표시되었다.

결국 우리에게도 메모리릭이 발생하는구나...
어쨌든 어떤 부분이 문제인지 알아야 하기 때문에 하단의 Leak 내용을 봤는데, RxSwift의 코드에서 문제가 발생한 것 같았다.
아마 데이터 바인딩을 하며 disposable이 제대로 이루어지지 않은 것 같은데, 정확한 위치 포인트를 찾기가 어려웠다.
그래서 다시 Instrument를 실행하고 동작을 하나하나 실행해보며 메모리릭을 체크해보니 모달뷰를 띄우고 닫을 때 메모리릭이 발생하는 것을 알 수 있었다.
우선 모달뷰의 코드를 보자
static func showModal(state: ModalViewState) -> Observable<EntityDataSendable> {
guard let view = AppHelpers.getTopViewController() else {
return .error(NSError(domain: "No top view controller", code: -1))
}
let modalVC = ModalViewController(state: state)
view.present(modalVC, animated: true)
return Observable<EntityDataSendable>.merge(
modalVC.rx.sendCashBookData.map { $0 as EntityDataSendable },
modalVC.rx.sendConsumptionData.map { $0 as EntityDataSendable }
)
}
위 코드는 모달뷰를 편히 호출할 수 있도록 만든 메소드로, return 타입이 Observable이기 때문에 모달뷰가 닫힌 이후의 동작을 구독해서 사용할 수 있도록 하고 있었다.
다만 여기서 문제점이 발생하는데, return 타입인 옵저버블이 모달이 해제될 때 스트림이 자동으로 해제되지 않으면 메모리리깅 발생할 가능성이 있다.
또, 모달을 호출하는 메소드가 여러번 호출될 경우 모달이 여러개 동시에 띄워져 충돌이 발생할 가능성이 있다.
위의 두 문제를 해결하기 위해 명시적으로 모달뷰가 닫힐 때 옵저버블의 스트림을 해제하도록 하고, 동시에 모달이 띄워지지 않도록 방지하는 코드를 작성해보자.
static func showModal(state: ModalViewState) -> Observable<EntityDataSendable> {
guard let view = AppHelpers.getTopViewController(),
view.presentedViewController == nil
else {
return .error(NSError(domain: "No top view controller", code: -1))
}
let modalVC = ModalViewController(state: state)
let dismissSignal = modalVC.rx.deallocated.map { _ in EntityDataSendable?.none }
view.present(modalVC, animated: true)
return Observable<EntityDataSendable>.merge(
modalVC.rx.sendCashBookData.map { $0 as EntityDataSendable },
modalVC.rx.sendConsumptionData.map { $0 as EntityDataSendable }
)
.take(until: dismissSignal)
}
위 코드를 하나하나 살펴보자.
먼저 guard문을 통해 최상위 뷰 컨트롤러를 불러오고, 해당 뷰 컨트롤러에 presentedViewController 즉, 모달 뷰 컨트롤러가 없는지 확인한다.
guard let view = AppHelpers.getTopViewController(),
view.presentedViewController == nil
else {
return .error(NSError(domain: "No top view controller", code: -1))
}
만약 둘 중 하나라도 조건에 충족하지 않으면 메소드가 return되어 종료되기 때문에 모달뷰가 중복 호출되어도 충돌하는 일은 방지할 수 있다.
다음으로는 deallocated를 사용한 스트림 명시적 해제이다.
let dismissSignal = modalVC.rx.deallocated.map { _ in EntityDataSendable?.none }
deallocated는 클래스의 deinit과 비슷한데, 메모리에서 객체가 해제되는 것을 의미한다. 즉, 특정 객체가 메모리에서 해제되면 deallocated가 이벤트를 방출하는 것이다.
이를 변수 dismissSignal로 지정하고, return에서 .take(until:)을 통해 메모리 해제를 명시적으로 지정해주면 모달뷰가 메모리에서 해제될 때 이벤트가 방출되어 take에게 전달되고, 데이터 스트림이 메모리에서 해제되어 메모리릭을 방지할 수 있다.
여기서 .take(until:)은 특정 옵저버블의 데이터 스트림을 어떤 이벤트가 방출될 때 까지만 이벤트가 발생하도록 제한하는 연산자이다.
만약 A라는 옵저버블에 .take(until:)을 사용하고, 조건을 B 옵저버블로 해두면 B라는 옵저버블이 이벤트를 방출하지 않으면 A 옵저버블이 계속 이벤트를 방출하고, B 옵저버블이 이벤트를 방출하면 A 옵저버블의 이벤트가 더이상 발생하지 않게 되는 것이다.
이렇게 수정한 후 Instrument로 다시 메모리릭을 검사하니...

메모리릭이 발생하지 않는 것을 알 수 있었다
RxSwift에서 메모리릭이 발생하는 것은 처음이었는데,
안정성을 향상시키기 위해서 더욱 꼼꼼하게 옵저버블을 관리해야 한다는 것을 깨닫게 되었다.