아무것도 모르고 RxSwift를 오해했었다.

백상휘·2022년 6월 21일
2
post-thumbnail

매일 기술관련 포스트와 코드 뭉치들만 올려서 이번엔 프로그래밍판 '리더스 다이제스트' 같은 느낌으로 써 보려고 했다. 부디 가벼운 마음으로 읽어주시길...
이 이야기는 오동통한 물범처럼 메모리를 너무 많이 먹어버린 UIImageView에서 부터 시작하게 된다.

이 게시글은 우주 최강 부트캠프 코드스쿼드에서 마스터님과 메모리 관련 대화를 한 것을 게시글 형태로 옮긴 것입니다.
해당 게시글은 위에서 언급한 코드스쿼드 iOS 마스터분께 동의를 받았습니다.

필자는 현재 코드스쿼드라는 부트캠프의 마스터즈 코스에서 공부를 하고 있다(2022.06 기준).

오랜만에 오로지 공부만 할 수 있는 시간이 주어지니 '각오해라 RxSwift... 한달만에 끝장내주지...' 하면서 2주간 책으로 독학하고 부트캠프 과제에 적용해보기로 하였다.

하지만, 문제는 아주 빠르게 시작되었다.


메모리의 흑염룡이 깨어났다?!

처음에는 RxSwift를 쓰면 비동기 처리를 하니까 메모리 이슈가 원래 있는 건가??? 라는 생각을 했다. 결국 쓸데없는 시간을 2주 정도 소모하고 말았다. 하지만, 조용한 일요일 오전 스타벅스에서 이상한 점을 발견하고는 이마를 탁 쳤다.

굉장히 내용이 많은데, 짧게 말하면 UIImageView에 PNG 이미지를 반영하는 과정에서 메모리가 치솟고 해제가 되지 않는다는 것이었다.

대부분 앱은 UIImageView를 안쓰면 안썼지, 한번만 쓰진 않는다. 이 현상을 해결하지 않으면 메모리는 100MB, 200MB, 500MB 기하급수적으로 증가하면서 iOS에게 자동으로 종료당해 버릴 것이다.

예전에 수업에서 Xcode 의 Memory Graph, instruments 를 활용해서 메모리 관련 현상을 확인할 수 있다는 것이 기억나서 Memory Graph를 켜보니 약 50MB 메모리 중 PNG_Data 관련 메모리가 20MB 잡힌 것을 확인할 수 있었다.

구글링에선 UIImage 초기화 메소드 중 init(named:) 는 내부적으로 캐싱을 하기 때문에 그럴 수 있다고 했는데, 코드에서는 init(data:) 이용해서 Image를 사용하고 있었다. 캐싱에 대한 심증만 있을 뿐이다.

그렇게 iOS 마스터님과의 대화가 시작되었다.

이미지 뷰에 이미지가 보이면 메모리에 이미지 데이터가 있는 건 당연하다.

마스터님은 처음에 캐싱을 의심하셨다. 구글링과 마스터님의 의견이 일치하는 걸 보면 이런 문제의 대부분 원인이 캐싱인 것 같다.

마스터님이 주목한 점은 이것이었다.

화면에 이미지가 보인다면 메모리를 점유하는 것은 당연. 하지만, 화면이 사라지고 다음 화면에서도 똑같이 유지되는가?

결과는 화면이 사라져도 메모리는 그대로 였다. instruments 에서 확인한 PNG 메모리 점유는 그대로 유지되었다.

문제의 메모리 카테고리명은 VM: ImageIO_PNG_Data 였다.

Memory Graph 를 보자

그렇다면 UIImageView 도 그대로인가? 마스터님은 instruments 에 잡힌 메모리 카테고리명을 보시더니 ImageIO 에서 캐싱 문제가 있는 것을 지적하셨다.

위에서 말한 것 처럼 .init(named:) 메소드는 내부적으로 이미지를 캐싱을 한다. 만약 이걸 사용하였다면 다른 것으로 바꿔보고 .init(data:), .init(contentsOfFile:) 초기화 메소드도 테스트 해보라고 하셨다.

UIImage.init(named:) 는 캐싱을 하는 것으로 알고 있다. UIImage.init(data:) 는 기억에 확실치 않은데 UIImage.init(contentsOfFile) 은 어떠한가?

빠르게 코드를 수정해서 모든 초기화 메소드를 비교해 보았지만 변화는 없었다.

이미지 자체가 큰 것은 아닐까? 머릿 속이 혼란해지는 시점이었다.

ImageIO는 당연히 생겨야 하는 것. 없어지지 않는 것에 주목해야.

iOS는 자동으로 사용하지 않는 메모리를 해제한다.

ImageIO 도 마찬가지일 것인데, 자동으로 해제되지 않는다는 것이 결국 중요한 점이었다. 앞에서 말한 이미지 크기가 너무 크면 메모리 해제와도 관련 있는 것일까?

한번 PNG 이미지를 저화질 JPEG 으로 변경해보니 메모리 절약의 효과가 있었다.

메모리 절약을 위해 tinyPNG 같은걸 이용하는 경우가 있다. ViewController/View 가 남아있는 지 다시 확인해봐야 한다.

디버깅이 삼천포로 빠지기 시작하는 것을 올바로 잡아주셔서 다시 메모리 점유에 집중해 보았다.

PNG 를 bitMap 으로 변경.

우선 앞의 마스터님 말씀대로 맨 앞의 NavigationController를 삭제하고 바로 뷰 컨트롤러가 보이도록 한 뒤 화면 전환을 해 보았다. 하지만, 별다른 변화는 없었다.

어떻게 해야할지 고민하던 중 비슷한 현상에 대한 리팩토링 경험을 찾았다.

