프로퍼티 (Properties) - 프로퍼티 래퍼 (Property Wrappers)

00yhsp·2024년 4월 23일

프로퍼티 래퍼 (property wrapper)는 프로퍼티가 저장되는 방법을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리 계층을 추가한다. 예를 들어 쓰레드 안정성 검사를 제공하거나 기본 데이터를 데이터베이스에 저장하는 프로퍼티가 있는 경우 모든 프로퍼티에 해당 코드를 작성해야 한다. 프로퍼티 래퍼를 사용할 때 래퍼를 정의할 때 관리 코드를 한번 작성한 다음 여러 프로퍼티에 적용하여 해당 관리 코드를 재사용한다.

프로퍼티 래퍼를 정의하기 위해 wrappedValue 프로퍼티를 정의한 구조체, 열거형 또는 클래스를 만든다. 아래 코드에서 TwelveOrLess 구조체는 래핑하는 값이 항상 12와 같거나 더 작은 숫자가 포함된다. 더 큰 숫자를 저장하도록 하면 12가 저장된다.

@propertyWrapper
struct TwelveOrLess {
    private var number = 0
    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, 12) }
    }
}

setter는 새로운 값이 12 보다 작거나 같은 것을 확인하고 getter는 저장된 값을 반환한다.

Note
위의 예제에서 number 선언부는 TwelveOrLess 의 구현에서만 number 가 사용될 수 있도록 private 로 변수를 표기한다. 다른곳에서 작성된 코드는 wrappedValue 를 위한 getter와 setter를 사용하여 값에 접근하고 직접적으로 number 를 사용할 수 없다.

속성으로 프로퍼티 전에 래퍼의 이름을 작성하여 프로퍼티에 래퍼를 적용한다. 다음은 항상 12 이하인지 확인하기 위해 TwelveOrLess 프로퍼티 래퍼를 사용하여 사각형을 저장하는 구조체이다:

struct SmallRectangle {
    @TwelveOrLess var height: Int
    @TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height)
// Prints "0"

rectangle.height = 10
print(rectangle.height)
// Prints "10"

rectangle.height = 24
print(rectangle.height)
// Prints "12"

height 와 width 프로퍼티는 TwelveOrLess.number 를 0으로 설정하는 TwelveOrLess 의 정의로 부터 초기값을 얻는다. rectangle.height 에 숫자 10을 저장하는 것은 그 숫자가 작은 숫자이기 때문에 성공적으로 저장한다. 24를 저장하려고 하면 24는 프로퍼티 setter의 규칙에 비해 큰 숫자이므로 실질적으로 12가 저장된다.

프로퍼티에 래퍼를 적용할 때 컴파일러는 래퍼를 위한 저장소를 제공하는 코드와 래퍼를 통해 프로퍼티에 접근하는 코드를 합성한다 (프로퍼티 래퍼는 래핑된 값을 저장하는 역할을 하므로 이에 대한 합성 코드는 없다). 특수 속성 구문의 이점을 사용하지 않고도 프로퍼티 래퍼의 기능을 사용하는 코드를 작성할 수 있다. 예를 들어 다음은 속성으로 @TwelveOrLess 를 작성하는 대신에 TwelveOrLess구조체에 명시적으로 프로퍼티를 래핑하는 이전 코드 리스트로 부터의 SmallRectangle이다:

struct SmallRectangle {
    private var _height = TwelveOrLess()
    private var _width = TwelveOrLess()
    var height: Int {
        get { return _height.wrappedValue }
        set { _height.wrappedValue = newValue }
    }
    var width: Int {
        get { return _width.wrappedValue }
        set { _width.wrappedValue = newValue }
    }
}

_height 와 _width 프로퍼티는 프로퍼티 래퍼의 인스턴스인 TwelveOrLess 를 저장한다. height 와 width 에 대한 getter와 setter는 wrappedValue 프로퍼티에 접근한다.

래핑된 프로퍼티를 위한 초기값 설정 (Setting Initial Values for Wrapped Properties)

위 예제의 코드는 TwelveOrLess 에 정의한 number 초기값으로 래핑된 프로퍼티에 대한 초기값을 설정한다. 이 프로퍼티 래퍼를 사용한 코드는 TwelveOrLess 로 래핑된 프로퍼티에 다른 초기값을 지정할 수 없다. 예를 들어 SmallRectangle 에 정의에 height 와 width 초기값을 지정할 수 없다. 초기값 설정을 지원하거나 다른 커스터마이징을 지원하려면 프로퍼티 래퍼는 초기화를 추가해줘야 한다. 다음은 래핑과 최대값을 설정하는 초기화를 정의하는 SmallNumber 라는 확장된 TwelveOrLess 를 나타낸다:

