Search
Duplicate
🛠️

이미지와 메모리 족적

Created
2019/12/04
Tags
Programming
Performance
거의 대부분의 앱에 이미지가 들어갑니다. 그리고 이미지는 메모리를 아주 많이 차지합니다. 설령 수 kb밖에 되어보이지 않는 작은 파일에 들어가는 이미지라고 하더라도 말이죠.
앱에서 쓰이는 이미지는 대부분 수십~수백 킬로바이트수준입니다. 기가바이트 단위의 메모리를 가지고 있는 우리에게 킬로바이트 단위의 이미지들은 별로 부담되지 않는다고 생각 할 수도 있습니다. 하지만 이미지의 크기가 킬로바이트 단위인 것은 어디까지나 하드디스크에 파일로 저장 되었을 때의 이야기입니다. 실제로 파일이 메모리에 올라와 화면에 보여지는 과정에서, 이미지는 그보다 훨씬 더 큰 메모리를 차지하게 됩니다.

이미지가 화면에 보일 때 까지

먼저 이미지가 화면에 보이게 될 때 까지의 여정을 간단하게 되짚어 보겠습니다. 최초에 이미지는 하드디스크에 저장되어 있습니다. 저장된 파일의 이름을 “sample.jpg”라고 합시다. sample.jpg의 용량은 516kb이고, 크기는 1920×1440 입니다.
sample.jpg의 용량은 516kb이고, 크기는 1920×1440 입니다.
이미지의 크기가 1920×1440이라는 건, 이 이미지가 표현해야 하는 픽셀의 개수가 1920×1440= 2,764,800 개라는 이야기입니다. 그리고 각 픽셀은 빨간색, 초록색, 파란색, 투명도 총 4가지 값의 정보로 이루어져있고, 보통 각 색깔 정보는 1바이트로 표현합니다. 그러니 각 픽셀의 크기는 4바이트이고, 이 4바이트 픽셀이 2,764,800개 있으니 총 1920x1440x4= 11,059,200 바이트, 즉 약 10MB가 이 이미지를 표현하는데 필요하게 됩니다.
그런데 jpg의 크기는 MB는 커녕 516KB입니다. 어떻게 1920×1440짜리의 이미지가 516kb 안에 들어 갈 수 있는 걸까요? 간단히 설명하자면, jpg는 이미지가 “압축”된 형태이기 때문입니다. jpg이 어떻게 이미지를 압축하는지에 대해서는 JPEG ‘files’ & Colour (JPEG Pt1)- Computerphile를 참고해주세요.
결론적으로 우리는 정직하게 표현 되었을 때 최소한 10MB에 달하게 되는 데이터를 516KB에 저장하고, 또 네트워크를 통해 이를 주고 받을 수 있게 되었습니다. 사실 이런 종류의 압축 기술이 없었다면, 아무리 인터넷 속도가 빨라졌다고 해도 지금처럼 풍부한 컨텐츠를 소비 할 수는 없었을 것입니다. 문제는 이렇게 작게 압축된 데이터가 디스플레이에 표시 될 때입니다.
하드디스크의 파일이 디스플레이에 표시되기 위해선, 먼저 메모리에 올라가야 합니다. 이 과정을 로딩이라고 하지요. 로딩의 결과, jpg파일은 Data Buffer 라고 불리는 형태가 되어 메모리에 존재하게 됩니다. Buffer는 별게 아니고 그저 메모리의 연속된 공간을 차지하는 정보를 말합니다. Swift에서는 Data타입으로 표현되지요. 이 시점까지만 해도 용량이라는 차원에서는 이전과 큰 변화가 없습니다. 사용자 입장에서는 512kb짜리 사진을 다운받았고, 메모리에서도 512kb를 차지하고 있는 형국이지요.
하지만 이렇게 로딩된 DataBuffer가 실제로 뷰에 그려지기 위해서는 Image Buffer가 되어야 합니다. 이는 피할 수 없는 일이에요. 압축된 한글 파일을 이메일에 첨부해서 보낼 수는 있어도, 결국 그 한글 파일을 받아서 읽어보려면 압축을 풀어야 하잖아요? 마찬가지로 아무리 압축되었던 이미지라고 하더라도, 결국 1920×1440개의 픽셀을 표현하려면, 이 압축을 풀어야 합니다.
이 압축을 푸는 행위를,  decodinng 이라 하고, 그렇게 decoding된 형태가 바로 Image Buffer입니다. Image Buffer 형태가 되고 나서야, 운영체제는 각 픽셀 하나 하나를 어떻게 디스플레이에 표시해야 할 지를 알 수 있게 됩니다. 그리고 바로 이 시점에서 Image Buffer는, 유저의 메모리에서 10mb에 달하는 공간을 차지하게 되는 것이죠.
이상의 흐름을 그림으로 다시 표현하면 아래와 같습니다.