원문 : [iOS] 이미지 리사이징을 활용한 메모리 최적화

짧게 정리하자면, 이미지 자체의 픽셀 수와 실제 이미지가 채워져야 할 픽셀 수 차이가 불필요한 메모리 사용으로 이어지게 되니 Resizing 과정을 통해 메모리를 절약할 수 있다는 것이다.

이 게시글을 보고 아래와 같은 과정을 통해 메모리 누수를 개선해보고자 하였다.

  1. 메인 뷰 에 Navigation Controller 를 embed한 것을 풀고 실행 => 변화 없음
  2. 1 작업을 한 후 여러 개의 메뉴를 보여주는 화면에 Navigation Controller를 embed 하고 빈 ViewController로 이동 => 변화 없음
  3. 이미지 리사이즈 진행 => 변화 확인. ImageIO 내에 PNG 관련 내용 확인되지 않음.
if let url = Bundle.main.url(forResource: "InitialBackgroundImage", withExtension: "png") {
	let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
	let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions)!
	let pointSize = self.backgroundImageView.frame.size
	let maxDimensionInPixels = max(pointSize.width, pointSize.height) * 1.0
	let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true,
									kCGImageSourceShouldCacheImmediately: true,
								kCGImageSourceCreateThumbnailWithTransform: true,
										kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary
	let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions)!

	self.backgroundImageView.image = UIImage(cgImage: downsampledImage)
}

기쁜 마음에 이 사실을 정리해서 마스터님께 알려드렸다.

PNG 가 BitMap으로 바뀌었으니 ImageIO_PNG_Data 가 사라지는 것은 당연하다.

png 이미지를 캡처해서 bitmap으로 바꾸었으니 png 영역이 보이지 않는 것은 당연. 하지만 근본적인 해결책으로 보긴 어렵다.

아차 싶었다. 또 근본적인 문제에서 눈을 떼고 우회로를 통해 문제를 해결하려 했다.

이미지 관련 처리로 인한 메모리 누수를 해결한 것이 아니라, 이미지 자체를 바꿔버린 것이기 때문이다.

결국 이 대화는 정확한 해결이 되지 못한 채로 종료되고 말았다.

나름의 해결책

여러 방법을 시간날 때마다 디버깅을 하다가 공부할 때 보는 RayWenderlich 의 RxSwift 관련 책에 다음의 내용을 보았다.

<해석>
"Binder에 대한 흥미로운 사항은 에러 이벤트를 받지 않고, weak하게 base object를 다룬다는 것이다. 그러므로 이상한 메모리 누수 현상과 weak 참조에 대해서는 신경 쓸 필요가 없다."

라이브러리에는 에러 혹은 버그가 분명히 있을 수 있다. 없을 거라고 생각하는 경우가 많지만 분명히 있다. 본인도 몇번 겪은 적이 있어서 굉장히 당황한 적이 있다. 심지어 Xcode, 애플 라이브러리에도 그런 사례를 종종 찾아볼 수 있다.

혹시.... 라는 생각에 코드를 고쳐보았다.

useCase
  .getBackgroundImage() // Driver<Data> 반환
  .drive(onNext: { [weak self] data in // 일부러 weak로 참조한다.
    self?.backgroundImageView.image = UIImage(data: data)
  })
  .disposed(by: bag)

출처: 스타벅스

화면 전환을 하니 자동으로 PNG 관련 메모리 점유 내역이 사라졌다.

RxSwift에서 Observable 혹은 Trait 등에서 Next Event 시 참조한 객체에서 점유하는 메모리가 자동으로 해제되지 않는 경우 weak 참조를 이용해 메모리 해제 효과를 기대해 볼 수 있다.

이렇게 결론 짓게 되었다.

문제의 인식과 해결

내가 자주 하는 실수 중 하나는 문제를 해결하는 과정에서 문제에서 집중해야 할 주제에서 벗어나 버리는 것이었다.

위의 이미지 리사이징을 통해 현상이 개선된 것은 정확히 말해 우회하여 해결한 것이다. 근본적으로 해결해야 할 문제는 해제되지 않는 메모리였는데 말이다.

이번에도 머리를 한대 얻어맞은 기분이었다. 슬램덩크의(앞부분만 좀 봤지만) 안선생님처럼 전혀 성장하지 않았어...


그래도 이번이 다른 것은 이렇게 게시글로 남겨 놓는 것이었다.

어떻게 내용을 정리할까... 라면서 고민한 경험이 있고, 이 내용에 대한 피드백을 받으면서 한번 더 리마인드 하게 되면 좀 더 나은 개발자가 될 수 있지 않을까? 개발자는 코딩 실력보다는(웬만큼 한다 치고) 솔루션을 제시할 수 있는 사람이라고 생각한다.

물론 IT 기술분야 한정이다. 하수구가 막히면 하수구 전문 업체에 연락해야 하는 것이다.

이 말은 반대로 하면 IT 관련 문제가 발생했을 때 개발자는 자신만의 답을 내놓을 수 있어야 한다고 생각한다.

그러므로 이 문제로 인해 겪은 메모리 이슈 보단 아직 문제해결 능력이 부족하다는 것이 더 Issue다. 아직 부족한 점이 많은 이 개발자는 지금보다 조금 놀고 더 공부해야할 것 같다. 공부가 잘 되려고 책상을 완전 깔끔히 만들었으니 앞으로도 많은 기대 부탁드린다.

해당 게시글에 출연해주신 은하계 최고 부트캠프 코드스쿼드의 iOS 마스터님께 다시 한번 감사드립니다.

profile
plug-compatible programming unit

1개의 댓글

comment-user-thumbnail
2023년 1월 25일

해결하기까지의 사고의 흐름이 멋지네요 응원합니다!

답글 달기