Neoself의 기술 블로그

Kingfisher 라이브러리 분석하기 (Networking 레이어) 본문

개발지식 정리/Swift

Kingfisher 라이브러리 분석하기 (Networking 레이어)

Neoself 2025. 3. 6. 17:23

이번 글에서는 Kingfisher 라이브러리에서 가장 핵심이 되는 네트워킹 레이어에 대한 분석 내용을 정리해보겠습니다. 앞선 글에서 캐시 레이어(메모리 캐시, 디스크 캐시)에 대해 살펴보았다면, 이번에는 실제로 이미지를 다운로드하는 네트워킹 계층의 구조와 작동 방식을 심층적으로 분석해보겠습니다.

 

1. URLSession 개요

먼저 Kingfisher의 네트워킹 레이어를 이해하기 위해서는 iOS의 URLSession 시스템에 대한 기본적인 이해가 필요합니다.

URLSession이 뭐죠?

URLSession은 iOS에서 네트워크 요청을 관리하기 위한 시스템 레벨의 API입니다. 앱에서 서버와 데이터를 주고받기 위한 HTTP/HTTPS 통신의 기반이 됩니다. 간단한 데이터 다운로드부터 백그라운드 전송, 인증 관리, 캐싱 등의 고급 기능까지 제공합니다.

URLSession의 주요 구성 요소

 

  • URLSession: 네트워크 요청을 조율하는 최상위 객체
  • URLSessionConfiguration: 세션의 동작 방식을 정의하는 설정 객체
  • URLSessionTask: 실제 네트워크 작업을 수행하는 객체
    • URLSessionDataTask: 데이터를 메모리에 직접 받는 작업
    • URLSessionDownloadTask: 데이터를 파일로 다운로드하는 작업
    • URLSessionUploadTask: 데이터를 업로드하는 작업
  • URLSessionDelegate: 세션의 이벤트를 처리하는 델리게이트 프로토콜

URLSession 동작 방식

  1. URLSession 객체 생성 (설정 및 델리게이트 할당)
  2. URLSessionTask 생성 (요청 정보 포함)
  3. 태스크 실행 시작 (resume() 호출)
  4. 비동기적으로 네트워크 통신 수행
  5. 델리게이트 메서드를 통해 진행 상황, 완료, 오류 등의 이벤트 전달
  6. 완료 핸들러 또는 델리게이트를 통해 결과 처리

이러한 기본 개념을 바탕으로 Kingfisher가 어떻게 네트워킹 레이어를 구조화했는지 살펴보겠습니다.

 

 

Kingfisher는 URLSession을 기반으로 이미지 다운로드에 특화된 네트워킹 계층을 구현했습니다. 이 아키텍처는 다음과 같은 주요 구성 요소로 이루어져 있습니다

 

이제 각 컴포넌트를 자세히 살펴보겠습니다.

 

1. Delegate 클래스

Kingfisher의 Delegate 클래스는 라이브러리 전체에서 이벤트 핸들링을 위한 핵심 유틸리티 클래스입니다.

이벤트 핸들링을 위해 매번 클로저를 직접 관리하면 코드가 복잡해지고, 중복 코드가 발생할 수 있습니다. 특히 클로저를 생성하는 과정에서 매번 순환참조에 대한 고려를 해야하기 때문에 휴먼 에러가 발생할 가능성이 있습니다. 따라서 유연성을 위해 스레드 안전하고, 메모리 안전한 Delegate 클래스를 사용해 이벤트 핸들링을 구현합니다.

public class Delegate<Input, Output>: @unchecked Sendable {
    private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.DelegateQueue")
    
    private var _block: ((Input) -> Output?)?
    private var block: ((Input) -> Output?)? {
        get { propertyQueue.sync { _block } }
        set { propertyQueue.sync { _block = newValue } }
    }
    
    private var _asyncBlock: ((Input) async -> Output?)?
    private var asyncBlock: ((Input) async -> Output?)? {
        get { propertyQueue.sync { _asyncBlock } }
        set { propertyQueue.sync { _asyncBlock = newValue } }
    }
    
