제목: Ultimate Guide to JSON Parsing With Swift 4

스위프트4와 Foundation은 마침내 스위프트에서 JSON을 파싱하는 질문에대한 대답을 내놓았다.

파싱을 위한 훌륭한 라이브러리들이 많이 있었지만, 이렇게 적용하기 쉬울뿐 아니라 복잡한 시나리오에따라 커스터마이징이 가능한 솔루션은 꽤나 신선하다.

여기서 말하는 모든 것들을 어떤 Encode/Decoder 구현에 적용해보는 것은 의미없는 일이며, PropertyListEncoder도 마찬가지이다. 여러분이 XML같은 다른게 필요하면 커스텀 구현을 만들수도 있다. 이 블로그 포스트에서는 JSON 파싱에대해 초점을 맞추게 될것인데, 이것이 가장 많은 iOS 개발자와 연관되었기 때문이다.

기본
여러분의 JSON 구조와 오브젝트가 비슷한 구조를 가진다면 일은 쉽게 풀린다.

아래에는 맥주에대한 JSON 문서 예시이다.
{
   "name": "Endeavor",
   "abv": 8.9,
   "brewery": "Saint Arnold",
   "style": "ipa"
}
우리의 스위프트 자료구조는 이렇게 생길 수 있다.
enum BeerStyle : String {
   case ipa
   case stout
   case kolsch
   // ...

}


struct Beer {
   let name: String
   let brewery: String
   let style: BeerStyle
}
이 JSON 문자열을 Beer 인스턴스로 변환하기위해서, 우리는 타입에다 Codable을 넣을 것이다.

Codable은 사실 Encodable & Decoable로 구성된 유니온 타입(union type)으로, 만약 한방향으로 변환하는 기능만 원하면 적절한 프로토콜을 적용시키면 된다. 이것이 스위프트4의 새로운 기능이다.

Codable은 디폴트 구현이 따라오는데, 대부분의 경우 여러분은 그냥 이 프로토콜을 적용시키고 공짜로 유용한 디폴트 동작을 만끽하면 된다.
enum BeerStyle : String, Codable {
  // ...

}


struct Beer : Codable {
  // ...

}

다음으로 그냥 디코더를 만들어야한다.
let jsonData = jsonString.data(encoding: .utf8)!


let decoder = JSONDecoder()

let beer = try! decoder.decode(Beer.self, for: jsonData)
이게 다다!  우리의 JSON 문서를 beer 인스턴스에 파싱하였다. 키의 이름과 타입이 서로 일치하기 때문에 다른 커스터마이징이 필요없었다.

여기서 try!를 사용하는 것은 의미가 없겠지만, 여러분의 앱에서는 똑똑하게 에러를 캐치하여 다뤄줘야한다. 나중에 에러 핸들링에대해 더 다뤄볼 것이다...

이제 우리가 인위적으로 만든 예제에서는 완벽하게 정돈되었다. 그러나 만약 타입이 일치하지 않는다면 어떨까?

키 이름을 커스터마이징하기
API에서 키 이름을 snake-case로 하는 경우는 종종 있고, 이 스타일은 스위프트 속성에대한 네이밍 가이드라인과는 맞지 않는다.

이 부분을 커스터마이징하려면 잠시 Codable 디폴트 구현을 맞춰줘야한다.

키는 컴파일러가 자동으로 생성한 "CodingKeys" 열거형에의해 처리된다. 이 열거형은 CodingKey를 따르는데, 이것은 인코딩된 양식으로 속성을 값에 연결할 수 있게 정의한다.

키를 커스터마이징하기위해 우리는 이것에대한 우리만의 구현을 작성할 것이다. 이 경우, 스위프트 네이밍과는 다르며, 우리는 키에대한 문자열 값을 제공할 수 있다.
struct Beer : Codable {
     // ...

     enum CodingKeys : String, CodingKey {
         case name
         case abv = "alcohol_by_volume"
         case brewery = "brewery_name"
         case style
   }
}
만약 beer 인스턴스를 받아서 JSON으로 인코딩하려하면, 새롱누 포맷으로 동작하는 것을 볼 수 있다.
let encoder = JSONEncoder()

let data = try! encoder.encode(beer)
print(String(data: data, encoding: .utf8)!)
이것은 아웃풋이다.
{"style":"ipa","name":"Endeavor","alcohol_by_volume":8.8999996185302734,"brewery_name":"Saint Arnold"}
여기서 표현된 양식은 매우 인간-친화적이지않다. 더 보기좋게 만들기위해 outputFormatting 프로퍼티로 JSONEncoder 아웃풋 포맷을 커스터마이징 할 수 있다.

디폴트값은 .compact인데, 이것은 위의 결과처럼 만들어준다. 이것을 .prettyPintted로 바꿔서 더 가독성좋은 아웃풋으로 만들 수 있다.
encoder.outputFormatting = .prettyPrinted

{
  "style" : "ipa",
  "name" : "Endeavor",
  "alcohol_by_volume" : 8.8999996185302734,
  "brewery_name" : "Saint Arnold"
}
JSONEncoderJSONDecoder 둘 다 그 동작을 커스터마이징하기위한 더 많은 옵션들이 있다. 더 일반적으로 해야하는 것중 하나는 날짜 파싱을 어떻게 커스터마이징할것인지이다.

날짜 다루기
JSON은 날짜를 표현하는데 데이터 타입이 없으므로 클라이언트와 서버가 동의한 방법대로 표현하여 시리얼라이즈하게된다. 일반적으로 ISO 8601 날짜 포멧팅 방식으로 처리하며 문자열로 시리얼라이즈한다.
프로 팁: ISO 8601을 포함한 다양한 포멧으로 변환해보려면 nsdateformatter.com에서 해볼 수 있다.
다른 포멧들은 아마 레퍼런스 날짜로부터 샌 초단위(혹은 밀리초단위)로, JSON 문서에서 Number로 시리얼라이즈 될 수 있다.

예전에는 이것을 우리 스스로 처리하여야 했었는데, 아마 우리 데이터 타입에 문자열 칸을 제공하고, 문자열 값에서 날짜를 marshal하기위해 DateFormatter를 사용한다. 그 반대도 마찬가지이다.

JSONEncoderJSONDecoder로 모두 해결할 수 있다. 한번 확이해보자. 날짜를 다루는 스타일로 디폴트로 .deferToDate를 사용할 것이다. 이것은 아래처럼 생겼다.
struct Foo : Encodable {
    let date: Date
}


let foo = Foo(date: Date())
try! encoder.encode(foo)

{
  "date" : 519751611.12542897

}
이것을 .iso8601 포멧으로 바꿀 수 있다.
encoder.dateEncodingStrategy = .iso8601

{
  "date" : "2017-06-21T15:29:32Z"
}
다른 JSON 인코딩 전략도 가능하다.
  • .formatted(DateFormatter): 지원하고자하는 표준 날짜 포멧 문자열이 없을때. 여러분의 date formatter 인스턴스를 제공한다.
  • .custom( (Date, encoder) throws -> Void ): 아주 커스텀하고 싶을때, 여기 블럭을 전달하여 제공된 인코더로 데이터를 인코딩 할 것이다.
  • .millisecondsSince1970.secondsSince1970: API에서 아주 일반적인 양식은 아니다. 이것은 인코딩된 표현에서 타임 지역 정보를 완전히 손실해버리기 때문에 별로 추천하지 않는다. 그런 이유로 누군가가 잘못된 가정을 생각하기 쉽게 만든다.
날짜를 디코딩하는 것은 필수적으로 같은 옵션을 가지지만, .custom에대해서는 .custom( (Decoder) throws -> Date )의 모양을 받는다. 따라서 우리는 디코더를 받고, 디코더 안에서 된 것이 무엇이든 그것에서 날짜로 만들 책임을 가진다.

Float 다루기
Float은 스위프트의 Float 타입과 JSON과는 꽤 맞지 않는 또다른 부분이다. 만약 서버가 유효하지 않은 "NaN"을 문자열로 보내면 어떻게 될까? 양수나 음수 Infinity는 어떨까? 이것들은 스위프트의 어떤 값에도 매칭되지 않는다.

디폴트로 구현된 것은 .throw이다. 디코더가 이 값들을 만나면 에러가 나타날 것이며, 우리가 처리하고 싶은대로 맵핑할 것을 제공할 수 있다.
{
  "a": "NaN",
  "b": "+Infinity",
  "c": "-Infinity"
}

struct Numbers : Decodable {
  let a: Float
  let b: Float
  let c: Float
}
decoder.nonConformingFloatDecodingStrategy =  .convertFromString(
     positiveInfinity: "+Infinity",
     negativeInfinity: "-Infinity",
     nan: "NaN"
)


let numbers = try! decoder.decode(Numbers.self, from: jsonData)dump(numbers)
이렇게 나타난다.
▿ __lldb_expr_71.Numbers

  -a: inf


  -b: -inf


  -c: nan
JSONEncodernonConformingFloatEncodingStrategy로 반대로도 할 수 있다.

주로 일어나는 경우의 그런것은 아니지만 어느날 유용하게 다가올 것이다.

데이터 다루기
가끔 base64로 인코딩된 문자열의 작은 비트의 데이터를 보내는 API를 마주칠 수도 있겠다.

이것을 자동으로 다루려면 이런 인코딩 전략중 하나를 JSONEncoder에 넣어준다.
  • .base64
  • .custom( (Data, Encoder) throws -> Void)
디코딩하기 위해서 JSONDecoder에 디코딩 전략을 넣어준다.
  • .base64
  • .custom( (Decoder) throws -> Data)
당연하게도 여기서는 .base64가 일반적인 선택일 될 것이지만, 커스텀화된 것이 필요하다면 블럭기반 전략에 사용하면 된다.

랩퍼 키(Wrapper Keys)
종종 API는 랩퍼 키를 포함할 것인데, 최상위 JSON 엔티티는 항상 오브젝트이다.

이런식으로 생겼을 것이다.
{
  "beers": [ {...} ]
}
스위프트에서 표현하기위해 우리는 이 응답에대해 새로운 타입을 만든다.
struct BeerList : Codable {
   let beers: [Beer]
}
실제로 이게 다다! 우리 키 이름이 일치하고 Beer이 이미 Codable이므로 그냥 동작한다.

루트 수준 배열
만약 API가 루트 엘리먼트로 배열을 반환하고 있다면 이런식으로 응답을 파싱한다.
let decoder = JSONDecoder()

let beers = try decoder.decode([Beer].self, from: data)
여기 타입으로 Array를 사용하고 있다는 점을 주목하자. Array<T>T가 디코딩가능할때만 디코딩 가능하다.

랩핑키 오브젝트(Object Wrapping Keys) 다루기
여기에는 여러분이 만날 수 있는 또다른 시나리오가 있다. 배열안에 각 오브젝트가 있는 한 배열 응답이 키로 랩핑되있다.
[
  {
   "beer" : {
     "id": "uuid12459078214",
     "name": "Endeavor",
     "abv": 8.9,
     "brewery": "Saint Arnold",
     "style": "ipa"
   }
  }
]
이 키를 붙잡아두기위해 위의 랩핑 타입 방법을 쓸 수도 있지만, 이 구조는 이미 강타입으로 디코딩가능하게 구현되있음을 인지하는 것이 더 쉬우 방법일 수 있다.

보이는가?
[[String: Beer]]
혹은 이런 경우가 더 읽기 쉬울지도 모르겠다.
Array<Dictionary<String, Beer>>
Array<T>이 디코딩가능한것처럼, KT가 둘 다 디코딩가능하면 Dictionary<K, T>도 그렇다.
let decoder = JSONDecoder()

let beers = try decoder.decode([[String:Beer]].self, from: data)
dump(beers)

▿ 1 element
  ▿ 1 key/value pair
    ▿ (2 elements)
      - key: "beer"
      ▿ value: __lldb_expr_37.Beer
        - name: "Endeavor"
        - brewery: "Saint Arnold"
        - abv: 8.89999962


       - style: __lldb_expr_37.BeerStyle.ipa

더 복잡하게 네스티드된 응답(Nested Response)
때론 우리 API 응답은 간단하지 않다. 아마 제일 상위에는 응답의 오브젝트를 정의하는 키가 단순히 하나가 아니고, 여러 컬랙션을 받거나 페이지로 된 정보를 받을 것이다.

예를 들어보자.
{
   "meta": {
       "page": 1,
       "total_pages": 4,
       "per_page": 10,
       "total_records": 38
   },
   "breweries": [
       {
           "id": 1234,
           "name": "Saint Arnold"
       },
       {
           "id": 52892,
           "name": "Buffalo Bayou"
       }
   ]
}
json을 인코딩/디코딩 할때, 스위프트에서 실제로 타입을 중첩할 수 있고 구조체 표현을 가질 수 있다.
struct PagedBreweries : Codable {
   struct Meta : Codable {
       let page: Int
       let totalPages: Int
       let perPage: Int
       let totalRecords: Int
       enum CodingKeys : String, CodingKey {
           case page
           case totalPages = "total_pages"
           case perPage = "per_page"
           case totalRecords = "total_records"
       }
   }

   struct Brewery : Codable {
       let id: Int
       let name: String
   }

   let meta: Meta
   let breweries: [Brewery]
}
이런 방법의 커다란 이점은 다른 응답을 같은 타입의 오브젝트로 가질 수 있다는 점이다(이 경우 아마 보이는것처럼 응답 리스트에서 "brewery"idname만 가지지만, brewery를 선택하면 더 많은 속성을 가진다.(but has more attributes if you select the brewery by itself)) 여기서 Brewery 타입은 중첩되있으므로 다른 Brewery타입을 만들어서 다른 구조체를 디코딩/인코딩할 수 있다.

더 깊은 커스터마이징
지금까지 무거운 작업도 디폴트 EncodableDecoable 구현에 의존해왔다.

대부분 이렇게 처리하겠지만, 결국 우리는 인코딩과 디코딩을 더 컨트롤하기위해 깊이 들어가야 할것이다.

커스텀 인코딩
시작하면서, 컴파일러가 공짜루 우리에게 제공해줬던 것을 커스텀하는 버전을 만들어 볼 것이다. 인코딩으로 시작해보자.
extension Beer {
  func encode(to encoder: Encoder) throws {

   }
}
그리고 이 예제를 좀 더 다루기위해 beer 타입에 새로운 필드 몇개도 추가하고 싶다.
struct Beer : Coding {
   // ...

   let createdAt: Date
   let bottleSizes: [Float]
   let comments: String?


   enum CodingKeys: String, CodingKey {
       // ...

       case createdAt = "created_at",
       case bottleSizes = "bottle_sizes"
       case comments
   }
}
이 메소드에는 인코더를 넣고, "컨테이너(container)"를 받아서, 여기에 값을 인코딩한다.

컨테이너가 무엇일까?
컨테이너는 몇가지 타입중에 하나가 될 수 있다.
  • Keyed Container: 키로 값을 제공함. 이것은 원래 딕셔너리임.
  • Unkeyed Container: 키 없이 정렬된 값을 제공함. JSONEncoder에서는 배열을 의미함.
  • Single Value Container: 담겨진 엘리먼트의 어떤 종류도 없이 가공되지 않은 값을 만듦.
우리의 모든 프로퍼티를 인코딩하기위해 우리는 먼저 컨테이너를 받아야한다. 이 포스트 처음에 나왔던 JSON 구조체를 보면, keyed container가 필요해보인다.
var container = encoder.container(keyedBy: CodingKeys.self)
여기서 2가지를 짚어보자.
  • 컨테이너는 우리가 변경할 수 있도록 반드시 mutable 프로퍼티여야하는데, 변수를 var로 선언해야한다.
  • 키들을 지정해야하는데(그리하여 프로퍼티와 키를 매핑시킨다) 이 컨테이너로 인코딩시킬 수 있는 키가 무엇인지 안다.
후자는 아주 강력한 점이 될 것이다.

다음으로 컨테이너에 값을 인코딩한다. 이 호출들은 모두 에러를 던지므로, 각 줄마다 try로 시작할 것이다.
try container.encode(name, forKey: .name)
try container.encode(abv, forKey: .abv)
try container.encode(brewery, forKey: .brewery)
try container.encode(style, forKey: .style)
try container.encode(createdAt, forKey: .createdAt)
try container.encode(comments, forKey: .comments)
try container.encode(bottleSizes, forKey: .bottleSizes)
comments 필드에서, Encodable의 디폴트 구현은 옵션 값에 encodeIfPresent를 사용한다. 만약 nil이면 인코딩된 표현에서 키는 잃어버릴 것이다는 의미이다. 이것은 API에대해 일반적으로 좋은 해결책이 아니므로, 여기에 null 겂아 있다해도 키를 가지도록 하는 방법이 좋은 방법이다. 여기서 우리는 encodeIfPresent(_:forKey:) 대신 encode(_:forKey:)를 사용하여 이 키를 포함하는 아웃풋으로 만든다.

bottoleSizes 값은 자동으로 인코딩되있지만, 어떤 이유로 커스터바이징이 필요하다면 우리만의 컨테이너를 만들어야한다. 여기서 우리는 각 항목별로 처리하고(부동소숫점을 반올림하여) 순서대로 컨테이너에 추가한다.
var sizes = container.nestedUnkeyedContainer(
     forKey: .bottleSizes)

try bottleSizes.forEach {
     try sizes.encode($0.rounded())
}
그리고 끝났다! 여기에는 부동소수점이 따르는 전략이나 날짜 포멧팅에대한 얘기는 없다는 점을 인지하자. 사실 이 메소드는 전적으로 JSON agnostic인데, 이것이 설계의 부분이다. 인코딩과 디코딩 타입은 제네릭 기능이고, 포멧은 필요한 사람에의해 쉽게 명세된다.

이제 우리가 인코딩한 JSON은 이렇게 생겼다.
{
  "comments" : null,
  "style" : "ipa",
  "brewery_name" : "Saint Arnold",
  "created_at" : "2016-05-01T12:00:00Z",
  "alcohol_by_volume" : 8.8999996185302734,
  "bottle_sizes" : [
   12,
   16
  ],
  "name" : "Endeavor"
}
여기서 부동소수점 값이 원래 JSON 문서에서는 8.9였는데 메모리에 표현되면서 같지 않은 숫자로 되버린것은 의미없다. 만약 숫자의 정확한 표현이 필요하다면, NumberFormater로 매번 손수 포멧팅하길 원할지도 모르겠다. 특별히 통화를 다루는 API는 종종 정수형 값으로 센트 숫자를 보낸고(안전하게 반올림될 수 있) 100.0으로 나눠서 달러 값을 얻어낸다.있다

이제 반대로도 할수 있다. 다음으로 Decodable 프로토콜 요구사항에대한 구현을 작성해보자.

커스텀 디코딩
디코딩은 원래 다른 생성자를 작성하는 것이다.
extension Beer {
   init(from decoder: Decoder) throws {

   }
}
이번에도 디코더에서 컨테이너를 만들어야한다.
let container = try decoder.container(keyedBy: CodingKeys.self)
모든 기본 프로퍼티를 디코딩할 수 있다. 각각의 경우에서 우리는 기대하는대로 타입을 지정해야한다. 만약 타입이 매치가 안된다면 DecodingError.TypeMismatch가 던져지며 무슨 문제인지 알려주는 정보를 받는다.
let name = try container.decode(String.self, forKey: .name)

let abv = try container.decode(Float.self, forKey: .abv)

let brewery = try container.decode(String.self,
     forKey: .brewery)

let style = try container.decode(BeerStyle.self,
     forKey: .style)

let createdAt = try container.decode(Date.self,
     forKey: .createdAt)

let comments = try container.decodeIfPresent(String.self,
     forKey: .comments)
같은 메소드를 bottleSizes 배열에도 상ㅇ할 수 있는데, 비슷한 방법으로 각 값을 처리할 수 있다. 여기, 새로운 인스턴스에 저장하기 전에 값을 반올림한다.
var bottleSizesArray = try container.nestedUnkeyedContainer(forKey: .bottleSizes)

var bottleSizes: [Float] = []

while (!bottleSizesArray.isAtEnd) {
   let size = try bottleSizesArray.decode(Float.self)
   bottleSizes.append(size.rounded())
}
컨테이너에 더이상 엘리먼트가 없을때까지 계속 값을 디코딩할 것이다.

이제 모든 이 변수들이 정의되고, 디폴트 생성자 호출에대한 모든 대답을 가지게 되었다.
self.init(name: name,
             brewery: brewery,
             abv: abv,
             style: style,
             createdAt: createdAt,
             bottleSizes: bottleSizes,
             comments: comments)
encode(to encoder:)init(from decoder:)의 커스텀 구현으로, JSON 결과를 우리 타입에 맵핑시키는데 더욱 컨트롤할 수 있게 되었다.

오브젝트 평평하게 만들기(Flattening Objects)
JSON이 우리가 고려하지 못했던 중첩 수준을 가진다면 어떻게 될까? 위 예제를 수정하여 abvstyle이 이렇게 표현되도록 해보자.
{
  "name": "Lawnmower",
  "info": {
    "style": "kolsch",
    "abv": 4.9
  }
  // ...
}
이 구조로 동작하려면 인코딩 디코딩 구현을 모두 커스터마이징 해야한다.

중첩된 키를 위해 열거형을 정의(하고 메인 CodingKeys 열거형으로부터 제거)하면서 시작해 볼 것이다.
struct Beer : Codable {
  enum CodingKeys: String, CodingKey {
     case name
     case brewery
     case createdAt = "created_at"
     case bottleSizes = "bottle_sizes"
     case comments
     case info // <-- NEW

  }

  case InfoCodingKeys: String, CodingKey {
     case abv
     case style
  }
}
우리가 값을 인코딩할때 제일 먼저 info 컨테이너에 참조를 넣어야한다. (which if you recall is a keyed container)
func encode(to encoder: Encoder) throws {


     var container = encoder.container(
         keyedBy: CodingKeys.self)
     var info = try encoder.nestedContainer(
         keyedBy: InfoCodingKeys.self)
     try info.encode(abv, forKey: .abv)
     try info.encode(style, forKey: .style)

     // ...

}
디코딩가능한 구현을 만들기 위해 반대로도 할 수 있다.
init(from decoder: Decoder) throws {
   let container = try decoder.container(
         keyedBy: CodingKeys.self)

   let info = try decoder.nestedContainer(
         keyedBy: InfoCodingKeys.self)
   let abv = try info.decode(Float.self, forKey: .abv)
   let style = try info.decode(BeerStyle.self,
         forKey: .style)

   // ...

}
이제 인코딩된 포멧에 중첩된 구조를가 가질 수 있게 되었으나 우리 오브젝트에서는 평평하게(flatten) 만들었다.

자식 오브젝트 만들기
brewery가 간단한 문자열로 전달되고 분리된 Brewery 타입을 유지하고 싶다고 해보자.
{
  "name": "Endeavor",
  "brewery": "Saint Arnold",
  // ...
}
이 경우에는 다시 encode(to encoder:)init(from decoder:) 의 커스텀 구현을 해줘야한다.
func encode(to encoder: Encoder) throws {
     var container = encoder.container(keyedBy:
         CodingKeys.self)

     try encoder.encode(brewery.name, forKey: .brewery)

     // ...


}


init(from decoder: Decoder) throws {
     let container = try decoder.container(keyedBy:
         CodingKeys.self)
     let breweryName = try decoder.decode(String.self,
         forKey: .brewery)
     let brewery = Brewery(name: breweryName)

     // ...

}

상속
아래 클래스들을 가진다고 생각해보자.
class Person : Codable {
   var name: String?

}


class Employee : Person {
   var employeeID: String?

}
Person 클레스를 상속하여 Codable을 따르게 되었으나, Employee 인스턴스를 인코딩하려면 무슨일이 일어날까?
let employee = Employee()
employee.employeeID = "emp123"
employee.name = "Joe"


let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try! encoder.encode(employee)
print(String(data: data, encoding: .utf8)!)

{
  "name" : "Joe"
}
우리가 원하던 결과는 아니다. 자동으로 생성된 구현이 자식클래스에는 잘 동작하지 않는다. 따라서 다시 인코드/디코드 메소드를 커스터마이징 해야한다.
class Person : Codable {
   var name: String?



   private enum CodingKeys : String, CodingKey {
       case name
   }

   func encode(to encoder: Encoder) throws {
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(name, forKey: .name)
   }
}
자식클래스에도 같은 일을 할 것이다.
class Employee : Person {
   var employeeID: String?


   private enum CodingKeys : String, CodingKey {
       case employeeID = "emp_id"
   }

   override func encode(to encoder: Encoder) throws {
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(employeeID, forKey: .employeeID)
   }
}
이렇게 된다.
{
  "emp_id" : "emp123"
}
흠, 이것도 원하던 결과는 아니다. 우리는 부모클래스 구현의 encode(to:)으로 흘러가야한다.

당신은 그냥 부모클래스를 호출하여 인코더에서 넘겨줄 생각일 수 있다. 이것도 동작은 하지만, 현재 스냅샷은 EXC_BAD_ACCESS을 낸다. 내 생각엔 버그같고, 나중에 고쳐질 것이다.

만약 위에처럼 했다면, 같은 컨테이너하에 합쳐진 속성들을 얻을 수 있다. 그러나 스위프트팀은 여러 타입에대해 같은 컨테이너를 재사용하는 것에대해 이야기한다.
만약 공유되는 컨테이너가 필요하면 여전히 super.encode(to: encoder)과 super.init(from: decoder)을 호출할 수 있지만, 우리는 더 안전한 컨테이너화시킨 방법을 추천한다.
그 이유로는, 부모클래스는 우리가 설정한 값을 덮어쓸 수 있지만 우리는 그것에대해 모를 수 있다는 것이다.

대신 우리는 부모클래스의 인코더를 얻기위해 특별한 메소드를 사용할 수 있다. 이 인코더는 이미 컨테이너에서 가지고 있다.
try super.encode(to: container.superEncoder())
이렇게 된다.
{
  "super" : {
   "name" : "Joe"
  },
  "emp_id" : "emp123"
}
이렇게하면 "super"라는 새로운 키에 인코딩된 부모클래스를 만들어낸다. 필요하면 이 키 이름을 커스터마이징 할 수 있다.
enum CodingKeys : String, CodingKey {
  case employeeID = "emp_id"
  case person
}


override func encode(to encoder: Encoder) throws {
  // ...

  try super.encode(to:
     container.superEncoder(forKey: .person))
}
아래의 결과가 나온다.
{
  "person" : {
   "name" : "Joe"
  },
  "emp_id" : "emp123"
}
부모클래스에서 일반적인 구조에 접근하는것은 JSON 파싱을 간단화시킬 수 있고, 어던 경우에는 코드 중복을 줄일 수 있다.

UserInfo
인코딩 디코딩 중에 동작을 변경하거나 오브젝트에 컨텍스트를 제공해야하기위해 커스텀 데이터로 표현해야 한다면, 인코딩 디코딩 중에 사용자 정보(User Info)를 전달할 수 있다.

예를들어 고객을위한 이런 JSON을 만들어주는 버전1의 API를 물려받았다고 해보자.
{
  "customer_name": "Acme, Inc",  // old key name
  "migration_date": "Oct-24-1995", // different date format?
  "created_at": "1991-05-12T12:00:00Z"
}
여기서 우리는 created_at 필드와 다른 날짜 양식을 가지는 migration_date 필드를 가지고 있다. 그리고 이름 프로퍼티가 그냥 name으로 바뀌었다고 가정하자.

이 상황은 이상적이지 않은 상황이 분명하지만, 실제로 일어나고, 종종 더러운 API를 물려받기도 한다.