다운 샘플링을 통해 메모리 족적을 줄여봅시다.

그런데 사실 우리가 1920×1440 짜리 크기의 이미지를 다운받아도, 만약 이 이미지를 200×150사이즈의 뷰에 표시하는 거라면, 사실 (1920×1440 – 200×150) 만큼의 픽셀들은 무용지물이나 마찬가지입니다. 확대같은 것을 하지 않을 것이라면 더더욱 그렇지요.
그렇기 때문에 1920×1440짜리를 표시하는 DataBuffer를 재료로, 더 저화질의 200×150짜리 이미지를 만든 다음, 이 저화질의 이미지를 decoding 해서 훨씬 적은 용량의 image buffer를 만들게 된다면, 메모리를 훨씬 절약 할 수 있게 됩니다. 이 과정을 다운 샘플링 이라고 합니다. 이상의 내용을 그림으로 정리하면 아래와 같습니다.
주의할 점은 다운샘플링이 CoreGraphic을 사용하는, CPU자원을 많이 쓰는 녀석이라는 점입니다. 따라서 많은 다운샘플링이 main쓰레드에서 한 번에 발생하지 않도록 처리해 주어야 합니다.
아래는 이 다운샘플링을 실제로 적용하는 코드입니다.
func downsample(imageData: Data, for size: CGSize, scale:CGFloat) -> UIImage { // dataBuffer가 즉각적으로 decoding되는 것을 막아줍니다. let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, imageSourceOptions) else { return UIImage() } let maxDimensionInPixels = max(size.width, size.height) * scale let downsampleOptions = [kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceShouldCacheImmediately: true, // thumbNail을 만들 때 decoding이 일어나도록 합니다. kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary // 위 옵션을 바탕으로 다운샘플링 된 `thumbnail`을 만듭니다. guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else { return UIImage() } return UIImage(cgImage: downsampledImage) }
Swift
복사

마치며

iOS에서 메모리는 굉장히 소중하고 귀중한 자원입니다. 100만원 정도 주고 샀던 제 iPhone8도 메모리가 2기가 밖에 되지 않으니까요. 그 만큼 iOS가 메모리를 효율적으로 관리한다는 얘기일 수도 있지만, 우리가 정말 주의하지 않으면, 우리도 모르는 새 백그라운드에서 돌고 있었던 유저의 다른 앱들을 종료시키는 상황을 만들 수도 있다는 얘기이기도 합니다. 그런 상황에서 사용자가 우리를 욕하지는 않겠지만, 그런 경험이 몇 번 쌓이다 보면 자연스럽게 우리 앱에 손이 덜 가게 되겠죠.
이미지 다운샘플링은 이런 메모리를 효율적으로 사용 할 수 있는 가장 손쉬운 방법입니다. 하지만 AlamofireImage나 Kingfisher등 유명한 라이브러리에서도 다운샘플링은 기본 행동이 아닙니다. 따라서 별다른 옵션을 주지 않고 이런 라이브러리들을 사용하고 있었다면, 아마 필요한 것보다 훨씬 더 많은 메모리들을 사용하고 있었을 확률이 큽니다. 이미지 다운샘플링을 잘 이해하고, 꼭 필요한 곳에 잘 적용 할 수 있도록 해야겠습니다.

참고자료

Images and Graphics Best Practices : 아직 안 본 iOS개발자가 있다면 반드시 꼭 봐야하는 세션입니다.
JPEG ‘files’ & Colour (JPEG Pt1)- Computerphile: JPG가 어떻게 이미지를 압축하는지를 개략적으로 설명하는 영상입니다.
SDRemoteImageView : 여기서 설명한 개념들을 적용해 만든 UIImageView의 Subclass입니다. 네트워크에서 이미지를 가져와 다운샘플링하고 디코딩해서 화면에 보여줍니다. 이 포스트의 개념을 이해하는데 보조자료로 쓰셔도 되고, 간단한 프로젝트에는 바로 가져다 쓰실 수도 있을 것 같습니다.