# Modern Swift API Design ## 오늘 배우는 것 ![](https://hackmd.io/_uploads/SkfHhoB83.png) - API 잘 설계하는 법 - Swift 5.1의 새로운 기능들 ### 🐣 이름을 잘 짓자 - 문맥과 목적에 맞게 명확한 이름 짓기 - Swift는 명확성을 추구하기 때문에, Swift만의 라이브러리는 prefix를 가지지않는다. --- ## 1. Values and references - Classes (Reference Types) ![](https://hackmd.io/_uploads/rkgTnjHL3.png) - **참조**를 통해 값을 변경하면 두 변수가 참조하는 동일한 객체가 변경되기 때문에 동시에 변경된다. - Structs and Enums (Value Types) ![](https://hackmd.io/_uploads/HJ6UajS8n.png) - API에서 **값 타입**을 사용하면 **명확성 측면**에서 많은 이점이 있다. - **복사**를 통해 값을 변경하면 사본인 하나의 객체만 변경되기 때문에 이 객체가 어디서 왔는지, 어떤 참조를 가졌는지에 대해 걱정할 필요가 없다. ### ☑️ 값 타입 vs 참조 타입? ![](https://hackmd.io/_uploads/HJAG0sSLn.png) - 클래스를 사용할 타당한 이유가 없다면 구조체 사용하기. - 그치만 클래스를 사용해야 하는 경우? - 참조 타입이 중요할 때 - reference counting을 통해 리소스를 관리해야 할 때 - **값을 중앙에서 가지고 있고, 이를 공유해야 할 때** - type이 identity를 가져야할때는 class를 사용한다. 🤔 - struct의 equality와는 다른 것이다. ### 🐣 구조체 안에 참조타입이 있는 경우 >예시 ![](https://hackmd.io/_uploads/Sku87hSLn.png) ![](https://hackmd.io/_uploads/S1To73SI2.png) 여기서는 Material 구조체 내부에 texture라는 참조타입의 변수를 가지고 있다. ![](https://hackmd.io/_uploads/Bkx1NnS8h.png) - 결국 Material을 볼 때, 구조체는 값 타입이라 복사가 되지만, 내부에 texture는 동일한 객체를 참조하고 있기 때문에 궁극적으로 참조타입도 값타입도 아닌, 모호한 상태가 되어버린다. - 이를 해결하기 위해서 어떻게 해야할까? ### 🐣 해결 방법 #### 1️⃣ 참조 타입을 방어적으로 복사한다. ![](https://hackmd.io/_uploads/rJ4ENnrIh.png) ![](https://hackmd.io/_uploads/rkerN3rI3.png) - texture 변수를 public -> private으로 변경 ![](https://hackmd.io/_uploads/r1ldNnBIn.png) - 그다음 연산프로퍼티로 구현한다. - 하지만 이 방법은 texture을 get한 다음 texture 내부의 특정 프로퍼티를 변경하게 되면 결국 공유되는 것이기 때문에 완벽한 해결법이 아니다. #### 2️⃣ isKnownUniquelyReferenced 메소드 사용하기 ![](https://hackmd.io/_uploads/Syrkr3BI2.png) - 이미 copy가 되었는지 아닌지 확인 - 객체가 하나의 참조만 가지고 있을 때 true 반환, 아닐 시 false 반환 - 이 메서드를 사용하면 불필요하게 항상 전체 복사를 하지 않아도 된다. --- ### 2. Protocols and Generics ☑️ 서로 다른 타입간에 어떤 코드를 공유해야 할 때, 항상 클래스로 이를 구현해야 하는 것은 아니다. => **구조체에 프로토콜을 채택**하는 방식을 사용할 수 있다. ![](https://hackmd.io/_uploads/HyYOT3H83.png) - 프로토콜로 시작하지 마라. - 구체적인 use case로 시작해라. ➡️ 구체적인 비즈니스 로직을 생각해보라는 뜻인듯 - 이미 존재하는 Protocol로 해결하려고 시도해봐라. - Protocol 대신 Generic을 고려해봐라. >예시 ![](https://hackmd.io/_uploads/SySMAhBLh.png) - 기하학적 벡터에 대한 작업을 하는 프로토콜이다. - 내적이나 두 벡터 사이의 거리와 같이 내가 정의하고 싶은 연산을 줄 수 있다. ![](https://hackmd.io/_uploads/B1FiRnBI2.png) - 이 프로토콜에 Swift 5.1에 나온 SIMD 타입을 준수해줄 것이다. ![](https://hackmd.io/_uploads/H1fM1pSIn.png) - 그 다음 protocol-extension을 통해 기본 구현을 해준다. ![](https://hackmd.io/_uploads/Hk33JaHL3.png) 그런데 이렇게 구조체(SIMD2, SIMD3..)마다 프로토콜을 채택하면 번거롭다. protocol 없이 extension하는 게 컴파일러가 처리하는 데에 더 쉽다. 따라서 아래와 같이 프로토콜 자체가 아닌, 프로토콜을 채택하고 있는 SIMD 프로토콜 타입을 extension 해주는 것이 좋다. ![](https://hackmd.io/_uploads/rktyxaHL2.png) >장점 - 단순한 접근 방식 - 프로토콜 수를 줄임으로써 컴파일 시간을 줄일 수 있다. - ☑️ 그러나 확장성 문제가 발생함. - GeometricVector는 SIMD라고 할 수 있나? ❌ - 즉, is-a 관계가 성립되지 않는다. 사용하기 쉬운 API를 설계하는 경우 is-a 관계 대신 **has-a **관계로 구현하는 것도 고려해볼 수 있다. ➡️ 즉, 구조체 안에 SIMD 타입의 값을 래핑하는 것이다. SIMD를 has 하게 되는 것. ![](https://hackmd.io/_uploads/rkPY6bU82.png) 이렇게 Storge가 SIMD 프로토콜을 따르도록 구현해주면 된다. >바뀐 점 - 원래 프로토콜이었던 GeometricVector가 구조체가 됐다. - SIMD 타입을 채택하고 있는 것이 아니라 가지고 있다. ![](https://hackmd.io/_uploads/SkX-gzUU2.png) - 이부분 이해가 잘 안감 ### SIMD 구조 ![](https://hackmd.io/_uploads/SyKceGLUn.png) ![](https://hackmd.io/_uploads/rJrRlMII3.png) - SIMD3에서만 사용할 수 있는 기능인 X 연산 기능을 위와같이 정의할 수 있다. ![](https://hackmd.io/_uploads/BJg-c-GLU3.png) 이렇게 정의할 수도 있다. 그러나 위의 코드는 x,y,z 좌표를 얻기 위해 value에 계속 간접 접근을 해야하기 때문에 코드가 지저분해보인다는 단점이 있다. ➡️ 이 문제를 해결하기 위한 새로운 기능인 **Key Path Member Lookup** 가 나왔다. ### Key Path Member Lookup - Swift 5.1부터 생겼다. - 연산 프로퍼티의 선언 횟수를 줄일 수 있다. - GeometricVector에 있는 모든 속성을 한번에 노출할 수 있다. ![](https://hackmd.io/_uploads/HyJqQzU82.png) 사용하는 방법은 1. 타입 앞에 @dynamicMemberLookup을 붙인다. 2. subscript 어쩌구를 작성한다. - Storage에 대한 키 경로를 가져와서 Scalar를 반환하도록 하려고 한다. 그런 다음 해당 키 경로를 사용하여 value 타입으로 값을 전달하고 x,y,z등등 속성을 검색하여 반환한다. 3. 그러면 이제 자동으로 GeometricVector가 SIMD가 가진 모든 속성을 가져올 수 있게 된다. (x,y,z) > Key Path Member Lookup 적용 ![](https://hackmd.io/_uploads/SkwbSG8Un.png) - value에 접근하지 않아도 x,y,z를 자동으로 가져와주기 때문에 기존에 비해 이렇게 간단해졌다. ### 🐣 Property wrappers > 기존 코드 ![](https://hackmd.io/_uploads/ryLlpf8U2.png) > 개선한 코드 ![](https://hackmd.io/_uploads/B1h-6GLU3.png) - 긴 코드, 짧은 코드 둘이 같은 기능을 함. (가독성 차이...!) - 이 코드는 생각보다 일반적인(흔한) 문제이다. - 그래서 lazy 등의 프로퍼티 초기화, 접근 규칙을 언어 수준에서가 아닌, 코드 수준으로 높여야겠다고 생각하여 만든 것이 **Property Wrapper**라고 함. > @LateInitialized ![](https://hackmd.io/_uploads/HyJJCz883.png) > 내부 코드 ![](https://hackmd.io/_uploads/HkKVRf882.png) - lazy 키워드와 매우 유사하다. - 하지만 복잡한 코드보다 훨씬 읽기 쉽고 추론이 쉽다. - value 프로퍼티가 역할을 해준다. ![](https://hackmd.io/_uploads/r1LJmMDUn.png) 이렇게 사용한다면, 컴파일러는 아래와 같이 두개의 프로퍼티로 해석한다. ![](https://hackmd.io/_uploads/SJlRl7zw8n.png) - LateInitialized으로 초기화하는 $text 저장 프로퍼티와, 그 안에 구현된 value에 접근하는 연산프로퍼티로 나누어진다. ### SwiftUI의 Property Wrapper > Property Wrapper로 사용되는 @Binding, @State ![](https://hackmd.io/_uploads/HJijSGDI2.png) >@Binding 내부 ![](https://hackmd.io/_uploads/rkoIIGv8h.png) - Dynamic Member Lookup도 구현한 상태이다. - Keypath를 받아 새로운 Binding을 반환한다. (내가 지정한 모델 내부 프로퍼티인 title에 접근할 수 있게 됨) - $를 붙이면 Binding 타입의 인스턴스가 된다. ### 요약 ![](https://hackmd.io/_uploads/SJj4FMD8n.png) > 배운 것 - 값 의미론, 참조 의미론을 사용하는 시기와 함께 작동하는 법 - 코드의 재사용을 위해 프로토콜을 사용하자. - 프로퍼티 래퍼 내부에서 연산 프로퍼티를 사용하고 있는 것