우리를위해 중요한 값들을 담아둘 특별한 사용자 정보 구조체를 정의하자.
struct CustomerCodingOptions {
  enum ApiVersion {
     case v1
     case v2
  }
  let apiVersion = ApiVersion.v2
  let legacyDateFormatter: DateFormatter

  static let key = CodingUserInfoKey(rawValue: "com.mycompany.customercodingoptions")!

}
이제 이 구조체의 인스턴스를 만들어서 인코더와 디코더에 보낼 수 있다.
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy"

let options = CustomerCodingOptions(apiVersion: .v1, legacyDateFormatter: formatter)

encoder.userInfo = [ CustomerCodingOptions.key : options ]


// ...
encode 메소드 안의 모습이다.
func encode(to encoder: Encoder) throws {    var container = encoder.container(keyedBy: CodingKeys.self)


    // here we can require this be present...


    if let options = encoder.userInfo[CustomerCodingOptions.key] as? CustomerCodingOptions {



        // encode the right key for the customer name


        switch options.apiVersion {


        case .v1:


            try container.encode(name, forKey: .legacyCustomerName)


        case .v2:


            try container.encode(name, forKey: .name)

        }


        // use the provided formatter for the date


        if let migrationDate = legacyMigrationDate {


            let legacyDateString = options.legacyDateFormatter.string(from: migrationDate)


            try container.encode(legacyDateString, forKey: .legacyMigrationDate)

        }


    } else {

        fatalError("We require options")

    }



    try container.encode(createdAt, forKey: .createdAt)

}
디코드 생성자에도 정확하게 같은 것을 할 수 있다.

바깥으로부터 옵션을 제공받아 파싱을 더욱 컨트롤할 수 있는 좋은 방법이다. 게다가 DateFormatter같이 생성할때 비싼 오브젝트를 재사용할 수 있다.

다이나믹 코딩 키(Dynamic Coding Keys)
지금까지 이 가이드에서는 스위프트 네이밍과 다를때 코딩키를 표현하려고 enum을 사용했었다. 가끔 이것이 불가능할 수도 있다. 아래의 경우를 생각해보자.
{
  "kolsh" : {
   "description" : "First only brewed in Köln, Germany, now many American brewpubs..."
  },
  "stout" : {
   "description" : "As mysterious as they look, stouts are typically dark brown to pitch black in color..."
  }
}
beer 스타일의 리스트이나, 키들은 실제로 스타일의 이름이다. API는 시간이 지나면서 바뀌고 커질 수 있기 때문에 모든 가능한 상황을 표현할 수 없을 수 있다.

대신에 우리는 CodingKey의 더 다이나믹한 구현을 만들 수 있다.
struct BeerStyles : Codable {
  struct BeerStyleKey : CodingKey {
   var stringValue: String
   init?(stringValue: String)? {
     self.stringValue = stringValue
   }
   var intValue: Int? { return nil }
   init?(intValue: Int) { return nil }

   static let description = BeerStyleKey(stringValue: "description")!

  }

  struct BeerStyle : Codable {
   let name: String
   let description: String
  }

  let beerStyles : [BeerStyle]
}
CodingKeyStirngInt 값 프로퍼티를 둘 다 필요로하고 생성자를 필요로한다. 그러나 이 경우 정수 키를 지원하지 않아도 된다. 또한 스태틱 "destcription 속성을 위한 스태틱 키를 정의했었는데, 바뀌지 않을 것이다.

디코딩을 하며 시작해보자.
init(from decoder: Decoder) throws {
   let container = try decoder.container(keyedBy: BeerStyleKey.self)

   var styles: [BeerStyle] = []
   for key in container.allKeys {
       let nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
           forKey: key)
       let description = try nested.decode(String.self,
           forKey: .description)
       styles.append(BeerStyle(name: key.stringValue,
           description: description))
   }

   self.beerStyles = styles
}
여기서 우리는 컨테이너 안에서 찾은 모든 키들을 다이나믹하게 돌고, 그 키에대한 컨테이너 참조를 잡아둔다. 그리고 여기서 description을 뽑아낸다.

namedescription을 사용하여 직접 BeeryStyle 인스턴스를 생성하고 배열에 추가할 수 있다.

인코딩의 경우는 어떨까?
func encode(to encoder: Encoder) throws {
   var container = try encoder.container(keyedBy: BeerStyleKey.self)
   for style in beerStyles {
       let key = BeerStyleKey(stringValue: style.name)!

       var nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
           forKey: key)
       try nested.encode(style.description, forKey: .description)
   }
}
여기서 우리는 배열에 있는 모든 스타일들을 돌고, 스타일 이름을 위해 키를 만들어, 키에 컨테이너를 만든다. 그런다음 그 컨테이너에 description을 인코딩하기만 하면 끝난다.

우리가 볼 수 있듯, 커스텀 CodingKey를 만드는 것으로 우리가 다룰 수 있는 응답들의 타입에 많은 유연함을 제공해준다.

에러 처리
지금까지 우리는 어떠한 에러도 다루지 않았다. 이것들은 우리가 실행시킬지도 모르는 에러들이 몇개 있다. 각각은 연관된 값을 제공한다(DecodingError.Context는 언제, 어떤  문제가 생겼는지 알려주는 디버깅 설명을 제공해준다).
  • DecodingError.dataCorrupted(Context): 이 데이타는 오염되었다(즉, 우리가 생각한대로 생기지 않았다). 이것은 여러분이 디코더에 제공한 data가 JSON이 전혀 아니지만, 아마도 실패한 API 콜에서 HTML 에러 페이지일 수 있다.
  • DecodingError.keyNotFound(CodingKey, Context): 필요한 키가 발견되지 않았다. 이것은 질문에서 키를 보냈고 컨텍스트는 어디서, 왜 이런 일이 일어났는지에대한 정보를 제공한다. 이것을 받아다가 몇몇 키를 위한 fallback value를 적절하게 줄 수 있다.
  • DecodingError.typeMismatch(Any.Type, Context): 한 타입을 기대했지만 다른것을 찾았다. 아마도 그 데이터 포멧이 첫번째 버전의 API에서 변경되었을 것이다. 이 에러를 잡고 다른 타입을 사용한 데이터 찾아볼 수 있다.
인코더와 디코더에서 나온 에러들은 문제를 진단하는데 굉장히 유용하며, 특정 상황에 다이나믹하게 적용시킬 수 있는 유연함을 제공하여 적절하게 이것들을 다룰 수 있다.

한가지 이런 예로서 옛날 버전의 API 응답을 마이그레이션하는 것이다. 예를들어 디스크 어딘가에 영속의 캐시에 넣으려고 한 버전의 오브젝트를 인코딩했다고 하자. 나중에 포멧이 바뀌었지만 디스크의 자료는 그대로 있다. 이것을 로드해오려할때 에러가 발생할 수 있는데, 깔끔하게 새로운 데이터 포멧에 마이그레이션하기위해 처리할 수 있다.

더 읽을거리
  • Codable.swift: 스위프트가 오픈소스화되어서 좋은 점 중 하나는 이것이 어덯게 구현되었는지 그냥 볼 수 있다는 점이다. 꼭 한번 보자!
  • Using JSON with Custom Types: 애플이 제공하는 playground 샘플인데, 더 복잡한 JSON 파싱 시나리오를 볼 수 있다.

결론
여기까지 새로운 스위프트 4의 Codable API를 어떻게 사용하는지에대해 빠르게 훑어보았다. 추가하고 싶은게 있나? 아래에 댓글을 달아달라.

이 작업이 마음에 들었는가? 그렇다면 NSScreencast도 마음에 들것이라 생각된다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: All about Concurrency in Swift - Part 1: The Present

역자: 이 시리즈물의 2편이 나왔습니다! 조만간 번역할 예정이에요.

현재 배포된 스위프트 언어에서는 Go나 Rust가 한것 처럼 아직 네이티브 동시성 기능을 가지지 않는다.

작업들을 동시에 실행시키고 싶을때 경쟁상태의 결과를 다뤄야 한다면, 여러분이 할 수 있는 선택지는 몇개가 없다. libDispatch같은 외부 라이브러리를 사용하던지, 아니면 Foundation이나 OS가 제공하는 동기화 프리미티브(primitives)를 사용하는 것이다.

이 시리즈물의 첫번째 파트는, 스위프트3에서 우리가 처한 상황을 보고, Foundation의 락, 스레드, 타이머부터 언어의 게런티에대한 모든것과 최근에 만들어진 Grand Central Dispatch와 Operation Queues를 다룬다.

몇가지 기본 동시성 이론과 일반적인 동시성 패턴도 설명하게 될 것이다.

크리티컬 섹션과 동시 실행크리티컬 섹션과 동시 실행


스위프트가 돌아가는 모든 플랫폼에서 pthread를 사용할 수 있을지라도 이 라이브러이의 기능과 프리미티브는 이 글에서 설명하지 않을 것이며, 그것보다 더 높은 수준의 대안에대해 이야기할 것이다. NSTimer 클래스도 역서 이야기 하지 않으니 스위프트 3에서 어떻게 이것을 사용하는지 여기서 확인하자.

이미 여러번 발표했듯, 스위프트 4 이후의 주요 배포중 하나(꼭 스위프트 5는 아닐 것임)에서 더 나은 메모리 모델(memory model)을 정의하고, 새로운 네이티브 동시성 기능을 넣기위해 이 언어를 확장할 것이다. 새로운 동시성 기능은 외부 라이브러리없이 동시성 및 병렬처리를 다룰 수 있게 해주며, 동시성에대해 스위프트스러운 이상적인 방법을 정의할 것이다.

이것은 이 시리즈물의 다음 글의 주제가 될 것인데, 다른 언어에서 구현한 몇가지 대안의 방법과 패러다임을 토론하고, 이것들이 어떻게 스위프트로 구현될 수 있는지 이야기하게 된다. 그리고 오늘달에 이미 사용할 수 있는 몇가지 오픈소스의 구현을 분석하여 현재 배포된 스위프트로 Actors 패러다임, Go의 CSP 채널, Software Transactional Memory등을 이용할 수 있게 해줄 것이다.

이 두번째 글은 완전히 추측적인 것이다. 글의 주된 목표는, 이 주제에대해 소개해주어서 당신이 동시성을 어떻게 다룰지 정의하는 미래의 스위프트 배포에서 토론에 참여할 수 있게 해주는 것이다.

이글이나 나른 글의 Playground는 GitHub 나 Zipped에서 이용할 수 있다.

목차


멀티스레딩과 동시성 입문
오늘날 어떤 어플리케이션을 만들든 상관없이, 곧(혹은 훗날) 당신의 앱은 멀티스레드 실행의 환경에서 동작할 것이라는 사실을 고려해주어야한다.

하나 이상의 프로세서를 가진 컴퓨팅 플랫폼. 혹은 하나 이상의 하드웨어 실행 코어를 가진 프로세서는 10여년동안 우리 주변에 바짝 다가왔고 스레드프로세스 같은 개념은 나이를 먹어버렸다.

운영체제는 다양한 방법으로 사용자 프로그램에게 이 기능들을 제공해왔고, 모든 현대의 프레임워크나 앱은 유연성과 성능을 높히기위해 몇가지 잘 알려진 디자인 패턴들을 구현할 것이다. 그 중에는 다중 스레드도 포함되있다.

스위프트에서 어떻게 동시성을 다루는지 구체적으로 들어가보기전에, Dispatch QueuesOperation Queues를 사용할 때 필요한 기본 개념을 간단하게 설명하려 한다.

먼저 애플 플랫폼과 프레임워크가 스레드를 사용할지라도 왜 이것을 여러분의 어플리케이션에 넣으려하는지 먼저 질문해보아야한다.

일반적인 상황에서 다중스레드가 해결책이 될 수 있는 몇가지가 있다.
  • 작업 그룹 분리: 스레드는 실행 플로우의 관점에서 여러분의 어플리케이션을 모듈화하는데 사용할 수 있고, 각 스레드들은 예측할 수 있는 방법으로 같은 타입의 작업 그룹을 실행시키는데 사용할 수 있다. 여러분의 프로그램을 다른 실행 플로우로부터 고립시켜 앱의 현재 상태에대해 더 쉽게 만든다.
  • 데이터-독립의 컴포넌트들의 병렬화: 하드웨어 스레드를 지원받거나 아닌(다음에 보자) 다중 소프트웨어 스레드는 원래 입력 데이터 구조의 하위집합에서 작동하는 여러 동일한 작업 본사본들을 병렬화하는데 사용될 수 있다.
  • 조건이나 I/O를 기다리는데 깔끔한 방법: I/O를 블럭킹하거나 다른 종류의 오퍼레이션 블럭할때, 백그라운드 스레드는 이 오퍼레이션을 완료하기까지 깔끔하게 기다리는데 사용될 수 있다. 스레드의 이런 사용은 앱의 전반적인 설계를 증진하고 블럭된 호출 trivial을 다룰 수 있게 한다.

그러나 여러분의 코드를 단일 스레드의 관점에서 볼 때 이해했던 몇가지 가정이 다중 스레드가 실행있을때는 더이상 유효하지 않을 것이다.

각 스레드의 실행이 독립적으로 이루어지고, 데이터공유가 없는 이상적인 세계라면 단일 스레드에서 실행되는 코드처럼 그렇게까지 복잡하지는 않을 것이다. 그러나 보통의 경우처럼 같은 데이터에 동작하는 다중 스레드를 가진다면 이런 자료구조에 접근을 규제해야하고, 이 데이터에대한 모든 오퍼레이션이 다른 스레드의 오퍼레이션과 원치않은 인터렉션이 없도록 만드는 방법이 필요할 것이다.

동시성 프로그래밍은 그 언어나 운영체제로부터 추가적인 보증이 필요한데, 여러 스레드가 동시에 접근하려할때 변수("자원")는 어떻게 행동할지 명시적인 지정이 필요하다.

이런 언어는 메모리모델(Memory Model)을 정의해야한다. 메모리모델의 기본 진술서(basic statments)에는 동시성 스레드에서 어떻게 행동할지 명시적으로 지정해놓은 규칙들을 담아야하고. 메모리가 어떻게 공유될 수 있고 어던 종류의 메모리 접근이 유효한지 정의해야한다.

덕분에 사용자는 예상한대로 동작하는 언어를 가지게 되며, 컴파일러는 메모리 모델에 정의된 것만 반영하여 최적화를 수행할 것이라는 점을 우리는 알 것이다.

너무 엄격한 모델은 컴파일러가 발전할 것을 제안하기 때문에 메모리 모델을 정의하는 것은 언어의 발전에서 정교하게 해야한다. 독창적인 최적화는 메모리모델에서 과거의 결정에 유효하지 않을 수도 있다.

메모리모델을 정의하는 예시이다.
  • 어떤 언어의 진술서에는 atomic이 고려될 수 있는데, 어떤 스레드도 부분적인 결과를 결과를 내지 않는 완전한 곳에서만 오퍼레이션을 실행시킬 수 있다. 예를들어 필수적으로 변수들이 atomic하게 초기화될 수 있는지 없는지 알아야한다.
  • 공유된 변수를 어떻게 스레드에의해 다룰지, 디폴트로 캐싱을 할지, 특정 언어 변경자로 캐시 동작에 영향을 줄 수 있게 할지
  • 크리티컬 섹션(critical section, 공유된 자원에서 동작하는 코드 영역)에 접근을 표시하고 규제하는데 사용되는 동시성 연산자가 있다. 예로서 이것은 한번에 특정 한 코드 패스를 따르기위해 한 스레드만 허용한다.
이제 여러분의 프로그램의 동시성 사용 이야기로 돌아가자.

동시성을 올바르게 다루기위해 여러분 프로그램에서 크리티컬 섹션을 판단해야하고, 다른 스레드간에 공유된 데이터의 접근을 규제하기위해 동시성 프리미티브나 동시성을 인지하는 자료구조를 사용해야 할 것이다.

코드나 자료구조의 이런 영역에 접근 규칙을 만들면 또다른 문제들을 만들게된다. 모든 스레드가 실행하여 공유된 데이터를 수정할 기회를 제공하는 것이 바라는 결과겠지만, 어떤 환경아래 어떤 것들은 아예 실행되지 않을 수도 있고, 그 데이터는 예상하지 못했던 방법으로 변경될지도 모른다.

당신은 추가적인 과제들을 직면하게 될 것이고 어떤 일반적인 문제들과 함께 작업해야 할 것이다.
  • Race Conditions: 같은 데이터에 실행되는 여러 스레드(예를들면 동시에 읽기, 쓰기를 하는)는 오퍼레이션 시리즈의 실행 결과를 예측하기 힘들거나 스레드 실행 순서에 따라 다른 결과가 나올 수 있다.
  • Resources Contention: 다른 작업들을 실행시킬 수 있는 멀티 스레드가 같은 자원에 접근하려고하면, 요청했던 자원을 안전하게 얻는데 시간이 더 많이 요구될 것이다. 여러분이 필요한 자원을 얻는데 이런 지연은 기대하지 않았던 동작이 되버리거나, 아니면 이런 자원 접근을 규제하는 구조를 짜야한다.
  • Deadlocks: 여러 스레드에서 자원에 락을 걸었는데 서로 그 락이 풀리기를 기다리게된다. 이 스레드 그룹은 영원히 실행을 블락시킨다.
  • Starvation: 한 스레드가 절때 특정 순서에서 자원들을 얻지 못할 수 있다. 다양한 이유가 필요하며 영원히 성공하지 못할 자원 취득을 계속해서 시도한다.
  • Priority Inversion: 시스템에의해 할당된 우선순위 전환으로 높은 우선순위의 스레드가 필요로하는 자원을 낮은 우선순위의 스레드가 계속해서 취득하고 있을 수 있다.
  • Non-determinism과 Fairness: 우리는 언제 어느때의 순서에따라 스레드가 공유된 자원을 취득할 수 있을지 가정할 수 없다. 이런 지연은 우선순위를 결정할 수 없고 경쟁의 양에 크게 영향을 받는다. 그러나 크리티컬 섹션을 보호하는데 사용되는 동시성 프리미티브는 공평하게 만들어지거나, 공평을 지원하게 만들 수도 있다(used to guard a critical section can also be built to be fair or to support fairness). 기다리고 있는 모든 스레드가 크리티컬 섹션에 접근할 수 있게 보장하면서, 요청했던 명령을 침해하지 않는다.

언어 게런티
당장 스위프트 자체가 동시성과 관련된 기능을 가지고 있지 않더라도, 스위프트는 프로퍼티를 어떻게 접근할지와 관련된 몇가지 게런티를 제공한다.

예를들어 전역변수는 atomic하게 초기화되므로, 여러 스레드가 한 전역변수를 동시에 초기화하려는 상황을 직접 처리하지 않아도 되고, 초기화가 여전히 진행중일때 누군가 부분적으로 초기화된 모습을 볼 걱정을 할 필요가 없다.

아래에 싱글톤 구현을 이야기할때 이 동작에대해 다시 생각해볼 것이다.

그러나 레이지 프로퍼티(lazy property) 초기화는 atomic하게 수행되지 않는다는 것을 꼭 기억해줘야한다. 게다가 스위프트는 이제 이것을 바꾸기위한 지시자나 변경자를 제공하지 않는다.

클래스 프로퍼티에 접근도 atomic이 아니다. 만약 그렇게 만들어야 한다면, 락이나 다른 비슷한 메커니즘을 사용해서 직접 독점적 접근을 구현해야한다.

스레드
Foundation은 Thread 클래스를 제공하는데, 이 클래스는 내부적으로 pthread를 기반으로 하며, 새로운 스레드를 생성하고 클로저를 실행시키는데 사용할 수 있다.

Thread 클래스의 detachNewThreadSelector:toTarget:withObject: 메소드를 이용하여 스레드를. 생성하거나, 커스텀 Thread 클래스를 선언하고 main() 메소드를 오버리아딩하여 새로운 스레드를 만들 수도 있다.
class MyThread : Thread {
   override func main() {
       print("Thread started, sleep for 2 seconds...")
       sleep(2)
       print("Done sleeping, exiting thread")
   }
}
그러나 iOS10과 macOS Sierra부터는 마침내 모든 플랫폼에서 스레드가 실행시킬 클로저를 생성자뒤에 붙여 새로운 스레드를 생성할 수 있다. 이 글의 모든 예제는 기본 Thread 클래스를 확장한 것이므로 다른 OS에서 테스트해보지 않아도 된다.
var t = Thread {
   print("Started!")
}

t.stackSize = 1024 * 16
t.start()              //Time needed to spawn a thread around 100us
우리가 직접 시작시키기위해 필요한 스레드 인스턴스를 만들어보자. 부가적인 단계로 새로운 스레드를 위한 맞춤형 스택 크기도 지정할 수 있다.

exit()를 호출하여 갑자기 스레드를 중단시킬 수 있지만, 현재 작업들을 깔끔하게 끝낼 기회를 잃어버리므로 절때로 추천하지 않는다. 필요에따라 중단 로직을 스스로 구현하거나, cancel() 메소드를 사용하고 스레드가 자연스럽게 현재 작업을 끝내기전에 중단을 요청을 받았는지 메인 클로저 내에서 알기위해 isCancelled 프로퍼티를 확인할 수 있다.

동기화 프리미티브
공유된 데이터를 변경하고 싶은 다른 스레드들이 있을때는, 데이터 오염이나 결정되지 않은 동작을 막기위해 반드시 이런 스레드들을 어떤 방법으로 동기화해주어야한다.

스레드 동기화에 기본적으로 사용되는 것은 락(lock), 세마포어(semaphore), 모니터(monitor)이다.

Foundation은 이 모든것을 제공한다.

곧 보게 될것인데, 이런 구성들을 구현하는 클래스들(그렇다 모두 참조 타입이다)은 스위프트 3에서 접두를 빼진 않았지만 다음 스위프트 배포판 중 하나에서 빠질 수 있다.

NSLock
NSLock은 Foundation이 제공하는 락(lock)의 기본 타입이다.

스레드가 이 오브젝트에 락을 걸려고하면 두가지 일이 일어날 수 있다. 이전 스레드가 락을 걸지 않았다면 이 스레드는 락을 취득할 것이다. 혹은 락이 이미 걸려있다면 락을 건 소유자가 락을 풀때까지 스레드는 실행을 블락하고 기다릴 것이다. 즉 락은 한번에 한번에 한 스레드만 취득할 수 있는 오브젝트이며 이것이 크리티컬 섹션 접근을 완벽하게 감시할 수 있게 만들어준다.

NSLock과 Foundation의 다른 락은 공평하지 않다(unfair). 스레드의 시리즈가 락을 취득하기위해 기다릴때 원래 락은 시도했던 순서대로 락을 취득하지 않을 것이다.

스레드 경쟁이 커지는 경우에는 실행 순서를 예상할 수 없다. 많은 스레드가 자원을 취득하려 할때, 여러분의 스레드는 starvation을 겪을 수 있고, 아무리 기다려도 절때 락을 취득할 수 없을 수도 있다(혹은 적절한 시간안에 취득할 수 없을 것이다).

경쟁 없이 락을 취득하는데 필요한 시간은 100ns로 예상할 수 있겠지만, 하나 이상의 스레드가 락이 걸린 자원을 취득하려고 할때, 그 시간은 급격하게 증가한다. 따라서 성능의 관점에서 볼때 락은 자원 할당을 다루기에 최고의 해결책은 아니다.

두 스레드가 있는 예제를 보자. 락을 취득될 순서가 정해져있지 않으므로 T1이 한 row에 두번 락을 취득하는 일이 일어날 수 있다(일반적인 상황은 아니다).
let lock = NSLock()
class LThread : Thread {
   var id:Int = 0

   convenience init(id: Int) {
       self.init()
       self.id = id
   }

   override func main() {
       lock.lock()
       print(String(id)+" acquired lock.")
       lock.unlock()
       if lock.try() {
           print(String(id)+" acquired lock again.")
           lock.unlock()
       } else {  // If already locked move along.
           print(String(id)+" couldn't acquire lock.")
       }

       print(String(id)+" exiting.")




    }
}

var t1 = LThread(id:1)
var t2 = LThread(id:2)
t1.start()
t2.start()
락을 사용하기로 했을때 한가지 경고하고 싶은게 있다. 나중에 동시성 이슈를 디버깅해야할 것이다. 항상 어떤 종류의 자료구조 범위 안으로 락 사용을 제한하려 하고, 여러분의 코드베이스 여러곳에서 하나의 락 오브젝트를 직접 참조하지 않도록 노력해야한다.

동시성 문제를 디버깅하는동안, 여러분의 코드 어느 부분이 락을 잡고있는지 계속 추적해가면서 여러 함수들의 로컬 상태를 기억하는것보다는 몇가지 입장 지점으로 동기화된 자료구조의 상태를 확인하는 것이 더 좋은 방법이다. 남은 글로 가서(go the extra mile) 여러분의 동시적인 코드 구조를 잘 짜자.

NSRecursiveLock
재귀적인 락(recursive lock)은 이미 락을 건 스레드에서 여러번 락을 취득할 수 있는데, 재귀함수나 시퀀스에서 동일한 락을 확인하는 여러 함수를 호출할 시 유용하다. 이것은 기본 NSLock과는 함께 동작하지 않을 수 있다.
let rlock = NSRecursiveLock()

class RThread : Thread {

   override func main() {
       rlock.lock()
       print("Thread acquired lock")
       callMe()
       rlock.unlock()
       print("Exiting main")
   }

   func callMe() {
       rlock.lock()
       print("Thread acquired lock")
       rlock.unlock()
       print("Exiting callMe")
   }
}

var tr = RThread()
tr.start()

NSConditionLock
조건락(condition lock)은 더 복잡한 락 설정(소비자-생산자 시나리오)을 지원하는데, 각자 독립적으로 락과 언락될 수 있도록 추가적인 하위락을 제공한다.

하나의 전역의 락(특정 조건에 상관없이 락을 건다)도 사용할 수 있으며 원래의 NSLock처럼 동작한다.

공유하는 정수를 보호하는 락 예제를 보자. 소비자는 출력하고 생산자는 화면에 나타날 때마다 업데이트한다.
let NO_DATA = 1
let GOT_DATA = 2
let clock = NSConditionLock(condition: NO_DATA)
var SharedInt = 0

class ProducerThread : Thread {

   override func main() {
       for i in 0..<5 {
            clock.lock(whenCondition: NO_DATA) //Acquire the lock when NO_DATA
            //If we don't have to wait for consumers we could have just done clock.lock()
            SharedInt = i
           clock.unlock(withCondition: GOT_DATA) //Unlock and set as GOT_DATA
        }
   }
}

class ConsumerThread : Thread {

   override func main() {
       for i in 0..<5 {
            clock.lock(whenCondition: GOT_DATA) //Acquire the lock when GOT_DATA
            print(i)
            clock.unlock(withCondition: NO_DATA) //Unlock and set as NO_DATA
        }
   }
}

let pt = ProducerThread()
let ct = ConsumerThread()
ct.start()
pt.start()
락을 만들때 시작 조건을 지정해주어야하는데, 정수로 표현한다.

lock(whenCondition:) 메소드는 조건이 만족될때 락을 취득하거나 다른 스레드가 unlock(withCondition:)을 이용해서 값을 세팅할때까지 기다릴 것이다.

기본 락보다 조금 개선된 점은 좀 더 복잡한 시나리오를 만들 수 있게 해준다는 점이다.

NSCondition
조건락과 헷갈리지 말자. 한 조건(condition)은 발생 조건을 기다리기위한 명확한 방법을 제공한다.

락을 취득했던 스레드가 동작을 수행하는데 필요한 추가조건이 아직 만족되지 않았다면, 잠시 잡아두고 조건이 만족할때 작업을 계속하게 하는 방법이 필요하다.