    public func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) {
        self.block = { [weak target] input in
            guard let target = target else { return nil }
            return block?(target, input)
        }
    }
    
    public func delegate<T: AnyObject>(on target: T, block: ((T, Input) async -> Output)?) {
        self.asyncBlock = { [weak target] input in
            guard let target = target else { return nil }
            return await block?(target, input)
        }
    }

    public func call(_ input: Input) -> Output? {
        return block?(input)
    }
    
    public func callAsync(_ input: Input) async -> Output? {
        return await asyncBlock?(input)
    }
}

 

  1. 제네릭 타입: Input과 Output 타입을 통해 다양한 종류의 이벤트와 결과를 처리할 수 있도록 지원하고 있습니다.
  2. 메모리 관리: [weak target]을 사용하여 강한 참조 순환을 방지하고 있습니다.
  3. 스레드 안전성: propertyQueue를 사용하여 클로저 접근을 동기화하고 있습니다.
  4. 비동기 지원: 일반 클로저와 async 클로저 모두 지원합니다.

실제 사용 예시

// SessionDelegate 내부에서의 사용 예
let onValidStatusCode = Delegate<Int, Bool>()
let onResponseReceived = Delegate<URLResponse, URLSession.ResponseDisposition>()

// 델리게이트 등록
onValidStatusCode.delegate(on: self) { (self, code) in
    return (200..<400).contains(code)
}

// 이벤트 발생 시 호출
let isValid = onValidStatusCode.call(httpStatusCode)

이 Delegate 클래스는 Kingfisher 전체에서 콜백과 이벤트 핸들링을 위한 통일된 패턴을 제공합니다. 특히 네트워킹 레이어에서 비동기 작업의 완료, 진행 상황, 오류 등을 처리하는 데 광범위하게 사용됩니다.

 

2. SessionDataTask

SessionDataTask는 URLSessionDataTask를 추상화하여 Kingfisher에서 다운로드 작업을 관리하는 클래스입니다. 이 클래스는 실제 다운로드 데이터를 관리하고, 다운로드 진행 상황 및 완료 이벤트를 처리합니다.

public class SessionDataTask: @unchecked Sendable {
    public typealias CancelToken = Int
    
    struct TaskCallback {
        let onCompleted: Delegate<Result<ImageLoadingResult, KingfisherError>, Void>?
        let options: KingfisherParsedOptionsInfo
    }

    private var _mutableData: Data
    public var mutableData: Data {
        lock.lock()
        defer { lock.unlock() }
        return _mutableData // 스레드 안전성을 위해 lock을 사용하여 데이터 접근
    }
    
    public let originalURL: URL?
    public let task: URLSessionDataTask
    
    private var callbacksStore = [CancelToken: TaskCallback]()
    private var currentToken = 0
    private let lock = NSLock()
    
    let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>()
    let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>()
    
    var started = false
    
    // 메서드들...
}

 

 

1.1 다운로드된 데이터 처리

내부 버퍼(_mutableData)에 저장되며, 이는 추후 ImageDownloader에서 다운로드된 데이터를 처리하는 핸들러에 사용됩니다.

open class ImageDownloader: @unchecked Sendable {
    public init(name: String) {
        // 세션 핸들러 설정 (이벤트 콜백 등록)
        setupSe
        ssionHandler()
    }
    
    private func setupSessionHandler() {
        sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in
            (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task)
    	}
    }
}

 

 

1.2 데이터 동기화 로직

Kingfisher는 하나의 URL에 대한 다운로드 요청들을 하나의 SessionDataTask에서 해결하도록 설계하여, 동일 URL에 대한 중복된 다운로드를 막도록 설계하였습니다. 그 결과, SessionDataTask을 공유하는 여러 DownloadTask가 생성될 수가 있으며, 이는 자연스레 2개 이상의 스레드에서 동시에 SessionDataTask 내부의 mutableData로 접근할 수 있게 됩니다.

 

여기서 Kingfisher가 스레드 안정성을 보장받기 위해 선택한 메커니즘이 흥미로운데요.

 

ImageCache와 같은 공유 클래스에서 DispatchQueue 직렬화 큐를 사용하여 동기화 매커니즘을 구현한 것과 달리, Locking 메커니즘을 사용하고 있습니다. 물론, 여러명의 Contributer가 라이브러리를 발전시키고 있는 만큼, 단일 동기화 매커니즘으로 통합시키는 것이 힘들다는 것도 있겠지만, 저는 이를 오버헤드 최소화에 목적이 있다고 판단했습니다.

 

