-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Actor 를 활용한 이미지 캐싱처리 - WWDC21 Protect mutable state with Swift actors #9
Comments
아래의 세션을 참고해서 적용해보았습니다. Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer Create ImageDownloader with Actor이미지 다운로더 actor 를 만들어보겠습니다. 이미지 다운로더는 다른 서비스에서 이미지를 다운받는 역할을 수행하며, 다운 받은 이미지를 cache 에 저장하여 동일한 이미지에 대한 중복 다운로드를 막습니다.
actor ImageDownloader {
private var cache: [URL: Image] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from: url)
// Potential bug: `cache` may have changed.
cache[url] = image
return image
}
} Actor 의 동기화 메커니즘은 한 번에 하나의 작업만 cache 에 접근하도록 보장하므로 캐시가 손상될 수 있는 경우는 없다고 생각했… 습니다만. 여기서
이것은 프로그램의 다른 코드가 실행될 수 있도록 스레드 제어권을 포기해서 전체 프로그램의 상태에 영향을 줄 수 있도록 합니다. 이렇게 되면 await 이후 함수가 다시 실행되는 시점에 전체 프로그램 상태가 변경되어서 유지되지 않을 수 있는 상태가 생길 수 있습니다. 그리고 이런 경우를 정의하지 않았는지 확인하는 것이 중요합니다. actor 에서 await 가 만들어낼 수 있는 문제동일한 URL 에 대해서 이미지를 가져오는 작업인 Task 1 과 Task2 가 있다고 가정해 보겠습니다.
이렇게 Task1 이 suspend 된 동안 동일한 URL의 서버에 새로운 이미지가 올라올 수 있습니다…!
잠시 후, Task 1 다운로드 작업이 끝나고 cache 에 😸 이미지를 저장합니다. Task 2 역시 다운로드 작업이 끝나고 😿 이미지를 cache 에 덮어씌우게 됩니다. 즉, 동일한 URL 에 대해서 서로 다른 이미지를 다운로드하게 됩니다. Actor 가 low-level 의 data races 는 없지만 await 로 인해 버그가 발생한 것이지요! 어떻게 해야할까요? await 이후에 잘 수행되는지 확인하면 됩니다. async 함수를 다시 실행할 때, cache 에 값이 있으면 원래 버전을 유지하고 새로운 버전을 버리도록 하거나, 동일한 URL 에 대해서는 중복 다운로드를 못하게 하면 됩니다. actor ImageDownloader {
private var cache: [URL: Image] = [:]
func image(from url: URL) async throws -> Image? {
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from: url)
// Replace the image only if it is still missing from the cache.
// ✅ 딕셔너리의 Subscript 로 element 에 접근하면 기본 반환값이 optional type 입니다. 옵셔널이 싫다면 default 값을 직접 명시 할 수 있습니다.
// ✅ await 이후, cache[url] 이 있다면?(Task 2 가 suspend 되었을 때, Task 1 다운로드 완료 한 경우.) Task 2 가 다운로드한 이미지 대신 기존의 이미지 유지. 없다면 다운로드한 이미지 설정.
cache[url] = cache[url, default: image]
// ✅ 결과: 세션 속의 경우에 😸 이미지가 캐싱됨.
return cache[url]
}
} 적용하기자, Actor 를 활용한 ImageDownloader 에 대해서 알아보았습니다. 이제는 async/await 로 만든 ImageFetchProvider 를 활용해서 이미지를 다운받고, 그 이후의 캐싱 역할은 Actor 로 만든 ImageDownloader 에서 처리해보도록 하겠습니다.
import UIKit
struct ImageFetchProvider {
static let shared = ImageFetchProvider()
private init() { }
/// URL 을 가지고 data 를 다운받아서 UIImage 로 변환하는 메서드.
/// - Parameter url: 다운받을 URL 값.
/// - Returns: 다운 받은 data 를 UIImage 로 변환해서 리턴. 변환되지 않는 경우 에러를 던집니다.
public func fetchImage(with url: URL) async throws -> UIImage {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
throw ImageDownloadError.invalidServerResponse
}
guard let image = UIImage(data: data) else {
throw ImageDownloadError.unsupportImage
}
return image
}
}
import UIKit
actor ImageDownloader {
// ✅ 캐싱 구현하기 위해서 싱글톤 패턴 사용.
static let shared = ImageDownloader()
private init() { }
// 초기에는 ImageFetchProvider 에서 에러를 핸들링해주었고, 이 과정에서 유효한 URL 에 대한 핸들링도 진행해주었다.
// 그래서 ImageFetchProvider.shared.fetchImage(with:)에 매개변수로 String 을 넘겨주어서 에러 핸들링을 하는 구조였다. 그 결과, cache 의 자료형이 [String, UIImage] 로 설정되었고, 어떤 key 로 캐싱하는지에 대한 문맥상 표현이 안되서 [URL: UIImage] 자료형을 고수하게 되었다.
// 어떻게 보면 에러를 던지는 함수를 호출하는 함수에서 다시금 에러를 던질 수 있는 구조이다. 하지만 이건 내가 역할을 나누기 위해서 나눈것이지 결과적으로는 호출하는 맨위 함수에서 에러를 핸들링하고 있고, 에러라는 것은 어느 순간에나 등장할 수 있기 때문에 에러를 핸들링하는 역할을 특정 객체에 한정짓지 않아도 생각했습니다.(마치 초기에 ImageFetchProvider 가 모든 에러를 던지도록 했던 모양새처럼 말이죵)
private var cache: [URL: UIImage] = [:]
func image(from urlPath: String) async throws -> UIImage? {
guard let url = URL(string: Const.Path.imageURLPath + urlPath) else {
throw ImageDownloadError.invalidURLString
}
// ✅ 이미 다운 받은 URL 에 대해서 캐싱 처리.
if let cached = cache[url] {
return cached
}
let image = try await downloadImage(from: url)
cache[url] = cache[url, default: image]
return cache[url]!
}
private func downloadImage(from url: URL) async throws -> UIImage {
let imageFetchProvider = ImageFetchProvider.shared
return try await imageFetchProvider.fetchImage(with: url)
}
}
// MovieCollectionViewCell.swift
func initCellWith(urlPath: String, title: String) {
Task {
do {
// ✅ 싱글톤 패턴 사용
let posterImage = try await ImageDownloader.shared.image(from: urlPath)
posterImageView.image = posterImage
titleLabel.text = title
} catch ImageDownloadError.unsupportImage {
print("image download error - unsupportImage")
} catch ImageDownloadError.invalidServerResponse {
print("image download error - invalidServerResponse")
} catch ImageDownloadError.invalidURLString {
print("image download error - invalidURLString")
}
}
} 참고) Enabling Thread SanitizerThread Sanitizer 를 활성화해서
깃허브: https://github.com/28th-SOPT-iOS-CloneCoding/SpectaClone-KimHyunGyu 출처: Protect mutable state with Swift actors - WWDC21 - Videos - Apple Developer Protect mutable state with Swift actors |
The text was updated successfully, but these errors were encountered: