Swift 5.1 에서 추가된 Property Wrapper의 Apple Docs 번역글입니다. 제 블로그에서 Swift 번역글이 스테디하게 인기가 좋길래 새로운 피처가 추가된 기념으로 한번 해보았습니다 :) 제 번역 포스트는 원문 그대로의 직역이 아닌 이해를 돕기 위한 의역, 부연설명이 다소 많이 되어 있으니 참고해주세요. 오역 발견 시 꼭 제보해주시면 감사하겠습니다.
* 이 문서는 Swift에서의 기본 Property 동작에 대한 이해가 선행되어야 이해하실 수 있습니다.
Property Wrappers
Property wrapper는 프로퍼티가 저장되는 방식을 관리하는 코드와 프로퍼티를 정의하는 코드 사이에 분리 계층을 추가합니다. Property wrapper는 어떨 때 사용할까요? 예를 들어보겠습니다. 개발을 하다보면 Thread-safe한 프로퍼티 또는 값을 DB에 저장하는 프로퍼티가 여러 개 필요한 경우가 있을 것입니다. 지금까지는 이럴 때 Thread-safe 처리, DB 커넥션 처리 등을 위한 코드를 각각의 프로퍼티에 모두 작성해주어야 했습니다. Property wrapper를 사용하게 되면 이런 코드(값 저장을 관리하는 코드)들을 Property wrapper를 정의하는 부분에서 최초 한 번만 작성하면 됩니다. 그 후 필요한 프로퍼티들에서 이 wrapper를 가져다 적용함으로써 하나의 코드를 재사용할 수 있게 됩니다.
Property wrapper를 정의하려면 wrappedValue 프로퍼티가 정의되어 있는 구조체, ENUM, 클래스 등을 만드세요. 예제로 Property wrapper를 하나 만들어 보겠습니다. 아래 코드에서 TwelveOrLess 구조체는 그 안에서 감싸고 있는 값(value it wraps. 이것이 즉 wrappedValue 입니다.)이 언제나 12 이하임을 보장해줍니다. 12보다 큰 값을 저장하려고 시도할 경우에는 12를 저장합니다.
Setter는 새로 들어온 값이 12 이하가 되는 것을 보장하며 값을 저장합니다. Getter는 저장된 값을 그대로 리턴합니다.
NOTE
위 예제에서의 number는 private 변수로 선언되어 있습니다. 따라서 number는 TwelveOrLess의 내부에서만 사용된다는 것이 보장됩니다. TwelveOrLess 외부에서는 number를 바로 사용할 수 없고, wrappedValue의 getter/setter를 사용해 이 값에 접근할 수 있습니다. private에 대한 더 많은 정보는 Access Control에서 보실 수 있습니다.
프로퍼티에 wrapper를 적용하려면 프로퍼티의 이름 앞에 wrapper명을 attribute로 입력하세요. 여기에 작은 직사각형을 가지는 구조체가 있습니다. “작은” 이라는 기준을 정해두기 위해서 TwelveOrLess property wrapper를 사용했습니다. (12 이하의 변 길이를 가진다면 작은 직사각형이라고 정의한 것입니다.)
height와 width 프로퍼티에 따로 초기값을 설정하지 않아도 이 프로퍼티들은 초기값으로 0을 가지고 있습니다. TwelveOrLess의 number가 0을 초기값으로 가지고 있기 때문입니다. rectangle.height를 10으로 셋팅했을 때는 12이하의 작은 수이기 때문에 그대로 10이 셋팅됩니다. 그러나 24로 셋팅하려 할 경우에는 TwelveOrLess의 setter 안에서 min(newValue, 12)이 적용되기 때문에 실제로는 12가 셋팅됩니다.
프로퍼티에 wrapper를 적용하면 컴파일러는 wrapper를 저장하는 코드와 wrapper를 통해 프로퍼티에 접근하는 코드를 자동으로 synthesize 해줍니다. (실제로 래핑된 값을 저장하는 것은 Prperty wrapper에서 이미 담당하고 있기 때문에 이 값을 synthesize 하는 코드는 필요없습니다.) Attribute syntax(@wrapper명을 프로퍼티 선언 시에 붙이는 것) 외에도 property wrapper의 동작을 사용하는 방법은 존재합니다. 예를 들면, 조금 전 예제로 보았던 SmallRectangle의 내부 프로퍼티들에 @TwelveOrLess attribute를 붙이는 대신, 이 내부 프로퍼티들로 TwelveOrLess 구조체를 명시적으로 사용할 수도 있습니다.
_height와 _width 프로퍼티는 각각 property wrapper TwlveOrLess의 인스턴스입니다. height와 width의 getter/setter 안에서는 wrappedValue 프로퍼티에 접근하고 있습니다.
Setting Initial Values for Wrapped Properties
위 예제에서는 래핑된 프로퍼티의 초기값이 TwelveOrLess에 정의된 number의 초기값으로 셋팅되었습니다. 이 Property wrapper(TwelveOrLess)를 사용하는 코드에서는 따로 초기값을 셋팅할 수 없습니다. 예를 들어 SmallRectangle이 정의되는 시점에서는 height나 width의 초기값을 따로 줄 수 없습니다. 초기값(혹은 그 외의 다른 커스터마이징)을 설정하기 위해서는 property wrapper에 이니셜라이저를 추가해야 합니다. TwelveOrLess를 확장한 버전인 SmallNumber를 봅시다. wrappedValue(초기값)와 max값을 설정할 수 있는 이니셜라이저가 정의되어있습니다.
SmallNumber에는 세 개의 이니셜라이저가 정의되어있습니다. init(), init(wrappedValue:), 그리고 init(wrappedValue:maximum:). 아래 예제에서는 이것들을 사용하여 wrappedValue(초기값)와 max값을 셋팅하고 있습니다. 이니셜라이저에 대한 자세한 설명은 Initialization을 참조하세요.
프로퍼티에 이 SmallNumber wrapper를 적용할 때는 초기값을 따로 명시하지 않아도, 알아서 SmallNumber의 init() 이니셜라이저를 통해 wrapper가 setup됩니다. 예시를 보겠습니다.
SmallNumber()의 호출을 통해 SmallNumber(실제 height와 width값을 감싸고 있는 구조체)의 인스턴스가 생성됩니다. 이니셜라이저의 내부에서는 wrappedValue와 max값의 초기값이 각각 셋팅됩니다. 이 경우에는 init() 안에서 0과 12로 셋팅됩니다. 이 SmallNumber가 기존의 TwelveOrLess와 다른 점은 초기값을 프로퍼티의 선언부에서 셋팅할 수 있도록 해준다는 것입니다. (위 예제에서는 초기값을 따로 명시하지 않아도 기존의 TwelveOrLess처럼 초기값을 알아서 제공해준다는 것을 보여주고 있습니다.)
프로퍼티의 초기값(wrappedValue)을 따로 명시하는 경우에는 init(wrappedValue:) 이니셜라이저가 사용됩니다. 이것도 예제를 보겠습니다.
프로퍼티 선언부에 작성된 = 1 에 따라 init(wrappedValue:) 이니셜라이저가 호출되게 됩니다. SmallNumber의 인스턴스들(실제 height, width 값을 감싸고 있는)이 SmallNumber(wrappedValue: 1)의 호출을 통해 생성된다는 뜻입니다. 이 이니셜라이저는 넘겨받은 값을 wrappedValue로 사용하고, 12를 default max값으로 사용합니다.
프로퍼티 선언 시 커스텀 attribute 뒤에 괄호를 열어 인자들을 넘기면 이 인자들에 부합하는 이니셜라이저로 wrapper가 생성됩니다. 예를 들어, SmallNumber 프로퍼티를 선언할 때 초기값(wrappedValue)과 max값을 전부 제공한다면 init(wrappedValue:maximum:) 이니셜라이저가 사용됩니다.
height를 감싸는 SmallNumber의 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 5)를 통해 생성됩니다. width를 감싸는 인스턴스는 SmallNumber(wrappedValue: 3, maximum: 4)를 통해 생성됩니다.
이처럼 property wrapper에 인자를 포함시키면 인스턴스 생성시점에서 wrapper의 초기상태를 설정하거나, 별도의 옵션을 넘길 수도 있습니다. 이 문법은 property wrapper를 사용하는 가장 제너럴한 방법입니다. 필요하면 어떤 인자라도 attribute에 넘길 수 있으며, 이 인자는 이니셜라이저로 전달됩니다. (물론 이것을 처리할 수 있는 이니셜라이저를 만들어두어야하겠지요?)
Property wrapper에 인자를 포함하면서, 동시에 초기값을 할당하는 것도 가능합니다. 초기값 할당은 곧 wrappedValue 인자로 취급되고, 이것을 적용할 수 있는 이니셜라이저가 사용됩니다. 예제를 보겠습니다.
위 예제에서 height를 감싸는 SmallNumber의 인스턴스는 SmallNumber(wrappedValue: 1)를 통해 생성됩니다. width를 감싸는 인스턴스는 SmallNumber(wrappedValue: 2, maximum: 9)를 통해 생성됩니다.
Projecting a Value From a Property Wrapper
Property wrapper에는 wrappedValue 외에도 projected 값(ProjectedValue)을 정의하여 추가적인 기능을 제공할 수 있습니다. 예를 들어, 데이터베이스에 대한 접근을 관리하는 property wrapper는 flushDatabaseConnection() 메소드를 projectedValue에 노출시킬 수 있습니다. projectedValue에 접근하기 위해서는 프로퍼티 이름 앞에 달러 기호($)를 붙입니다(그냥 접근하면 wrappedValue에 접근, $기호를 붙이면 projectedValue에 접근하게 됩니다.). 개발자가 직접 작성하는 코드에서는 $로 시작하는 프로퍼티를 정의할 수 없기 때문에, 이 값은 다른 프로퍼티와 간섭될 여지(이름 중복 등)가 없습니다.
위의 SmallNumber 예제에서는 프로퍼티에 너무 큰 수를 할당하려 시도할 경우 property wrapper가 자체적으로 조정을 거쳐서 값을 저장했습니다(max값보다 클 경우 max값을 셋팅). 이렇게 새로운 값을 저장하기 전에 값의 조정이 일어났는지의 여부를 기록하기 위해 SmallNumber 구조체에 projectedValue 프로퍼티를 추가해보겠습니다.
s.$someNumber는 wrapper의 projectedValue에 접근합니다. 작은 수(4)를 저장하면 값의 조정이 이루어지지 않기 때문에 s.$someNumber는 false가 됩니다. 그러나 큰 수(55)를 저장하려고 시도했을 때는 값의 조정이 이루어지기 때문에 true가 됩니다.
projectedValue는 어떤 타입이라도 될 수 있습니다. 예제에서는 오직 하나의 정보(수의 조정이 이루어졌는지의 여부)만을 노출하면 되었으니 Bool 타입을 projectedValue로 사용했습니다. 더 많은 정보를 노출하고 싶다면 다른 데이터 타입을 사용할 수 있으며, self를 사용할 수도 있습니다. 이 경우에는 wrapper 인스턴스 자체가 리턴됩니다.
프로퍼티 getter 또는 인스턴스 메소드같이 타입을 정의하는 코드 안에서는 projectedValue에 접근할 때 self. 를 생략할 수 있습니다(self.프로퍼티명 대신 프로퍼티명만 써도 된다는 뜻입니다. 일반적인 프로퍼티 접근방식과 동일한 부분입니다.). 아래 예제 코드에서 이 부분을 확인해보세요.
Property wrapper는 프로퍼티의 getter/setter를 위한 쉽고 간편한 문법입니다. 그러니 위 예제에서의 height와 width에 접근하는 부분 역시 다른 프로퍼티들과 다를 바 없이 동작합니다. 예를 들어 resize(to:) 안에서의 height/width에 대한 접근은 property wrapper를 통해 이루어집니다. resize(to: .large)를 호출하면 switch case 중 .large를 거쳐 사각형의 height와 width를 100으로 셋팅하려 시도합니다. 이때 wrapper는 12보다 큰 수의 셋팅을 막기 때문에 height와 width는 각각 12로 셋팅되고 projectedValue는 true가 됩니다. resize(to:)의 마지막 라인에서는 $height와 $width를 체크하여 property wrapper가 둘 중 하나라도 조정했는지의 여부를 리턴하고 있습니다.
이번 가이드의 요약
- 프로퍼티의 getter/setter에 들어갈 코드가 여러 프로퍼티에서 재사용되는 경우, 그것을 Property wrapper로 정의해놓으면 쉽게 그 동작을 끌어다 쓸수 있다. (프로퍼티의 저장부-getter/setter 동작이 정의된 코드-와 정의부-실제로 프로퍼티를 사용하는 코드-를 분리하고 싶을때도 유용)
- Property Wrapper를 정의하는 방법: 구조체, ENUM, 클래스 등을 만들고 @propertyWrapper라고 명시해주기. 실제 저장값은 wrappedValue로 정의, 추가로 제공하고 싶은 정보가 있다면 projectedValue를 정의할 것
- Property Wrapper를 밖에서 가져다쓰는 방법: 나의 프로퍼티를 정의하고 @프로퍼티래퍼명으로 attribute를 붙인다. 그러면 이 프로퍼티는 get/set동작이 이루어질때마다 알아서 Property wapper의 getter/setter 코드를 통해 실행됨
'Swift 공식 가이드 > Swift 5.1' 카테고리의 다른 글
Opaque Types (3) | 2019.12.22 |
---|