Extension Wrapping
Extension Wrapping은 유명 라이브러리에서 자주 보이는 구조입니다.
(참고로 Extension Wrapping은 정식 명칭이 아니라 제가 임의로 정한 이름입니다.)
대표적으로 Kingfisher, RxSwift가 있습니다.
imageView.kf.setImage(with: url)
Kingfisher 메서드를 사용할 때 UIImageView가 아니라 kf를 이용해 메서드를 호출하죠?
바로 그게 Extension Wrapping입니다.
Extension으로 구현한 Kingfisher 메서드를 UIView에서 직접 호출하지 않고, KingfisherWrapper로 호출하는 것입니다.
Extension Wrapping 장점
Extension Wrapping은 안정적인 확장을 위해 사용합니다.
Swift의 Extension은 자유도가 높습니다.
만약 Wrapping을 하지 않는다면 클래스의 역할이 점점 커지고, 중복 코드도 늘어날 것입니다.
Extension Wrapping을 적용하면 개방-폐쇄 원칙(OCP)을 지키는 확장 코드를 작성할 수 있고,
struct를 이용하기 때문에 불변성도 보장할 수 있습니다.
Extension Wrapping 방법
1. Wrapper 구조체 구현
Wrapping을 담당하는 구조체를 구현합니다.
public struct JeongfisherWrapper<Base> {
public let base: Base
public init(base: Base) {
self.base = base
}
}
Wrapper 구조체는 Generic을 사용해 여러 타입을 받을 수 있도록 합니다.
Generic 타입 프로퍼티인 base는 Extension에서 사용됩니다.
base를 통해 클래스 멤버 프로퍼티를 다룰 수 있습니다.
2. Compatible 프로토콜 구현
다음은 Compatible 프로토콜을 구현해야 합니다.
Compatible 프로토콜을 채택했을 때 kf, rx, jf처럼 Wrapper를 사용할 수 있게 합니다.
public protocol JeongfisherCompatible: AnyObject {}
extension JeongfisherCompatible {
public var jf: JeongfisherWrapper<Self> {
return JeongfisherWrapper(base: self)
}
}
JeongfisherCompatible 프로토콜에서 jf 이름으로 JeongfisherWrapper를 생성합니다.
계산 프로퍼티를 이용해 매번 JeongfisherWrapper 인스턴스를 생성하고 있는데요.
JeongfisherWrapper를 구조체로 구현했기 때문에 불변성을 보장할 수 있습니다.
JeongfisherCompatible은 AnyObject를 채택했습니다만, NSObject 등을 채택해도 됩니다.
학습하면서 안건데 Kingfisher는 AnyObject를, RxSwift는 NSObject를 채택했더라고요.
Jeongfisher는 이미지 관련이고 소규모를 유지할 것이므로 AnyObject가 과할 수 있지만,
Kingfisher를 모티브로 하고 있기 때문에 AnyObject를 채택했습니다.
3. 사용할 클래스에 채택
jf를 사용할 클래스에 JeongfisherCompatible을 채택합니다.
extension UIImageView: JeongfisherCompatible {}
JeongfisherCompatible에서 기본적으로 jf 프로퍼티를 제공하므로 추가로 구현할 내용은 없습니다.
이때 UIImageView를 typealias 해서 사용하기도 하더라고요.
typealias JFImageView = UIImageView
extension JFImageView: JeongfisherCompatible {}
Kingfisher도 저 방식으로 구현되어 있고요.
클래스가 변경될 가능성이 있을 때 편리할 것 같다는 생각이 듭니다.
예를 들면, UIImageView에서 UIImageView의 서브클래스로 변경할 때 파일을 하나하나 바꾸지 않고 typealias만 바꿔주면 되겠죠.
4. 기능 구현
마지막으로 Base 타입에서 지원하는 기능을 구현하면 됩니다.
extension JeongfisherWrapper where Base: UIImageView {
public func setImage(
with url: URL,
placeHolder: UIImage? = nil,
waitPlaceHolderTime: TimeInterval = 1.0,
options: Set<JFOption> = [])
{
...
}
}
특정 타입에만 제공할 경우, where문을 이용해 Base 타입을 제한합니다.
예를 들어, setImage는 UIImageView에서만 제공하기 때문에 Base 타입을 UIImageView로 제한했습니다.
실제로 사용해보면,
UIImageView 객체에서 jf에 접근할 수 있고,
Base 타입이 UImageView이기 때문에 setImage 메서드를 할 수 있습니다.
물론 UIImageView에서 직접 접근은 되지 않고요 ㅎㅎ
마무리
Extension Wrapping을 사용해 보니 매우 만족스러웠습니다.
UIImageView의 extension에 구현했을 때와 비교해 봤을 때
Jeongfisher의 역할이 UIImageView에서 분리되었고,
그 덕분에 Jeongfisher가 확장, 변경이 되더라도 UIImageView에 영향을 주지 않게 되었습니다.
Jeongfisher의 기능만 테스트하기에도 수월하겠죠.
유용하면서 방법이 쉽고 응용 범위도 넓은... ㄷㄷ
대형 오픈소스에서 사용하는 이유가 있구나 싶었습니다.
감사합니다.
참고
https://hh963103.medium.com/유명-오픈소스-라이브러리-처럼-swift-확장-기능-만들기-8a97c901a5c6
https://github.com/onevcat/Kingfisher
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.