끊임없이나 주기적으로 조건을 확인하도록 구현할 수도 있지만(busy waiting), 그렇게하면 스레드가 잡고있는 락에서 무슨일이 일어날까? 조건이 만족할때 다시 이들을 취득하길 바라면서 기다리거나 풀어주는 동안 잡아둬야 하는가(Should we keep them while we wait or release them hoping that we’ll be able to acquire them again when the condition is met)?

조건은 이 문제에대해 명확한 솔루션을 제공한다. 한번 취득한 스레드는 그 조건에대해 기다리고 있는 목록에 들어갈 수 있고, 한번 깨어난 다른 스레드는 조건이 만족했다고 신호를 보낸다.

예제를 보자.
let cond = NSCondition()
var available = false
var SharedString = ""

class WriterThread : Thread {
       override func main() {
       for _ in 0..<5 {
           cond.lock()
           SharedString = "😅"
           available = true
           cond.signal() // Notify and wake up the waiting thread/s
           cond.unlock()
       }
   }
}

class PrinterThread : Thread {
       override func main() {
       for _ in 0..<5 { //Just do it 5 times
           cond.lock()
           while(!available) {  //Protect from spurious signals
                cond.wait()
           }
           print(SharedString)
           SharedString = ""
           available = false
           cond.unlock()
       }
   }
}

let writet = WriterThread()
let printt = PrinterThread()
printt.start()
writet.start()

NSDistributedLock
분산된 락(distributed lock)은 지금까지 우리가 봤던 것과는 꽤 다르고, 이것이 자주 필요해보이진 않는다.

이것은 여러 어플리케이션 간에 공유되도록 만들어졌고 파일시스템 출입을 지원한다. 이 파일시스템은 이것을 취득해야하는 모든 앱이 분명하게 접근할 수 있어야 할것이다.

이런 종류의 락은 try() 메소드를 사용하여 취득될 수 있는데, 이 논-블락킹 메소드는 락이 취득되있는지 아닌지를 알려주는 boolean을 바로 반환한다. 락을 얻으려면 보통 한번 이상 시도해야 할것이다. 직접 실행시키거나 적절한 딜레이를 두고 연속적으로 시도할 수 있다.

분산될 락은 보통 unlock() 메소드를 사용하여 락을 푼다.

아래 기본 예제를 보자.
var dlock = NSDistributedLock(path: "/tmp/MYAPP.lock")

if let dlock = dlock {
   var acquired = false
   while(!acquired) {
       print("Trying to acquire the lock...")
       usleep(1000)
       acquired = dlock.try()
   }

   // Do something...
   dlock.unlock()
}

OSAtomic 어디있는가(Where Art Thou)?
OSAtomic가 제공하는 것과 비슷한 atomic 오퍼레이션들은 기존의 락 로직을 사용하지 않고 변수를 set, get, compare-and set 할 수 있게 해주는 간단한 오퍼레이션이다. 이들은 CPU의 특정 기능(종종 네이티브 atomic 인스트럭션)을 이용하여 앞에서 설명했던 락보다 더 좋은 성능을 낸다.

동시성을 다루는데 필요한 오버헤드가 최소한으로 줄기 때문에, 동시성 자료구조를 만들때 극도로 편리하다.

OSAtomic은 macOS 10.12부터 디프리케이트되었고 리눅스에서는 아예 사용할 수 없으나, 이것처럼 스위프트의 유용한 익스텐션을 사용한 오픈소스 프로젝트나 이것은 비슷한 기능을 제공한다.

synchronized 블럭에서
Objective-C에서 했던것처럼 @synchronized 블럭은 스위프트에서는 만들 수가 없는데, 동일한 키워드가 없다.

다윈에서는 objc_sync_enter(OBJ)objc_sync_exit(OBJ)를 직접 사용하여, 비슷한 어떤것을 준비할 수 있고, 내부적으로 @synchronized와 비슷하게 동작하는 @objc 오브젝트 모니터도 있다. 그러나 별로 의미는 없고, 이런것이 필요할때는 간단하게 락을 쓰는게 더 낫다.

그리고 Dispatch Queues를 설명할때 보게될 것인데, 동기화 호출을 수행하는 작은 코드로 이 기능을 큐로 이용할 수 있다.
var count: Int {
   queue.sync {self.count}
}

이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

GCD: Grand Central Dispatch
이 API에 친숙하지 않은 이들을 위해 Grand Central Dispatch(GCD)를 설명하자면, 이것은 큐 기반 API로 작업자 풀(worker pools)에서 클로저를 실행할 수 있게 해준ㄷ.

실행되야하는 작업을 담은 클로저는 이것을 실행시킨 큐에 담을 수 있는데, 큐의 구성 옵션에따라 순차적으로 할지, 병렬적으로 할지 정한 스레드 시리즈를 이용한다. 그러나 큐의 타입에 상관없이 작업은 항상 먼저 들어온 것이 먼저 나가는(FIFO, First-in First-out) 순서로 시작될 것이다. 즉, 작업은 항상 들어온 순서대로 시작할 것이다. 완료 순서는 각 작업의 지속시간에따라 다르다.

이것은 상대적으로 현대의 언어 런타임이 동시성을 처리할때 일반적으로 발견할 수 있는 패턴이다. 스레드 풀(thread pool)은 일련의 프리 스레드(free thread)나 연결되지 않은 스레드보다 더 쉽게 관리하고 조사하며 컨트롤 할 수 있는 방법이다.

스위프트 3에서 GCD API는 조금 바뀌었다. SE-0088는 설계를 현대화시키고 더 객체지향적으로 만들었다.

Dispatch Queues
GCD는 커스텀 큐를 생성할 수 있을 뿐만 아니라, 몇몇 미리 선언된 시스템 큐에 접근하게도 해준다.

일련의 기본 큐(이 큐는 여러분의 클로저를 차례로 실행시킬 것이다)를 생성하기 위해서는 큐를 식별하는데 쓰이는 문자열 레이블을 제공해야하며, 스택 트레이스(stack trace)에서 큐의 소유자를 간단히 추적하기위해 이 레이블은 도메인 앞부분을 뒤집어 사용하는 것을 추천한다.
let serialQueue = DispatchQueue(label: "com.uraimo.Serial1")  //attributes: .serial
let concurrentQueue = DispatchQueue(label: "com.uraimo.Concurrent1", attributes: .concurrent)
우리가 생성한 두번째 큐는 동시에 된다. 큐는 작업이 실행될때 스레드 풀에 있는 모든 사용가능한 스레드를 사용할 것이다. 이 경우에 실행 순서는 예측할 수 없고, 여러분의 큐를 추가한 순서와 어떤 방법으로도 연관시켜 완료 순서를 가정해서는 안된다.

디폴트 큐는 DispatchQueue 오브젝트에서 찾아볼 수 있다.
let mainQueue = DispatchQueue.main
let globalDefault = DispatchQueue.global()
main 큐는 순차적인 메인 큐인데, 이 큐는 iOS나 macOS에서 그래픽적인 어플리케이션을 위한 메인 이벤트 루프를 처리한다. 이 큐는 이벤트에 응답하고 사용자 인터페이스를 업데이트한다. 우리가 알고 있듯, 사용자 인터페이스에서 일어나는 모든 변경은 이 큐에서 실행되고, 이 스레드에서 오퍼레이션이 길어지면 둔감한 사용자 인터페이스로 렌더링하게 될 것이다.

이 런타임은 다른 프로퍼티로 다른 전역의 큐에 접근할 수 있게 해주는데, Quality of Service(QoS)라는 파라미터로 식별된다.

높은 우선순위부터 낮은 우선순위까지 다양한 우선순위가 DispatchQoS 클래스에 정의되있다.
  • .userInteractive
  • .userInitiated
  • .default
  • .utility
  • .background
  • .unspecified
모바일기기에서 저전력모드로 해놨을때 베터리양이 작으면 background 큐는 중단될 것이다. 이 점을 기억하자.

특정 디폴트 큐를 얻기위해 원하는 우선순위를 지정하는 global(qos:) 게터를 사용하자.
let backgroundQueue = DispatchQueue.global(qos: .background)
동일한 우선순위 명시는 커스텀 큐를 생성할때 다른 속성들과 함께(혹은 없이) 사용될 수 있다.
let serialQueueHighPriority = DispatchQueue(label: "com.uraimo.SerialH", qos: .userInteractive)

Queue 사용하기
클로저 형태의 작업들은 두가지 방법으로 큐에 담긴다. 동기적으로 sync 메소드를 사용하거나 비동기적으로 async 메소드를 사용할 수 있다.

전자를 사용하면 sync 호출은 블락되며 즉 이 클로저가 완료될 때 sync 메소드를 호출할 수 있는 반면(클로저가 끝날때까지 기다려야할 때는 유용하지만, 더 나은 방법이 있다), 후자는 클로저를 큐에 넣고 완료되는대로 계속해서 실행할 수 있게 해준다.

짧은 예제를 보자.
globalDefault.async {
   print("Async on MainQ, first?")
}

globalDefault.sync {
   print("Sync in MainQ, second?")
}
예제처럼 여러 디스패치 호출은 중첩될 수 있는데, background(낮은 우선순위)가 끝나고 사용자 인터페이스를 갱신하는 오퍼레이션이다.
DispatchQueue.global(qos: .background).async {
    // Some background work here
    DispatchQueue.main.async {
        // It's time to update the UI
        print("UI updated on main queue")
   }
}
클로저는 지정된 지연 이후에 실행될 수도 있는데, 스위프트 3은 마침내 더 편리한 방법으로 지정할 수 있게 되었다. .seconds(Int), milliseconds(Int), microseconds, nanoseconds(Int)의 네가지 시간 단위를 사용하여 인터벌을 구상할 수 있는 DispatchTimeInterval 열거형을 사용할 수 있다. 이 열거형으로 원하는 인터벌을 지정할 수 있다.

나중에 실행될 클로저의 스케줄을 짜기위해, 시간 인터벌과함께 asyncAfter(deadline:excute:) 메소드를 사용하자.
globalDefault.asyncAfter(deadline: .now() + .seconds(5)) {
   print("After 5 seconds")
}
같은 클로저를 여러번 순회하면서 실행시켜야 한다면(dispatch_apply를 사용하는 것과 같이), concurrentPerform(iterations:execute:) 메소드를 사용할 수 있다. 그러나 현재 큐의 맥락중에 가능하다면 이 클로저는 동시에 실행된다는 것을 알고 있어야 한다. 그러니 동시성을 지원하는 큐에서동작하는 sync 호출이나 async 호출에 항상 concurrentPerform(iterations:execute:) 호출을 넣어야 함을 기억하자.
globalDefault.sync {
     DispatchQueue.concurrentPerform(iterations: 5) {
       print("\($0) times")
   }
}
큐가 정상적으로 생성되는데 클로저를 처리할 준비를 하는동안 ,필요한 것을 할 수 있게 설정할 수 있다.
let inactiveQueue = DispatchQueue(label: "com.uraimo.inactiveQueue", attributes: [.concurrent, .initiallyInactive])
inactiveQueue.async {
   print("Done!")
}

print("Not yet...")
inactiveQueue.activate()
print("Gone!")
하나 이상의 속성을 지정하는 것은 처음이지만, 여러분도 볼 수 있듯, 필요하면 배열로 여러 속성들을 추가할 수 있다.

작업의 실행은 DispatchObject에서 상속한 메소드로 중단되거나 잠시 멈출 수 있다.
inactiveQueue.suspend()
inactiveQueue.resume()
setTarget(queue:)는 비활성 큐의 우선순위 구성에만 쓰이는데(이것을 활성 큐에 사용하면 크레쉬가 난다), 이 메소드도 사용할 수 있다. 이 메소드를 호출하면, 큐의 우선순위가 파라미터로 주어진 큐의 우선순위와 같아진다. 

Barriers
특정 큐에 (서로다른 지속시관과 함께) 일련의 클로저를 넣었지만, 이제 당신은 이전의 비동기 작업이 모두 완료되고나서 그 작업을 실행시키고 싶다. 이를위해 barriers를 사용할 수 있다.

우리가 이전에 만들었던 동시적 큐에 5개의 테스크(1초에서 5초까지 sleep하는 테스크)를 추가하고, 다른 작업이 완료되면 뭔가를 출력하기위해 barrier를 사용해보자. 마지막 async 호출에 DispatchWorkItemFlags.barrier를 지정할 것이다.
globalDefault.sync {
    DispatchQueue.concurrentPerform(iterations: 5) { (id:Int) in
       sleep(UInt32(id) + 1)
       print("Async on globalDefault, 5 times: " + String(id))
    }
}

globalDefault.async (flags: .barrier) {
   print("All 5 concurrent tasks completed")
}

싱글톤과 Dispatch_once
이미 알고 있을지도 모르겠지만 스위프트 3은 dispatch_once와 동일한 함수가 없다. 이 함수는 싱글톤을 스레드-세이프하게 만드는데 많이 사용되었었다.

운좋게도 스위프트는 전역변수가 atomic하게 초기화됨을 보장한다. 그리고 상수가 한번 초기화되고나면 값을 바꿀 수 없다는 점을 생각해본다면, 이 두 프로퍼티를 이용한 전역 상수는 싱글톤을 쉽게 구현하기에 좋은 후보자가 될 것이다.
final class Singleton {

   public static let sharedInstance: Singleton = Singleton()

   private init() { }

   // ...
}
우리 클래스를 final로 선언하여 상속할 수 없게 만들고, 지정된 생성자를 private으로 만들어서 이 오브젝트의 인스턴스를 추가로 생성하지 못하게 만든다. public static 상수는 싱글톤에서 들어가는 부분이고, 이것은 단 하나의 공유된 인스턴스를 찾는데 사용될 것이다.

한번만 실행될 코드블럭을 만들때도 똑같이하면 된다.
func runMe() {
   struct Inner {
       staticlet i: () = {
           print("Once!")
       }()
   }
   Inner.i
}

runMe()
runMe() // Constant already initialized
runMe() // Constant already initialized
가독성은 떨어지지만 동작은 한다. 한번만 쓰이기위한 코드이면 수용할 수 있는 구현이다.

그러나 만약 그 기능과 dispatch_once API를 정확하게 복제해야한다면, 동기화된 블럭 섹션에서 한 익스텐션으로 표현하듯 처음부터 구현해야한다(But if we need to replicate exactly the functionality and API of dispatch_once we need to implement it from scratch, as described in the synchronized blocks section with an extension).
import Foundation public extension DispatchQueue {

   private static var onceTokens = [Int] ()
   private static var internalQueue = DispatchQueue(label: "dispatchqueue.once")

   public class func once(token: Int, closure: (Void) -> Void) {
       internalQueue.sync {
           if onceTokens.contains(token) {
               return
           } else {
               onceTokens.append(token)
           }
           closure()
       }
   }
}

let t = 1
DispatchQueue.once(token: t) {
   print("only once!")
}
DispatchQueue.once(token: t) {
   print("Two times!?")
}
DispatchQueue.once(token: t) {
   print("Three times!!?")
}
예상한대로 세개중 첫번째 클로저만 실제 호출될 것이다.

대신 여러분의 플랫폼에서 쓸 수 있다면 objc_sync_enterobjc_sync_exit를 사용하여 약간 더 좋은 성능을 낼 수도 있다.
import Foundation

public extension DispatchQueue {

   private static var _onceTokens = [Int] ()

   public class func once(token: Int, closure: (Void) -> Void) {
       objc_sync_enter(self);
       defer { objc_sync_exit(self) }
       if _onceTokens.contains(token) {
           return
       } else {
           _onceTokens.append(token)
       }
       closure()
   }
}

Dispatch Groups
(다른 큐에 추가할지라도) 여러 테스크를 가지고 있으면서 그것들의 완료를 기다린다면 dispatch group으로 그룹화할 수 있다.

예제를 보면 syncasync 호출로 한 테스크를 직접 특정 그룹에 추가할 수 있다.
let mygroup = DispatchGroup()

for i in 0..<5 {
   globalDefault.async(group: mygroup) {
       sleep(UInt32(i))
       print("Group async on globalDefault:" + String(i))
   }
}
이 테스크는 globalDefault에서 실행되지만 mygroup 완료를위한 핸들러를 등록할 수 있다. 이 핸들러는 모든 테스크가 완료되면 우리가 원하는 큐에서 한 클로저를 실행시킬 것이다. wait() 메소드는 블럭킹 지연(blocking wait)을 실행시키는데 사용할 수 있다.
print("Waiting for completion...")
mygroup.notify(queue: globalDefault) {
   print("Notify received, done waiting.")
}
mygroup.wait()
print("Done waiting.")
그룹으로 작업을 추적할 수 있는 또다른 방법이 있다. 큐에서 호출할 때 그룹을 지정하는 것 대신, 직전 그룹을 들어가고(enter) 나오도록(leave) 설정하는 것이다.
for i in 0..<5 {
   mygroup.enter()
   sleep(UInt32(i))
   print("Group sync on MAINQ:" + String(i))
   mygroup.leave()
}

Dispatch Work Items
클로저는 큐에서 실행될 작업을 지정해주는 일만 하는것이 아니라, 그것의 실행 상태를 계속 추적할 수 있도록 컨테이너 타입도 필요하다. 이를위해 DispatchWorkItem이 사용된다. 클로저를 받는 모든 메소드는 work item 종류를 가진다.

work item은 스레드풀의 큐에의해 실행될 클로저를 캡슐화하는데, perform() 메소드를 호출한다.
let workItem = DispatchWorkItem {
   print("Done!")
}
workItem.perform()
그리고 WorkItems은 다른 유용한 메소드를 제공한다. notify는 특정 큐에서 완료될때 클로저를 실행시킨다.
workItem.notify(queue: DispatchQueue.main) {
   print("Notify on Main Queue!")
}
defaultQueue.async(execute: workItem)
클로저가 실행될때까지 기다리게 할 수도 있고, cancel 메소드를 써서 큐가 실행하려하기 전에 제거하라고 알릴 수도 있다.
print("Waiting for work item...")
workItem.wait()
print("Done waiting.")
workItem.cancel()
여기서 한가지 알아야할 중요한 사실은 wait()는 완료를 기다리기위해 현재 스래드를 블럭하지 않고 큐 안에서 바로 이전의 work item들의 우선순위를 올려, 이 특정 item을 가능한 빨리 완료시키려 한다는 점이다.

Dispatch Semaphores
dispatch semaphore는 락이다. 이것은 카운터의 현재 값에따라 하나 이상의 스레드가 락을 취득할 수 있게 해준다.

카운터(세마포어를 취득할대마다 감소시킴)가 0이 될때 스레드는 세마포어를 wait한다.

세마포어에 접근하기위한 슬롯은 대기중인 스레드 호출 signal에대해 열리는데, 이 signal은 카운터를 증가시킨다.

예제를 보자.
let sem = DispatchSemaphore(value: 2)
// The semaphore will be held by groups of two pool threads
globalDefault.sync {
   DispatchQueue.concurrentPerform(iterations: 10) { (id:Int) in
       sem.wait(timeout: DispatchTime.distantFuture)
       sleep(1)
       print(String(id) + " acquired semaphore.")
       sem.signal()
   }
}

Dispatch Assertions
스위프트 3은 현재 실행 맥락안에서 assertion을 실행시킬 수 있는 함수를 소개했다. 이것은 원했던 큐에서 크롤저가 실행되고 있는지 확인할 수 있게 한다. 우리는 DispatchPredicate 열거형의 3가지 case로 만들 수 있다. .onQuquq는 특정 큐에 있는지 확인한다. .notOnQueue는 그 반대를 확인한다. .onQueueAsBarrier는 현재 클로저나 work item이 한 큐의 barrier로 동작하고 있는지 확인한다.
dispatchPrecondition(condition: .notOnQueue(mainQueue))
dispatchPrecondition(condition: .onQueue(queue))
이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

Dispatch Sources
dispatch sources는 이벤트 핸들러를 사용하는 이벤트(커널 시그널이나 시스템, 파일, 소켓)와 관련된 시스템단의 비동기 이벤트를 편리하게 처리할 수 있게 해준다.

몇가지 종류의 Dispatch Sources를 사용할 수 있는데, 아래처럼 묶을 수 있다.
  • Timer Dispatch Sources: 시간내에 혹은 주기적인 이벤트에서 특정 시점에 이벤트를 만드는데 사용된다(DispatchSouceTimer).
  • Signal Dispatch Souces: UNIX 시그널을 다루는데 사용된다(DispatchSourceSignal).
  • Memory Dispatch Sources: 메모리 사용 상태 관련 알림을 등록하는데 사용된다(DispatchSourceMemoryPressure).
  • Discriptor Dispatch Sources: 파일이나 소캣 관련 여러 이벤트를 등록하는데 사용된다(DispatchSourceFileSystemObject, DispatchSourceRead, DispatchSourceWrite).
  • Process Dispatch Sources: 어떤 이벤트에대해 그들의 실행 상태 관련 외부 프로세스를 모니터링하는데 사용된다(DispatchSourceProcess)
  • Mach related Dispatch Source: Mach 커널의 IPC 기능과 관련된 이벤트를 처리하는데 사용된다(DispatchSourceMachReceive, DispatchSourceMachSend).
그리고 필요시 여러분만의 Dispatch Sources를 만들 수도 있다. 모든 Dispatch Sources는 DispatchSourceProtocol을 따르며, 이 프로토콜은 핸들러를 등록하고 Dispatch Sources의 활성 상태를 수정하는데 필요한 기본 오퍼레이션을 정의한다.

이 오브젝트를 어떻게 사용하는지에대한 이해를 돕기위해 DispatchSourceTimer 예제를 한번 보자.

Sources는 DispatchSource가 제공하는 실용적인 메소드로 생성할 수 있다. 이 단편코드에서는 makeTimerSource를 사용하여 핸들러 실행하는데 사용하고 싶은 dispatch 큐를 지정할 것이다.

Timer Sources는 다른 파라미터가 없으므로 큐만 지정하면 source를 생성할 수 있다. 곧 보겠지만, 여러 이벤트를 처리할 수 있는 dispatch source는 항상 이벤트 식별자를 지정해야 할 것이다.
let t = DispatchSource.makeTimerSource(queue: DispatchQueue.global())
t.setEventHandler{ print("!") }
t.scheduleOneshot(deadline: .now() + .seconds(5), leeway: .nanoseconds(0))
t.activate()
source가 생성되면 setEventHandler(closure:)로 이벤트 핸들러를 등록하고, 다른 설정이 필요없으면 active()로 dispatch source를 켜자(이전에 libDispatch는 resume()을 사용해야 했다).

Timer Sources는 오브젝트가 전달할 이벤트에 어떤 종류의 시간을 설정할지에대한 추가적인 단계가 필요하다. 위 예제에서 우리는 엄격한 시간제한 등록 후 5초 딜레이될 이벤트를 정의하고 있다.

이벤트를 전달하기위한 오브젝트도 설정할 수 있는데, Timer 오브젝트로 하는 것처럼 가능하다.
t.scheduleRepeating(deadline: .now(), interval: .seconds(5), leeway: .seconds(1))
dispatch source로 작업을 끝내고나서 이벤트 전달을 완전히 멈추려면, 이밴트 소스를 중지시키는 cancel()을 호출하고, 핸들러를 설정했다면 취소를 호출한 뒤, 핸들러를 등록 해제하는것처럼 최종 정리 작업을 실행한다.
t.cancel()
handleRead() 함수는 소켓에 들어오는 데이터 버퍼에서 새로운 바이트를 사용할 수 있게 되면 전용 큐에서 호출하게 될 것이다. Kitura도 버퍼로된 쓰기를 위해 WriteSource를 사용하는데 쓰기 속도(pace)를 효율적으로 맞추기위해 dispatch source 이벤트를 사용하며, 소켓 채널이 보낼 준비가 되는대로 새로운 바이트를 쓴다. I/O를 할때 read/write dispatch sources는 보통 *NIX 플랫폼에서 사용하는 저수준 API보다 더 좋은 고수준의 대안이 될 수 있다.

나머지 source 타입들도 비슷하게 동작한다. 사용할 수 있는 모든 항목들은 libDispatch의 문서에서 확인할 수 있지만, 그 중 Mach source나 memory pressure source 같은 몇몇은 다윈 플랫폼에서만 동작한다는 것을 기억하자.

Operations과 OperationQueues
Operation Queues에대해 간단히 이야기해보자. 이것은 GCD의 상위에 탑재된 추가 API이다. 이것은 동시적인 큐와 모델 테스크를 오퍼레이션으로 사용하고, 취소하기도 쉬우며, 다른 오퍼레이션이 완료됨에따라 그들의 실행을 가질 수 있다.

Operations은 실행 순서를 정하는 우선순위를 가질 수 있다. 그리고 이것은 OperationQueues에 추가되어 비동기적으로 실행된다.

기본적인 예제를 보자.
var queue = OperationQueue()
queue.name = "My Custom Queue"
queue.maxConcurrentOperationCount = 2

var mainqueue = OperationQueue.main //Refers to the queue of the main thread

queue.addOperation{
   print("Op1")
}
queue.addOperation{
   print("Op2")
}
Block Operation 오브젝트를 생성하여 큐에 넣기전에 설절할 수도 있고, 필요하면 이런 종류의 오퍼레이션에 한개 이상의 클로저를 넣을 수도 있다.

target과 selector로 오퍼레이션을 생성하는 NSInvocationOperation은 스위프트에서는 사용할 수 없다.
var op3 = BlockOperation(block: {
   print("Op3")
})
op3.queuePriority = .veryHigh
op3.completionBlock = {
   if op3.isCancelled {
       print("Someone cancelled me.")
   }
   print("Completed Op3")
}

var op4 = BlockOperation {
   print("Op4 always after Op3")
   OperationQueue.main.addOperation {
       print("I'm on main queue!")
   }
}
Operation은 우선순위(priority)와 완료 클로저를 가질 수 있다. 여기서 이 클로저는 메인 클로저가 완료되면 실행될 것이다.

op4에서 op3까지 의존성(dependency)을 추가할 수 있으므로 op4op3의 완료를 기다렸다가 실행될 것이다.
op4.addDependency(op3)

queue.addOperation(op4)  // op3 will complete before op4, always
queue.addOperation(op3)
removeDependency(operation:)으로 의존성을 제거할 수 있고, 이 의존성들은 public으로 접근할 수 있는 dependencies 배열에 담겨있다.

한 오퍼레이션의 현재 상태는 특정 프로퍼티를 이용해서 알 수 있다.
op3.isReady       //Ready for execution?
op3.isExecuting   //Executing now?
op3.isFinished    //Finished naturally or cancelled?
op3.isCancelled    //Manually cancelled?
cancelAllOperations 메소드를 호출하여 한 큐에 있는 모든 오퍼레이션 프레젠트를 취소할 수 있다. 이 메소드는 큐에 남아있는 오퍼레이션의 isCancelled 플레그를 on으로 설정한다. 한 오퍼레이션을 취소할때는 그것의 cancel 메소드를 호출하면 된다.
queue.cancelAllOperations()

op3.cancel()
어느 큐에서 실행될지 스케줄이 잡힌 뒤에 오퍼레이션이 취소되었다면, 그 오퍼레이션 안에서 실행을 스킵하기위해 isCancelled 프로퍼티를 확인할 것을 추천한다.

이제 마침내 당신은 한 오퍼레이션 큐에서 새로운 오퍼레이션 실행도 멈출 수 있게 되었다(현재 실행되고 있는 오퍼레이션에는 영향을 주지 않는다).
queue.isSuspended = true
이 글이나 다른 글의 Playground는 GitHub 이나 압축된파일에서 이용할 수 있다.

마지막 생각들
이 글은 오늘날 스위프트에서 사용할 수 있는 외브 프레임워크를 이용하여, 동시성 관점에서 무엇을 할 수 있는지 좋은 정리를 제공할 것이다.

