또 늦었다. Swift 5.1 이 나온지가 언제인데 지금 Property Wrappers 를 보고있다. (그래도 해를 안넘긴게 어디인가…) 시작해보자.
Swift-Evolution (아래 링크) 에 정의된 내용을 정리해보았다.
소개
반복적으로 나타나는 Property 구현 패턴에 대해, 고정된 패턴 셋을 컴파일러에 하드코딩 (lazy 및 @NSCopying 등) 을 하는 대신에, 이러한 패턴을 라이브러리로 정의 할 수 있도록 Property Wrapper 매커니즘을 제공한다.
동기
lazy 및 @NSCopying 등과 같이 Properties 에 적용 할 패턴들을 수용하려고 시도했으나, 이는 좁은 범위에서만 유용하다. 예를 들면, Swift 는 기본 언어 기능으로 lazy Properties 를 제공한다. 이는 종종 Property 가 Optional 로 노출되는 것을 피하기 위해 필요한데, 만약 이를 지원하지 않았다면 이와 동일한 효과를 얻기 위해 다음과 같은 보일러플레이트(boilerplate) 코드를 계속 작성해주어야한다.
struct Foo {
// lazy var foo = 1738
private var _foo: Int?
var foo: Int {
get {
if let value = _foo { return value }
let initialValue = 1738
_foo = initialValue
return initialValue
}
set {
_foo = newValue
}
}
}
다만, 몇 가지 단점이 있는데, 이는 컴파일러를 더 복잡하고 덜 직교하게(?) 만든다. 또한 융통성이 없다. 컴파일러에 하드코딩 그만하고 싶다!!
또한, 암시적으로 언래핑된 Optional 은 Non-Optional let 에 비해서 안전하지 못하므로, 한 번 할당 한 후에 불변 Property 로 만드는 것이 종종 합리적이다.
class Foo {
let immediatelyInitialized = "foo"
var _initializedLater: String?
// initializedLater 를 non-optional 'let' 으로 만들고 싶다!
// 한 번만 할당될 수 있고, 그 전엔 액세스 할 수 없다!
var initializedLater: String {
get { return _initializedLater! }
set {
assert(_initializedLater == nil)
_initializedLater = newValue
}
}
}
지금은 위와 같이 작성한다… 그래서 해결책을 제시했다!
해결책
Property 선언이 그것을 구현하는 데에 사용된 Wrapper 를 명시 할 수 있는 Property Wrapper 를 도입한다. Wrapper 는 Attribute 를 통해 설명된다.
@Lazy var foo = 1738
Lazy 를 Property Wrapper 방식으로 구현하여 적용한 것이다.
@propertyWrapper
enum Lazy<Value> {
case uninitialized(() -> Value)
case initialized(Value)
init(wrappedValue: @autoclosure @escaping () -> Value) {
self = .uninitialized(wrappedValue)
}
var wrappedValue: Value {
mutating get {
switch self {
case .uninitialized(let initializer):
let value = initializer()
self = .initialized(value)
return value
case .initialized(let value):
return value
}
}
set {
self = .initialized(newValue)
}
}
}
Property Wrapper 타입은 Wrapper 로 사용하는 Property 에 Storage 를 제공한다. wrappedValue Property 는 Wrapper 의 실제 구현을 제공하는 반면, init(wrappedValue:)은 Property 의 값으로부터 Storage 를 초기화 할 수 있다.
다음과 같은 Property 의 선언은,
@Lazy var foo = 1738
아래처럼 해석된다.
private var _foo: Lazy<Int> = Lazy<Int>(wrappedValue: 1738)
var foo: Int {
get { return _foo.wrappedValue }
set { _foo.wrappedValue = newValue }
}
다음과 같이 초기화도 가능하다.
extension Lazy {
init(body: @escaping () -> Value) {
self = .uninitialized(body)
}
}
func createAString() -> String { ... }@Lazy(body: createAString) var bar: String
Property Wrapper 를 통해 자체적으로 API 의 관계를 설명 할 수도 있다. 다음은 name 을 통해 Database 의 Field 를 참조하는 Property Wrapper 의 예이다.
@propertyWrapper
public struct Field<Value: DatabaseValue> {
public let name: String
private var record: DatabaseRecord?
private var cachedValue: Value?
public init(name: String) {
self.name = name
}
public func configure(record: DatabaseRecord) {
self.record = record
}
public var wrappedValue: Value {
mutating get {
if cachedValue == nil { fetch() }
return cachedValue!
}
set {
cachedValue = newValue
}
}
public func flush() {
if let value = cachedValue {
record!.flush(fieldName: name, value)
}
}
public mutating func fetch() {
cachedValue = record!.fetch(fieldName: name, type: Value.self)
}
}
다음과 같이 Field Property Wrapper 를 사용 가능하다.
public struct Person: DatabaseModel {
@Field(name: "first_name") public var firstName: String
@Field(name: "last_name") public var lastName: String
@Field(name: "date_of_birth") public var birthdate: Date
}
기존 값을 Flush 하고 Database 에서 새 값을 가져오고 싶을 수 있다.
그러나, 모델의 각 Property 의 밑줄있는 변수 (_firstName, _lastName 및 _birthdate) 는 Private 이므로 직접 접근 할 수 없다.
따라서 원하는 API (Wrapper 에서 정의한 메서드라고 이해하면 될 듯) 를 제공하기 위해, Property Wrapper 는 선택적으로 Projection 을 제공한다. Wrapper 내 projectedValue Property 를 정의하면 Projection 이 제공된다.
@propertyWrapper
public struct Field<Value: DatabaseValue> {
// ... API as before ...
public var projectedValue: Self {
get { self }
set { self = newValue }
}
}
Projection Property 앞에는 $가 붙는다. (예를 들면, firstName Property 의 Projection 의 경우 $firstName)
Projection Property 를 정의한 경우 다음의 선언은
@Field(name: "first_name") public var firstName: String
다음과 같다.
private var _firstName: Field<String> = Field(name: "first_name")
public var firstName: String {
get { _firstName.wrappedValue }
set { _firstName.wrappedValue = newValue }
}
public var $firstName: Field<String> {
get { _firstName.projectedValue }
set { _firstName.projectedValue = newValue }
}
결과적으로 이렇게 쓸 수 있다.
somePerson.firstName = "Taylor"
$somePerson.flush()
예제
대표적인 예제는 User Defaults 에 적용한 예제이다.
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
return UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}enum GlobalSettings {
@UserDefault(key: "FOO_FEATURE_ENABLED", defaultValue: false)
static var isFooFeatureEnabled: Bool
@UserDefault(key: "BAR_FEATURE_ENABLED", defaultValue: false)
static var isBarFeatureEnabled: Bool
}
이처럼 Property Wrapper 를 이용하면, Boilerplate 코드를 줄일 수 있을 것 같다.