Extension Wrapper
Extension Wrapper는 최근에 포스팅을 했었죠? (Generic, Protocol을 이용한 Extension Wrapping)
Jeongfisher에서 적용을 해서 공유하고 싶은 마음에 포스팅을 작성했었습니다 ㅎㅎ
이번 포스팅에서는 Jeongfisher에 Extension Wrapper를 적용하면서 어떤 고민을 했는지 작성해보겠습니다.
참고로 이번 포스팅에서 구현 방법은 다루지 않습니다.
구현 방법은 위 링크를 참고해 주세요.
이번 글에서는 적용한 이유, 기술적 고민 두 가지를 다루었습니다.
적용 이유
Extension Wrapper 적용 전에는 UIImageView extension에 메서드를 추가했습니다.
메서드가 늘어남에 따라 UIImageView의 역할이 커지는 것을 느꼈고,
Jeongfisher의 기능을 안 쓸 때도 라이브러리 메서드가 노출되는 것이 불편했습니다.
킹피셔에서는 kf를 통해 킹피셔 메서드에 접근하는 것을 보고 코드를 살펴보았습니다.
처음 킹피셔를 볼 당시에는 Extension Wrapper가 어려워보여서 외면을 했었는데...
개선될 게 명확하고, 필요성이 느껴지니 외면할 수 없더라고요 ㅎㅎ;;
이번 기회에 얄팍하게나마 분석을 해서 Extension Wrapper를 적용해보기로 결정했습니다.
cancelDownloadImage 구현
cancelDownloadImage는 Task를 중간에 취소하는 메서드입니다.
다운로드 중에 CollectionViewCell이 안 보이게 됐을 때 Task를 취소해서 메모리 효율을 높이는 용도로 사용됩니다.
중간에 Task를 취소하기 위해서는 key 값이 필요합니다.
이미지 다운로더에서 key값으로 Task를 얻은 후 취소시켜야 하니까요.
(이미지 다운로드의 구조는 Jeongfisher 3. JFImageDownloader, 중복 Request 처리를 참고해 주세요.)
발생한 문제점
처음에는 key를 메서드 파라미터로 전달했습니다.
근데 UIImageView를 사용하는 곳에 URL을 전달하기 애매한 부분이 있었습니다.
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
guard let cell = cell as? PosterCollectionViewCell else { return }
cell.cancelDownloadImage(urlString: testItem[indexPath.row])
}
}
저는 주로 CollectionViewCell의 이미지뷰에서 사용하는데요.
didEndDisplaying에서 cell의 메서드를 호출하는 방식으로 사용합니다.
하지만 cancel에 URL을 전달하는건 비효율적인 구조라고 생각했습니다.
cancel은 의도치 않은 상황에서 자유롭게 호출할 수 있어야 하는데
URL 데이터 의존성 때문에 자유도를 헤친다고 느껴졌기 때문입니다.
킹피셔도 따로 URL을 전달하지 않고 있으니까 개선할 수 있겠다고 생각했습니다.
해결 방법
처음에는 막막했습니다.
UIImageView의 Extension에 저장 프로퍼티를 추가해야 하는데 Extension에는 그게 불가능하니까요.
킹피셔 코드를 보고 구글링을 하면서
Objective-C 기능인 objc_getAssociatedObject과 objc_setAssociatedObject를 사용하면 된다는 것을 배웠습니다.
/// UIImageView가 사용한 URL
private var downloadUrl: String? {
get { getAssociatedObject(base, &JFAssociatedKeys.downloadUrl) }
set { setRetainedAssociatedObject(base, &JFAssociatedKeys.downloadUrl, newValue) }
}
...
func getAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer) -> T? {
return objc_getAssociatedObject(object, key) as? T
}
func setRetainedAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer, _ value: T) {
objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
downloadUrl 자체는 계산 프로퍼티이기 때문에 Extension에 선언할 수 있고,
get, set을 이용해 저장 프로퍼티 효과를 낼 수 있는 것입니다.
imageView.jf.cancelDownloadImage()
결국 이렇게 파라미터 없이 cancel 기능을 구현할 수 있었습니다.
한 가지 아쉬운 점은 Objective-C 기능으로 해결을 한건데요.
어쩔 수 없긴 하지만... 그래도 아쉬움이 느껴졌습니다.
언젠가 Swift로 가능한 방법이 나온다면 꼭 개선해야겠습니다.
(까먹지 않기 위해 이렇게 포스팅도 하는겁니다 ㅋㅋ)
JFOption 구현
Jeongfisher는 setImage를 할 때 필요한 옵션을 지정할 수 있습니다.
발생한 문제점
public func setImage(url: String,
placeHolder: UIImage? = nil,
waitPlaceHolderTime: TimeInterval = 1,
useCache: Bool = true) {
...
}
기존에는 옵션이 1~2개라서 파라미터를 이용해 전달 받았는데요.
지원하는 옵션이 추가될 때마다 메서드 파라미터가 늘어나고 호출부와 정의부를 모두 수정해야해서
확장성이 나쁜 구조라고 느껴졌습니다.
해결 방법
이 문제는 JFOption Enum을 정의해서 해결했습니다.
/// Jeongfisher Option
public enum JFOption {
/// 메모리 캐시만 사용. 디스크 캐시는 사용하지 않음
case cacheMemoryOnly
/// 데이터를 캐시에서만 얻음. 네트워크 사용하지 않음
case onlyFromCache
/// 캐시를 무시하고 네트워크 다운로드 진행
case forceRefresh
/// 다운샘플링을 진행하지 않음
case showOriginalImage
/// ETag를 체크하지 않음
case disableETag
}
JFOption은 setImage를 할 때 적용되는 옵션 모음입니다.
public func setImage(
with url: URL,
placeHolder: UIImage? = nil,
waitPlaceHolderTime: TimeInterval = 1.0,
options: Set<JFOption> = [])
setImage 파라미터로 JFOption Set을 전달해서 옵션이 추가되도 수정이 필요 없도록 개선했습니다.
배열이 아니라 Set을 사용한 이유는 옵션 중복을 제거하고, contains 효율을 높이기 위해서입니다.
사실 contains 효율은 배열과 큰 차이가 없습니다 ㅎㅎ;
옵션이 10개 미만이라 O(1)과 O(n)의 차이가 없기 때문입니다.
하지만 옵션에 중복이 필요 없는 점이 Set과 더 잘 어울린다고 생각해서 Set을 선택했습니다.
개선 후 느낀 점
이렇게 개선하니 확장성이 좋아졌습니다.
기존에는 옵션이 추가될 때마다 메서드 파라미터도 추가되고,
메서드를 사용하는 곳, 정의한 곳 모두 수정이 필요했습니다.
개선 후에는 추가된 옵션과 상관 없는 호출부는 수정이 필요 없어졌습니다.
수정이 쉬워지면서 확장성이 개선된거죠!
그리고 다른 메서드로 옵션들을 전달할 때도 편리해졌습니다.
파라미터를 하나하나 넘겨줬었는데 options 그대로 전달하면 되니 편리하더라고요.
적절한 메서드들에서 옵션을 캐치해서 기능을 구현하면 되니 역할이 분리되었다는 생각도 들었네요 ㅎㅎ
감사합니다.
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.