DispatchQueue를 사용하는 동기화 방식은 간편하여 휴먼 에러가 발생할 우려가 적지만, 큐에 작업을 넣고 실행하는 오버헤드가 여전히 존재합니다. 특히, 작은 청크로 자주 도착하는 네트워크 데이터의 경우, 빈번하고 짧은 연산이 주를 이룹니다. 때문에, 직접적인 locking 매커니즘을 택해 오버헤드를 최소화하려는 것이라고 판단했습니다.

 

2. URLSessionDataTask 래핑 및 추상화

앞서 말씀드렸던, SessionDataTask는 이름에서도 알수 있듯, Foundation에서 제공하는 URLSessionDataTask를 추상화하고 있습니다. 즉, URLSessionTask의 기능을 내부적으로 사용하면서 추가적인 기능을 구현하였습니다.

open class ImageDownloader: @unchecked Sendable {
	public private(set) var sessionTask: SessionDataTask? {
        get { propertyQueue.sync { _sessionTask } }
        set { propertyQueue.sync { _sessionTask = newValue } }
    }
    
    private func startDownloadTask() -> DownloadTask {
        ...
        sessionTask.resume()
        ...
    }
}

ImageDownloader에서는 네트워크 작업을 실행시키는 startDownloadTask 메서드 내부에서 URLSessionDataTask가 아닌 자체 구현한 SessionDataTask의 resume 메서드를 호출하고 있습니다. 

public class SessionDataTask: @unchecked Sendable {
	public let task: URLSessionDataTask

    func resume() {
        guard !started else { return } // 이미 시작된 작업은 다시 시작하지 않음
        started = true // 작업 상태를 시작됨으로 표시
        task.resume() // 내부 `URLSessionDataTask` 시작
    }
}

 

그리고, SessionDataTask 내부에서는 URLSessionDataTask를 내부속성으로 사용하고 있으며, 이의 내부 메서드인 resume() 기능을 사용하고 있습니다.

 

이렇게 자체구현한 SessionDataTask 클래스를 중간에 의존성 그래프에 삽입한 이유는 앞서 콜백 관리를 통해 이벤트 처리를 추가로 수행하기 위함입니다.

struct TaskCallback {
    let onCompleted: Delegate<Result<ImageLoadingResult, KingfisherError>, Void>? // 작업 완료 시 호출될 콜백
    let options: KingfisherParsedOptionsInfo // 작업에 사용된 옵션
}

private var callbacksStore = [CancelToken: TaskCallback]() // 콜백을 저장하는 딕셔너리

 

3. URLSessionDataTask 래핑 및 추상화

SessionDataTask 클래스는 여러 콜백을 토큰으로 식별하여 관리합니다. 이때 콜백은 아래와 같은 이벤트가 발생할때 호출되는 콜백들입니다.

  • 다운로드가 완료되었을 때
  • 다운로드 진행 상태가 업데이트되었을 때
  • 다운로드 중 오류가 발생했을 때

 

4. defer 키워드

// 콜백 추가
func addCallback(_ callback: TaskCallback) -> CancelToken {
    lock.lock()
    defer { lock.unlock() }
    callbacksStore[currentToken] = callback
    defer { currentToken += 1 }
    return currentToken
}

여기서 defer 키워드는 메서드가 종료되기 직전에 호출됩니다. 때문에, addCallback이 정상적으로 종료될 때에만 DownloadTask 간의 식별자를 갱신하도록 하여, 식별자가 잘못된 DownloadTask를 가리키지 않게끔 설계한 것을 볼 수 있습니다.

// 특정 토큰의 콜백 제거
func removeCallback(_ token: CancelToken) -> TaskCallback? {
    lock.lock()
    defer { lock.unlock() }
    if let callback = callbacksStore[token] {
        callbacksStore[token] = nil
        return callback
    }
    return nil
}

removeCallBack 메서드는 특정 토큰에 해당하는 콜백을 제거하고 반환합니다. 다운로드 작업의 중단 및 리소스 관리 관련 상황에서 호출되는데요. 이렇게 콜백을 추가하는 것 뿐만 아니라, 콜백을 제거하는 메서드도 지원하는 이유는 불필요한 메모리 사용을 줄이는 것도 있지만, 추후 메모리 누수가 발생할 수도 있기 때문입니다.

3. DownloadTask

DownloadTask는 Kingfisher 네트워킹 레이어에서 사용자에게 노출되는 가장 상위 수준의 추상화입니다. 실제 네트워크 작업을 수행하는 SessionDataTask를 래핑하여, 사용자가 다운로드 작업을 제어할 수 있는 간단한 인터페이스를 제공합니다.

public final class DownloadTask: @unchecked Sendable {
    private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.DownloadTaskPropertyQueue")
    
    private var _sessionTask: SessionDataTask?
    public private(set) var sessionTask: SessionDataTask? {
        get { propertyQueue.sync { _sessionTask } }
        set { propertyQueue.sync { _sessionTask = newValue } }
    }
    
    private var _cancelToken: SessionDataTask.CancelToken?
    public private(set) var cancelToken: SessionDataTask.CancelToken? {
        get { propertyQueue.sync { _cancelToken } }
        set { propertyQueue.sync { _cancelToken = newValue } }
    }
    
    init(sessionTask: SessionDataTask, cancelToken: SessionDataTask.CancelToken) {
        _sessionTask = sessionTask
        _cancelToken = cancelToken
    }
    
    init() { }
    
    public func cancel() {
        guard let sessionTask = sessionTask, let cancelToken = cancelToken else { return }
        sessionTask.cancel(token: cancelToken)
    }
    
    public var isInitialized: Bool {
        propertyQueue.sync { _sessionTask != nil && _cancelToken != nil }
    }
    
    func linkToTask(_ task: DownloadTask) {
        self.sessionTask = task.sessionTask
        self.cancelToken = task.cancelToken
    }
}

4. SessionDelegate

URLSession은 네트워크 작업 중 다양한 이벤트를 델리게이트를 통해 전달합니다. SessionDelegate는 URLSessionDataDelegate 프로토콜을 구현하여 이러한 이벤트를 처리하고, 적절한 동작을 수행하여 Kingfisher의 내부 컴포넌트에 전달합니다.

뿐만 아니라, SSL/TLS 인증 요청과 같은 보안 관련 이벤트를 처리하며, trustedHosts를 통해 신뢰할 수 있는 호스트도 지정할 수 있습니다. 

open class SessionDelegate: NSObject, @unchecked Sendable {
    private var tasks: [URL: SessionDataTask] = [:]
    private let lock = NSLock()
    
    // 이벤트 델리게이트
    let onValidStatusCode = Delegate<Int, Bool>()
    let onResponseReceived = Delegate<URLResponse, URLSession.ResponseDisposition>()
    let onDownloadingFinished = Delegate<(URL, Result<URLResponse, KingfisherError>), Void>()
    let onDidDownloadData = Delegate<SessionDataTask, Data?>()
    let onReceiveSessionChallenge = Delegate<SessionChallengeFunc, (URLSession.AuthChallengeDisposition, URLCredential?)>()
    let onReceiveSessionTaskChallenge = Delegate<SessionTaskChallengeFunc, (URLSession.AuthChallengeDisposition, URLCredential?)>()
    
    // 메서드들...
}

 

태스크 관리: URL을 키로 하여 앞서 설명드린 SessionDataTask 인스턴스를 관리합니다.

이벤트 델리게이트: 다양한 URLSession 이벤트를 처리하기 위한 Delegate 인스턴스들을 제공합니다. 이는 추후 URLSessionDataDelegate를 구현하는 과정에서 이벤트 호출에 사용됩니다.

 

 

URLSessionDataDelegate 구현

extension SessionDelegate: URLSessionDataDelegate {
    // 응답 수신 처리
    open func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        didReceive response: URLResponse
    ) async -> URLSession.ResponseDisposition {
        // 응답 유효성 검사 및 처리
    }
    
    // 작업 완료 처리
    open func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didCompleteWithError error: (any Error)?
    ) {
        // 완료 또는 오류 처리
    }
    // 기타 인증 및 리다이렉션 처리 메서드...
}

데이터 수신, 응답 수신, 인증 요청, 오류 발생 등 이미지 다운로드 과정에서 발생하는 다양한 이벤트를 처리합니다.

 

5. ImageDownloader

ImageDownloader는 Kingfisher의 네트워킹 레이어에서 가장 상위 인터페이스로, 사용자에게 이미지 다운로드 API를 제공합니다.

open class ImageDownloader: @unchecked Sendable {
    // 싱글톤 인스턴스
    public static let `default` = ImageDownloader(name: "default")
    
    private let propertyQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloaderPropertyQueue")
    
    // 설정 속성
    open var downloadTimeout: TimeInterval
    open var trustedHosts: Set<String>?
    open var sessionConfiguration = URLSessionConfiguration.ephemeral
    open var sessionDelegate: SessionDelegate
    open var requestsUsePipelining = false
    