@propertyWrapper
struct SmallNumber {
    private var maximum: Int
    private var number: Int

    var wrappedValue: Int {
        get { return number }
        set { number = min(newValue, maximum) }
    }

    init() {
        maximum = 12
        number = 0
    }
    init(wrappedValue: Int) {
        maximum = 12
        number = min(wrappedValue, maximum)
    }
    init(wrappedValue: Int, maximum: Int) {
        self.maximum = maximum
        number = min(wrappedValue, maximum)
    }
}

SmallNumber 의 정의는 init(), init(wrappedValue:), init(wrappedValue:maximum:) 의 3개의 초기화를 포함한다. 아래는 래핑값과 최대값 설정을 사용하는 예이다.

프로퍼티에 래퍼를 적용하지 않고 초기값을 지정하지 않으면 Swift는 래퍼를 설정하기 위해 init() 을 사용합니다. 예를 들어:

struct ZeroRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int
}

var zeroRectangle = ZeroRectangle()
print(zeroRectangle.height, zeroRectangle.width)
// Prints "0 0"

height 와 width 를 래핑한 SmallNumber 의 인스턴스는 SmallNumber() 호출로 생성된다. 초기화 내부의 코드는 기본값 0과 12를 사용하여 초기 래핑값과 초기 최대값을 설정한다. 프로퍼티 래퍼는 SmallRectangle 에 TwelveOrLess 를 사용한 이전 예제처럼 모든 초기값을 제공한다. 예제와 반대로 SmallNumber 는 프로퍼티 선언의 일부분으로 초기값 작성을 제공한다.

프로퍼티에 초기값을 지정할 때 Swift는 래퍼를 설정하기 위해 init(wrappedValue:) 를 사용한다. 예를 들어:

struct UnitRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber var width: Int = 1
}

var unitRectangle = UnitRectangle()
print(unitRectangle.height, unitRectangle.width)
// Prints "1 1"

래핑한 프로퍼티에 = 1 을 작성하면 init(wrappedValue:) 초기화 호출에 전달됩니다. height 와 width 를 래핑한 SmallNumber 의 인스턴스는 SmallNumber(wrappedValue: 1) 호출로 생성된다. 이 초기화는 래핑된 값을 사용하고 기본 최대값으로 12를 사용한다.

사용자 속성 후에 소괄호 안에 인수를 작성하면 Swift는 래퍼를 설정하기 위한 인수를 받을 수 있는 초기화를 사용한다. 예를 들어 초기값과 최대값을 제공하면 Swift는 init(wrappedValue:maximum:) 초기화를 사용한다:

struct NarrowRectangle {
    @SmallNumber(wrappedValue: 2, maximum: 5) var height: Int
    @SmallNumber(wrappedValue: 3, maximum: 4) var width: Int
}

var narrowRectangle = NarrowRectangle()
print(narrowRectangle.height, narrowRectangle.width)
// Prints "2 3"

narrowRectangle.height = 100
narrowRectangle.width = 100
print(narrowRectangle.height, narrowRectangle.width)
// Prints "5 4"

height 를 래핑한 SmallNumber 의 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 5) 를 호출하여 생성되고 width 를 래핑한 인스턴스는 SmallNumber(wrappedValue: 3, maximum: 4) 를 호출하여 생성된다.

프로퍼티 래퍼에 인수를 포함하여 래퍼에 초기상태를 설정하거나 래퍼가 생성될 때 다른 옵션을 전달할 수 있다. 이 구문은 프로퍼티 래퍼를 사용하는 가장 일반적인 방법이다. 속성에 필요한 어떠한 인수도 제공할 수 있으며 초기화 구문에 전달된다.

프로퍼티 래퍼 인수를 포함하면 할당을 사용하여 초기값을 지정할 수도 있다. Swift는 할당을 wrappedValue 인수처럼 취급하고 이 인수를 받을 수 있는 초기화를 사용한다. 예를 들어:

struct MixedRectangle {
    @SmallNumber var height: Int = 1
    @SmallNumber(maximum: 9) var width: Int = 2
}

var mixedRectangle = MixedRectangle()
print(mixedRectangle.height)
// Prints "1"

mixedRectangle.height = 20
print(mixedRectangle.height)
// Prints "12"