Part2는 언어 기능의 관점에서 외부 라이브러리에 의존하지 않고 "네이티브한" 동시성을 처리하는 것에 대해 초점을 맞출 것이다. 오늘날 이미 있는 몇가지 오픈소스 구현의 도움을 받아 몇가지 흥미로운 패러다임을 설명할 것이다.

이 두개의 글이 동시성의 세계에 입문하기 좋게 만들어주고, 이것이 스위프트 5(희망하길)에서 고려되기 시작할때 swift-evolution 메일링 리스트의 토론을 이해하고 참여하는데 도움이 될 것이다.

동시성이나 스위프트에 더 흥미로운 자료는 Cocoa With Love 블로그에서 확인할 수 있다.

이 글이 마음에 든다면 나에게 윗해달라!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: Fun with String Interpolation

Update: 2017.02.02 API 설계 가이에따라 escape(unsafe:)가 escaping(unsafe:)로 바뀌었다.

스위프트 프로그래머가 제일 처음 배우는것 중 하나가 문자열 보간(string interpolation)이거나, 혹은 변수와 수식을 리터럴 문자열로 만드는 것이다.
let a = 6
let b = 12
let message = "\(a) × \(b) = \(a * b)"
// → "6 × 12 = 72"
보관된 문자열로 여러분의 커스텀 타입을 초기화할때, 문자열 보간이 하는 일을 커스텀할 수 있다는 사실은 잘 모를것이다. 이 글이 그것에 대한 내용이다.

이스케이핑 언세이프 문자열(Escaping unsafe strings)
나는 (아마) 언세이프한 사용자 입력값을 세니타이즈(sanitize: 문자열을 안전하게 만들기위해 처리하는 과정) 하기위해서 이스케이프된 문자열 작업을 하려 한다.

동기
사용자로부터 받은 문자처럼 외부로부터 받은 데이터를 다루는 프로그램이 있는데, 보장된 프로그램을 만들기 위해 외부 데이터가 공격경로(attack vector)로 사용될 수 있는 부분을 반드시 안전에 대비되어 있엉 한다. 예를들어 공격자가 계정을 등록할때 사용자 이름란에 <script>를 넣을 수 있다. 만약 웹앱이 그대로 텍스트를 렌더링하면 공격자는 제어불능의 스크립트를 실행시킬 수 있게 해주는 셈이 되는데, 공격자가 다른 사용자의 쿠키를 훔칠 수 있게 할 수도 있다. 이스케이프하는 HTML 태그처럼 외부로부터 받은 모든 인풋들을 세니타이즈 해야하는 이유이다.

나는 몇주전에 Joel Spolsky가 쓴 트윗를 발견했는데, 여기에는 2005년도에 그가 쓴 Making Wrong Code Look Wrong 글이 링크되있었다. Spolsky는 이 글에서 변수 네이밍의 특정 스타일에대해 이야기하며, 한 변수가 세이프 혹은 언세이프(이케이프 되지 않은) 문자를 가진다면, 프로그래머들이 따라오기 더 쉬워진다고 말한다. 좋은 읽을거리이고 Hyngrian Notation의 역사 이야기도 담고있다.

모든 문자열이 동등하지 않다.
그러나 네이밍 규약을 따르려고 하는것보다는 스위프트처럼 강타입 언어에서 이 문제를 해결하는게 (더 안전하고) 더 나은 것이며, 이것이 타입 시스템의 이점을 취할 수 있는 것이다.

"언세이프 문자열"과 "세이프 문자열"은 근본적으로 너무 달라서 이것들을 서로 다르게 다루어야한다. 그러나 이 둘은 같은 String 타입으로 사용하는 경향이 있는데, 문제의 근원지는 바로 이것이다. 이제 이 개념을 배우기위해 타입을 분리시켜보자. 나는 이것들을 UnsafeString 그리고 SantinizedHTML이라 부르고 있다. 각각은 내부 저장소로 String을 사용한다.
/// An unescaped string from a potentially unsafe
/// source (such as user input)
struct UnsafeString {
    var value: String
}

/// A string that either comes from a safe source
/// (e.g. a string literal in the source code)
/// or has been escaped.
struct SanitizedHTML {
    fileprivate(set) var value: String

    init(unsafe input: UnsafeString) {
        value = SanitizedHTML.escaping(unsafe: input.value)
    }
}
또한 생성자가 UnsafeString으로부터 SantinizedHTML을 만들도록 하고, 이 과정에서 인풋을 이스케이핑한다. 이 escape 메소드는 모든 꺾쇠를 해당하는 HTML 요소로 대치한다. 아주 간단해 보이는 예제이지만 실상은 조금 복잡할 수도 있다.
import Foundation // required for String.replacingOccurrences(of:with:)

extension SanitizedHTML {
    /// Escapes a string.
    fileprivate static func escaping(unsafe input: String) -> String {
        return input
            .replacingOccurrences(of: "<", with: "&lt;")
            .replacingOccurrences(of: ">", with: "&gt;")
    }
}
두 타입의 value 프로퍼티에대한 선언의 차이를 주목하자. UnsafeString의경우 종종 값타입으로 필요할 것이기 때문에 valuevar이다. 이렇게하여 세이프티(단순 소유 모델(ownership model)은 값이 대입될때 복사가 일어남) 값 타입들은 포기하지 않는다. 반면 SantinizedHTMLvalue 프로퍼티는 fileprivate(set)으로 수정되는데, 모든 제3자는 그 타입의 공식적인 API를 우회하여 수정할 수 없고, 언이스케이프된 문자열 값을 주입하기 위함이다. 이것은 타입의 그 구현으로부터 계속 변경을 허락받아야한다.

세니타이즈한 문자열에 새로운 것을 붙일 수 있는 방법을 만들어보자. append(_:) 메소드를위해 두가지 오버로드를 제공한다. 하나는 UnsafeString을 받고, 다른 하나는 SantinizedHTML을 받는다. 나중에는 이것이 이미 세니타이즈함을 보장할 것이라서 이것을 다시 이스케이프할 필요가 없다.
extension SanitizedHTML {
    mutating func append(_ other: SanitizedHTML) {
        // other is already safe
        value.append(other.value)
    }

    mutating func append(_ other: UnsafeString) {
        let sanitized = SanitizedHTML(unsafe: other)
        append(sanitized)
    }
}

안전한 자료로부터 이스케이프하지 않은 입력 받기
또한 우리는 이미 안전하다고 알고있는 것을 SantinizedHTML에 추가하는 방법도 필요하다. <h1>태그나 <p>태그(혹은 <script>태그까지도)를 HTML 템플릿에서 이스케이프되도록 원하지 않을 수도 있다. 문자열 리터럴(literals), 즉 상수 문자열을 통해 그렇게 할 수 있다. 코드에서의 문자열 리터럴은 항상 안전하다고 가정할 수 있다. 여러분의 소스코드가 보장되있다면 어떤 경우라도 장담할 수 있다(all bets are off in any case).

여러분의 타입을 문자열 리터럴과 함께 초기화시키는 기능을 넣기위해, ExpressibleByStringLiteral 프로토콜을 따르게 한다. 이 프로토콜은 3개의 생성자를 필요로한다. 그래도 제네럴하게 각각에게 전달할 수 있어서 생각보다 쉽게 만들 수 있다.
// Initialization with a string literal should not escape the input.

extension SanitizedHTML: ExpressibleByStringLiteral {

    init(stringLiteral value: String) {

        self.value = value

    }

    init(unicodeScalarLiteral value: String) {

        self.init(stringLiteral: value)

    }

    init(extendedGraphemeClusterLiteral value: String) {

        self.init(stringLiteral: value)

    }

}
우리가 여기에 있는 동안 UnsafeString의 능력과 같게 만드는 것이 좋아보인다. 이 구현은 SantinizedHTML의 것과 동일하다.
extension UnsafeString: ExpressibleByStringLiteral {

    // Same implementation, see above

}
이제 문자열 리터럴로 UnsafeString 값과 SantinizedHTML 값을 생성할 수 있다. 여기에는 타입을 지시해주어야하는데, String값을 받을 수도 있기 때문이다.
let userInput: UnsafeString = "<script>alert('P0wn3d');</script>"

var sanitized: SanitizedHTML = "<strong>Name:</strong> "

sanitized.append(userInput)

sanitized.value

// → "<strong>Name:</strong> &lt;script&gt;alert('P0wn3d');&lt;/script&gt;"
이제 됐다! 세이프 문자열 리터럴은 이스케이프하지 않았지만 언세이프한 사용자 입력이 있다.

문자열 보간법
이제 기본적인 것들이 해결되었다. 모든 렌더링 API가 입력받을때, SantinizedHTML만 받을 수 있어야 한다면, 그 새로운 타입을 언이스케이프된 문자열을 렌더링하지 못하게 만들어야한다.

그러나 아래처럼 문자열 보간법을 통해 초기화할 수 있다면 SantinizedHTML을 사용하여 편리하게 만들 수 있다.
let sanitized2: SanitizedHTML = "<strong>Name:</strong> \(userInput)"

// error: cannot convert value of type 'String' to specified type 'SanitizedHTML'
현재, 이것은 컴파일타임 에러의 결과를 내뱉는다. 유용하게 만드려면 바로 잡아야하는데 보간법 문자열 부분은 안전하고 \()안의 부분은 안전하지 않게 하는 그런 작업을 수행해야한다.

이상한 ExpressibleByStringInterpolation 프로토콜
우리가 다음에 보려하는, 이것이 동작할 수 있다. 표준 라이브러리에는 우리가 따라야할 ExpressibleByStringInterpolation이라는 프로토콜을 제공하고 있다. 현재(Swift3에서) 이 프로토콜은 디프리케이트 되었다. 스위프트 팀이 이것을 "잘못된 설계한계가있다"고 인지했기 때문이다. 이 말은, 우리가 이것을 사용하면 나중에 경고를 보게될 것이고, 스위프트4나 그 후에 어떤 새로운 API(더 강력해 질 것으로 보인다)로 대체되어 우리 코드를 고칠 준비를 해야한다. 그전까지 현재 API가 직관적이진 않지만 놀랍도록 유용하게 쓰일것이다.

이 프로토콜은 두가지 생성자를 필요로한다. 문자열 보간법은 두가지 단계로 처리된다.
1. 먼저 첫번째 단계에서는, 컴파일러가 보간법 문자열을 문자열 리터럴의 세그먼트와 변수 표현식으로 분디한다. 그리고 세그먼트들은 init<T>(stringInterpolationSegment:) 생성자로 보내진다.

그 세그먼트들은 항상 문자열 리터럴 과변수 표현식을 번갈아간다. 그리고 첫번째 세그먼트는 항상 리터럴(보간법 문자열이 한 변수로 시작하면 아마 비어있을 것이다)이다. 두 변수 표현식이 보간법 문자열 안에서 직접 붙어있으면 다시 빈 리터럴 세그먼트가 그 사이에 들어갈 것이다.

보간법 문자를 위한 세그먼트들에대한 예시이다.
"\(name) says \(greeting1)\(greeting2)!"
이것이 아래처럼 된다.
""

name

" says "

greeting1

""

greeting2

"!"
이런 동작이 현재에 공식적으로 문서에 나와있진 않을거라 생각된다.

2. 그 두번째 보간법 단계는, 보간법 문자열에서 나타난 그 요구대로 첫번째 생성자의 결과물을 두번째 생성자인 init(stringInterpolation:)으로 보낸다.

이런 세그먼트들의 순서의 특징을 이용하여, 짝수번째(0을 포함한)의 세그먼트들은 항상 문자열 리터럴이니 세이프하고, 반면 홀수번째의 세그먼트들은 언세이프하니 반드시 이스케이프 해주어야한다.

ExpressibleByStringInterpolation을 따르기
이 API에서 이상한 점은 보간법 과정에서 첫 단계에서 타입을 따르는 생성자를 사용하는 것이다. 이 말은 각기 다른 보간법 세그먼트에서 유효한 SantinizedHTML 값을 만들어서 오직 두번째 단계에서 이 세그먼트들을 완성된 값으로 합쳐야만한다. 우리는 SantinizedHTML 안에 각 세그먼트를 담아두어야하니, 타입 정의에따라 새로운 프로퍼티를 추가해보자.
struct SanitizedHTML {

    fileprivate(set) var value: String

    // Required for string interpolation processing

    fileprivate var interpolationSegment: Any? = nil

    ...

}
 이 프로퍼티의 타입은 Optional<Any>이다. Any인 이유는 보간법 문자열로 들어온 어떤 값이라도 변환하지 않은채 가지고 있기 위함이고, Optional의 이유는 문자열 보간법 처리중에 이것이 필요하기 때문이다. 모든 예외는 nil이 될것이다.

아래 보간법의 모든 단계를 담은 전체 구현이 있다.
extension SanitizedHTML: ExpressibleByStringInterpolation {

    // Step 1

    public init<T>(stringInterpolationSegment expr: T) {

        // Store the segment

        interpolationSegment = expr

        // Dummy initialization, this is never used

        value = ""

    }


    // Step 2

    public init(stringInterpolation segments: SanitizedHTML...) {

        let stringSegments = segments.enumerated()

            .map { index, segment -> String in

                guard let segment = segment.interpolationSegment else {

                    fatalError("Invalid interpolation sequence")

                }

                if index % 2 == 0 {

                    // Even indices are literal segments

                    // and thus already safe.

                    if let string = segment as? String {

                        return string

                    } else {

                        return String(describing: segment)

                    }

                } else {

                    // Odd indices are variable expressions

                    switch segment {

                    case let safe as SanitizedHTML:

                        // Already safe

                        return safe.value

                    case let unsafe as UnsafeString:

                        return SanitizedHTML.escaping(unsafe: unsafe.value)

                    default:

                        // All other types are treated as unsafe too.

                        let unsafe = UnsafeString(value: String(describing: segment))

                        return SanitizedHTML(unsafe: unsafe).value

                    }

                }

        }

        value = stringSegments.joined()

    }

}

1단계 생성자는 받은 값을 저장하고 실제 변환인 2단계로 넘어간다. 생성자는 반드시 모든 프로퍼티를 초기화해주어야 하므로, 2단계까지 계속 그 인스턴스가 살아있더라해도 value 프로퍼티를 위한 더미 값을 제공해주어야한다.

2단계에서는 SantinizedHTML값의 배열로 세그먼트를 받는다. 우리의 목적은 이 값들을 문자열로 변환하는 것이기 때문에, 이 과정에서 변수 표현식 세그먼트들을 이스케이프한다. 그리고 문자열들을 하나로 합쳐 우리 value 프로퍼티에 결과를 저장한다. 배열과 그것의 인덱스들을 연결하고 짝수 인덱스가 세이프하다는 정보를 이용한다. 또한 우리는 이미 짝수 인덱스들이 문자열이라는 것을 알지만, 안전을 위해 필요에따라 세그먼트를 확인하여 문자열로 변환하게 만든다.

홀수 인덱스의 경우, 3가지 상황으로 나뉜다. 첫번째, 세그먼트가 이미 SantinizedHTML 값이라면 다시 이스케이프하지 말고 바로 그 값을 반환한다. 두번째, 세그먼트가 UnsafeString이면 이스케이프하고 그 결과를 반환한다. 세번째, 세그먼트가 그 밖의 타입(String이나 Int)이면 먼저 UnsafeString으로 만들어서 이스케이프한다.

더 확장하여, 연속된 SantinizedHTML 값을 위한 커스텀 로직을 추가할 수도 있지만, 이것만으로도 충분히 강력해 보인다. 위의 것으로 예제를 시험해보자.
let sanitized2: SanitizedHTML = "<strong>Name:</strong> \(userInput)"

sanitized2.value

// → "<strong>Name:</strong> &lt;script&gt;alert('P0wn3d');&lt;/script&gt;"
우리가 원하는데로, 문자열 리터럴은 그대로 보내지고 변수 표현식은 이스케이프될 것이다. 멋지다!

이게다다. 새로운 타입을 더 편하게 사용하고 싶으면, 다음 단계로 CustomStringConvertible 혹은/그리고 CustomDebugStringConvertible를 따르게 할 수 있는데, 이것은 여러분에게 남겨두겠다.

모든 코드는 Gist에 올려놓았다. 플레이그라운드에 붙여넣어서 가지고 놀아보자.

결론
여러분의 타입을 어떻게 보간법 문자열로 번역하는지는 특히 DSLs에게 강력한 기능이다. 이번에 이 기술로 SQL 쿼리를 만들거나 다국어 문자열을 만드는데 적용해볼 수 있겠다(Brent Royal-Gordon이 다국어 문자열을 만든 구현 예시이다). 구성요소로 만든 문자열을 쓰는 모든 작업은 아마 이것이 도움이 되리라 믿는다.

이 API가 완벽하진 않아도 스위프트에서 이것이 가능하게 만들어 놓았다는 점이 매우 멋지다. 문자열 보간법 API가 나중 스위프트 버전에서 바뀌게되면 더 표현력 있고 사용하기 쉬워질 것이라 생각된다.

이 글에 아이디어를 제공해준 Bandes-Storch에게 특별히 감사하다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret

제목: Swift: UIView Animation Syntax Sugar
클로저가 못생기게 엮어버리기 때문에..


들어보았을지도 모르겠지만, 여러분의 스위프트 코드에서 클로저는 유용하게 쓰인다. 이것은 일급(first-class) 종류이고, API의 끝에 있을때는 후행 클로저(trailing closure)로 만들 수 있다. 그리고 이제는 디폴트 @noescape이며 참조 사이클 싸움에서 엄청난 승리이다(now they’re @noescape by default which is a massive win in the fight against reference cycles).

그러나 우리는 이따금씩 한개 이상의 클로저를 전달해야하는 API들과 함께 작업하게 되는데, 한개 이상의 클로저는 클로저의 아름다운 기능을 덜 매력적인 기능으로 만들어버리기도 한다. UIView를 한번 보자,
class func animate(withDuration duration: TimeInterval,
    animations: @escaping () -> Void,
    completion: ((Bool) -> Void)? = nil)