    // 델리게이트
    open weak var delegate: (any ImageDownloaderDelegate)?
    open weak var authenticationChallengeResponder: (any AuthenticationChallengeResponsible)?
    
    // 내부 속성
    private let name: String
    private var session: URLSession
    
    // 메서드들...
}

워낙 클래스 규모가 방대하여서, 모든 로직들을 완전히 파악하기는 힘들었습니다만, 인상깊었던 분석내용들을 아래와 같이 정리해볼 수 잇었습니다.

 

인증서 검증 과정 우회목록 제공

HTTPS 연결을 할 때, 서버는 SSL 인증서를 제공하는데, 이 URL을 통해 데이터를 요청하고자 할 경우 인증서가 신뢰할 수 있는지 확인하는 과정이 있습니다. 여기서 자체 서명된 인증서는 기본적으로 신뢰되지 않습니다. 이에 Kingfisher는 trustedHosts 집합을 통해 인증서 검증과정을 우회하고 자동으로 신뢰하도록 설계하였습니다.

open var trustedHosts: Set<String>?

 

세션 설정 최적화

Downloader 역할을 수행하는 URLSession의 설정값을 .ephemeral로 설정한 것을 볼 수 있습니다. 여기서 ephemeral은 캐시 정책 중 하나로 디스크에 캐시를 기록하지 않겠다는 의미입니다. 이를 사용하지 않을 경우, 시스템 레벨에서 디스크 캐싱을 자동으로 사용하게 되는 만큼, ImageCache와의 캐싱 시스템과 병행 동작하게 됩니다. 이는 디스크 공간의 낭비로 이어질 수 잇습니다.

open var sessionConfiguration = URLSessionConfiguration.ephemeral {
    didSet {
        session.invalidateAndCancel() // sessionConfiguration을 변경할 때마다 기존 URLSession은 invalidate 됩니다.
        session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil)
    }
}

 

 

이미지 다운로드 작업 시작 및 관리
더보기
private func startDownloadTask(
        context: DownloadingContext,
        callback: SessionDataTask.TaskCallback
    ) -> DownloadTask
    {
        /// addDownloadTask를 통해 DownloadTask 생성 혹은 반환
        let downloadTask = addDownloadTask(context: context, callback: callback)

        guard let sessionTask = downloadTask.sessionTask, !sessionTask.started else {
            return downloadTask
        }

        // SessionTask에 대해 테스크 완료에 대한 콜백 클로저 정의
        sessionTask.onTaskDone.delegate(on: self) { (self, done) in
            // Underlying downloading finishes.
            // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
            let (result, callbacks) = done

            // 이미지에 대한 후처리를 하기에 앞서,ImageDownloadingDelegate의 델리게이트 패턴을 통해 이를 알림
            self.reportDidDownloadImageData(result: result, url: context.url)

            switch result {
            // 이미지 후처리 시작
            case .success(let (data, response)):
                
                /// callbacks 내부 각 콜백에 대해 callback.options.processor에 접근해 순차적으로 이미지 처리작업 수행할 수 있는 ImageDataProcessor 클래스 생성
                /// processingQueue 또한 비동기, 동기 여부 자유롭게 커스텀 가능
                let processor = ImageDataProcessor(
                    data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
                )
                
                /// onImageProcessed Delegate로 인해, 매 callback에 대한 이미지 decode가 완료될때마다 해당 클로저가 반복 실행됩니다.
                processor.onImageProcessed.delegate(on: self) { (self, done) in
                    // `onImageProcessed` will be called for `callbacks.count` times, with each
                    // `SessionDataTask.TaskCallback` as the input parameter.
                    // result: Result<Image>, callback: SessionDataTask.TaskCallback
                    let (result, callback) = done

                    self.reportDidProcessImage(result: result, url: context.url, response: response)
                    
                    // 결과 전달
                    let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
                    let queue = callback.options.callbackQueue
                    /// callback은 startDownloadTask 호출 시 createTaskCallback 메서드로 생성됨
                    /// 다운로드 작업을 시작할 때, 외부로부터 제공받은 callback의 onCompleted Delegate(완료 핸들러)에 이미지 결과를 그대로 전달
                    /// 이 callback은 동일한 SessionDataTask를 사용하고 있다면, 공유되고 있는 속성이기에 처리 작업의 결과를 요청자에게 다시 전달되게 됨.
                    queue.execute { callback.onCompleted?.call(imageResult) }
                }
                
                // 이미지 처리 시작
                processor.process()

            case .failure(let error):
                callbacks.forEach { callback in
                    let queue = callback.options.callbackQueue
                    queue.execute { callback.onCompleted?.call(.failure(error)) }
                }
            }
        }

        // 다운로드 시작 보고 및 태스크 실행
        reportWillDownloadImage(url: context.url, request: context.request)
        sessionTask.resume()
        return downloadTask
    }

