Swift 프로퍼티 래퍼

프로퍼티 래퍼는 스위프트 5.1부터 나온 개념으로, 클래스와 구조체 구현부에 게터(getter), 세터(setter), 연산 프로퍼티(computed property) 코드의 중복을 줄이는 방법을 제공한다.

 

프로퍼티 래퍼란?


클래스나 구조체 인스턴스에 값을 할당하거나 접근할 때 값을 저장하거나 읽어내기 전에 변환 작업이나 유효성 검사를 해야 할 경우가 존재한다. 물론 연산 프로퍼티로도 위와 같은 작업은 할 수 있다. 그러나 여러 클래스나 구조체에 생성한 연산 프로퍼티들이 유사한 패턴을 갖는 경우가 빈번하게 발생한다. 

 

클래스나 구조체의 구현부마다 비슷한 역할을 하는 연산 프로퍼티를 복사 붙여넣기 할 수도 있다. 이것은 생산성이 매우 떨어질 뿐만 아니라, 계산 방법이 수정되는 일이 생기면 각각의 클래스나 구조체에 복사해둔 연산 프로퍼티를 일일이 찾아 직접 수정해야 했다.

 

아래 예제를 보자

 

struct User {
    private var _name : String = ""
    
    var name : String {
        get {
            _name
        }
        set {
            _name = newValue.uppercased()
        }
    }
}

 

위 User 구조체는 유저 이름 프로퍼티를 가진다. 그러나 이름을 어떻게 입력하든 항상 대문자로 저장하고 싶은 경우가 있다. 이 때 연산 프로퍼티를 사용해서, 새로운 값을 set 하기 전에 모두 대문자로 변환하는 작업을 거칠 수 있다.

 

struct User {
    private var _name : String = ""
    var name : String {
        get {
            _name
        }
        set {
            _name = newValue.uppercased()
        }
    }
    
}

var user = User()
user.name = "Park Jong Ho"
print(user.name)

위의 코드를 실행하면, "PARK JONG HO" 가 출력되는 것을 알 수 있다.

 

프로퍼티 래퍼 구현하기


연산 프로퍼티로도 충분히 동일한 기능을 구현할 수 있지만, 코드의 재사용성을 고려해볼 때 그리 현명한 방법은 아니다. 프로퍼티 래퍼를 사용하면, 재사용이 가능한 코드를 만들 수 있다.

 

동일한 로직을 하는 프로퍼티 래퍼를 구현해보자

 

@propertyWrapper
struct FixCase{
    private(set) var value : String = ""
    var wrappedValue : String {
        get {
            value
        }
        set {
            value = newValue.uppercased()
        }
    }
    
    init(wrappedValue initialValue: String ){
        self.wrappedValue = initialValue
    }
}

 

  1. 프로퍼티 래퍼는 @propertyWrapper 지시자를 사용해 선언된다.
  2. 프로퍼티 래퍼는 클래스나 구조체 안에 구현된다. 
  3. 모든 프로퍼티 래퍼는 값을 변경하거나 유효성을 검사하는 게터와 세터 코드가 포함된 wrappedValue 프로퍼티를 가져야 한다.
  4. 초기화 메소드는 선택사항으로 포함될 수 있다.

 

이제 프로퍼티 래퍼를 선언했으니 이 프로퍼티 래퍼를 사용해보자

 

struct User {
    @FixCase var name : String = ""
}

var user = User()
user.name = "Park Jong Ho"
print(user.name)

//출력결과
PARK JONG HO

 

위와 같이 @{프로퍼티 래퍼 이름} 지시자를 사용한 프로퍼티를 선언함으로써, 해당 프로퍼티 래퍼를 사용할 수 있다.

 

여러 변수와 타입 지원하기


어떤 작업을 수행할 때 사용될 여러 값을 받도록 더 복잡한 프로퍼티 래퍼를 구현할 수도 있다. 추가되는 값들은 프로퍼티 래퍼 이름 다음의 괄호 안에 둔다.

 

예를 들어 출생연도를 나타내는 프로퍼티를 생각해보자. 이 글을 쓰는 시점이 2022년이므로, 2022년 보다 큰 값은 출생연도로 들어올 수 없다.

 

따라서 출생연도를 1900 ~ 2022 에 해당하는 값만 받도록 프로퍼티 래퍼를 구현해보자

 

@propertyWrapper
class MinMaxValue {
    let min : Int
    let max : Int
    private(set) var value : Int
    var wrappedValue : Int {
        get {
            value
        }
        set {
            if newValue >= min && newValue <= max {
                value = newValue
            } else if newValue > max {
                value = max
            } else {
                value = min
            }
        }
    }
    
    init(wrappedValue initialValue : Int,min : Int,max : Int){
        value = initialValue
        self.min = min
        self.max = max
        
    }
}

 

위 프로퍼티 래퍼는 wrappedValue 말고도 추가적으로 min 과 max 라는 프로퍼티를 가지고 있다. 그리고 이 프로퍼티 범위 안에 있는 값만 프로퍼티에 할당한다.

 

struct User {
    @MinMaxValue(min: 1900, max: 2022) var birthYear : Int = 1006
}

이제 1900 ~ 2022 까지의 값만 받도록 프로퍼티 래퍼로 감싸 프로퍼티를 선언할 수 있다.

 

var user = User()
user.birthYear = 3000
print(user.birthYear)

//출력결과
2022

birthYear 프로퍼티에 3000이란 값을 할당해도 setter 에 의해 2022 라는 max 값으로 할당되는 것을 확인할 수 있다.

 

제네릭을 사용한 프로퍼티 래퍼 구현


위의 프로퍼티 래퍼는 오로지 Int 타입만을 지원한다. 하지만 코드의 재사용성을 극대화하기 위해서 제네릭 타입을 사용해서 구현할 수 있다.  Comparable 프로토콜을 따르는 모든 데이터 타입은 비교 연산이 가능하므로, Comparable 프로토콜을 구현하는 타입을 제네릭으로 받도록 프로퍼티 래퍼를 수정할 수 있다.

 

@propertyWrapper
class MinMaxValue<V:Comparable> {
    let min : V
    let max : V
    private(set) var value : V
    var wrappedValue : V {
        get {
            value
        }
        set {
            if newValue >= min && newValue <= max {
                value = newValue
            } else if newValue > max {
                value = max
            } else {
                value = min
            }
        }
    }
    
    init(wrappedValue initialValue : V,min : V,max : V){
        value = initialValue
        self.min = min
        self.max = max
        
    }
}

 

위 프로퍼티 래퍼는 Comparable 프로토콜을 구현하는 모든 타입을 지원하므로, Date 나 Char 도 사용할 수 있다.

 

 

'Swift' 카테고리의 다른 글

Swift 에러 핸들링  (0) 2022.02.11
Swift 딕셔너리  (0) 2022.02.10
구조체와 클래스  (0) 2022.02.10
Swift 함수 - 2  (0) 2022.02.09
Swift 5 - 함수 1  (0) 2022.02.09