후행 클로저
UIView.animate(withDuration: 0.3, animations: {
    // Animations
}) { finished in
    // Compeleted
}
우리는 기존의 클로저와 후행 클로저를 함께 쓸 수 있다. animations:는 여전히 그 파라미터의 타이틀을 가지지만, completion:후행 클로저로 만들어 파라미터 타이틀을 없앴다. 나는 후행 클로저가 이 타입의 문맥에서 API로부터 동떨어진 느낌을 받았다. 아마 그 이유는 API의 닫힘 괄호와 뒤에 따르는 열림 괄호의 내부 클로저 때문이라 생각된다.
}) { finished in // yuck
Note: 후행 클로저가 무엇인지 확신하기 힘들다면, 그것이 무엇이고 어떻게 쓰이는지 설명해놓은 Swift: Syntax Cheat Codes라는 글을 보자.

가독성을 위한 들여쓰기
애니메이션 클로저들이 기본 선언과 같은 선상의 들여쓰기를 하기 때문에 그것에대해 이야기해보자. 최근에 나는 함수형 프로그래밍 쿨피스(kool-aid) 많이 마셨고, 함수형 코드를 작성하는 것에대해 내가 완전히 좋아하는 방법은 뷸렛 포인트(bullet point) 형식으로, 이것은 명령을 열로 나타내는 것이다.
[0, 1, 2, 4, 5, 6]
    .sorted { $0 < $1 }
    .map { $0 * 2 }
    .forEach { print($0) }
두 클로저 API도 당연히 이렇게 된다.
Note: $0 문법이 이해가 안된다면, 그것이 무엇이고 어떻게 쓰이는지 설명해놓은 Swift: Syntax Cheat Codes라는 글을 보자.

못생긴 것을 강제로 아름답게 만들기
UIView.animate(withDuration: 0.3,
    animations: {
        // Animations
    },
    completion: { finished in
        // Compeleted
    })
나는 Xcode의 자동완성에 맞춰서 내 스스로 UIView 애니메이션 API를 이런식으로 배치하는 방법을 함수형 프로그래밍 문법에서 찾아냈고 사용해보기로 했다. 내 개인적인 의견은, 이런 배치가 이전 것보다 더 읽기 좋은데, 많이 귀찮아진다. 이 코드를 복사 붙여넣기 할때마다 들여쓰기는 헝클어지겠지만 스위프트 문제라기 보단 Xcode의 문제로 생각된다.

클로저 전달하기
let animations = {
    // Animate
}
let completion = { (finished: Bool) in
    // Completion
}

UIView.animate(withDuration: 0.3,
               animations: animations,
               completion: completion)
포스팅의 시작 부분에서 말했듯이 스위프트-토피아(스위프트 세상)에서 클로저는 일급(first-class) 종류이다. 이 의미는 클로저를 변수에 할당할수도 있거니와 당연히 전달도 할 수 있다는 뜻이다. 그러나 이 코드는 이전 코드만큼 읽기 좋은지 납득하기는 어렵고, 다른 오브젝트들이 다른 목적으로 이 클로저에 접근할 수 있어서 이 방법은 주저하게 된다. 결국엔 전자의 선택을 할것이다.

해결책
많은 프로그래머들이 그렇게 하듯, "장기적으로 시간을 절약"하고 싶은 타협 아래 현실적인 문제와 관련하여 해결책을 만들고자 한다.
UIView.Animator(duration: 0.3)
    .animations {
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()
위에서 볼 수 있듯, 스위프트의 함수형 프로그래밍 API에서 나온 방식에서 영감을 받는 문법과 구조이다. 우리는 두 클로저의 API를 일련의 고차함수(higher-order fuction)로 바꾸었고, 이제 우리 코드는 훨씬 더 읽기 쉬워졌으며, 우리 코드를 복사/붙여넣기 할 때 컴파일러가 들여쓰기를 도와줄 것이다.
"장기적으로 시간을 단축시킬 것이다"

Animator
UIView.Animator(duration: 0.3)
    .animations {
        // Animations
    }
    .completion { finished in
        // Completion
    }
    .animate()
우리 Animator 타입은 꽤 간단하게도 3가지 프로퍼티를 가진다. duration, 두 클로저, 한 생성자. 그리고 곧 익숙해질 몇몇 함수들이 있다. 반드시 필요한 것은 아니지만 우리 코드의 가독성을 증진하고, 우리가 구현한 후, 여러곳에서 클로저 시그니처를 변경하고자할때 에러를 줄여주는 typealias를 사용하는데, 우리 클로저의 시그니처를 미리 정의하기위해 두 typealias 선언한다.

클로저 프로퍼티들은 가변(mutable)인데, 어디선가 그것을 저장하고 인스턴스로 만들 뒤에 그 값을 바꿀 수 있다. 그러나 외부에서 변경가능한 상황을 막기위해 private로 하였다. 기존의 UIView API처럼 만들기 위해 completion은 옵셔널이지만 animations는 옵셔널이 아니다. 생성자 구현에서 컴파일러가 불평하는것을 막기 위해 클로저 프로퍼티에 디폴트값을 넣었다.
func animations(_ animations: @escaping Animations) -> Self {
    self.animations = animations
    return self
}

func completion(_ completion: @escaping Completion) -> Self {
    self.completion = completion
    return self
}
클로저 행렬 구현은 놀랍도록 간단하다. 하는 일이라곤 특정 클로저 인자를 받아서 그것을 해당 클로저 값에 넣는 것이다.

자기자신을 반환하기
멋진 점은 이 API들이 Self의 인스턴스를 반환한다는 점인데, 이 부분이 바로 마법같은 부분이다. 우리가 Self라 쓰면 행렬방식(sequence-style) API로 만들 수 있기 때문이다.

함수에서 Self를 반환할때, 그 자리에 다른 함수들이 다시 실행될 수 해준다.
let numbers =
    [0, 1, 2, 4, 5, 6]  // Returns Array
    .sorted { $0 < $1 } // Returns Array
    .map { $0 * 2 }     // Returns Array
그러나 행렬에서 마지막 함수가 오브젝트를 반환하면 반드시 어디 변수에 할당하여야한다. 위의 numbers 상수에 할당한 이유이다.

마지막 함수가 Void를 반환하면 실행시에 아무것도 할당하지 않아도 된다.

[0, 1, 2, 4, 5, 6]         // Returns Array
    .sorted { $0 < $1 }    // Returns Array
    .map { $0 * 2 }        // Returns Array
    .forEach { print($0) } // Returns Void

애니메이션하기
func animate() {
    UIView.animate(withDuration: duration,
        animations: animations,
        completion: completion)
}
나의 수많은 방법처럼, 원래있던 API를 감싸는 것으로 깔끔하게 끝난다. 그러나 이것이 나쁜 방법은 아니다. 스위프트는 우리를 '생각하는자(thinker)', '고쳐쓰는자(tinkerer)'라 생각하고 있으며, 우리에게 제공된 툴을 다시 생각해보고 다시 가공하는 프로그래머로서 가능하게 해준다고 나는 확고히 믿고있다.

UIView를 익스텐션하기
extension UIView {
    class Animator { ... }
}
마지막으로 두가지 이유로서 우리 Animator 클래스를 잡아다가 UIView의 익스텐션에 놓는다. 그 이유는 첫째로 UIView의 네임스페이스를 원하기 때문이다. 따라서 우리가 만든 API에 문맥을 만들어준다. 두번째로는 기능이 UIView와 직접적으로 연관되어 홀로 클래스로 존재하는 것은 의미가 없게된다.

옵션
UIView.Animator(duration: 0.3, delay: 0, options: [.autoreverse])
UIView.SpringAnimator(duration: 0.3, delay: 0.2, damping: 0.2, velocity: 0.2, options: [.autoreverse, .curveEaseIn])
애니메이션 API와 작업할때, 여러 옵션 선택사항이 있으니 문서를 확인해보자. 함수에서 디폴트값과 클래스 상속을 통해, SpringAnimator 클래스만큼 Animator는 여러분이 일반적으로 사용할 수 있는 많은 애니메이션 타입을 이제 커버한다.

언제나처럼 여러분이 확인해볼 수 있게 GitHub에 플레이그라운드를 만들어 놓았고, Xcode가 없는 사람들을 위해  Gist에도 담아두었다.

오늘 읽은 글이 마음에 든다면 나의 다른 글 도 확인해보고, 혹은 여러분 프로젝트에 이 방식을 적용시켜보고자 한다면, 나에게 트윗 해주거나 Twitter에서 나를 팔로우해달라. 매우 기분이 좋은 하루가 될것이다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: That One Optional Property

때론 새로운 것을 위해 여러분의 뷰컨트롤러를 수정해야할 필요가 있다. 한가지는 다른것을 야기하는데, 뷰컨트롤러에 옵셔널 변수를 추가하는 자신을 발견할 것이다. 이것은 몇몇 케이스에서 설정될 것이고 몇몇은 아닐 것이다.

내 생각엔 더 자주 일어나고 이것은 결점이있는 방법이다. 여기에는 몇가지 이유가 있다. 첫째로 어떨때만 쓰이는 옵셔널 프로퍼티를가진 클래스는 정체성이 약한 느낌을 가진다. 바꿔 말해보면, 옵셔널 프로퍼티를 추가하면 그 타입의 근본적인 의미가 흐려진다. 첫번째로 이 옵셔널 프로퍼티는 어떤 시멘틱 의미를 고려하지 않는다. 이 타입이 nil이라면 이 오브젝트의 상태가 무엇이라 할 수 있을까? 여러분의 코드를 가볍게 읽고있는 사람들은 어떤 경우에 프로퍼티가 nil이될지, 혹은 그 오브젝트에서 가지는 분기가 무엇인지 말할 수 없을 것이다. 세번째로 코드는 가변으로 계속 될것이다. 한 옵셔널 프로퍼티는 누가지 선택적 프로퍼티로 될것이고, 이것은 세개로 되고, 당신이 알기 전에 미끄러운 경사의 아래에 가있을 것이다. 이 값이 반드시 존재하는지 이 값이 nil인지 표현하고 싶으면 간단한 옵셔널로는 그렇게 할 수 없다.

내가본 모든 코드베이스에서는, 여러분의 뷰컨트롤러에 왜 옵셔널 프로퍼티가 필요한지에대한 두가지 주된 이유를 발견했다. 나는 이 두가지 다를 탐험할 것이고 각 문제를 해결하기위한 더 나은 패턴을 제안할 것이다.

문제의 옵셔널 프로퍼티를 가지는것에대한 첫번째 이유는 이 뷰컨트롤러가 쓰일때마다 오브젝트나 몇 데이터를 반드시 가질 필요가 없을때이다.

최근에 만난 이것에대한 예시는 뷰컨트롤러가 어떤 경우 푸시 노티피케이션으로부터 표시되는 상황이다. 이때 노티피케이션에서 나온 특정 메시지를 보여줘야한다. 이 문제를 해결하기위한 가장 간단한 방법은 옵셔널 문자열 프로퍼티를 추가하는 것이었다.
class LocationViewController: UIViewController {
     //...
     var notificationMessage: String?
     //...
}
뷰컨트롤러에 다른 코드는 어떤 뷰에 할당할지, 어떻게 배치할지등을 결정하기위해 메시지가 있다고 전환했다. 프로퍼티의 선택성은 그냥 문자열의 존재보다 더 표현한다. It had implications in the rest of the view controller and the rest of the view layer.


여기서 더 중요한 점은 문자열이 더이상 거기에 있을지 아니면 없을지 표현하는게 아니게 된다. 이제 뷰컨트롤러 안에있는 표현 스타일이나 모드를 표현한다. 이것이 푸시 노티피케이션의 문맥에서 만들어진 것인가 혹은 일반 브라우징을 통해 만들어진 것인가? 답은 가까스로 관련된 프로퍼티에 있다.

이 문제를 해결하기위해, 이 모드를 모두 명시적으로 만들어야한다. 이 뷰컨트롤러에서 이것은 일급시민이면, 뷰컨트롤러의 그 영향은 더 분명해질것이다.
class LocationViewController: UIViewController {
     //...
     enum Mode {
          case fromNotification(message: String)
          case normal
     }

     var mode: Mode
     //...
}
Sandi Metz가 말한것처럼, 한 특수화는 절때 없다(there's never one specialization). 옵셔널 프로퍼티를 쓰면 그 코드는 이 프로퍼티의 nil 상태에대한 의미를 가지거나 고유의 의미를 가진다고 할 수 없다. 열거형을 사용하면 코드에서 구체화되고 형식화된다.

새로운 열겨형이 Optional 타입 정의의 열거형 형태와 아주 비슷하다는 점을 인지하자.
enum Optional<Wrapped> {
     case some(Wrapped)
     case none
}
그러나 여기에는 몇가지 분명히 다른 유용한점이 존재한다.

첫째로 시멘틱이다. somenone은 추상적이다. normalfromNotification은 그것과 관련된 의미를 가진다. 여러분의 코드를 읽는 사람들은 여러분에게 감사할 것이다.

두전째로 확장성이다. 뷰 컨트롤러에 다른 모드가 추가되면 그것을 완전히 설명하는 더나은 의미를 가진다. 만약 새로운 모드가 두 모드와 절때 겹치지 않으면, 필요한 연관된 데이터가 어떻든 새로운 열거형 케이스를 추가할 수 있다. 새로운 모드가 현재 모드들과 겹친다면, 새로운 열거형이되어 새로운 프로퍼티를 함께할 수 있다. 오브젝트의 상태는 읽기에 더욱 설명가능한 것이 된다. 어떤 선택이든 새 모드를 위해 또다른 옵셔널을 추가하는 것보다는 낫다.

세번째도 확장성이다. LaunchViewController.Mode가 일급 타입이기 때문에 우리는 함수와 계산된 프로퍼티를 추가할 수 있다. 예를들어 노티피케이션 메시지를 잡아두는 레이블의 높이는 아마 그 메시지의 존재에 달렸을 것이다. 따라서 코드를 열거형으로 옮길 수 있다.
extension Mode {
     var notificationLabelHeight: CGFloat {
          switch self {
          case .normal: return 0
          case .fromNotification(let message): return message.size().height
          }
     }
}
더 풍요로운 타입으로 데이터를 옮기는 것은 적은 코드를 투자하여 이 모든 앞단에서의 이득을 제공한다.

옵셔널 프로퍼티를 사용할지도 모르는 두번째 이유는 첫번째 이유의 부분집합이다. 어떤 경우, 옵셔널 프로퍼티는 뷰컨트롤러의 다른 모드를 표현하지 않는데, 코드에서 임시적인 특징을 표현한다. 아직 값을 가지고 있지 않기 때문에 값으로 프로퍼티를 초기화할 수 없다. 이것의 일반적인 예시는 네트워크로부터 뭔가 패치를 하거나, 시스템으로부터 뭔가 긴 검색시간이 걸려 비동기가 필요해서 기다려야 할때이다.
class UserViewController: UIViewController {
     //...

     // will be loaded asynchronously
     var user: User?
     //...
}
이런 종류의 문제는 어떨때 데이터가 배열형식으로 들어오면 준비될 수 있다. 빈 배열로 존재하지 않는 상태를 표현할 수 있기 때문에, 옵셔널을 사용하지 않더라도 이 문제는 여전히 숨어있다. 테이블 뷰 컨트롤러에 빈 상태를 추가하는 힘든 상황의 횟수가 바로 이런 문제의 정도를 나타낸다.

이 문제는 첫번째 문제의 부분집합이기 때문에, 열거형으로 같은 방법으로 해결할 수 있다.
enum LoadingState<Wrapped> {
     case initialcase loading
     case loaded(Wrapped)
     case error(Error)
}
이 솔루션이 동작하는동안, 비동기 상태를 관리하기위한 더 나은 추상화가 있다. 아직 존재하진 않지만 미래의 어느 시점엔 있을 한 값은 Promise에의해 잘 표현된다. 프로미스는 여러분의 유스케이스에따라 여러 이점을 가진다.

먼저, 프로미스는 가변을 허락하지만, 오직 한번만 그리고 오직 결정되지 않을때까지만이다. 프로미스가 한번 변경되면, 다시는 변경할 수 없다. (여러번에걸처 변경해야한다면, Signal이나 Observable이 여러분이 찾던 것일 것이다.) 이 말은 let과같은 시멘틱을 가지지만 여전히 비동기 데이터를 관리한다.

다음으로, 프로미스는 값이 들어왔을때, 없어질 블럭을 추가하는 기능을 가진다. 만약 프로퍼티가 간단한 옵셔널 프로퍼티를 남긴다면, 추가된 블럭들은 didSet 프로퍼티 옵저버에서 코드와 동일하다. 그러나 프로미스 블럭은 하나 이상 추가할 수 있고, 클래스 어디에서든 추가될 수 있기 때문에 어욱 강력하다. 게다가 프로미스가 이미 값으로 채워져 있을때 추가한다면 그들은 즉시 실행될것이다.

마짐가으로 필요에따라 프로미스는 에러도 처리한다. 특정 종류의 비동기 데이터에대해, 이것이 중요하며, 이것으로부터 자유로울 것이다.

Promise 라이브러리를 사용하고 있다면 빈 생성자로 미결정된 Promise를 생성할 수 있고, fulfill 메소드로 언제든지 채울 수 있다.

옵셔널 프로퍼티를 사용할 때는, 가끔 예전에 보이지 않던 것들이 드러낸다. 뷰컨트롤러에 옵셔널 파라미터를 추가하는 자신을 발견할 때 스스로에게 물어보자. 이 옵셔널이 정말로 의미하는바는 무엇인가? 이 데이터를 표현할 더 좋은 방법은 없는가?



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: Picking the right way of failing in Swift

스위프트의 주요 초점중 한가지는 바로 컴파일타임 세이프티이다. 런타임 에러의 경향을 줄여주고 더욱 예상가능한 코드를 작성하는데 개발자들이 집중할 수 있게 해준다. 그러나 때론 여러 이유로 실패한다. 그래서 이번주에는 어떻게 적절하게 실패를 다루는지 보고, 이것을 처분하기위해 어떤 도구를 가져야하는 볼것이다.

몇주전에 우리는 실제 옵셔널이 아닌 옵셔널을 어떻게 다루는지("Handling non-optional optionals in Swift")에대해 보았었다. 지난 포스트에서 나는 강제 언랩핑하는것 대신 guard와함께 preconditionFailure() 사용에대한 경우를 만들었고, 이것을 위해 Require라는 경량의 프레임워크를 소개했었다.

그 포스트 이후로, 많은 사람들이 preconditionFailure()assert()의 차이가 무엇인지, 스위프트의 throwing 기능에 어떻게 연관시킬 수 있는지 물어보았다. 그래서 이번 포스트에서는 각각 그것을 사용할때 그 모든 언어 특징을 좀 더 살펴보자.

한 리스트로 시작해보자.
아래는 스위프트에서 에러를 처리하는 (내가 아는) 모든 것이다.
  • nil을 반환하던지 에러 열거형 케이스를 반환한다. 에러 처리의 가장 간단한 형식은 에러를 만난 함수에서 nil을 반환하는 것이다 (혹은 리턴타입으로 Result 열거형을 사용한다면 .error를 반환한다). 이것은 많은 상황에서 아주 유용할 수 있지만, 모든 에러 처리에대해 과용하면 사용하기 성가셔지고, 로직 결점이 숨어있는 위험을 안게 된다.
  • 에러를 throw한다(throw MyError를 사용하여). 잠재적인 에러 처리를 위해 호출자에서 do, try, catch 패턴을 사용해야한다. 혹은 호출 시점에서 try?를 사용하여 에러를 무시할 수도 있다.
  • assert()assertionFailure()를 사용하여 특정 조건이 참인지 검증한다. 디폴트로, 디버그 빌드에서는 fatal error를 내고, 배포 빌드에서는 무시한다. assert를 유발하면 실행이 멈출것이라는 보장이 없으므로 위험한 런타임 경고처럼 보인다.
  • assert대신 precondition()preconditionFailure()을 사용한다. 핵심적으로 다른점은 배포 빌드일지라도 항상* 판별한다는 점이다. 이 의미는 조건이 성립하지 않으면 절때 계속하지 않음을 보장한다는 뜻이다.
  • fatalError()를 호출한다. 이것은 UIViewController같은 시스템 클래스를 따르는 NSCoding을 상속할때, 아마 Xcode가 생성한 init(coder:) 구현에서 보았을 것이다.
  • exit()를 호출한다. 이것은 코드와함께 여러분의 프로세스를 종료한다. 이것은 전역 범위에서 종료하고 싶을때, 커멘드라인 툴이나 스크립트에서 매우 유용하다(예를들어 main.swift에서)
*Ounchecked 최적화 모드를 사용하여 컴파일을 하지 않는다면

복구가능한 vs 복구불가능한
실패를 올바르게 잡아낼때 생각해야하는 키포인트는 발생한 에러가 복구가능한지 불가능한지 정하는 것이다.

예를들어 우리가 서버에 호출하고있는데 에러를 받았다고 하자. 우리가 얼마나 멋진 프로그래머인지, 서버 기반이 얼마나 탄탄한지 상관없이 종종 이런일이 일아난다. 따라서 fatal과 복구불가능하게 이런 에러 타입을 처리하는것은 종종 실수이다. 대신, 우리가 원하는것은 복구하여 사용자에게 에러 화면의 양식을 보여주는 것이다.

따라서, 이런경우 어떻게 적절한 방법으로 실패를 뽑아낼까? 위의 리스트를 한번 보면, 복구가능한 기술과 복구불가능한 기술로 나눌 수 있다. 아래처럼 말이다.

복구가능한 기술
  • nil을 반환하던지 에러 열거형 케이스를 반환한다.
  • 에러를 throw한다.

복구불가능한 기술
  • assert()를 사용한다.
  • precondition()을 사용한다.
  • fatalError()를 호출한다.
  • exit()를 호출한다.

이 경우 비동기 처리를 다루기 떄문에, 아마 nil을 반환하거나 에러 열거형 케이스를 반환하는게 제일 좋은 방법이다. 아래처럼 말이다.
class DataLoader {
     enum Result {
          case success(Data)
          case failure(Error?)
     }

     func loadData(from url: URL, completionHandler: @escaping (Result) -> Void) {
          let task = urlSession.dataTask(with: url) { data, response, error in
               guard let data = data else {
                    completionHandler(.failure(error))
                    return
               }

               completionHandler(.success(data))
          }

          task.resume()
     }
}
적절한 방법으로 에러를 처리하려고 우리 API를 사용자들에게 강요하는 것은 비동기 API에서, throw는 좋은 선택이다.
class StringFormatter {
     enum Error: Swift.Error {
          case emptyString
     }

     func format(_ string: String) throws -> String {
          guard !string.isEmpty else {
               throw Error.emptyString
          }

          return string.replaceOccurrences(of: "\n", with: " ")
     }
}
그러나 때론 에러가 복구되지 않는다. 예를들어 앱을 실행하는동안 설정파일을 불러와야한다고 하자. 만약 설정파일을 놓힌다면 앱은 정의되지 않은 상태로 갈것이다. 이 경우는 프로그램을 계속 실행하는것 보단 크래쉬를 내는게 낫다. 그러니 더 강한것을 사용하여 실패를 복구하지 않는 방법이 더 적절하다.

이 경우, 설정 파일을 놓혔을 경우에 실행을 멈추기위해 preconditionFailure()을 사용한다.
guard let config = FileLoader().loadFile(named: "Config.json") else {
     preconditionFailture("Failed to load config file")
}

프로그래머 에러 vs 실행 에러
만드는데 중요한 또다른 구별은 결점이있는 로직에의해 에러가 만들어진것인지 잘못된 설정에 의해 에러가 만들어진 것인지이다. 혹은 에러가 앱 플로우의 합법적인 부분으로 고려될 수 있는지로 구별된다. 기본적으로 프로그래머가 만든 것읹 외부 요인이 만든 것인지이다.

프로그래머 에러에대해 대비할 때는 거의 항상 복구하지않는 기술을 사용하고 싶을 것이다. 이런식으로 앱 전체에 걸쳐 특별한 상황을 코딩하지 않아도 된다. 좋은 테스트 수트가 가능한빨리 이런 에러를 잡을 수 있게 해줄 것이다.

예를들어, 한 뷰를 만들것인데 이것을 사용하기전에 바인딩된 ViewModel이 필요하다고 가정하자. 이 ViewModel은 우리 코드에서 옵셔널이지만 사용할때마다 언랩핑하고 싶지 않응ㄹ 것이다. 그러나 제품의 상태에선 ViewModel을 잃어버렸을때 크래쉬를 내고 싶지 않다. 디버그에서 에러를 받는것으로 충분하다. assert를 사용하는 경우가 되겠다.
class DetailDView: UIView {
     struct ViewModel {
          var title: String
          var subtitle: String
          var action: String
     }

     var viewModel: ViewMode?

     override func didMoveToSuperview() {
          super.didMoveToSuperview()

          guard let viewMode = viewModel else {
               assertionFailure("No view model assigned to Detailview.")
               return
          }

          titleLabel.text = viewModel.title
          subtitleLabel.text = viewModel.subtitle
          actionButton.setTitle(viewModel.action, for: .normal)
     }
}
assertionFailure()는 배포빌드에서 묵묵히 실패할것이기 때문에 guard문에서 return해야함을 인지하자.

결론
스위프트에서 가능한 기술을 다루는 여러 에러들 사이에 차이를 명확하게하는데 도움이 되었으면 좋겠다. 내 조언은 한 기술만을 고수하는게 아니라 그 상황에 맞는 가장 적절한 것을 고르는 것이다. 에러가 치명적으로 다룰 수 없어도 사용자 경험을 방해하지 않아야 하기 때문에, 나는 보통 가능한 항상 에러를 복구하려고 노력하는것을 제안하는 편이다.

또한 print(error)는 에러 처리가 아님을 기억하자.

질문이 생기거나 피드백을 주고 싶다면 Twitter로 연락할 수 있다. 또한 나의 다음 주간 플로그 포스트에서 다뤄보고 싶은 주제가 있으면 나에게 알려달라.

읽어주어서 감사하다!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.


WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: Unsafe Swift: Using Pointers and Interacting with C


스위프트는 디폴트로 메모리 세이프하다. 메모리 세이프 하다는 의미는 메모리에 직접 접근하는 것을 막아주고, 당신이 사용하기 전에 모든것이 초기화 되어있음을 보장한다는 뜻이다. 핵심은 "디폴트"이다. 언세이프 스위프트는 여러분이 필요할때 포인터를 이용해서 메모리에 직접 다룰 수 있게 해준다.

이 튜토리얼은 스위프트의 이른바 "언세이프"라 불리는 소용돌이의 여행에 데려다 줄것이다. "언세이프"라는 용어는 종종 혼란을 만든다. 이것이 당신이 도작하지도 않을, 위험하고 나쁜 코드를 작성하고 있다는 의미가 아니라, 오히려 컴파일러가 도와줄 수 있는 부분의 한계를 뛰어넘고 추가적인 주의를 필요로하는 코드를 작성한다는 의미이다.

C같은 언세이프 언어와 함께 작업할때, 추가적인 런타임 성능이 필요할때, 혹은 그냥 그 내부를 살펴보고 싶을때 이러한 기능이 필요함을 발견할 수 있을 것이다. 이 주제가 한걸음 더 나아간 주제이긴 하지만 여러분이 합리적인 스위프트 언어 지식을 가지고 있다면, 따라올 수 있을 것이다. 또한 C언어 경험이 도움이 될것이지만 필수조건은 아니다.

시작하기
이 튜토리얼은 3가지 플레이그라운드로 구성된다. 첫번째 플레이그라운드에서는 메모리 레이아웃(Memory Layout)을 살펴보고 언세이프 포인터를 사용해보는 코드를 몇개 만들어 볼 것이다. 두번째 플레이그라운드에서는 스위프트 인터페이스로 데이터 압축을 스트리밍하는 저수준 C API를 가볍게 다룰것이다. 마지막 플레이그라운드에서는 arc4random에대한 독립적인 대안의 플랫폼을 만들 것인데, 언세이프 스위프트를 사용했지만 사용자들이 그 세부내용은 모르도록 만들것이다.

UnsafeSwift라는 새 플레이그라운드를 생성하면서 시작해보자. 이 튜토리얼의 모든 코드들은 플랫폼에 의존하지 않기 때문에 아무 플랫폼이나 선택해도 된다. Foundation 프레임워크를 불러왔는지 확인하자.

메모리 레이아웃

샘플 메모리샘플 메모리


언세이프 스위프트는 메모리 시스템에 직접적으로 동작한다. 메모리는 일련의 상자라고 생각할 수 있고(실제로 10억개의 상자이다), 각 상자 안에는 숫자가 들어있다. 각 상자는 그것과 연관된 유일한 메모리 주소를 가진다. 저장소의 가장 작은 주소로 가능한 단뒤는 한 바이트이며, 한 바이트는 8비트로 이루어져있다. 8비트는 0에서 255의 값을 저장할 수 있다. 프로세서는 메모리 워드(word)에는 효율적으로 접근할 수 있는데, 워드는 보통 1바이트 이상이다. 64비트 시스템에서는 한 워드가 8바이트 혹은 64비트의 길이이다.

스위프트는 여러분의 프로그램에서 어떤것의 크기나 계열(alignment)에대해 이야기해주는 메모리 레이아웃(Memory Layout) 기능을 가지고 있다.

여러분의 플레이그라운드에 아래의 코드를 추가하자.
MemoryLayout<Int>.size          // returns 8 (on 64-bit)
MemoryLayout<Int>.alignment     // returns 8 (on 64-bit)
MemoryLayout<Int>.stride        // returns 8 (on 64-bit)

MemoryLayout<Int16>.size        // returns 2
MemoryLayout<Int16>.alignment   // returns 2
MemoryLayout<Int16>.stride      // returns 2

MemoryLayout<Bool>.size         // returns 1
MemoryLayout<Bool>.alignment    // returns 1
MemoryLayout<Bool>.stride       // returns 1

MemoryLayout<Float>.size        // returns 4
MemoryLayout<Float>.alignment   // returns 4
MemoryLayout<Float>.stride      // returns 4

MemoryLayout<Double>.size       // returns 8
MemoryLayout<Double>.alignment  // returns 8
MemoryLayout<Double>.stride     // returns 8
MemoryLayout<Type>은 컴파일시간에 특정 Type의 size, alignment, stride를 셜정하는 제네릭 타입이다. 반환된 값은 바이트 단위이다. 예를들어 Int6size에서는 2바이트이고 alignment도 같다. 이것은 2로 나누어 떨어지는 주소에서 시작해야한다는 의미이다.

예를들어 Int16100이라는 주소에 할당할 수 있지만 1이 주소에 할당하는 것은 필요 alignment를 위반하는 것이기 때문에 불가능하다. 만약 Int16들을 함께 모은다면 stride 간격에 모일 것이다. 이 기본 타입을 위해 sizestride와 같다.

다음으로 사용자가 정의한 구조체의 레이아웃을 살펴보고 아래를 플레이그라운드에 추가하자.

struct EmptyStruct {}

MemoryLayout<EmptyStruct>.size      // returns 0
MemoryLayout<EmptyStruct>.alignment // returns 1
MemoryLayout<EmptyStruct>.stride    // returns 1

struct SampleStruct {
  let number: UInt32
  let flag: Bool
}

MemoryLayout<SampleStruct>.size       // returns 5
MemoryLayout<SampleStruct>.alignment  // returns 4
MemoryLayout<SampleStruct>.stride     // returns 8
빈 구조체의 size0이다. 이것은 alignment1이기때문에 어떤 주소에도 가능하다.(모든 자연수는 1로 나누어진다) stride는 특이하게도 1이다. 그 이유는 여러분이 만든 각 EmptyStructsize0이더라도 유용한 메모리 주소를 가져야하기 때문이다.

SampleStruct의 경우는 size5이지만 stride8이다. 이것은 alignment 필요조건의 4바이트 바운더리에의해 그렇게된다. 이것을 고려해볼때 스위프트가 할 수 있는 최고의 일은 8바이트 간격으로 묶는 것이다.

다음을 플레이그라운드에 추가하자.

class EmptyClass {}

MemoryLayout<EmptyClass>.size      // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<EmptyClass>.alignment // returns 8 (on 64-bit)

class SampleClass {
  let number: Int64 = 0
  let flag: Bool = false
}

MemoryLayout<SampleClass>.size      // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.stride    // returns 8 (on 64-bit)
MemoryLayout<SampleClass>.alignment // returns 8 (on 64-bit)
클래스들은 참조 타입이므로 Memory Layout은 참조 크기가 8바이트라고 알려준다.

메모리 레이아웃에대해 더 알고싶다면 Mike Ash의 멋진 글을 보자.

포인터
한 포인터는 한 메모리 주소를 가지고있다. 메모리에 직접 접근하는 타입은 "Unsafe"라는 접두를 가지는데, 따라서 이 포인터 타입은 UnsafePointer라 부른다. 이런식으로 추가적인 타이핑이 성가셔 보일수도 있다. 그러나 이것은 정의되지 않은 행동을 만들 수도 있게 해놓았을때, 컴파일러 없이 메모리 접근의 체크를 하고 있다고 (단지 크레쉬를 예방할 뿐만 아니라) 당신이나 코드를 읽는 사람이 알게 해준다.

스위프트 설계자는 C의 char*와 동일한 UnsafePointer 타입을 만들 수 있게 해놓았다. char*은 구조화되지 않은 방법으로 메모리에 접근할 수 있다. 스위프트는 그렇게 하지 않고, 대게 몇몇개의 포인터 타입을 가지는데, 각각은 다른 기능과 목적을 가진다. 포인터 타입을 가장 알맞게 사용하면, 더욱 의도에 맞게 소통하고, 더 낮은 에러, 그리고 정의되지 않은 동작을 피할 수 있게 도와준다.

언세이프 스위프트 포인터는 그 기능이 예상가능한 네이밍 형식으로, 그 포인터의 특징이 무엇인지 알 수 있게 도와준다. 가변적(mutable)인지 불가변적인지, raw한지 typed인지, 버퍼 스타일인지 말이다. 아래와같이 전체적으로 8가지 조합이 있다.



다음 섹션들에서 우리는 이 포인터 타입들에대해 배워볼 것이다.

Raw 포인터 사용하기
아래 코드를 여러분의 플레이그라운드에 추가하자.
// 1
let count = 2
let stride = MemoryLayout<Int>.stride
let alignment = MemoryLayout<Int>.alignment
let byteCount = stride * count

// 2
do {
  print("Raw pointers")
  // 3
  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  // 4
  defer {
    pointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }
  // 5
  pointer.storeBytes(of: 42, as: Int.self)
  pointer.advanced(by: stride).storeBytes(of: 6, as: Int.self)
  pointer.load(as: Int.self)
  pointer.advanced(by: stride).load(as: Int.self)
  // 6
  let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount)
  for (index, byte) in bufferPointer.enumerated() {
    print("byte \(index): \(byte)")
  }
}
이 예제는 두 정수를 저장하고 불러오기 위해 언세이프 스위프트 포인터를 사용한다. 여기서 무슨 일이 일어나는지에대한 설명이다.
  1. 이러한 상수들은 사용된 값들을 종종 들고 있다.
    • count는 저장하기 위해 정수를 가지고 있다.
    • strideInt 타입의 stride를 가지고 있다.
    • alignmentInt 타입의 alignment를 가지고 있다.
    • byteCount는 필요한 모든 바이트 수를 가지고 있다.
  2. 스코프 레벨을 추가하기위해 do 블럭을 추가하였기 때문에, 나중에 예제에서 변수 이름을 재사용할 수 있다.
  3. UnsafeMutableRawPointer.allocate 메소드는 필요한 바이트를 할당하는데 사용한다. 이 메소드는 UnsafeMutableRawPointer를 반환한다. 저 타입의 이름에서 알 수 있듯, 포인터는 (가변의) Raw 바이트를 불러오고 저장하는데 사용될 수 있다.
  4. defer 블럭은 포인터가 적절하게 해제되었는지 확인하기위해 추가된 부분이다. 여기서 ARC는 도움이 되지 않는다. 여러분은 스스로 메모리 관리를 해야한다! 여기에서 defer에대해 더 읽어보길 바란다.
  5. storeBytesload 메소드는 바이트를 저장하고 불러오는데에 쓰인다. 두번째 정수의 메모리 주소는 그 포인터의 stride 바이트를 증가시켜서 계산된디.포인터는 stride 하기때문에, (pointer+stride).storeBytes(of: 6, as: Int:Self)로 포인터 계산 또한 할 수 있다.

  1. UnsafeRawBufferPointer는 메모리가 마치 바이트의 모음인것처럼 접근할 수 있게 해준다. 이 의미는 바이트들을 돌면서 차례로 접근할 수 있게 해주고, 서브스크립트를 이용해 접근하며 filter, map, reduce와같은 멋진 메소드까지 사용할 수 있게 한다. 이 버퍼 포인터는 raw 포인터를 초기화한다.

Typed 포인터 사용하기
typed 포인터를 사용하면 이전의 예제를 간단하게 만들 수 있다. 아래 코드를 플레이그라운드에 추가하자.
do {
  print("Typed pointers")
  let pointer = UnsafeMutablePointer<Int>.allocate(capacity: count)
  pointer.initialize(to: 0, count: count)
  defer {
    pointer.deinitialize(count: count)
    pointer.deallocate(capacity: count)
  }
  pointer.pointee = 42
  pointer.advanced(by: 1).pointee = 6
  pointer.pointee
  pointer.advanced(by: 1).pointee
  let bufferPointer = UnsafeBufferPointer(start: pointer, count: count)
  for (index, value) in bufferPointer.enumerated() {
    print("value \(index): \(value)")
  }
}
아래의 다른 점들을 인지하자.
  • UnsafeMutablePointer.allocate 메소드를 사용하여 메모리를 할당한다. 이 제네릭 파라미터는 포인터로 Int 타입의 값을 불러오고 저장하는데 사용될 것이라는 것을 스위프트에게 알려준다.
  • typed 메모리는 사용하기전과 소멸하기전에 반드시 초기화되어야한다. 이것은 initialize 메소드와 deinitialize 메소드로 할 수 있다. Update: atrick라는 유저가 달아놓은 커멘트처럼 소멸은 non-trivial 타입들만 필요로 한다. 이 말은, 여러분이 non-trivial의 무언가를 바꿀 경우에 소멸을 가지고 있는 것이 여러분 코드의 훗날을 위해 좋은 방법이다. 또한 컴파일러가 이것을 최적화할것이기 때문에 항상 비용이 발생하지 않는다.
  • typed 포인터는 pointee라는 프로퍼티를 가지고 있는데, 이것은 값을 불러오고 저장할때 타입 세이프한 방법을 제공하는 프로퍼티이다.
  • typed 포인터를 증가시키면, 여러분의 숫자의 값을 원하는대로 증가해가며 나타낼 수 있다. 그 포인터는 그 타입의 포인터가 가리키는 값을 기반으로 옳바른 stride를 계산할 수 있다. 다시말해 포인터 계산 또한 가능하다. (pointer+1).pointee=6 이런것 또한 가능하다.
  • typed 버퍼 포인터와 같은 점은, 바이트 대신에 값을 차례로 반복해갈 수 있다.

Raw 포인터를 Typed 포인터로 변환하기
typed 포인터는 항상 직접 초기화 할 필요가 없다. 또한 raw 포인터로부터 만들어질 수 있다.

여러분의 플레이그라운드에 아래 코드를 추가하자.
do {
  print("Converting raw pointers to typed pointers")
  let rawPointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  defer {
    rawPointer.deallocate(bytes: byteCount, alignedTo: alignment)
  }
  let typedPointer = rawPointer.bindMemory(to: Int.self, capacity: count)
  typedPointer.initialize(to: 0, count: count)
  defer {
    typedPointer.deinitialize(count: count)
  }

  typedPointer.pointee = 42
  typedPointer.advanced(by: 1).pointee = 6
  typedPointer.pointee
  typedPointer.advanced(by: 1).pointee
  let bufferPointer = UnsafeBufferPointer(start: typedPointer, count: count)
  for (index, value) in bufferPointer.enumerated() {
    print("value \(index): \(value)")
  }
}
이 예제는 이전 것과 비슷한데, 다른점은 처음에 raw 포인터를 생성한다는 것이다. typed 포인터는 필요한 Int 타입으로 메모리를 바인딩하면서 생성된다. 메모리를 바인딩함으로서 타입 세이프한 방법으로 접근할 수 있게 된다. 메모리 바인딩은 typed 포인터를 생성할때 이미 완료된다.

예제에서 남은 부분은 이전것과 동일하다. 한번 typed 포인터의 세계에 발을 드리는 순간 그 예시로 'pointee'를 사용할 수 있다.

한 인스턴스의 바이트를 뽑아내기
종종 현재 있는 인스턴스의 타입에서 바이트를 조사하고 싶을 것이다. 이것은 withUnsafeBytes(of:) 메소드를 호출하여 조사할 수 있다.

아래 코드를 플레이그라운드에 추가하자.
do {
  print("Getting the bytes of an instance")
  var sampleStruct = SampleStruct(number: 25, flag: true)

  withUnsafeBytes(of: &sampleStruct) { bytes in
    for byte in bytes {
      print(byte)
    }
  }
}
이 코드는 SampleStruct 인스턴스의 raw 바이트를 출력해낸다. withUnsafeBytes(of:) 메소드는 UnsafeRawBufferPointer에 접근할 수 있게 해주는데, 클로저 안에서 사용할 수 있다.

withUnsafeBytes 또한 ArrayData의 인스턴스 메모리로서 사용할 수 있다.

체크섬 계산하기
withUnsafeBytes(of:)를 사용하면 결과를 반환할 수 있다. 이것의 사용에대한 예제는 구조체에서 32비트 체크섬의 바이트를 계산하는 것이다.

아래 코드를 플레이그라운드에 추가하자.
do {
  print("Checksum the bytes of a struct")
  var sampleStruct = SampleStruct(number: 25, flag: true)
  let checksum = withUnsafeBytes(of: &sampleStruct) { (bytes) -> UInt32 in
    return ~bytes.reduce(UInt32(0)) { $0 + numericCast($1) }
  }
  print("checksum", checksum) // prints checksum 4294967269

}
reduce를 호출해서 모든 바이트를 합쳐서 ~연산자로 비트를 뒤집는다. 특별히 강력한 에러 감지는 아니지만 그 컨셉을 보여준다.

언세이프 클럽에서의 규칙
정의되지 않은 행동을 피하기위해 언세이프한 코드를 작성할때 주의를 기울여야한다. 아래 나쁜 코드의 몇 예시가 있다.

withUnsafeBytes로부터 포인터를 반환하지마라!
// Rule 1
do {
  print("1. Don't return the pointer from withUnsafeBytes!")

  var sampleStruct = SampleStruct(number: 25, flag: true)

  let bytes = withUnsafeBytes(of: &sampleStruct) { bytes in
    return bytes // strange bugs here we come ☠️☠️☠️
  }

  print("Horse is out of the barn!", bytes)  /// undefined !!!
}
절때 포인터를 withUnsafeBytes(of:) 클로저 밖으로 내보내지 마라. 그 코드가 오늘은 동작할지라도...

오직 한번에 한 타입을 바인드하라!
// Rule 2
do {
  print("2. Only bind to one type at a time!")

  let count = 3
  let stride = MemoryLayout<Int16>.stride
  let alignment = MemoryLayout<Int16>.alignment
  let byteCount =  count * stride
  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)

  let typedPointer1 = pointer.bindMemory(to: UInt16.self, capacity: count)

  // Breakin' the Law... Breakin' the Law  (Undefined behavior)
  let typedPointer2 = pointer.bindMemory(to: Bool.self, capacity: count * 2)

  // If you must, do it this way:
  typedPointer1.withMemoryRebound(to: Bool.self, capacity: count * 2) {
    (boolPointer: UnsafeMutablePointer<Bool>) in
    print(boolPointer.pointee)  // See Rule 1, don't return the pointer
  }
}

badpunbadpun


절때 한번에 두가지 연관된 타입을 메모리에 바인딩하지마라. 이것을 Type Punning(역자: 컴퓨터 과학에서, 타입 펀닝(type punning)이란 언어의 범주에서 달성하기가 힘들거나 불가능한 기능을 만들기 위해 언어의 형식 시스템을 우회하는 프로그래밍 기법을 말함)이라 부르며 스위프트는 pun을 좋아하지 않는다. 대신에, withMemoryRebound(to: capacity:)같은 메소드로 임시적인 메모리를 리바인드할 수 있다. 또한 이 룰은 trivial 타입(Int와같은)부터 non-trivial 타입(클래스와같은)까지 리바인드하는 것은 불법이라고 말하고 있다. 그러지 말자.

하나 더 남았다
// Rule 3... wait
do {
  print("3. Don't walk off the end... whoops!")

  let count = 3
  let stride = MemoryLayout<Int16>.stride
  let alignment = MemoryLayout<Int16>.alignment
  let byteCount =  count * stride

  let pointer = UnsafeMutableRawPointer.allocate(bytes: byteCount, alignedTo: alignment)
  let bufferPointer = UnsafeRawBufferPointer(start: pointer, count: byteCount + 1) // OMG +1????

  for byte in bufferPointer {
    print(byte)  // pawing through memory like an animal
  }
}
현재 OBOE(off-by-one error: 원하지 않았던 추가적인 사이즈에 관한 에러) 문제는 특히 심각하게 언세이프한 코드이다. 조심하고, 검토하며, 테스트하라!

언세이프 스위프트 예제1: 압축
여러분의 모든 지식을 가져와서 C.API를 감싸볼 시간이다. 코코아는 일반적으로 데이터를 압축하는 알고리즘을 구현한 C 모듈을 포함한다. 여기서 LZ4는 속도가 중요시될때, LZ4A는 속도보다는 높은 압축률이 중요시될때, ZLIB는 공간과 속도가 균형있게 중요시되고 새로운것(오픈소스)이고, LZFSE도 공간과 속도의 균형 면에서 잘 동작한다.

Compression이라 부르는 플레이그라운드를 생성하자. Data를 사용하는 순수 스위프트 API를 정의함으로서 시작해보자.

그러고나서 아래 코드를 여러분의 플레이그라운드에 넣자.
import Foundation
import Compression

enum CompressionAlgorithm {
  case lz4   // speed is critical
  case lz4a  // space is critical
  case zlib  // reasonable speed and space
  case lzfse // better speed and space
}

enum CompressionOperation {
  case compression, decompression
}

// return compressed or uncompressed data depending on the operation
func perform(_ operation: CompressionOperation,
             on input: Data,
             using algorithm: CompressionAlgorithm,
             workingBufferSize: Int = 2000) -> Data?  {
  return nil
}
이 함수는 압축과 해제를 하는 perform 함수인데 지금은 nil을 리턴하게 해두었다. 여기에 짧은 언세이프 코드를 추가하게 될것이다.

다음 프레이그라운드 마지막에 아래의 코드를 추가하자.
// Compressed keeps the compressed data and the algorithm
// together as one unit, so you never forget how the data was
// compressed.

struct Compressed {
  let data: Data
  let algorithm: CompressionAlgorithm
  init(data: Data, algorithm: CompressionAlgorithm) {
    self.data = data
    self.algorithm = algorithm
  }
  // Compress the input with the specified algorithm. Returns nil if it fails.
  static func compress(input: Data,
                       with algorithm: CompressionAlgorithm) -> Compressed? {
    guard let data = perform(.compression, on: input, using: algorithm) else {
      return nil
    }
    return Compressed(data: data, algorithm: algorithm)
  }
  // Uncompressed data. Returns nil if the data cannot be decompressed.
func decompressed() -> Data? {
    return perform(.decompression, on: data, using: algorithm)
  }

}
entryData 타입의 익스텐션이다. 여러분은 옵셔널 Compressed 구조체를 반환하는 compressed(with:)라는 메소드를 추가했었다. 이 메소드는 간단하게 Compressed에서 compress(input:with:)라는 스테틱 메소드를 호출한다.

마지막에있는 예제사용은 현재 동작하지는 않지만, 지금 고쳐보자!

여러분이 처음 코드로 들어왔던 곳으로 스크롤을 올려보고, perform(_: on: using: workingBufferSize:) 함수를 아래처럼 만들어보자.
func perform(_ operation: CompressionOperation,
             on input: Data,
             using algorithm: CompressionAlgorithm,
             workingBufferSize: Int = 2000) -> Data?  {
  // set the algorithm
  let streamAlgorithm: compression_algorithm
  switch algorithm {
  case .lz4:   streamAlgorithm = COMPRESSION_LZ4
  case .lz4a:  streamAlgorithm = COMPRESSION_LZMA
  case .zlib:  streamAlgorithm = COMPRESSION_ZLIB
  case .lzfse: streamAlgorithm = COMPRESSION_LZFSE
  }
  // set the stream operation and flags
  let streamOperation: compression_stream_operation
  let flags: Int32
  switch operation {
  case .compression:
    streamOperation = COMPRESSION_STREAM_ENCODE
    flags = Int32(COMPRESSION_STREAM_FINALIZE.rawValue)
  case .decompression:
    streamOperation = COMPRESSION_STREAM_DECODE
    flags = 0
  }
  return nil /// To be continued
}
압축 알고리즘과 실행 작업을 위해 여러분의 스위프트 타입에서 압축 라이브러리에 필요한 C 타입으로 변환하게 된다.

다음으로 return nil을 아래로 바꾸자.
// 1: create a stream
var streamPointer = UnsafeMutablePointer<compression_stream>.allocate(capacity: 1)
defer {
  streamPointer.deallocate(capacity: 1)
}

// 2: initialize the stream
var stream = streamPointer.pointee
var status = compression_stream_init(&stream, streamOperation, streamAlgorithm)
guard status != COMPRESSION_STATUS_ERROR else {
  return nil
}
defer {
  compression_stream_destroy(&stream)
}

// 3: set up a destination buffer
let dstSize = workingBufferSize
let dstPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: dstSize)
defer {
  dstPointer.deallocate(capacity: dstSize)
}