height 를 래핑한 SmallNumber 의 인스턴스는 기본 최대값은 12를 사용하는 SmallNumber(wrappedValue: 1) 을 호출하여 생성된다. width 를 래핑한 이 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9) 를 호출하여 생성된다.

프로퍼티 래퍼에서 값 투영 (Projecting a Value From a Property Wrapper)

래핑된 값 외에도 프로퍼티 래퍼는 투영된 값 (projected value) 정의에 의해 추가적인 기능을 노출할 수 있다. 예를 들어 데이터베이스 접근을 관리하는 프로퍼티 래퍼는 투영된 값으로 flushDatabaseConnection() 메서드를 노출할 수 있다. 투영된 값의 이름은 앞에 달러 표시 ($)가 붙는 것을 제외하면 래핑된 값과 동일하다. 코드에서 $ 로 시작하는 프로퍼티를 정의할 수 없기 때문에 투영된 값은 정의한 프로퍼티를 절대 방해하지 않습니다.

위의 SmallNumber 예제에서 프로퍼티에 큰 숫자로 설정하려고 하면 프로퍼티 래퍼가 이전에 저장한 숫자로 변경합니다. 아래의 코드는 새로운 값을 저장하기 전에 프로퍼티 래퍼가 프로퍼티에 새로운 값을 변경하는지 판단하기 위해 SmallNumber 구조체에 projectedValue 프로퍼티를 추가합니다.

@propertyWrapper
struct SmallNumber {
    private var number: Int
    private(set) var projectedValue: Bool

    var wrappedValue: Int {
        get { return number }
        set {
            if newValue > 12 {
                number = 12
                projectedValue = true
            } else {
                number = newValue
                projectedValue = false
            }
        }
    }

    init() {
        self.number = 0
        self.projectedValue = false
    }
}
struct SomeStructure {
    @SmallNumber var someNumber: Int
}
var someStructure = SomeStructure()

someStructure.someNumber = 4
print(someStructure.$someNumber)
// Prints "false"

someStructure.someNumber = 55
print(someStructure.$someNumber)
// Prints "true"

someStructure.$someNumber 는 래퍼의 투영된 값에 접근한다. 4와 같은 작은 숫자를 저장한 후에 someStructure.$someValue 의 값은 false이다. 그러나 55와 같은 큰 숫자를 저장한 후에 투영된 값은 true이다.

프로퍼티 래퍼는 투영된 값으로 어떤 타입의 값도 반환할 수 있다. 이 예제에서 프로퍼티 래퍼는 숫자가 변경되었는지에 대한 정보만 노출한다. 그래서 투영된 값으로 부울 값을 노출한다. 더 많은 정보의 노출이 필요한 래퍼는 다른 타입의 인스턴스를 반환하거나 투영된 값으로 래퍼의 인스턴스를 노출하기 위해 self 를 반환할 수 있다.

타입의 부분으로 코드로 부터 투영된 값에 접근할 때 프로퍼티 getter 나 인스턴스 메서드 처럼 다른 프로퍼티에 접근하듯이 프로퍼티 이름 전에 self. 을 생략할 수 있다. 다음 예제의 코드는 $height 와 $width 로 height 와 width 래퍼의 투영된 값을 참조합니다:

enum Size {
    case small, large
}

struct SizedRectangle {
    @SmallNumber var height: Int
    @SmallNumber var width: Int

    mutating func resize(to size: Size) -> Bool {
        switch size {
        case .small:
            height = 10
            width = 20
        case .large:
            height = 100
            width = 100
        }
        return $height || $width
    }
}

프로퍼티 래퍼 구문은 getter와 setter가 있는 프로퍼티의 짧은 구문이기 때문에 height 와 width 접근은 다른 프로퍼티 접근과 동일한 동작이다. 예를 들어 resize(to:) 에 코드는 프로퍼티 래퍼를 사용하는 height 와 width 에 접근한다. resize(to: .large) 를 호출하면 .large 스위치 케이스가 사각형의 높이와 너비를 100으로 설정한다. 래퍼는 프로퍼티의 값이 12보다 더 큰 값으로 되는 것을 막고 값이 변경되었다는 사실을 기록하기 위해 투영된 값을 true 로 설정한다. resize(to:) 끝에서 반환 구문은 프로퍼티 래퍼가 height 또는 width 를 변경했는지 판단하기 위해 $height 와 $width 를 체크한다.

profile
iOS Dev

0개의 댓글