Swift

[Swift] 이미지를 로드하는 여러 방법과 Data(contentsOf:)의 priority inversion 이슈 | Kingfisher, Data(contentsOf:), URLSession, Alamofire

하이D:) 2024. 6. 25. 13:51

 

이미지를 로드하는 방법에는 매우 여러 가지가 있다.

Asset 카탈로그에 추가해서 불러오는 경우도 있겠지만, 대게는 원격 저장소에 저장되어 있는 이미지에 대해 url을 사용하여 이미지를 로드시켜야 할 것이다.

 

 

아직 그 많은 방법들을 다 경험해보지는 못했지만, 이번 글에서는 크게 세 가지를 방법을 소개하고자한다!

1) 라이브러리를 사용하는 방법(Kingfisher)

2) Data(contentsOf:)를 사용하는 방법

3) URLSession, Alamofire을 사용하는 방법

 

 

왜 하필 이 세 가지 방법을 소개하는가? 하면..
이미지가 대량으로 뜨는 작은 프로젝트를 만들었는데 이미지를 로드하는 코드에 대해  위의 세 가지 방법으로 점진적으로 리팩토링을 하며 느낀 점이 많기 때문 ..!!! 나의 블로그 글은 지극히 개인적인 동기와 흥미를 기반으로 작성하게 된다 ㅎㅎ

 

 

 

그리고 위에서 말한 세 가지 방법들로 구현하는 과정에서 마주친 스레드 우선 순위 역전(priority inversion) 이슈 이미 로드된 이력이 있는 이미지에 대해 캐싱을 해보며 NSCache 대해 정리하겠다.

 

 

 

 

 

 

 

 

 

 

 

 

# 📌 Kingfisher로 이미지 로드하기

  • Kingfisher란, 고유의 원격 저장소 주소를 가지고 있는 이미지에 대해 네트워킹을 통해 앱 내에서 보여지게 해주는 라이브러리이고
  • 웹에서 이미지를 비동기로 다운로드하고 이미지를 캐싱하는 기능까지 내장되어 있기 때문에 쉽게 사용할 수 있다는 장점이 있다.

 

 

아직 Kingfisher를 세세하게 만져본 건 아니지만 캐시동작도 상세하게 제어가 가능하고 이것저것 제공해 주는 기능이 많다고 한다!

https://github.com/onevcat/Kingfisher?tab=readme-ov-file#features

 

GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.

A lightweight, pure-Swift library for downloading and caching images from the web. - onevcat/Kingfisher

github.com

 

 

이렇게 아주 간단하게도 사용이 가능하고

let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)

 

 

여러 가지를 커스텀하게 설정해 줄 수 있다.

        let url = URL(string: "https://example.com/image.png")
        productImageView.kf.indicatorType = .activity
        productImageView.kf.setImage(
            with: url, 
            placeholder: UIImage(systemName: "star"),
            options: [
                 .transition(.fade(2)),
                 .cacheOriginalImage
             ],
            progressBlock: nil
        )

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

# 📌 Data(contentsOf:)로 이미지 로드하기

이미지 url 사용하여 이미지를 Data 객체화하여 이미지를 로드할 수 있는데, 이 때 사용할 수 있는게 Data(contentsOf : )이다.

 

extension UIImageView {
    func setImageDataFromUrlString(url:String) {
        guard let url = URL(string: url) else {return }
        
        DispatchQueue.global().async {
            do{
                let data = try Data(contentsOf: url)
                DispatchQueue.main.async {
                    self.image = UIImage(data: data)
                }
            } catch {
                print(error)
            }
        }
        
    }
     

}

 

 

이렇게 이미지를 세팅하는 함수를 만들어주었다고 했을 때, 실제로 UIImageView에 이미지를 넣기 위해 이렇게 메서드를 실행해 줄 수 있다.

 imageView.setImageDataFromUrlString(url: "https://example.com/image.png")

 

 

 

 

 

 

 

 

이렇게 해서 이미지 잘 뜨네~ 했는데 Xode터미널에 낯선 경고문이 떴다.

🚨 Thread Performance Checker: Thread running at User-initiated quality-of-service class waiting on a lower QoS thread running at Default quality-of-service class. Investigate ways to avoid priority inversions

 

 

 

이런 경고는 초면이어서 찾아보니 🚨 스레드 우선순위 역전(priority inversion)🚨 에 대한 경고라고 한다!

 

 

 

 

 

 

 

 

 

 

원인

근본적인 원인은 Data(contentsOf:)가 동기적으로 데이터를 로드하기 때문!!

잉? 이미지를 로드하는데 동기적으로 처리한다구? 문제가 될 수 있겠군..ㅇㅇ라는 생각이 들었고 , 좀 더 자세히 보자면

  • Data(contentsOf:)는 동기적으로 데이터를 로드하므로, 이 작업이 비록 DispatchQueue.global().async 내에서 수행되고 있어도 여전히 해당 작업이 완료될 때까지 대기하게 된다고 한다!
  • 이미지 로딩 작업이 동기적으로 처리되면, 이 작업이 완료될 때까지 스레드를 차단하기 때문에 높은 우선순위의 메인 스레드가 낮은 우선 순위의 이미지 로딩 작업을 기다리게 되어 우선 순위 역전(priority inversion) 문제가 발생할 수 있다는 것이다.
  • UserInitiated와 같이 qos가 높은 작업이 있다고 했을 때에도 네트워크 응답을 기다리는 작업이 동기적으로 실행된다면 이 작업에 의해  qos가 높은 작업이 방해받을 수도 있다.

 

 

 

 

 

 

 

 