return nil /// To be continued
위 코드에서 무슨일이 일어나는지 보자.
  1. compression_stream을 할당하고 소멸을위해 defer 블럭으로 준비한다.
  2. 그러고 pointee 프로퍼티를 사용하여 스트림을 가져와서, compression_stream_init 함수에 보내준다. 이 컴파일러는 여기서 좀 특별한 일을 한다. inout & maker를 이용하여 여러분의 compression_stream을 받아서 자동으로 UnsafeMutablePointer<compression_stream>으로 바꾼다.(streamPointer로 보낼 수도 있고 이 특정 변환을 필요로 하지 않을 수도 있다)
  3. 마지막으로 여러분이 작업할 버퍼인 목적지 버퍼를 생성한다.
return nil을 바꾸어서 perform 함수를 완성했다.
// process the input
return input.withUnsafeBytes { (srcPointer: UnsafePointer<UInt8>) in
  // 1
  var output = Data()
  // 2
  stream.src_ptr = srcPointer
  stream.src_size = input.count
  stream.dst_ptr = dstPointer
  stream.dst_size = dstSize
  // 3
  while status == COMPRESSION_STATUS_OK {
    // process the stream
    status = compression_stream_process(&stream, flags)
    // collect bytes from the stream and reset
    switch status {
    case COMPRESSION_STATUS_OK:
      // 4
      output.append(dstPointer, count: dstSize)
      stream.dst_ptr = dstPointer
      stream.dst_size = dstSize
    case COMPRESSION_STATUS_ERROR:
      return nil
    case COMPRESSION_STATUS_END:
      // 5
      output.append(dstPointer, count: stream.dst_ptr - dstPointer)
    default:
      fatalError()
    }
  }
  return output
}
이것이 실제로 일어나는 것이다. 그리고 무슨 일이 일어나는지 보자.
  1. 아웃픗(압축된 데이터든 압축해제된 데이터든, 그 작업에따라 다르다)을 담은 Data 오브젝트를 생성한다.
  2. 당신이 할당한 포인터와 그 크기와함께 소스버퍼와 목적지버퍼를 설정한다.
  3. 그리고 COMPRESSION_STATUS_OK를 반환할때까지 계속 compression_stream_process를 호출한다.
  4. 목적지버퍼는 마침내 그 함수로부터 반환된 아웃풋으로 복사된다.
  5. 마지막 패킷이 들어오면, COMPRESSION_STATUS_END가 나오고, 오직 목적지버퍼의 부분만 잠재적으로 복사되는것이 필요하다.
사용 예제에서 10000 요소의 배열이 153바이트로 압축된 것을 볼 수 있다. 나쁘지 않은 결과이다.

언세이프 스위프트 예제2: 난수 생성기
난수는 게임에서부터 기계학습에 이르기까지 많은 어플리케이션에서 중요한 부분으로 다뤄진다. macOS는 훌륭한 (암호학적으로 풀기힘든) 난수를 생성하는 arc4random(A Replacement Call 4 random)을 제공한다. 불행히도 이것은 리눅스에서 사용할 수 없다. 게다가 arc4randomInt32 난수만 제공한다. 그러나 dev/urandom파일은 무제한으로 난수를 제공한다.

이번 섹션에서는 이 파일을 읽은 새로운 정보로 완전히 타입 세이프한 난수를 만들어 볼 것이다.

hexdumphexdump


RandomNumbers라는 새 플레이그라운드를 만듦으로서 시작해보자. 이번시간에는 macOS 플랫폼을 선택했는지 확인하자.

만들고나면 원래 있던 것을 아래의 것으로 바꾸자.
import Foundation

enum RandomSource {
  static let file = fopen("/dev/urandom", "r")!
  static let queue = DispatchQueue(label: "random")
  static func get(count: Int) -> [Int8] {
    let capacity = count + 1 // fgets adds null termination
    var data = UnsafeMutablePointer<Int8>.allocate(capacity: capacity)
    defer {
      data.deallocate(capacity: capacity)
    }
    queue.sync {
      fgets(data, Int32(capacity), file)
    }
    return Array(UnsafeMutableBufferPointer(start: data, count: count))
  }
}
file이라는 변수는 static으로 선언해 놓았으므로 시스템에 오직 하나만 존재하게 될것이다. 그 프로세스가 종료될때 당신은 그것에 접근하는 시스템에 의존할 것이다. 여러 스레드에서 난수를 요구할 수도 있기 때문에 일련의 GCD큐로 접근하는 상황을 막아야한다.

get 함수가 작업이 일어나는 곳이다.  먼저, 할당되지 않은 저장소를 생성하는데, fgets은 항상 0에 종료되므로, 당신이 원하는 것보다 하나 더 많게 준비한다. 다음으로 GCD 큐에서 작업하면서 파일로부터 데이터를 가져온다. 마지막으로는, Sequence처럼 동작하는 UnsafeMutableBuffePointer로 감싸서 표준 배열에 데이터를 복사한다.

지금까지 Int8값의 배열만 (안전하게) 줄것이다. 이제 이것을 확장시켜보자.

플레이그라운드의 마지막 부분에 아래를 추가하자.
extension Integer {
  static var randomized: Self {
    let numbers = RandomSource.get(count: MemoryLayout<Self>.size)
    return numbers.withUnsafeBufferPointer { bufferPointer in
      return bufferPointer.baseAddress!.withMemoryRebound(to: Self.self, capacity: 1) {
        return $0.pointee
      }
    }
  }

}

Int8.randomized
UInt8.randomized
Int16.randomized
UInt16.randomized
Int16.randomized
UInt32.randomized
Int64.randomized
UInt64.randomized
Integer 프로토콜의 모든 하위타입에 static randomized 프로퍼티를 추자했다(Portocol Oriented Programming를 읽어보자!) 먼저 난수를 얻어서 반환된 배열의 바이트와함께 요청된 타입으로 Int8 값을 리바인드한 뒤(C++의 reinterpret_cast 처럼) 복사본을 반환한다. 간단하다! :]

이게 다다! 언세이프 스위프트를 사용한 안전한 방법으로의 난수이다.

여기서 어디로 가야할까?
여기 모든 플레이그라운드가 있다. 그리고 여러분이 더 배우려고 찾아볼 수 있는 추가적인 자료들이 있다.

여러분이 이 튜토리얼을 즐겼기를 바란다. 만약 질문이 생기거나 공유하고 싶은 경험을 겪게되면 이 포럼(링크)에서 그것을 기대하고 있겠다!



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: You Probably Don't Want enumerated

스위프트 표준 라이브러리에서 한가지 종종 오용되는 부분은 시퀀스(Sequence)의 enumerated()함수이다. 이 함수는 새로운 시퀀스를 만들어주는 데, 이 시퀀스는 원래 시퀀스의 각 요소와 그 요소에 해당되는 번호를 가진다.

enumerated()는 잘못 사용되고 있다. 이 함수는 각 요소에 번호를 제공하기 때문에 그 번호 문제에대한 쉬운 해결법이 될 수 있다. 그러나 이런 번호 문제는 더 나은 방법으로 해결될 수 있다. 그런 경우가 무엇인지 보자. 우리가 어떤 실수를 하고 있는지 보고, 이것을 추상적인 수준에서 한번 해결해보자.

enumerated()를 사용할때 주된 이슈는 이 함수가 요소와 그에 해당하는 각 인덱스(index)를 반환한다고 생각해버리는 것이다. 이 함수는 모든 시퀀스에서 사용가능하지만, 시퀀스는 인덱스를 가진다는 것을 보장하는 녀석이 아니기 때문에, 우리는 이것이 인덱스가 아니라는 것을 기억해야한다. 코드에서 index라 부르지않고 offset이라 부르고 있는데, 이 네이밍 컨벤션은 글 마지막에 소개해 놓았다. 오프셋(offset)은 항상 0에서 시작하여 각 요소마다 증가하는 정수형태를 말한다. Array의 경우 인덱스와 일치하겠지만, 기본적으로 다른 타입에서는 그렇지 않다. 아래 예제를 한번 보자.
let array = ["a", "b", "c", "d", "e"]
let arraySlice = array[2..<5]
arraySlice[2] // => "c"
arraySlice.enumerated().first // => (0, "c")
arraySlice[0] // fatalError
arraySlice라는 변수는 당연하게도 ArraySlice이다. 그러나 startIndex는 특별하게도 0이아닌 2이다. 이때 enumerated()first를 호출하면 오프셋이 0인 튜플을 반환해주고, 그 첫번째 요소인 "c"를 반환한다.

다른 방법으로 예시를 보자.
zip(array.indices, array)
실제로는 이렇게 한다.
zip((0..<array.count), array)
그리고 Array가 아닌 다른 것과 작업을 하면 언제든지 틀린 동작 결과를 만들 것이다.

enumerated()를 사용하면서 (인덱스가 아닌) 오프셋을 사용것으로 생긴 이슈 말고도 다른 이슈들이 있다. enumerated() 사용에대해 여러번 생각해볼 수 있는데, enumerated()를 사용할때 얻을 수 있는 더 나은 이점이 있다. 조금 더 살펴보자.

내가 본 enumerated()의 가장 일반적인 사용은, 다른 배열로부터 일치하는 요소를 잡기위해 열거된(enumerated) 배열로부터 오프셋으로 사용하는 것이다.
for (offset, model) in models.enumerated() {\
     let viewController = viewControllers[offset]
     viewController.model = model
}
이 코드는, 배열이된 modelsviewControllers에 의존하는데, 이 배열은 0에서 시작하고 정수에의해 색인(index)된다. 그리고 이 배열의 길이가 같다는 것에도 의존하고 있다. 만약 models 배열이 viewControllers 배열보다 짧다면, 별다른 나쁜일이 일어나지 않겠지만, viewControllersmodels보다 짧다면 크레쉬가 일어날 것이다. 또한 큰 역할을 하고 있지도 않은 추가적인 offset 변수까지 가지고 있어야한다. 더 스위프트한 방법으로 다시 짜보면 아래처럼 될 수 있다.
for (model, viewController) in zip(models, viewControllers) {
     viewController.model = model
}
이 코드는 읽는이를 집중시키며, 모든 Sequence 타입에서 동작한다. 또한 배열의 길이가 일치하지 않는 것도 알아서 처리해준다. 더 나은 방법일 것이다.

다른 예제를 보자. 이 코드는 첫번째 imageView와 그 컨테이너 사이에 오토레이아웃 제약(constraint)를 추가하고 쌍의 이미지뷰 사이의 오토레이아웃 제약을 만든다.
for (offset, imageView) in imageViews.enumerated() {
     if offset == 0 {
          imageView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
     } else {
          let imageToAnchor = imageView[offset - 1]
          imageView.leadingAnchor.constraint(equalTo: imageToAnchor.trailingAnchor).isActive = true
     }
}
이 코드 예제도 비슷한 문제가 있다. 우리는 쌍의 요소가 필요하지만, 고수준에서 작업할때 enumerated()를 사용하는 것은 지긋지긋한 인덱스를 다뤄가며 필요한 번호를 뽑아내야 한다는 의미이다. 이 부분도 마찬가지로 zip이 도와줄 것이다.

먼저 첫번째 요소에서 컨테이너 제약을 다루는 코드를 작성하자.
imageViews.first?.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true
다음으로 이 쌍의 요소를 다루자.
for (left, right) in zip(imageViews, imageViews.dropFirst()) {
     left.trailingAnchor.constraint(equalTo: right.leadingAnchor).isActive = true
}
이제 됐다. 인덱스는 보이지 않고, 어떠한 (다중-전달) 시퀀스로 동작하므로 더 집중할 수 있게 되었다.

(한 익스텐션에 쌍으로 만드는(pairing) 코드를 넣을 수도 있고, 필요에 따라서는 .eachPair()을 호출할 수도 있다.)

enumerated()의 몇몇 유효한 사용이 있을 수 있다. 여러분이 얻어내고 있는 것이 인덱스가 아니라 그냥 정수이기 때문에 각 요소에 해당하는 (인덱스가 아닌) 번호로 작업해야 할 때가 바로 옳바른 사용 시점이다. 예를들어 여러 뷰들의 각 수직 좌표 y를 높이와 시퀀스의 오프셋의 곱으로 만들어야 한다면 enumerated()가 적절할 것이다. 아래에 구체적인 예시가 있다.
for (offset, view) in views.enumerated() {
     view.frame.origin.y = offset * view.frame.height
}
여기의 offset은 번호의 속성으로 사용되고 있기때문에 enumerated()가 잘 동작한다.

이제 간단하게 요약해보자면, enumerated()를 인덱스로 사용하고 있다면 그 문제를 해열하는데 더 좋은 방법이 있을 것이며, enumerated()를 번호로 사용한다면 좋아요를 표시한다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
제목: Swift: When to use guard vs if


최근에 내 코드베이스에서 내가 느낀 것은 guard를 디폴트로 하냐, if를 디폴트로 하냐이다. 나는 그렇게 많이 생각하지 않고 guard로 바꾼다.

그러나 이것이 문제가 되기도 하는데, guardif에는 차이점이 있고, 어느것을 사용할 것인지 생각해볼 필요가 있는것 같다.

차이점은 미묘하지만 존재한다. guard는 어떤 값이 의도한것처럼 기능하길 원하도록 표현할때 사용된다.

예를들어 try! Swift app 에서 발표 세션 타입을 표시할때, 발표 타이틀이 세션 타이틀이다.



그러나 모든 세션이 발표를 가지는 것은 아니므로 프레젠테이션은 선택적이다. 사실 특정 세션 타입에서는 발표를 표시하고 타이틀을 가질거라 기대한다. 이때가 guard의 최고의 유스케이스이다!
@objc public enum SessionType: Int {
    case workshop
    case meetup
    case breakfast
    case announcement
    case talk
    case lightningTalk
    case sponsoredDemo
    case coffeeBreak
    case lunch
    case officeHours
    case party
}


public class Session: Object {
    // this is optional because not all sessions have presentations
    // e.g. no presentation during breakfast
    open dynamic var presentation: Presentation?
    // other properties here


    /** The main name of this session */
    public var formattedTitle: String {

        switch self.type {
        case .talk, .lightningTalk:
            // for the talk / lighting talk session type
            // we expect the presentation to be there
            // if it's not there, it's a fail, so guard is used
            guard let presentation = presentation else { return defaultTitle }
            return presentation.localizedTitle
        // other cases continued...
        }
    }
이 발표 타이틀은 항상 발표 세션 타입을 위해 표시될 수 있다. 만약 없다면, 실패한다. 이것이 이 경우에 왜 guard를 써야하는지의 이유이다.

그러나 다른 경우도 생각해보자. 쉬는시간세션(coffee break session)은 스폰서를 받을수도 있다. 이 경우, 쉬는시간의 타이틀에 스폰서 이름을 넣을 수 있다. 스폰서가 있으면 스폰서의 이름을 넣고, 없으면 넣지 않는 두가지가 다 맞는 경우이다. 이 경우가 if를 사용할 수 있는 경우다.
public class Session: Object {