개인적으로 이해하기 굉장히 어려웠던 메서드입니다... 

전체적인 동작 흐름은 다음과 같습니다:

  1. 다운로드 태스크 생성: addDownloadTask 메서드를 호출하여 주어진 context와 callback으로 DownloadTask 인스턴스를 생성하거나 기존 인스턴스를 반환받습니다.
  2. 세션 태스크 확인: guard 문을 통해 세션 태스크가 존재하고 아직 시작되지 않았는지 확인합니다. 이미 시작된 태스크이거나 세션 태스크가 없으면 즉시 다운로드 태스크를 반환합니다.
  3. 태스크 완료 콜백 설정: 세션 태스크의 onTaskDone 델리게이트에 클로저를 설정합니다. 이 클로저는 다운로드가 완료되었을 때 실행됩니다:
    • 다운로드 결과를 reportDidDownloadImageData 메서드를 통해 보고합니다.
    • 결과에 따라 성공 또는 실패 처리를 수행합니다.
  4. 성공 케이스 처리:
    • 다운로드가 성공하면 ImageDataProcessor를 생성하여 이미지 데이터 처리를 준비합니다.
    • 각 이미지 처리 완료마다 실행될 onImageProcessed 델리게이트를 설정합니다.
    • 이미지 처리가 완료되면 reportDidProcessImage를 통해 결과를 보고합니다.
    • 처리된 이미지 결과를 원래 콜백의 onCompleted 핸들러에 전달합니다.
    • 이미지 처리 작업을 시작합니다.
  5. 실패 케이스 처리:
    • 다운로드가 실패하면 모든 콜백에 에러를 전달합니다.
  6. 다운로드 시작:
    • reportWillDownloadImage 메서드를 통해 다운로드 시작을 보고합니다.
    • 세션 태스크의 resume() 메서드를 호출하여 실제 다운로드를 시작합니다.
    • 생성된 다운로드 태스크를 반환합니다.

이 메서드는 비동기적으로 이미지를 다운로드하고, 다운로드된 이미지 데이터를 처리한 후 결과를 콜백을 통해 전달하는 일련의 과정을 관리합니다. 여러 델리게이트 패턴을 활용하여 다운로드 및 처리 과정의 각 단계를 모니터링하고 보고하는 구조로 설계되어 있습니다.

 

이미지 다운로드

@discardableResult
open func downloadImage(
    with url: URL,
    options: KingfisherParsedOptionsInfo,
    completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil
) -> DownloadTask {
    let downloadTask = DownloadTask()
    //다운로드 컨텍스트 생성
    // URL, 옵션을 바탕으로 URLRequest를 생성하고 요청 수정자 적용
    createDownloadContext(with: url, options: options) { result in
        switch result {
        case .success(let context):
            let actualDownloadTask = self.startDownloadTask(
                context: context,
                callback: self.createTaskCallback(completionHandler, options: options)
            )
            // 생성된 빈 태스크와 실제 태스크 연결
            downloadTask.linkToTask(actualDownloadTask)
            
            // 요청 수정자에 태스크 시작 알림
            if let modifier = options.requestModifier {
                modifier.onDownloadTaskStarted?(downloadTask)
            }
        case .failure(let error):
            options.callbackQueue.execute {
                completionHandler?(.failure(error))
            }
        }
    }
    
    return downloadTask
}

이 메서드는 URL과 옵션을 받아 이미지 다운로드를 시작하고, DownloadTask 인스턴스를 반환합니다. 내부적으로는 DownloadingContext를 생성하고, 적절한 SessionDataTask 인스턴스를 생성하거나 재사용하여 실제 다운로드를 수행합니다.

 

struct DownloadingContext {
    let url: URL
    let request: URLRequest
    let options: KingfisherParsedOptionsInfo
}

그리고 이 DownloadingContext는 이미지 다운로드에 필요한 정보를 캡슐화하는 구조체로서, ImageDownloader 내부 createDownloadContext에서 생성되어, startDownloadTask 메서드로 전달됩니다.

 

