본문 바로가기

MOBILE/ios

[swift] Property Wrapper: what does @ mean in swift?

Property Wrapper 그게 왜 필요한데

swift에서는 struct, calss의 프로퍼티의 종류가 두 가지이다. 저장 프로퍼티 vs 연산 프로퍼티 로 구분된다.

이때 연산 프로퍼티는 값에 접근할 때 계산을 하거나 변환 작업을 해야할 때 사용된다. 즉 값을 검사하는 수문장이 있는 프로퍼티이다.

 

예를 들어, 다음과 같이 값의 최대 범위를 계산하여 막아주는 연산 프로퍼티가 있다고 생각하자.

 

struct A
{
    private var realValue: Int = 0
    
    var value: Int {
        get {
            return realValue
        }
        set {
            if (realValue > 100) {
                realValue = 100
            }
            else if (realValue < 0) {
                realValue = 0
            }
            else {
                realValue = newValue
            }
                
        }
    }
}

struct B
{
    private var realValue: Double = 0
    
    var value: Double {
        get {
            return realValue
        }
        set {
            if (realValue > 100.0) {
                realValue = 100
            }
            else if (realValue < 0) {
                realValue = 0
            }
            else {
                realValue = newValue
            }
                
        }
    }
}

struct C
{
    private var realValue: String = ""
    
    var value: String {
        get {
            return realValue
        }
        set {
            if (realValue > "z") {
                realValue = "z"
            }
            else if (realValue < "a") {
                realValue = "a"
            }
            else {
                realValue = newValue
            }
                
        }
    }
}

 

분명 구조체는 3개밖에 없는데 코드는 엄청나게 길어졌다. 심지어 로직도 똑같은데 반복되는 비효율적인 코드이다.

그럼 어떻게 코드의 길이를 줄여볼까?

 

혹시 연산 프로퍼티를 계산하는 부분만 따로 모듈화를 진행할 수 없을까?

이 물음에서 나온것이 swift 5.1에서 출시된 property wrapper이다. 


Property Wrapper 사용하기

앞에서 말했듯이 property wrapper는 연산 프로퍼티의 기능을 개별 클래스와 구조체로 분리할 수 있고, 재사용이 가능하다는 점에서 효율을 제공한다.

@propertyWrapper 를 사용하여 정의한다.

 

위 코드에서 보여진 연산 프로퍼티를 property wrapper를 사용하여 모듈화를 진행하자.

이때 사용되는 지시자가 @propertyWrapper 이다.

 

@propertyWrapper
struct isValid<V: Comparable>
{
    var value: V
    var min: V
    var max: V
    
    init(wrappedValue: V, min: V, max: V) {
        self.value = wrappedValue
        self.min = min
        self.max = max
    }
    
    var wrappedValue: V {
        get {
            return value
        }
        set {
            if (value > self.max) {
                value = self.max
            }
            else if (value < self.min) {
                value = self.min
            }
            else {
                value = newValue
            }
        }
    }
}

 

위에서 사용된 구조체에서 저장되는 값의 타입은 Int, Double, String 으로 다양하다.

이럴때는 generic 을 사용한다.

연산 프로퍼티는 기본적으로 값의 비교, 연산을 하는 것이 목적이다. 즉 들어올 수 있는 값은 Comparable 프로토콜을 따르는 모든 데이터 타입을 가질 수 있도록 데이터 타입을 generic으로 선언해둔다.

 

property wrapper는 값을 검사하는 수문장 역할을 하는 wrappedValue property가 있어야 하며, 이름이 변경되어서는 안된다. 이는 init() 초기화 함수에서도 마찬가지로, 파리미터에 wrappedValue를 쓸 때 이름이 변경되어서는 안된다.

 

이렇게 선언한 property wrapper는 다음과 같이 property 앞에 @propertywrappername 을 붙여서 사용한다.

 

struct A
{
    @isValid(wrappedValue: 150, min: 100, max: 200) var value: Int
}

struct B
{
    @isValid(wrappedValue: 145.6, min: 100.0, max: 200.0) var value: Double
}

struct C
{
    @isValid(wrappedValue: "a", min: "a", max: "d") var value: String
}

 

그렇다면 값을 변경할 때는 어떻게 작동할까?

다음과 같이 구조체를 할당하고 value 의 값을 범위를 벗어나게 바꿔보았다. 이때 값을 변경하기 때문에 var로 구조체를 선언한다.

 

var a = A()
a.value = 19392342
print(a.value)

var b = B()
b.value = 124935.2342
print(b.value)

var c = C()
c.value = "zzzz"
print(c.value)

 

결과는 당연히 다음과 같다.

 

 

연산 프로퍼티가 하던 계산을 wrapped property로 사용하는 구조체에서 하고 이를 저장한다.