    /** A sponsor, if any, responsible for this session. */
    open dynamic var sponsor: Sponsor?


    /** The main name of this session */
    public var formattedTitle: String {

        switch self.type {
        case .coffeeBreak:
            // some sessions are sponsored, some aren't
            // it's not a fail if there is no sponsor
            // so if is used
            if let sponsor = sponsor {
                return "Coffee Break, by \(sponsor.name)".localized()
            }
            return "Coffee Break".localized()
        // other cases continued...
        }
    }
@ecerney puts it so well의 말처럼, guard를 약한 Assert로 생각하면 된다.

if절처럼 guard는 불리언값의 표현에따른 상태를 실행한다. if절과는 다르게, guard절은 조건이 충족되지 않을때만 실행된다. guardAssert처럼 생각할 수 있는데, 크레쉬를 내버리는 Assert가 아니라 우아하게 빠져나올 수 있는 Assert이다.

그러니 guard를 쓰기전에 if를 생각해보자!


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
제목: Naming Things in Swift

최근에 나는 여러 프로그래밍 언어와 환경을 사용해보고 있고, 내 기술을 다양하게 만드려 노력하고 있다. 나는 보통 리액트(React), 스위프트, Objective-C, 스칼라로 작업해왔다. 이것들은 각자 그들의 어풍과 규약을 가지고 있다. 나는 실제 경험을 통해 배우면서 언어를 비교하고 언어의 차이를 발견하여 더 나은 스위프트 개발자가 되는데 적용시켜보기로했다.

내가 스칼라를 배울 수 있게 도와준 내 상사는 네이밍에관한 블로그 포스팅(링크)을 보내주었고 스칼라에서 다른 수준의 장황함을 사용할 때 그것의 포괄적인 설명으로 나에게 깊은 인상을 주었다. 오늘 내 목표는 스위프트에서 언제 간결해야하고 언제 설명을 덧붙여야하는지에대한 위 포스팅과 비슷한 멋진 글을 쓰려한다. 이 스칼라 포스팅에서 몇 예시를 빌려 스위프트와 iOS 앱을 연관시킬 것이다.

여러분이 프로그래밍할때 간결함을 좋아하든 하지않든, 스위프트는 당신이 선호하는 수준의 말수로 코드를 작성할 수 있게 하는 기능을 가지고 있다. 네이밍을 넘어, 트레일링 클로저 문법, 이름없는 파라미터, 익명의 클로저 인자는 어떨때는 간결하게 해주고, 어떨때는 풀어서 설명해준다.

간결해야하는지 아닌지 그 질문이 아니라, 바로 어디에 간결하게(혹은 풀어서 길게) 하면 되는지이다.

스위프트는 꽤 오랫동안 겪어왔는데, 이것은 코드가 제네럴하게 접목될 수 있는 관용구를 개발하면서 시작되었었다. 스위프트 창시자들은 휼륭한 문서인 offical API design guidelines(링크)를 배포하는데 충분히 친절해왔다. 이것들이 좋긴하지만 나는 좀 더 원하는게 따로 있다. 우리가 어떻게 스위프트의 관용적인 직감을 만들 수 있는지 이야기해보고 싶다. 우리는 세부적으로 직관적으로 네이밍하는 것에 대해 다룬 뒤 언어의 특성에대해 토론해볼것이다.

철학
스위프트 API 설계의 원리에서 특히 네이밍이 언급하는 것은 다음과 같다.
  • 사용되는 시점에서 명료함은 가장 중요한 목표이다.
  • 명료함이 간단함보다 더 중요하다.
멋진 가이드라인인데, 좀 더 깊게 들어가보자. Haoyi의 스칼라 블로그 포스팅에는 우리가 뭔가 네이밍을 붙일때 우리의 목표가 무엇인지 말해준다.
프로그래머가 아직 모르지만 알고싶어하는 것을 보여주어라
매우 흥미로운 가이드라인이며, 이것은 우리에게 코드의 맥락을. 생각하게 만들고, 미래에 어디에서 동작할지 생각하게 만든다.코드는 한번만 쓰여지지만, 계속해서 읽혀진다. 따라서 프로그래머들은 읽기 편하게 최적화시키고 작성하는 것에 힘을 들여야함을 잊지말자. 그리고 읽기 최적화될때 고려해야할 가장 중요한 점은 바로 문맥이다. 스칼라 블로그 포스팅에서 이것들을 잘 정리해 놓았다(링크). 그리고 문맥이란 프로그래머가 이미 알고 있는 것과 프로그래머가 알고 싶어 하는 것 둘 다를 모두 포함한다.

프로그래머가 이미 아는 것
  • 당신의 코드베이스에서 예전에 이미 본 것
  • 다른 코드베이스에서 예전에 이미 본 것
  • 이전 작업에서 그들이 골랐던 사실들
프로그래머가 알고 싶어 하는 것
  • 그들이 하는 것에 영향을 주는 것
  • 그들이 이해할 필요가 있는 것
  • 그들이 익숙하지 않은 것
  • 정확함, 보안, 성능등의 이유로 특별히 위험한 것
이것은 포괄적인게 아니다.

누구나 그리고 언제나, 당신의 코드를 읽고 있다고 생각해라. 코드를 매일 사용하는 사람이 직장동료일까? 아니면 지금으로부터 6개월뒤의 자기자신일까? 당신의 오픈소스 프로젝트에 가볍게 컨트리뷰트를 하려고 하고있는것일까? 이러한 여러 상황들은 어떤 함수의 이름을 어떻게 정할지 영향을 받을 것이다. 설명해보자.

당신의 코드를 매일 사용하는 한 동료의경우 당신의 코드베이스와 그것의 규약에 완전히 친숙하다면 간결한 코드가 최고일 것이다. 만약 6개월동안 그 코드베이스에서 작업할 계획이 없다면 그 규약이 결국 생소하게 되어갈 것이므로 설명하는 말처럼 만드는게 더 도움이 될 것이다. 오픈소스 프로젝트의 가벼운 컨트리뷰터들은 아마 큰 코드베이스가 어떻게 서로 맞춰지는지 이해할 수 없을 것이다. 따라서 지나친 설명은 당신 프로젝트의 컨트리뷰터가 이해하는데 도움을 줄 수 있을 것이다.

어떤 사람이 당신의 코드를 읽고싶어할지, 그리고 그들의 목적이 무엇인지 생각해보아라.

가이드라인
이것은 원칙이 아닌 가이드라인이다. 여러분의 직감이 규칙을 지키기 싫어한다면 그렇게하지 말아라. 이제 중요한 순서로 네이밍에 관한 가이드라인에대해 이야기해보고자한다. 그리고 항상 마음속에 문맥을 기억하자!

(네기 지금 이 글에서 나온(링크) 가이드라인을 스위프트에 적용시키는 점을 기억해달라-우리는 이 포스팅과 그 저자인 Li Haoyi(링크)에게 감사해야한다)

넓은 범위의 이름은 길어야한다
이 예제에서 i라는 이름이 왜 괜찮을까?
for var i in 0..<10 {
  print(i)
}
그러나 여기서는 왜 안될까?
struct MyStruct {
  let i: Int
}
i라는 놈이 코드베이스 어디에서 쓰이는지 생각해보아라. 처음 예제에서 i는 for문 안에서만 접근되었다. 그러나 두번째 예제는 구조체의 맴버이며 저 구조체를 사용하는 어디에서나 i를 접근할 수 있는데, 잠재적으로 모든 코드베이스에서 사용가능하다. 기볍게 본 것으로 i는 매우 널리 쓰일 수도 있다는 이유때문에 i의 문맥을 찾을 수 없다.

우리는 프로그래머들이 아직 모르지만 알고싶어할 코드 어떤것을 알려주어야한다는 점을 잊지말자. 위 구조체를 고쳐보자.
struct MyStruct {
  let numberOfInteractions: Int
}

여기서 말하고자하는 바는, 루프에서 쓰이는 모든 변수가 짧아야함을 의미하는게 아니라, 넓게 쓰일 것의 이름은 길어야한다는 의미이다. 이에 반해 아래 예제를 보자 이 예제는 루프 안에서도 짧은 변수가 나쁜 방법일 수 있음을 보여준다.
for var i in 0..<10 {

  ...

  ...

  let data = Data(repeating: 0, count: i)

  ...

  ...

  writeToDb(transformedData, i) // Tricky C API...

  ...

  ...

  ...

  let temp = i + 1

  ...
}
우리 모드 i가 길어야 좋을 거라는 점에 동의할 것이라 생각된다. 왜냐? 그 사용의 범위가 넓은데다가 더 사용되기 때문이다. 이것이 다음 가이드라인으로 연결시켜준다.

더 사용된 이름들은 짧아야한다.
스위프트에서 처음 배웠던 함수인 print를 한번 보자. 함수의 이름처럼 "print"는 아주 완벽하게 동작한다.
print("Hi there!")
그렇다면 왜 "cache"가 별로 좋지 않은 것일까?
class Downloader {
  func cache() { ... }
}

...

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  func applicationDidFinishLaunching(_ application: UIApplication) {
    ...
    downloader.cache() // Only called at app startup.
  }
}
print는 아주 많이 사용되고 모든 스위프트 개발자들이 그것에 익숙하다고 생각할 것이다. cache는 한번만 쓰이며 거의 모습을 보지 못하는 커스텀 객체에 정의해 놓는다. 이런것은 긴 이름이 의미가 있다.
class Downloader {
  func initializeCache() { ... }
}
훨씬 낫다.

위험한 이름은 길어야한다. 
몇 함수들은 그들이 하는 때문에 이름이 길다. 위험한 함수들은 이름이 길어야하는 반면 빈번하게 쓰이는 일상의 함수들은 짧아야한다. 여기 아주 긴 함수이다.
extension Downloader {
  func loadDataFieldsFromOfflineCache() { ... }
}
대신 loadFromCache으로 될 수 있습니다.
extension Downloader {
  func loadFromCache() { ... }
}
그러나 이 함수를 길게하는 것이 의미가 있는지 생각해보자.
extension Downloader {
  func deleteAPICredentialsFromCache() { ... }
}
이 함수를 호출하는 것이 위험하여 긴 이름을 가지게 되었다. 우리는 사고로 사용자의 데이터를 지워버리는 일은 항상 피하고 싶을 것이다. 그렇기 때문에 이렇게 너무 간결하게 호출하고 싶지 않을 것이다.
extension Downloader {
  func delToken() { /* deletes user data omg! */ }
}
우리는 프로그래머들이 아직 모르지만 알고싶어할 코드 어떤것을 알려주어야한다는 점을 잊지말자. 이것이 사용자 데이터를 제거할 때 함수를 호출하는 누군가가 당연히 그 사실을 알고 싶어할 것이라고 상상한다.

소스 문맥의 이름들은 짧아야한다.
내부 타입이 존재하는 타입 이름은 짧아야한다. 그리고 외부 타입이 존재하면 길어야한다. 아래를 한번 생각해보자.
protocol Delegate {
  ...
}
Delegate 프로토콜이 무엇을 위한 것인지 알 수 없으므로 이것은 너무 짧다. 좀 더 긴 이름을 붙여서 더 낫게 해보자.
protocol DownloaderDelegate {
  ...
}
멋지다! 이제 저 프로토콜이 무엇을 위한 것인지 알도록 도와준다.

스위프트 컴파일러가 타임으로 프로토콜을 도와준다면 대안의 방안이 될 수 있다.
class Downloader {
  protocol Delegate {
    ...
  }
}
이것은 충분히 자격이 있는 Downloader.Delegate로 확장할 수 있다. 그러나 슬프게도 스위프트는 아직 이런식으로 감쌓여진 프로토콜의 종류를 지원하지 않는다.

그냥 이름으로 타입 정보 중복을 피해야한다.
class Downloader {
  protocol DownloaderDelegate {
    ...
  }
}
개발자들은 이미 Downloader 클래스 안에 타입들이 이 클래스와 동작해야한다는 것을 알기 때문에 정보의 중복은 무의미하다. 이제 마지막 가이드라인으로 넘어가자.

강타입(Strongly Typed) 이름들은 짧아야한다.
스위프트는 강력하게 표현력있는 타입 시스템을 가지며, 우리는 이를 이용하여 이름을 짧게 만들 수 있다. 예를들어 아래 프로퍼티를 생각해보자.
class Downloader {
  var downloaderDelegate: Delegate
}
우리는 이미 저 델리게이트 프로퍼티가 DownLoader 클래스에 속한다는 것을 알기 때문에 프로퍼티 이름으로서 downloaderDelegate를 부르는 것이 불필요하다.

아래에 또다른 카운터 예제가 있다.
func zipTwoSequences<...>(_ sequence1: Sequence1, _ sequence2: Sequence2) -> ...
대신에 표준 라이브러리는 이것만 포함한다.
func zip<...>(_ sequence1: Sequence1, _ sequence2: Sequence2) -> ...
타입 시그니처에서 인자가 시퀀스라는게 확실하기 때문이다.

여기까지가 네이밍 가이드라인에대한 이야기이고, 이제는 간결하게 만들어주는 스위프트 특징들을 이야기해보자!

전반적으로 이름들을 생략하기
설명이 긴 것부터 간결한 것까지 그 범주중에 굉장히 "간결함"의 끝에는 아예 이름이 없는 것이다. 트레일링 크로저 문법, 이름없는 파라미터, 익명의 클로저 인자들로 이름없이 할 수 있다. 그것들을 사용할 때는 아래 가이드라인을 넘는 문제이다.

클로저 문법 추적은 매우 편하고 함수 호출을 더 간결하게 하도록 도와준다. Ray Wenderlich의 스위프트스위프트 스타일 가이드에서 쿨로저 섹션(링크)의 말을 빌리자면, 클로저의 목적이 모호하다면 트레일링 클로저 문법은 사용하지 마라고 한다. 예를들자면 아래같은 경우가 나쁜 경우이다.
UIView.animate(withDuration: 1.0, animations: {
  ...
}) { finished in
  ...
}
이렇게하는 것이 훨씬 더 명료하다.
UIView.animate(withDuration: 1.0, animations: {
  ...
}, completion: { finished in
  ...
})
이름없는 파라미터(unnamed parameters)의경우는 인자 레이블로 공식 스위프트 API 가이드라인을 참고할것이다.
  • 인자들이 유용하게 구별되지 않을때 모든 레이블들을 생략하라.(ex. union(set1, set2))
  • 함수 이름의 문법이 첫번째 인자가 무엇인지 명확할때 레이블들을 생략하라.(ex. addSubview(y))
  • 타입 규약을 위해서는 레이블들을 생략하라.(ex. Int64(someUInt32))
  • 그렇지 않으면 (일반적으로는) 인자 레이블을 명시하라.

마지막으로 익명의 클로저 인자가 남았다. 대부분 클로저의 길이에따라 이것을 사용하는데, "넓은 범위의 이름은 길어야한다"는 규칙과 일맥상통한다.

만약 여러분의 클로저가 몇가지 안되는 일을 한다면 익명의 클로저 인자를 사용하라.
(0..<10).map({ String($0) })
아래는 과하게 설명이 긴 카운터 예제이다.
(0..<10).map({ number in String(number) })
그리고 아래의 것은 네이밍에관해 처음 두가지 가이드라인을 접목하지 않을 때 어떤식으로 생겼을 수 있는지 보여준다.
(0..<10).map({
  ...

  ...

  let data = Data(repeating: 0, count: $0)

  ...

  ...

  return Model(fromData: data, index: $0)
})
다시한번 Ray Wenderlich 가이드로가서 클로저에대한 정보를 살펴보길 바란다.

오늘 다루었던 가이드라인이라는 것은 절대적인 어떤 것이 아님을 기억하자. 경험하고 다른 사람에게 물어보고 그리고 배우자. 그 과정을 즐길 수 있길 바란다!


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  2개가 달렸습니다.
  1. 글 내용 잘 읽었습니다. 별 중요한 건 아닐지도 모르지만, 카운터 예제라는 말은 반례라는 용어로 번역하는 편이 좀 더 이해하기 쉽지 않을까요? 혹은 반대되는 / 반하는 예제라는 말도 괜찮을 것 같습니다.

    예를 들어,

    만약 여러분의 클로저가 몇가지 안되는 일을 한다면 익명의 클로저 인자를 사용하라.
    ...
    아래는 과하게 설명이 긴 카운터 예제이다.

    이 부분은

    만약 여러분의 클로저가 몇가지 안되는 일을 한다면 익명의 클로저 인자를 사용하라.
    ...
    아래는 이에 반하는 과하게 설명이 긴 예제이다.