6. ImageDownloaderDelegate

ImageDownloaderDelegate 프로토콜은 이미지 다운로드 과정에서 발생하는 다양한 이벤트를 외부로 알리기 위한 인터페이스를 정의합니다.

public protocol ImageDownloaderDelegate: AnyObject {
    // 다운로드 시작 알림
    func imageDownloader(
        _ downloader: ImageDownloader,
        willDownloadImageForURL url: URL,
        with request: URLRequest?
    )
    
    // 다운로드 완료 알림
    func imageDownloader(
        _ downloader: ImageDownloader,
        didFinishDownloadingImageForURL url: URL,
        with response: URLResponse?,
        error: (any Error)?
    )
    
    // 데이터 처리 기회 제공
    func imageDownloader(
        _ downloader: ImageDownloader,
        didDownload data: Data,
        with task: SessionDataTask
    ) -> Data?
    
    // 이미지 처리 완료 알림
    func imageDownloader(
        _ downloader: ImageDownloader,
        didDownload image: KFCrossPlatformImage,
        for url: URL,
        with response: URLResponse?
    )
    
    // 상태 코드 유효성 검사
    func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
    
    // 응답 처리 결정
    func imageDownloader(
        _ downloader: ImageDownloader,
        didReceive response: URLResponse
    ) async -> URLSession.ResponseDisposition
}

이 델리게이트 프로토콜은 외부 코드(예: UIImageView 확장)가 이미지 다운로드 과정에 개입하거나 알림을 받을 수 있는 훅을 제공합니다. 모든 메서드는 선택적으로 구현할 수 있도록 기본 구현이 제공됩니다.

 

 

7. CallbackQueue를 통한 콜백 실행 최적화

Kingfisher는 콜백이 실행되는 스레드나 큐를 세밀하게 제어하기 위해 CallbackQueue라는 추상화를 사용합니다. 이는 비동기 작업(다운로드, 이미지 처리, 캐시 작업)의 결과를 어떤 스레드에서 처리할지 제어합니다.

public enum CallbackQueue: @unchecked Sendable {
    /// 항상 메인 큐에서 비동기적으로 콜백을 실행합니다.
    case mainAsync
    
    /// 현재 메인 스레드에 있다면 즉시 실행하고, 그렇지 않으면 메인 큐에 비동기적으로 전달합니다.
    case mainCurrentOrAsync
    
    /// 현재 큐를 변경하지 않고 콜백을 실행합니다.
    case untouch
    
    /// 지정된 DispatchQueue에서 콜백을 실행합니다.
    case dispatch(DispatchQueue)
    
    public func execute(_ block: @escaping () -> Void) {
        switch self {
        case .mainAsync:
            DispatchQueue.main.async { block() }
        case .mainCurrentOrAsync:
            if Thread.isMainThread {
                block()
            } else {
                DispatchQueue.main.async { block() }
            }
        case .untouch:
            block()
        case .dispatch(let queue):
            queue.async { block() }
        }
    }
}

이 추상화를 통해 Kingfisher는 콜백 실행 컨텍스트를 세밀하게 제어하고, 사용자 요구에 맞게 유연하게 조정할 수 있습니다. 특히 UI 업데이트와 같이 스레드 관련 요구사항이 있는 작업에서 유용합니다.

 

이 CallbackQueue는 주로 completion handler 패턴을 사용하는 비동기 작업의 콜백 실행 컨텍스트를 관리하기 위해 사용됩니다.

func retrieveImage(..., completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) {
    // 이미지 작업 수행 후...
    options.callbackQueue.execute { 
        completionHandler?(.success(result))
    }
}

Kingfisher가 개발된 시점에는 Swift의 모던 비동기 패턴(async/await)이 없었기 때문에, completion handler를 통한 비동기 처리가 주된 방식이었습니다. async/await 패턴에서는 이러한 명시적인 큐 관리가 덜 필요한데, 이는 Swift 런타임이 자동으로 적절한 컨텍스트 전환을 처리하기 때문입니다. 하지만 Kingfisher는 여전히 광범위한 지원을 위해 두 가지 패턴을 모두 제공하고 있습니다.

 

이제 다음 글부터 본격적으로 Kingfisher의 핵심로직들을 직접 Swift Concurrency로 재작성해보도록 하겠습니다.