Data(contentsOf : ) 공식문서

Data(contentsOf:) 에 대해 더 궁금해져서 공식문서를 보니 이미 안내가 되어 있었다..!

https://developer.apple.com/documentation/foundation/nsdata/1413892-init

 

init(contentsOf:) | Apple Developer Documentation

Creates a data object from the data at the specified file URL.

developer.apple.com

 

이 메서드는 동기식으로 실행되고 완료될 때까지 호출 스레드를 차단하므로 main 스레드에서 호출하지 마세요.
대신 file coordination이나 nonblocking file-related APIs 중 하나를 사용하세요.

 

라고 중요 표시까지 해서 안내가 되어 있다!

(다른 블로그 글에서 이전 공식문서 캡처한걸 보니 이전에는 좀 더 명시적으로 네트워크 기반 url은 요청하지 말라고 쓰여있던데 지금은 돌려서 안내하고 있는 듯하다?!)

 

 

 

 

네트워킹 기반으로 하는 url은 사용하지 말고 파일 기반 url은 사용해도 되는 것 같다!

그 이유는 네트워킹 처리를 하려면 아무래도 시간이 걸리고 그동안 스레드가 block되어 있으면 중요한 작업도 기다리는 상태가 되니까..겠죠??

 

 

 

 

 

 

 

 

 

 

 

해결하려면?

그래서 해결 방법을 찾아보니 크게 아래 두 가지 방법으로 처리해 볼 수 있을 것 같았다.

  1. URLSession을 사용하여 비동기적으로 데이터를 로드. URLSession을 사용하면 네트워크 요청이 비동기적으로 처리되므로, 메인 스레드나 다른 높은 QoS 스레드가 블로킹되지 않음.
  2. QoS 조정: 적절한 QoS 에서 작업을 수행하여 우선순위 역전을 방지.

 

일단 2번 방법대로 해결을 해보려고 했는데,

큐의 우선순위를 설정해 줄 수 있는 qos를 수정해서 DispatchQueue.global(qos:.background).async 이렇게 코드 수정해 줬더니 경고 메시지는 없어졌다
=> DispatchQueue.global(qos:.background).async 내에서 실행되고 있으므로 이미지 데이터 동기적으로 불러와도 qos가 낮으니 Data(contentsOf:)로 데이터 불러오는 게 좀 느려도 괜찮겠군 ㅇㅇ하는 것! 이기 때문이라고 사료된다.

 

 

 

하지만... 2번 방법으로 해줄 경우 이미지가 눈에 띄게 느리게 로딩되는 것을 볼 수 있다..
21세기에 살고 있는 앱 유저들은 참지 못해..!
그래서 1번 방법으로 코드를 개선해 보았다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

# 📌 URLSession, Alamofire으로 이미지 로드하기

URLSession을 사용하여 이미지를 로드하는 게 가장 정석인 방법이 아닐까 싶다!

물론 kingfisher에서는 쉽게 해줄 수 있는 비동기, 캐싱 처리를 하려면 따로 작업을 해주어야 하지만..🤯

extension UIImageView {
    func setImageDataFromUrlString(urlString: String) {
        ImageLoadManager.shared.downloadImage(urlString: urlString) { data in
            DispatchQueue.main.async() { [weak self] in
                guard let self else {return}
                self.image = UIImage(data: data)
            }
        }
    }
}


class ImageLoadManager {
    static let shared = ImageLoadManager()
    private init() {}
    
    private func getData(from url: URL, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
        URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
    }
    
    func downloadImage(urlString: String, completion: @escaping (_ data : Data) -> Void ) {
        guard let url = URL(string: urlString) else { return }
        
        getData(from: url) { data, response, error in
            guard let data = data, error == nil else { return }
            completion(data)
        }
    }
    
}

 

 

 

 

URLSession 사용하여 이미지를 불러왔다면 URlSession에 기반하여 만들어진 라이브러리인 alamofire을 사용하는 것도 가능하다!

import Alamofire

extension UIImageView {
    func setImageDataFromUrlString(urlString: String) {
        guard let url = URL(string: urlString) else {return}
        
        let request = AF.request(url, method: .get)
        
        request.responseData{ [weak self] response in
            guard let self else {return }
            switch response.result {
            case .success(let imageData):
                DispatchQueue.main.async {
                    guard let image = UIImage(data: imageData) else {return}
                    self.image = image
                }
            case .failure(let error):
                print(error)
            }
            
        }
    }
}

 

 

 

 

 

 

 

이번 글에서는 이미지를 로드할 수 있는 여러 가지 방법 & 이미지 로드하기 위해 Data(contentsOf:)를 사용했을 때 발생할 수 있는 문제점에 대해 알아보았다.

 

이 글에서 이미지 캐싱하는 방법 중 메모리 캐시 방법까지 이어서 쓰려고 했는데 그럼 글이 너무 길어질 것 같아서 다음 글에 이어서 메모리 캐시와 디스크 캐시의 내용을 작성해 보겠다!