    정도의 문장이 좀 더 이해하기 쉬울 것 같습니다.

모든 문제는 또다른 프로토콜을 추가하여 해결할 수 있다.

옵셔널은 멋지다. 이제까지 나는 Objective-C의 "messages to nil return nil" 버그를 너무 많이 봐왔었고 다시 그때로 돌아가고 싶지도 않다.

그러나 당신은 옵셔널이나 특정 타입의 옵셔널이 필요할 때가 종종 있다. 아래에는 내가 즐겨쓰는 그 경우들이다.

isNilOrEmpty
가끔씩 nilisEmpty==true의 차이를 신경쓰지 않아도 될 때가 있다. 먼저 _CollectionOrStringish 프로토콜을 만든다. 이 프로토콜은 비어있고, 이 타입이 isEmpty 프로퍼티를 가진다는 것을 표시하여 사용한다.
protocol _CollectionOrStringish {
    var isEmpty: Bool { get }
}

extension String: _CollectionOrStringish { }
extension Array: _CollectionOrStringish { }
extension Dictionary: _CollectionOrStringish { }
extension Set: _CollectionOrStringish { }
다음으로 Optional where Wrapped: _CollectionOrStringish를 확장(extension)하자.

extension Optional where Wrapped: _CollectionOrStringish {
    var isNilOrEmpty: Bool {
        switch self {
        case let .some(value): return value.isEmpty
        default: return true
        }
    }
}

let x: String? = ...
let y: [Int]? = ...

if x.isNilOrEmpty || y.isNilOrEmpty {
    //do stuff
}

value(or:)
이것은 아주 간단하다. 이것은 함수로 표현된 ?? nil-coalescing 연산자이다.
extension Optional {
    func value(or defaultValue: Wrapped) -> Wrapped {
        return self ?? defaultValue
    }
}
이것은 아주 코드에서 연산자의숲(operator-soup)에 들어갈때 사용하는데, 어디서 사용하든 함수형태의 것이 명확하다. 혹은 함수 파라미터로 nil-coalescing을 써야할 때 사용한다.
// operator form
if x ?? 0 > 5 {
    ...
}

// function form
if x.value(or: 0) > 5 {
    ...
}

apply(_:)
이것은 리턴 값이 없는(혹은 ()을 리턴할 수도 있다) 버전의 map이다.
extension Optional {
    /// Applies a function to `Wrapped` if not `nil`
    func apply(_ f: (Wrapped) -> Void) {
        _ = self.map(f)
    }
}

flatten()
Update: VictorPavlychoko가 댓글로 짚어주었듯, ExpressibleByNilLiteral으로 flatten을 더 간단하게 만들 수 있다!
protocol OptionalType: ExpressibleByNilLiteral { }

// Optional already has an ExpressibleByNilLiteral conformance
// so we just adopt the protocol
extension Optional: OptionalType { }

extension Optional where Wrapped: OptionalType {
    func flatten() -> Wrapped {
        switch self {
        case let .some(value):
            return value
        case .none:
            return nil
        }
    }
}
ExpressibleByNilLiteral이 적용되지 않았을 때 사용할 수 있다는 것을 설명하기 위해, 교육의 목적으로 원래의 구현을 남겨두고 있다.

원래의 flatten
이중 옵셔널로 작업해본적이 있다면 이 익스텐션의 진가를 인정할 수 있을 것이다. 여기서 몇 프로토콜과 익스텐션을 필요로 하는데, 어떤 임의의 Wrappednone 케이스를 구성하는 방법을 찾기위한 꼼수이다. 이 이야기가 와닫지 않는다면 축하한다. 당신에게 평범하고 생산적인 삶을 살 수 있는 희맘ㅇ이 아직 있다. 아래에다가 설명을 갈게 쪼게어 해놓았으니 보자.
  1. 보통 컴파일러 마법은 모든 Optional<Wrapped>들에(감쌓인것 까지도) nil을 대입하게 해주고, 그냥 모든것이 잘 동작한다.
  2. flatten()으로부터 리턴을 표현하기 위해 추상 타입 맴버(연관타입)을 제공할 수 있다.
    * 익스텐션에서 self를 참조하고 아래처럼 제네릭 파라미터를 생략할 수 있다면
    extension Optional where Wrapped: Optional
    flatten() -> Wrapped.Wrapped 이렇게도 할 수 있을 것이나, 불행히도 지금 이렇게 할 수 없다.
  3. 일반적인 옵셔널 마법은 동작하지 않아야한다. 왜냐하면 프로토콜에 익스텐션이 연관타입 WrappedType을 반환할 것이라 약속했기 때문이다. 컴파일러 마법은 nil을 .none으로 만들 수 없다.
    * 만약 WrappedType: Optional<?>으로 만든다면: 동작은 할것이나 그렇게 할 수 없을 것이다.
    * 만약 WrappedType: Self로 만든다면: 스스로 동작은 할 것이나 그렇게 할 수 없을 것이다.
    (If we could constrain WrappedType: Optional<?> it would work but we can't.
    If we could constrain WrappedType: Self it would work but we can't.)
  4. 우리 프로토콜에서 init()를 요구조건으로 추가한다. 이것으로 WrappedType의 인스턴스를 구성하여 반환하는데 사용할 수 있다.
  5. OptionalType 익스텐션에서 self=nil을 사용할 수 있다. 그 이유는, 컴파일러가 self는 옵셔널이라는 것을 알고 있기 때문에 마법이 일어난다.
protocol OptionalType {
    associatedtype WrappedType
    init()
}

extension Optional: OptionalType {
    public typealias WrappedType = Wrapped
    public init() {
        self = nil
    }
}

extension Optional where Wrapped: OptionalType {
    func flatten() -> WrappedType {
        switch self {
        case .some(let value):
            return value
        case .none:
            return WrappedType()
        }
    }
}
언급된 몇 제약들은 결국 타입 시스템에대한 여러 증진으로 드러날 수 있다.

valueOrEmpty()
한 타입이 빈 것으로 표현될때의 작은 규약이며 이것으로 nil-coalesce하여 성가시지 않게 만들 수 있다.

/// A type that has an empty value representation, as opposed to `nil`.
public protocol EmptyValueRepresentable {
    /// Provide the empty value representation of the conforming type.
    static var emptyValue: Self { get }

    /// - returns: `true` if `self` is the empty value.
    var isEmpty: Bool { get }

    /// `nil` if `self` is the empty value, `self` otherwise.
    /// An appropriate default implementation is provided automatically.
    func nilIfEmpty() -> Self?
}

extension EmptyValueRepresentable {
    public func nilIfEmpty() -> Self? {
        return self.isEmpty ? nil : self
    }
}

extension Array: EmptyValueRepresentable {
    public static var emptyValue: [Element] { return [] }
}

extension Set: EmptyValueRepresentable {
    public static var emptyValue: Set { return Set() }
}

extension Dictionary: EmptyValueRepresentable {
    public static var emptyValue: Dictionary { return [:] }
}

extension String: EmptyValueRepresentable {
    public static var emptyValue: String { return "" }
}

public extension Optional where Wrapped: EmptyValueRepresentable {
    /// If `self == nil` returns the empty value, otherwise returns the value.
    public func valueOrEmpty() -> Wrapped {
        switch self {
        case .some(let value):
            return value
        case .none:
            return Wrapped.emptyValue
        }
    }

    /// If `self == nil` returns the empty value, otherwise returns the result of
    /// mapping `transform` over the value.
    public func mapOrEmpty(_ transform: (Wrapped) -> Wrapped) -> Wrapped {
        switch self {
        case .some(let value):
            return transform(value)
        case .none:
            return Wrapped.emptyValue
        }
    }
}

descriptionOrEmpty
Swift3에서 보간법(interpolated) 문자열 옵셔널을 포함한 새로운 경고는 유용하다; 대부분 여러분은 문자열이 "(nil)"으로 표사되길 원하진 않을 것이다. 그러나 그런 동작을 원하든 아니면 그냥 빈 문자열을 원할때든 간편한 프로퍼티들이 있다.
eextension Optional { 
     var descriptionOrEmpty: String { 
         return self.flatMap(String.init(describing:)) ?? ""
     } 

     var descriptionOrNil: String { 
         return self.flatMap(String.init(describing:)) ?? "(nil)" 
     } 
} 

결론
이게 유용하고 재미있었다면 이런 형식으로 임의의 익스텐션으로 몇몇 포스팅을 해왔다.

또한 이런 동작들에대한 아주 커다란 포스팅을 준비하고 있는데, 시간이 많이 걸리는 중이다. 글을 써내려가는 중이니 기다려주길 바란다.


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret
제목: Safety In Swift


스위프트는 일반적으로 "세이프한" 언어로 불린다. 실제로 swift.orgAbout 페이지에서 이렇게 말한다.
스위프트는 세이프티, 퍼포먼스, 소프트웨어 설계 패턴의 현대적인 방법 사용을 내장한 일반 목적 프로그래밍 언어이다.
그리고
  • 세이프. 코드를 작성하는 가장 명백한 방법은 안전한 방법으로 동작할 수 있다. 정의되지않은 동작은 세이프티의 적이고, 개발자 실수들은 스프트웨어가 제품으로 되기 전에 잡힌다. 가끔 세이프티를 선택하는것이 스의프트가 엄격하게 느껴질 것이나, 장기적으로 봤을땐 명쾌하게 시간을 절약해줄 것이라 믿는다.
  • 빠름. 스위프트는 C 기반 언어(C, C++, Objective-C)를 대체하려는 목적이 있다. 이것처럼! 스위프트는 많은 작업의 퍼포먼스에서 이런 언어들과 반드시 비교된다. 또한 퍼포먼스는 단지 나중에 깨끗하게 만들어야하는 짧은 폭발적인 빠름이 아니라, 예상가능하고 일관되어야한다. 진귀한 기능을 가진 많은 언어가 있다. 빠른것은 희귀하다.
  • 표현력. 스위프트는 개발자들이 기대하는 현재의 기능과 함께, 즐겁게 사용할 수 있는 문법을 제공하기위해 십여년에서 나온 컴퓨터 사이언스의 증진의 이점이 있다. 그러나 스위프트는 아직 끝난게 아니다. 우리는 언어 증진을 탐색하고 계속해서 어떤 일이 스위프트가 더 나아지게 만드는지 포용할 것이다.

예를들어 우리가 Optional 타입과같은 것과 작업할때, 스위프트가 세이프티를 끌어올리는 것은 명확하다. 이전에는 어떤 변수들이 null이 될 수 있는지 없는지 몰랐다. 이런 새로운 널러빌리티(nullability) 정보로, 우리는 명시적으로 null 경우를 다루게 되었다. 이런 "널러빌리티" 타입으로 작업할때, 우리는 크레쉬를 선택할 수 있고, 보통 느낌표(!)를 포함한 연산자를 사용한다. 여기서 세이프티에의한 의미는 명확하다. 여러분의 위험에대해, 여러분이 잠글지 말지 정할 수 있는 안전띠 역할을 하는 것이다.

그러나 다른 경우에, 세이프티가 부족해 보인다. 한 예제를 보자. 한 딕셔너리를 가지고 있다면, 주어진 키(key)로 값을 쥐는것은 옵셔널을 반환한다.
let person: [String: String] = //...
type(of: person["name"]) // => Optional<String>
그러나 비슷하게 배열에서 하면, 옵셔널을 받지 않는다.
let users: [User] = //...
type(of: users[0]) // => User

왜 그러지 않을까? 배열은 비어있을수도 있다. 만약 users 배열이 비어있다면 프로그램은 다른 실제 선택없이 크레쉬될 수 있다. 이것은 세이프하다고 보기 힘들다. 다시 환불받고싶다!

흠, 좋다. 스위프트는 오픈 개발 프로세스니, 아마도 스위프트 에볼루션 메일링리스트에 이 변경사항을 제안할 수 있다.

안된다. 어느쪽도 하지 못할것이다. 깃헙 저장소의 스위프트-레볼루션 페이지에있는 "일반적으로 거절된" 프로포절들는이런 변경을 받아드리지 않을것이라고 말했다.
  • Array<T> 서브스크립 접근을 T?T! 대신에 T를 반환하는 것으로 만든다. 범위를 넘는 배열 접은은 로직 에러라는 사실을 정확하게 반영하기 때문에, 현재 배열의 동작은 의도적이다. 현재 동작을 바꾸는 것은 수용될 수 없는 정도로 배열 접근을 느리게할 수 있다. 이 주제는 이전에도 여러번 나왔지만 매우 받아드려지지 않을것으로 보인다.

무엇을 주는가? 진술된 이유는 이 특정 상황은 속도가 매우 중요하기 때문이라 했다. 그러나 위의 About 패이지 링크로 돌아가보면, "세이프"는 "빠름" 이전에 언어의 표현으로 목록에 나와있다. 세이프티가 속도보다 중요하기라도 한걸까?

여기엔 근본적인 논쟁이 있고, 그 해결책은 "세이프"라는 단어의 정의를 잡아야한다. 일반적인 "세이프"의 이해는 정도의 차이가 있어도 "크레쉬가 나지 않음"인 반면, 스위프트 코어 맴버들은 종종 "의도치않게 틀린 메모리에 절때 접근하지 않는것"이라는 의미로 쓴다.

이 경우, 스위프트의 배열 서브스크립션은 "세이프"이다. 배열이 절때로 할당된 범위 넘어서 메모리에있는 데이터에 접근하지 않을것이다. 메모리에 무엇이있든, 포함하지 않는 메모리에 접근하려고 하기 전에 크레쉬를 낼것이다. 같은 방법에서, 옵셔널 타입은 현존하는것으로부터 모든 클래스와 버그들(null에 접근하려는것)을 막으며, 이런 동작은 현존하는것으로부터 다른 클래스와 버그들(버퍼 오버플로우)을 막는다.

Chris Lattner가 ATP와했던 그 인터뷰의 24:39에서이런 구별을 만든것을 들어볼 수 있다.
 커뮤니티에 혼란에서 비용이라는 관점에서 보면 이해할 수 있는 유일한 방법은 우리가 세이프한 프로그래밍 언어를 만들면이다. "버그가 없다"는 것의 "세이프"가 아니라, 높은 퍼포먼스를 제공하고 프로그래밍 모델 앞으로 가는동안 메모리 세이프티 관점에서의 "세이프"이다.

아마 "메모리-세이프"는 그냥 "세이프"라는 용어보다 더 낫다. 방법은 이렇다. 어던 어플리케이션 프로그래머가 옵셔널로 돌아가는걸 좋아하는것 대신에 범위밖의 배열 접근에 트랩을 거는것을 좋아하는 반면, 모두가 유요하지 않은 데이터를 담은 변수로 계속 하는것보다 그 프로그래을 크래쉬 내는 것을 더 좋아할 수 있다는 것에 동의할 수 있다. 한 변수는 잠재적으로 버퍼 오버플로우 공격에 이용될 수 있다.

이 두번째 등가교환(버퍼 오버플로우를 허용하는것 대신 크래쉬 나는것)은 당연해 보이지만, 몇 언어들은 이 보장을 하지 않는다. C에서는 배열의 범위밖을 접근하는것이 여러분에게 정의되지 않은 동작을 할 수 있게 해주고, 어떤일이든 일어날 수 있다는 뜻이며, 우리가 사용했던 컴파일러 구현에 의존한다. 특히 프로그래머가 실수를 만들었다고 빠르게 말할 수 있을때(배열의 범위 밖 접근 같은), 그들이 옵셔널을 반환하는것 대신에 결정적으로 정크 메모리를 반환하는것 대신에 수용되는 곳에 크래쉬를 내는것을 좋게 느낀다는 것을 스위프트팀은 봐왔다.

"세이프" 정의를 사용하는 것도 "언세이프"한 API가 무엇을위해 설계되었는지 분명하게 한다. 왜냐면 그들은 직접적으로 메모리를 더럽히고, 프로그래머가 절때 유효하지않은 메모리에 접근하지 않을거라는 보장의 특별한 신경을 쓰게 만든다. 이것은 극도로 힘들고, 전문가들도 틀릴 수 있다. 이 주제에대한 글에 흥미가 있다면, 세이프한 방법으로 C를 스위프트에 연결시키는 Matt Gallagher’의 글 확인해보자.

스위프트와 그 코어팀의 "세이프"의 정의는 여러분의 생각에 100% 맞춰지지 않을 것이나, 그들은 클래스의 버그를 막아주어서 여러분같은 프로그래머들이 매일매일 그것에대해 생각하지 않아도 된다. 그 의미를 이해할때, "세이프"를 "메모리 세이프"로 대체하여 사용하면 종종 도움이 뒬 수 있다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret

Swift는 escaping 클로저와 non-escaping 클로저에 차이를 두고 있다. escaping 클로저는 한번 호출되고나면 리턴값으로 클로저를 반환하는 함수이다. 클로저는 인자로 받은 함수 스코프를 escape한다.

클로저를 escape 하는 것은 종종 아래 예시처럼 비동기 컨트롤 플로우와 연관되있다. 
  • 함수가 백그라운드 작업을 시작하고 즉시 리턴하면, 완료 핸들러를 통해 백그라운드 작업의 결과를 알린다.
  • 뷰 클래스가 버튼 탭 이벤트를 다르기위해 프로퍼티에 클로저를 저장해둔다. 이 클래스는 사용자가 버튼을 탭 할때마다 클로저를 호출한다. 클로저 프로퍼티는 세터를 escape한다.
  • 여러분은 DispatchQueue.async를 사용하여 디스패치 큐에서 비동기 실행을 위한 작업을 스케줄링한다. 이 테스크 클로저는 비동기에 호출된 이후에도 소멸되지 않은 채 살아있다.
DispatchQueue.sync와 대조되는데, 이것은 리턴되기 전에 테스크 클로저의 실행이 끝나기 전까지 기다린다. 이 클로저는 절때 escape하지 않는다. 표준 라이브러리에 map, 다린 일반적인 sequence, 그리고 collection 알고리즘에도 동일하다.

escaping 클로저와 non-escaping 클로저의 차이가 왜 중요할까?
간단히 말해 메모리관리 때문이다. 클로저가 붙잡고있는 모든 오브젝트는 강참조로 들고 있으며, 클로저 안에서 self의 프로퍼티나 self의 메소드에 접근하려 한다면 이 모든것들이 묵시적으로 self 파라미터를 다루기 때문에 self까지 포함하여 들고 있는다.

이러한 방식은 굉장히 참조 사이클(reference cycle)을 마주치기 쉬운데, 이것이 왜 컴파일러가 클로저 안에서 명시적으로 self를 쓰게 만드는지에대한 이유이다. 명시적으로 쓰게 만듦으로서 당신에게 잠재적인 참조 사이클에대해 생각해볼 수 있게 해주고, 붙잡고 있는 항목들을 이용해 손수 해결할 수 있게 해준다.

그러나 non-escaping 클로저로는 참조 사이클을 만드는게 불가능하다. 클로저는 함수 리턴시점까지 붙잡아둔 모든 오브젝트를 릴리즈(release) 시킬 것이라는 것을 컴파일러가 보장한다. 이러한 이유로 escaping 클로저에서만 명시적으로 self를 사용하여 참조할 것을 요구한다. 이 점이 non-escaping 클로저 사용을 더 확실히 즐겁게 해준다.

non-escaping 클로저의 또다른 장점은 컴파일러가 더 적극적으로 퍼포먼스 최적화를 수행할 수 있다는 점이다. 예를들어 클로저의 라이프타임을 알고 있을때 몇몇 리테인(retain)과 릴리즈(release) 호출은 생략할 수 있다. 또, non-escaping 클로저라면 클로저의 컨텍스트를 위한 메모리가 힙이 아닌 스택에 담아둘 수 있다.(현재 Swift 컴파일러가 이러한 최적화를 시키는지는 잘 모르지만 2016년3월 버그리포팅에서 그렇게 하지말자고 제안이 들어왔었다)

디폴트로 클로저는 non-escaping이다.
Swift3부터 non-escaping 클로저가 디폴트이다. 만약 클로저 파라미터에 escape 시키고 싶으면 그 타입에다가 @escaping 지시자를 써야한다. 예를들어 DispatchQueue.async (escaping)와 DispatchQueue.sync (non-escaping) 선언이 있다.

Swift3 전까지는 좀 다른 방식으로 동작했었는데, escaping이 디폴트이고 오버라이드에 @nonescape를 추가할 수 있었다. 이러한 새로운 동작은 디폴트에의해 더 안전해진다는 면에서 더 좋은데, 이제 함수 인자는 반드시 참조 사이클의 잠재성이 있다는 것을 명시적으로 표시해주어야한다. 따라서 @escaping 지시자는 이 기능을 사용하는 개발자에게 경고를 해주는 역할을 한다.

...그러나 오직 즉석 함수 파라미터(immediate function parameters)로서
디폴트에의한 non-escaping 규칙에 대해 주목할 것이 있다. 이는 직접 함수 매개 변수 위치의 클로저에만 적용된다. 즉, 함수 타입을 가지는 모든 함수 인자에 적용된다. 다른 모든 클로저는 escaping하고 있다.

직접 파라미터 위치가 무슨 뜻일까?
예제를 한번 보자. 가장 간단한 예제로는 map이 있다. 이 함수는 직접 클로저 파라미터를 받는다. 우리가 보았듯 클로저는 non-escaping이다. (여기서 실제 map의 표시를 말하려는게 아니므로, 그것들을 조금 생략하겠다)

함수 타입의 변수들은 항상 escaping이다.
반대로, 변수나 프로퍼티가 함수 타입을 가질 수 있다. 이때는 명시적인 지시 없이 자동으로 escaping이 된다.(사실은 @escaping을 넣으면 에러가 뜬다) 이렇게 이해할 수 있는데, 변수에 값을 할당하면 묵시적으로 값을 변수의 범위로 escape할 수 있기 때문이다. 이것은 non-escaping 클로저로 허가될 수 없다. 그러나 드물게 지시되지 않는 함수는 파라미터에서의 의미가 아닌 다른 곳에서는 다른 의미를 가지기 때문에 혼란스러울 수 있다.

옵셔널 클로저는 항상 escaping이다.
더욱 놀라운 점은, 파라미터로 쓰이지만 다른 타입(튜플이나 enum case, 옵셔널 같은)으로 감쌓여진 클로저들 또한 escaping이라는 것이다. 이 경우에 클로저는 더이상 직접 파라미터가 아니므로 자동으로 escaping된다. 그 결과 Swift3에서는 파라미터가 옵셔널이면서 동시에 non-escaping한 곳에 함수인자로 받는 함수를 만들지 못한다. 아래의 다소 인위적인 예제를 생각해보자. transform 함수는 정수 n과 옵셔널 변환 함수인 f를 받아 f(n)를 반환하거나 f가 nil이면 n을 반환한다.

여기서 ( (Int) -> Int )?가 Optional<(Int) -> Int>의 축약이기 때문에 함수 f는 escaping이며, 따라서 함수 타입은 직접 파라미터 위치에 있지 않다. 이 결과는 우리가 바라는 것이 아닌데, 여기서 f가 non-escaping 될 수 없는 이유가 없기 때문이다.

옵셔널 파라미터를 디폴트 구현으로 대체하자.
Swift 팀은 이 한계를 알고있고, 미래의 배포에서 고치기로 계획했다. 그전까지 우리가 이것을 알고 있어야한다. 현재 강제로 옵셔널 클로저를 non-escaping할 방법은 없지만, 많은 경우에 클로저에다 디폴트 값을 제공하여 옵셔널 인자를 피할 수 있을 것이다. 우리 예제에서는 디폴트 값이 항등 함수(identity function)이고, 이 함수는 간단하게 인자를 바꾸지않고 그대로 반환한다.

옵셔널과 non-escaping 변형을 제공하기 위해 오버로드를 사용하자
디폴트 값을 제공하기 힘든 경우라면, Michael Ilseman이 오버로드를 사용할 것을 제안했다. 여러분은 함수의 두기자 변형을 만드는데, 하나는 옵셔널(escaping) 함수 파라미터를 받고, 하나는 non-옵셔널, non-escaping 파라미터를 받는다.

어떤 함수가 호출되었는지 설명하기 위해 print 상태를 추가했다. 여러 인자로 이 함수를 테스트해보자. 당연하게도 nil을 보내면, 그 인풋과 일치하는 것이 하나밖에 없으므로 타입 체커가 첫번째 오버로드를 선택한다.
동일하게, 옵셔널 함수 타입을 가지고 있는 변수를 보낸다.
그 변수가 non-옵셔널 타입일지라도, Swift는 여전히 첫번째 오버로드를 선택할 것이다. 그 이유는 변수에 저장된 함수는 자동으로 escaping되고, 따라서 non-escaping 인자를 예상한 두번째 오버로드와는 일치하지 않는다.

그러나 클로저 표현식을 보낼때 이것은 바뀐다. 즉 함수 리터럴이 이 자리에 있을때 말이다. 이제 두번째 오버로드 non-escaping이 선택된다.

리터럴 클로저 표현식으로 higher-order 함수를 호출하는 것은 굉장히 일반적이므로, 대부분의 경우 선택적으로 여전히 nil을 보낼 수 있게 해줌으로서 여러분을 즐거운 길(참조 사이클을 생각할 필요 없는 non-escaping)로 안내해줄것이다. 이런 방식으로 한다면 왜 두가지 오버로드가 필요한지 증명할 수 있을 것이다.

타입에일리어스(typealiases)는 항상 escaping이다.

마지막으로 한가지 알고 있어야 하는 것은 Swift3에서는 타입에일리어스에 escaping 혹은 non-escaping 지시자를 넣을 수 없다는 것이다. 함수 선언에서 함수 타입을 위해 타입에일리어스를 사용한다면 그 파라미터는 항상 escaping으로 생각될 것이다. 이 버그 수정은 이미 마스터 브런치에서 이루어졌고, 다음 배포에 적용될 수 있을 것이다. 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret

Swift는 Objective-C와 같은 벤더를 가지고 있기 때문에 Objective-C의 후계자이다. 이것은 비슷하게 생기지도 않고 똑같이 동작하지도 않으며 비슷한 느낌이 들지 않는다. Objective-C에서 (메소드 이름처럼) 잘 동작했던 패러다임은 새로운 Swift 세계로 넘어오기 위해 천천히 바뀌고 있다. 예를들어 아래 메소드는 Objective-C에서 이런식의 긴 형태로 호출되었다.
[string dataUsingEncoding:NSUTF8StringEncoding];

Swift2.2에서는 다소 어색하게 바뀌었고

string.dataUsingEncoding(NSUTF8StringEncoding)

Swift3에서 이 메소드는 비로소 간결해졌다.

string.data(using: .utf8)

Swift3 버전의 메소드가 Swift에게 알맞는 것이고, 같은 의미에서 Objective-C 버전은 Objective-C 버전에 알맞는 것이다. 여러분의 메소드를 어떻게 Swift에 맞춰 사용할 수 있는지 다루는데 이 글이 도움을 줄 것이다.


우리 앱을 만들기 위해 사용하는 것들 중에 Swift하게 바꿔야하는 프레임워크나 언어적인 부분들이 있다. 오늘 나는 델리게이트 네이밍에대해 이야기하려 한다.

Swift에서 델리게이트는 Obejctive-C에서처럼 부드럽게 잘 바뀌지 않는다. Objective-C는 "sender"와 "receiver"에대해 굉장히 친숙하다. 애플의 Objective-C 문서에서는 이 용어를 자주 사용한다. 예를들어 UIResponer에있는 isFirstResponder 메소드 문서를 확인해 보아라.

receiver가 첫 응답자(first responder)인지 나타내는 불리언 값을 반환한다.
- (void)buttonTapped:(UIButton *)sender { }
델리게이트는 비슷한 방식으로 동작한다:: Objective-C에서 델리게이트 메소드의 첫 인자도 항상 sender였다. 이것이 왜 유용할까? 만약 receiver가 같은 타임의 여러 오브젝트 델리게이트라면 그것들을 구분해야할 것이다. 델리게이트는 첫번째 인자를 제공하면서 그것을 구분할 수 있게 해준다.

나는 Backchannel SDK 예제로 몇 클래스 이름을 간단히 해볼 것이다.

여기엔 두가지 타입의 델리게이트 메소드가 있는데, 첫번째는 이벤트가 일어났는지 나타낸다.
- (void)messageFormDidTapCancel:(BAKMessageForm *)messageForm;
Swift로 번역하면 다음과 같다.
func messageFormDidTapCancel(_ messageForm: BAKMessageForm)
이것은 더이상 Swift3에 알맞지 않다. Swift3에서는 불필요한 것(두번 나타난 messageForm)을 제거하고, 언더바를 사용해서 없에는게 아니라 자동으로 첫번째 인자 이름을 따른다.

두번째 타입의 델리게이트 메소드는 이벤트가 일어나고 거기에 몇 데이터를 가지고 있는지 나타낸다. 아래 예제를 보자.
- (void)messageForm:(BAKMessageForm *)messageForm didTapPostWithDraft:(BAKDraft *)draft;
- (void)messageForm:(BAKMessageForm *)messageForm didTapAttachment:(BAKAttachment *)attachment;
Swift3으로 번역하면 다음과 같다.
func messageForm(_ messageForm: BAKMessageForm, didTapPostWithDraft draft: BAKDraft)
func messageForm(_ messageForm: BAKMessageForm, didTapAttachment attachment: BAKAttachment)
흠, 좋지 않아 보인다. 왜 이 메소드를 둘다 messageForm이라 부르나? 또한 명사로 시작하는 메소드는 별로 좋지 않다. 보통 그 타입의 오브젝트를 반환할때 사용한다(NSStringdata(using:)를 생각해보면 Data를 반환한다). 여기서는 아무 메시지 형식 오브젝트도 반환하지 않을 것이다. 그 "메시지 형식"은 사실 첫번째 파라미터의 이름이다. 매우 혼란스러운 메소드 이름이 아닐 수 없다!

이 두가지 타입의 델리게이트 메소드는 그 줄 마지막에 "sender"를 보내고 동사를 앞으로 옮겨서 고칠 수 있다. 먼저 sender가 델리게이트를 알려주는 이벤트는 messageFormDidCancel대신 didTapCancel이다.
func didTapCancel(messageForm: BAKMessageForm)
굉장히 좋아졌다. 액션이 앞으로 오면서 메소드 이름이 되었다. 그리하여 메소드가 무슨 일을 하는지 더 명확해졌다. 내 생각엔 읽을때 좀 더 좋게 하기 위해서 파라미터 이름 대신에 전치사를 써도 괜찮을 것 같다.
func didTapCancel(on messageForm: BAKMessageForm)
이렇게 써보면서 아직까지는 전치사를 사용하기에 어색해보이는 상황을 발견하지 못했다. 그리고 다른 여러 상황에서도 "on", "for", "with", "in" 모두 유용하게 쓰인다는 것도 알아냈다. 사용자가 "on"형식을 탭 하면서 나는 "on"이 이곳에 적합하다고 생각한다.

이제 뒤에 데이터를 전달하는 델리게이트 메소드도 한번 보자. 동사를 앞으로 보내는게 도움이 될 것이고, 델리게이트 이름에 전치사로 바꾸는 것도 이런 메소드 타입을 깔끔하게 해준다. 아래 예제 대신
func messageForm(_ messageForm: BAKMessageForm, didTapPostWithDraft draft: BAKDraft)
좀 더 Swift하게 만들고
func didTapPost(with draft: BAKDraft, on messageForm: BAKMessageForm)
좀 더 바꾼다.
func didTap(attachment: BAKAttachment, on messageForm: BAKMessageForm)

이러한 규칙은 나를 빼고 누구도 보증하진 않지만 우리가 그런식으로 쓰면 현재 규칙보다 더 이해하기 쉬워질거라 생각된다. 더 나아가 아마 내 Swift 델리게이트 메소드를 이런 구조로 짜기 시작할 것이다.

UITableView의 델리게이트와 데이터소스 메소드를 보면서 이것이 나중엔 어떻게 생겼을지 생각해보자.
func numberOfSections(in tableView: UITableView) -> Int
numberOfSections는 이 스킴을 따르고 이미 꽤 좋아보인다.

그러나 아래 메소드는 그렇게 좋아 보이진 않는다.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
여기 좀 더 사랑스러운 방법이다.
func numberOfRows(inSection section: Int, in tableView: UITableView) -> Int
func cellForRow(at indexPath: IndexPath, in tableView: UITableView) -> UITableViewCell
func didSelectRow(at indexPath: IndexPath, in tableView: UITableView)

 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret

델리게이트도 괜찮지만, 더 나은 방법이 있다.

당신은 iOS앱을 만들고 있는 개발자라 가정하자. 당신은 당신이 할 수 있는 최선의 견고한(SOLID) 아키텍처로 앱의 구조를 만들어 놓았다. 앱은 모델, 네트워크 레이어, UI 레이어 또는 그것을 돕는 것들로서 구성된다. 이 레이어들 사이에 데이터를 주고 받을때 책에서는 델리게이션을 이용해라고 알려준다. 실제로 iOS 개발에서 일반적으로 사용되는 유용한 패턴이기도 하다.

델리게이션은 간단히 말하자면, 어떤것의 변화로부터 알림받기 원할때 그 어떤것에 알림받기 원하는 대상을 등록해놓는 방식의 패턴이다. 이렇게하면 그 어떤것으로부터 반응(react)할 수 있다. 예를들면 ViewController가 네트워크 서비스에게 말을 걸어서 (ViewController를 네트워크 서비스 델리게이트로 등록해서) 어떤 요청이 완료될때 자신에게 알려달라고 한다. 이때 ViewController를 네트워크 서비스의 델리게이트로 만들어 가능하게 된다. 네트워크 서비스는 요청이 완료되었을 때 델리게이션 메소드를 호출 할 것이다.
델리게이트 참조를 보내는 것은 괜찮은 방법이고 기능적으로도 아무 문제가 없다. 그러나 Swift에서는 더 나은 방법이 있고, 왜 이 방법이 더 나은지 설명해 보겠다.


델리게이션을 위해 콜백을 사용
콜백은 델리게이트 패턴과 비슷한 기능을 가진다. 어떤 일이 발생할때 다른 오브젝트가 알게 해주고, 데이터를 전달하는 기능이다.

델리게이트 패턴과 다른점은, 응답받고 싶은 객체 자체를 넘겨주는 것 대신에 함수만을 넘겨준다. 함수는 Swift에서 클래스의 첫번째 요소이다. 따라서 함수를 프로퍼티로 가지고 있을수도 있다.
MyClass는 이제 myFunction 이라는 프로퍼티를 가지는데, 어딘가에서 호출할 수도 있고, 누구나 값을 바꿀 수도 있다(Swift에서 정의된 규칙에의해 프로퍼티는 디폴트로 internal이 된다). 이것이 델리게이션 대신 콜백을 사용하는 기본 아이디어이다. 아래 예제는 위 예제에서 델리게이트 대신 콜백으로 대체한 것이다:
콜백을 사용하는 다른 상황은 데이터가 바뀔때 알림을 받고 싶을 경우이다. 프로퍼티 옵저버에서 콜백을 호출함으로서 가능하다:
콜백에대한 간단한 노트 : 델리게이트에서는 리테인(retain) 사이클을 멈추기위해 weak 프로퍼티로 만들어야하는 것처럼, 여러분도 클로저 안에서는 self를 weak 변수로 해두어야한다.

그래서 왜 콜백이 더 나은가?
1. 분리됨(Decoupling)
델리게이트는 코드를 분리하는 경향이 있다. 프로토콜로 구현하는 한 NetworkService에게 누가 그것의 델리게이트인지 알 필요가 없다. 그러나 델리게이트가 프로토콜 구현을 가지고 있고, @objc 프로토콜 대신 Swift를 사용한다면, 델리게이트는 프로톸콜에서 모든 메소드 구현을 가진다.(따라서 옵셔널 프로토콜 일치도 필요없다)

다르게 말해보면 콜백을 사용할 때, NetworkService는 메소드를 호출하기위한 델리게이트를 가지고 있을 필요가 없고 누군가가 이 메소드를 구현할 것이라는 것을 알고 있으면 된다. 메소드가 호출되는 시점만 관리하면 되고, 이 메소드가 어떻게 구현되있을지는 알 필요가 없는 것이다.

2. 다중 델리게이션
요청이 끝나고 ViewController에 알림을 주고 싶은데, 그때 로그를 남기는 클래스나 통계를 남기는 클래스를 넣고 싶을 수 있다.

델리게이트로 구현하면, 델리게이트의 배열을 가지고 있어야 할 것이다. 아니면 세개의 서로 다른 프로토콜을 가지는 델리게이트 프로퍼티를 가질 것이다!

그러나 콜백으로 구현한다면 함수의 배열을 선언하고(Swift의 이런 점이 좋다!) 뭔가 처리가 끝날때 각각 호출하면 된다. 따라서 리테인 사이클의 위험을 감수하거나 어마어마한 양의 코드로 작성되거나 하는 수많은 오브젝트와 프로토콜이 필요없어진다.

3. 일을 분리하기에 더 명확하다.
내가 생각하는 델리게이트와 콜백의 차이는 이렇다. 델리게이트는 NetworkService가 델리케이트에게 "이봐, 나 갱신됐어!"라고 말하는 반면 콜백은 델리게이트가 NetworkService를 응시하고 바라보고 있는 느낌이다.

실제로 작은 차이 같지만, 후자의 방법으로 생각하면 NetworkService가 자신의 기능을 하지 못하고 화면 표시의 기능으로 변질되는 그런 것을 방지해주는 패턴으로서는 크게 도움이 될 것이다!

4. 테스트하기 쉬움!
유닛테스트와 함께 구현하면 항상 코드베이스가 두배로 늘어난다고 느낄것이다. 그 이유는 앱의 모든 델리게이트를 포함하여 매 프로토콜마다 목(mock)을 만들어야하기 때문이다.

콜백으로 구현하면 어떠한 델리게이트에 목을 할 필요 없을 뿐만 아니라 각 테스트에서 원하는 어떠한 콜백이든 사용할 수 있게 해준다.

한 테스트에서 콜백이 제대로 호출되는지 호출는지 테스트해보고, 다른 테스트에서 그것이 호출될 때 옳바른 결과를 내는지 테스트 할 수 있다. 그리고 어떠한것도 someFuncDidGetCalled 불리언(boolean)이나 비슷한 프로퍼티를 가진 복잡하게 목(mocked)된 델리게이트를 필요로 하지 않는다.

나는 개인적으로 콜백이 코드와 테스트를 명확하게 만들어주고, 더 Swift스럽게 데이터를 주고 받는 방법이라 생각한다. 여러분이 오늘 뭔가 새로운 것을 배웠기를 바란다! 



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

트랙백  0 , 댓글  0개가 달렸습니다.
secret