'스위프트'에 해당하는 글 27건

제목: iOS11: Machine Learning for everyone

WWDC 2017에서 한가지는 확실하게 만들었다. 애플은 기기에서의 기계학습을 확실히 밀고 있다.

그리고 앱 개발자들이 여기에 조인할 수 있도록 가능한 쉽게 만들고 싶어한다.

작년 애플은 기본 컨볼루셔널 신경망(basic convolutional networks)을 위한 Metal CNN and BNNS frameworks를 발표했다. 올해는 Metal, 새로운 컴퓨터 비젼 프레임워크, Core ML의 수많은 수가사항이 있다. 툴킷은 ML 모델을 여러분의 앱에 아주 쉽게 넣을 수 있게 해준다.


이 블로그 포스팅에서는, iOS11와 macOS10.13에서 새로운 기계학습에대한 내 생각(과 내 경험)을 공유하려고 한다.

Core ML
Core ML은 WWDC에서 많은 주목을 받았고, 그 이유는 알기 쉽다. 바로 많은 개발자들이 그들의 앱에 넣으려고 했던 프레임워크이기 때문이다.

이 API는 꽤 간단하다. 당신이 할 수 있는 것은 다음과 같다.
  1. 학습된 모델을 불러오기
  2. 예측 만들기
  3. 개이득!!!
이 말이 제한적으로 보일 수 있지만, 실제로 모델을 불러와서 예측을 하는 것은 보통 여러분의 앱에서 하고 싶었던 모든 것일 수 있다.

예전에는 학습된 모델을 불러오는 것이 굉장히 힘들었다(사실 library to take away some of the pain에대한 글을 썼었다). 그러니 이제 단지 두단계로 간단해져버려 굉장히 행복하다.

모델은 .mlmodel 파일안에 담겨있다. 이것은 새로나온 공개형 파일 포멧(open file format)으로 여러분의 모델, 입력과 출력, 클래스 레이블 그리고 데이터에 일어나는데 필요한 전처리에서 그 레이어를 표현한다. 또한 학습된 모든 파라미터들을 담고있다(가중치와 기저).

모델을 사용하는데 필요한 모든것이 이 한 파일 안에 들어 있는 것이다.

여러분은 간단하게 여러분의 프로젝트에다 이 mlmodel 파일을 떨어뜨리면 Xcode가 자동으로 스위프트(혹은 Objective-C)를 감싸는 클래스로 생성하며, 이런 과정이 모델을 사용하기 굉장히 쉽게 만들어준다.

예를들어, 여러분의 Xcode 프로젝트에 ResNet50.mlmodel 파일을 추가했으면, 이렇게 작성할 수 있겠다.
let model = ResNet50()
모델을 인스턴트화한다. 그리고 아래에는 예측을 한다.
let pixelBuffer: CVPixelBuffer = /* your image */

if let prediction = try? model.prediction(image: pixelBuffer) {
  print(prediction.classLabel)
}
그리고 이게 전부이다. 모델을 불러오거나 스위프트에서 그 결과물을 변환하기위해 다른 코드를 짤 필요가 없다. 이 모든것을 Core ML과 Xcode가 관리해준다. 멋지다!
주의: 화면뒤에서 무슨일이 일어나는지 알고싶으면, Project Navigator에 mlmodel 파일을 선택하여 버튼을 클릭하면 생성된 헬퍼 코드로 파일을 볼 수 있다.
Core ML은 모델을 CPU에서 돌릴지 GPU에서 돌릴지 스스로 결정할 것이다. 그리하여 가용의 리소스 사용을 최적화할 수 있게 해준다. 또한 Core ML은 특정 부분만 GPU에서 동작하고(많은 계산이 많이 필요한 작업) 다른 부분은 CPU(메모리가 많이 필요한 작업)에서 동작하도록 쪼갤 수 있다.

Core ML이 CPU를 사용할 수 있는 능력은 개발자들에게 또다른 큰 이점을 준다. iOS 시뮬레이터에서 돌려볼 수 있다는 점이다(어떤것은 Metal에서 불가능한데, 이것 또한 유닛테스트로 잘 돌아가지 않는다).

Core ML이 지원하는 모델은 무엇인가?
위의 ResNet50 예제는 이미지 분류기이지만, Core ML은 여러 타입의 모델을 다룰 수 있다.
  • support vector machines(SVM)
  • tree ensembles such as forests and boosted trees
  • linear regression and locgistic regression
  • neural networks: feed-forward, convolutional, recurrent
이 모든 것들은 회기(regression)뿐만 아니라 분류(classification)에도 사용될 수 있다. 추가로 여러분의 모델은 one-hot encoding, feature scaling, imputation of missing values등과같은 전형적인 ML 전처리 과정을 담고 있을 수 있다.

애플은 학습시킨 여러 모델을 다운로드 받을 수 있게 해놓았다. Inception v3, ResNet50, VGG16같은 것이 있는데, 파이썬 라이브러리 Core ML Tools로 자신만의 모델을 변환할 수도 있다.

현재 여러분은 Keras, Caffe, scikit-learn, XGBoost, libSVM로 학습한 데이터를 변환할 수 있다. 이 변환 툴은 지원하는 버전에대한 약간의 명세이다. 예를들어 Keras 1.2.2는 동작하지만 2.0은 아닌. 다행인점은 이 도구가 오픈소스여서 미래에는 더 많은 학습 툴킷을 지원할 부분에는 확신할 수 있다.

그리고 다른 모든것이 실패하면 언제나 여러분만의 변환기를 만들 수 있다. mlmodel 파일 포멧은 공개형이고 꽤 직관적으로 사용할 수 있다(photobuf 포멧 내에 있고, 스팩은 애플이 명세해놓음).

한계
Core ML은 모델을 빨리 불러와서 여러분의 앱에서 실행하기에는 훌륭하다. 그러나 이런 간단한 API로는 그 한계를 가지고 있다.
  • 지원하는 모델 타입들은 감독 기계학습(supervised machine learning)이다. 비감독 학습 알고리즘이나 강화 학습은 안된다. (비록 "제네릭" 뉴럴 네트워크 타입을 지원하는데, 이것을 이용할 수는 있을것 같다)
  • 기기에서 학습은 지원하지 않는다. 당신은 오프라인 툴킷을 사용하여 모델을 학습시키고 모델을 Core ML 포멧으로 변환해야한다.
  • Core ML이 특정 레이어 타입을 지원하지 않는다면, 사용할 수 없다. 이 점에서 여러분이 직접 계산한 커널로 Core ML을 확장시키는게 불가능하다. TensorFlow같은 툴들이 질반 목적 컴퓨테이셔널 그래프를 만들기위해 사용되는 곳은, mlmodel 파일 포멧은 유연성과는 좀 멀다.
  • Core ML 변환 툴은 제한된 수의 학습 툴과 명세한 버전만 지원한다. 예를들어 TensorFlow에서 모델을 학습했으면, 이 툴을 사용할 수 없고 여러분의 변환 스크립트를 작성해야할 것이다. 그리고 내가 덫붙이자면, 여러분의 TensorFlow 모델이 mlmodel에서 지원하지 않는 것을 하고 있다면, Core ML에서 모델을 사용할 수 없다.
  • 중간 레이어(intermediate layer)에서 만들어진 아웃풋을 볼 수 없다. 네트워크의 마지막 레이어에서 나온 예측만 볼 수 있다.
  • 100% 확신이 되진 않지만, 모델 업데이트를 다운받는 것이 문제가 있어 보인다. 여러분이 다시 학습시키고 싶은데, 바뀐 모델바다 앱의 새로운 버전을 내고 싶지 않으면 Core ML은 여러분에게 적절하지 않을 것이다.
  • Core ML은 (편의를위해) CPU에서 실행하는지 GPU에서 실행하는지 숨기는데, 이것이 앱을 위해 잘 동작할거라 믿어야만 한다. 당신이 아무리 아무리 원한다고 해도 Core ML에게 강제로 GPU에서 실행하라고 할 수 없다.
당신이 이런 한계들과 공존할 수 있다면, Core ML은 당신에게 맞는 프레임워크이다.

그렇지않거나, 전체를 컨트롤 하고 싶다면, Metal Performance Shaders나 Accelerate 프레임워크로 여러분만의 것을 만들어야할 것이다.
물론 실제 마법이 Core ML에 있진 않지만, 여러분의 모델에 들어있다. 여러분이 시작할때 적절한 모델을 가지고 있지 않다면 Core ML은 소용없을 것이다. 그리고 모델을 설계하고 학습시키는것은 기계학습에서 어려운 부분이다.

간단한 데모 앱
Core ML을 가지고 놀아보려고 간단한 데모를 만들었다. 언제나처럼 깃헙에서 소스를 확인할 수 있다.


이 데모 앱은 고양이 사진을 분류하기위해 MobileNet architecture을 사용했다.

원래 이 모델은 Caffe에서 학습되었다. 이것이 mlmodel 파일로 변환하는데 약간의 노력이 들게 만들었으나, 한번 변환한 모델로 만들고나니 앱에서 빌드하기 굉장히 쉬었다. (변환 스크립트는 깃헙 저장소에 들어있다)

앱은 아직 멋지지 않지만(단순히 정적인 이미지에대해 상위 5가지 예측의 아웃풋을 낸다) Core ML을 어떻게 쉽게 사용했는지 보여준다. 그냥 코드 몇줄만이 필요할 뿐이었다.
주의: 데모앱이 시뮬레이터에서는 잘 동작하지만 기기에서는 크래쉬가 난다. 읽어내려가서 왜 그런지 찾아내보자 ;-)
물론 안에서 무슨일이 일어나는지 알고싶었다. 실제로 mlmodel 파일이 mlmodelc 폴더에 컴파일되었으며 이 폴더는 여러분의 앱 번들 안에 있다. 이 폴더는 여러 다른 파일들, 몇몇 바이너리, 몇몇 JSON들을 가진다. 따라서 여러분의 앱에 실제로 넣기전에 Core ML이 어떻게 mlmodel을 변환하는지 볼 수 있다.

예를들어 MobileNet Caffe 모델은 소위 Batch Normalization 레이어라 불리는 것을 사용하며, 이것들이 변환된 mlmodel 파일안에 들어있다는 것을 확인했다. 그러나 컴파일된 mlmodelc에서는 Batch Normalization 레이어가 보이지 않았다. 이것은 Core ML이 모델을 최적화시켰다는 좋은 소식이다.

mlmodelc는 여전히 스케일링 레이어(scaling layer)를 포함하며 반드시 필요하지 않아 보이므로 좀 더 모델의 구조를 최적화할 수 있을것 같아 보인다.

물론 아직 iOS11 베타1버전이고 Core ML은 아마 더 개선될 것이다. 그 말은, Core ML에 넘겨주기전에 여러분의 모델을 최적화할 필요가 있다는 뜻이다. (“folding” the Batch Normaliza) 그러나 그렇게하면 특정 모델을위해 측정하고 비교해야할 것이다.


여러분의 모델이 CPU에서 실행될때와 GPU에서 실행될때 같은지도 확인해야할 것이다. 내가 언급한것처럼 Core ML은 모델을 CPU에서 돌릴지(Accelerate 프레임워크를 사용하여) GPU에서 돌릴지(Metal을 사용하여) 정한다. 결국 이 두 구현은 다르게 동작할 것이다. 그러니 둘 다 테스트해봐야한다!

예를들어 MobileNet은 "depthwise" 컨볼루션 레이어라 불리는 것을 사용한다. 이 원래 모델은 Caffe에서 학습되었고, 정규 컨볼루션의 groups 프로퍼티와 아웃풋 채널 수를 같게 만들어 depthwise 컨볼루션을 지원한다. 결과로나온 MobileNet.mlmodel 파일은 동일하지 않다. 이것이 iOS 시뮬레이터 안에서는 잘 동작하겠지만 실제 디바이스 위에서는 크래쉬가 난다!

시뮬레이터는 Accelerate 프레임워크를 사용하지만 실제 디바이스는 Metal Performance Shaders를 사용하여 생긴 문제이다. 그리고 Metal이 데이터를 인코딩하는 방법때문에 MPSCNNConvolution 커널은 제한되어 그룹의 수와 아웃풋 채널의 수를 같게 만들 수 없게 되었다. 아이고!

나는 애플에 버그리포팅을 제출했지만 내 요점은 이렇다. 시뮬레이터에서 모델이 잘 동작한다는게 디바이스에서 잘 동작할거라는 의미는 아니다. 테스트를 해보아라!

얼마나 빠른가?
나의 새로운 10.5" iPad Pro가 다음주까지 도착하지 않기 때문에(역자: 부럽습니다) Core ML의 속도를 시험해보지 못했다

나는 특별히 MobileNets을 실행시키는데 나의 Forge library를 이용할때와 Core ML을 이용할때 그 속도 차이가 어떻게 되는지 관심이 갔다(아직 초기 베타 단계에 있다).

채널을 고정하라! 공유할 데이터가 생기면 이 섹션을 업데이트 할 것이다.

Vision
다음으로 이야기할것은 새로나온 Vision 프레임워크이다.

여러분도 이름에서 추측했을지 모르겠지만, Vision은 컴퓨터비젼(computer vision) 테스크를 실행시켜준다. 과거에는 이를위해 OpenCV를 사용해야했었는데, 이제 iOS는 자신만의 API를 가지게 되었다.

Vision이 수행할 수 있는 일의 종류들이다
  • 이미지안에서 얼굴 찾아내기. 각 얼굴에대해 사각형을 만들어준다.
  • 안면의 세부적인 특징 찾아내기. 눈이나 입의 위치나, 머리의 모양 등.
  • 이미지에서 사각형모양으로된 것을 찾아내기. 표지판같은것들.
  • 비디오에서 움직이는 물체 추적하기.
  • 수평성 각도 알아내기
  • 두 이미지를 변형하여 그 내용을 정렬하기. 사진들을 합성할때 유용하다.
  • 이미지안에 포함된 텍스트 영역 감지하기.
  • 바코드 감지와 인지.
이 작업들중 몇몇은 이미 Core Image와 AVFoundation으로 가능하지만 이제 일관된 API로 한 프레임워크안에 들어왔다.

여러분의 앱이 이 컴퓨터비젼 작업중 하나가 필요할때, 더이상 여러분이 직접 구현하거나 다른 라이브러리를 사용할 필요가 없다. 그냥 Vision 프레임워크를 사용하면 된다. 더욱 강력한 이미지 처리를위해 Core Image 프레임워크와 합쳐서 사용할 수도 있다.

더 나은 것은, Core ML을 작동시키기위해 Vision을 사용할 수도 있다는 점이다. 그리하여 뉴럴 네트워크를 위한 전처리 단계로서 이런 컴퓨터 비젼 기술을 사용할 수 있게 해준다. 예를들어, 사람 얼굴의 위치와 크기를 감지하는데, 그 영역에 비디오 프레임을 자르는데, 이미지에서 얼굴이 있는 부분에 뉴럴럴 네트워크를 실행시키는데 Vision을 사용할 수 있다.

사실 이미지나 비디오와 함께 Core ML을 사용하면 항상 Vision을통해 가는것이 알맞은 방법이다. 가공되지않은 Core ML로는 여러분의 입력 이미지는 모델이 예상하는 포멧안에 있도록 해야하지만, Vision으로는 프레임워크가 이미지 리사이징 등에 주의해야한다. 이것으로 여러분의 추가 노력을 조금 절약해줄 것이다.

코드에서 Core ML을 작동시키기위한 Vision 사용은 다음과같이 생겼다.
// the Core ML machine learning model
let modelCoreML = ResNet50()

// link the Core ML model to Vision
let visionModel = try? VNCoreMLModel(for: modelCoreML.model)

let classificationRequest = VNCoreMLRequest(model: visionModel) {
  request, error in
  if let observations = request.results as? [VNClassificationObservation] {
   /* do something with the prediction */
  }
}

let handler = VNImageRequestHandler(cgImage: yourImage)
try? handler.perform([classificationRequest])
VNImageReuestHandler가 요청하는 오브젝트의 배열을 받아서, 아래처럼 여러 컴퓨터 비젼 작업을 함께 연결하여 할 수 있음을 인지하자.
try? handler.perform([faceDetectionRequest, classificationRequest])
Vision은 컴퓨터 비젼을 사용하기 아주 쉽게 만들어준다. 그러나 기계학습 사람들에게 멋진 일은 컴퓨터 비젼 작업의 아웃풋을 받아서 Core ML 모델에 넣을 수 있다는 점이다. Core Image의 파워와 합쳐지면 이미지 처리 파이프라인을 하나로 만들게된다!

Metal Performance Shaders
내가 말하고싶은 마지막 주제는 Metal이다. 이것은 애플의 GPU 프로그래밍 API이다.

올해 클라이언트를위한 많은 내 일거리는 Metal Performance Shaders (MPS)로 뉴럴 네트워크 구축과 최적화된 퍼포먼스로 맞추는 작업이 포함되있었다. 그러나 iOS10은 컨볼루션 네트워크를 생성하기위한 기본적인 몇 커널만을 제공했었다. 종종 이 갭을 채우기위해 커스터마이징한 커널을 짜야했다.

그러니 나는 iOS11에서 여러 이용가능한 커널 수가 늘었을때 행복했고, 그 이상의 기분이었다. 이제 그래프를 구축하는(building graphs) API를 가진다.

주의: 왜 Core ML 대신에 MPS를 사용할까? 좋은 질문이다! 가장 큰 이유는 Core ML이 여러분이 원하는 것을 지원하지 않거나, 프로세스 전체를 컨트롤 하고싶고 가능한 최대의 속도를 짜내고 싶을때 사용한다.

MPS에서 기계학습을위한 큰 변화들은 다음과 같다.
  • Recurrent neural networks. 이제 RNN, LSTM, GRU, MGU 레이어를 생성할 수 있다. 이것들이 MPSImage 오브젝트의 시퀀스에도 동작하고, MPSMatrix 오브젝트 시퀀스에도 동작한다. 이것이 흥미로운 이유는, 다른 모든 MPS 레이어들이 이미지만 다른다는 것이다(그러나 확실히 텍스트나 이미지가아닌 데이터와 작업할때는 매우 불편하다).
  • 더 많은 데이터 타입들. 이전의 가중치는 32비트 부동소수라고 가정했었는데, 이제 16비트 소수, 8비트 정수, 심지어 바이너리까지 될 수 있다. 컨볼루션과 완전히 연결된 레이어들은 바이너리 가중치와 바이너리화된 인풋으로 할 수 있다.
  • 더 많은 레이어들. 지금까지 우리는 plain-pld convolution과 max/average pooling으로 만들어야 했었다. 그러나 iOS11 MPS는 dilated convolution, subpixel convolution, transposed convolution, upsampling과 resampling, L2-norm pooling, dilated max pooling, 게다가 몇몇 새로운 활성 함수들(activation functions)을 가능하게 했다. 아직 MPS는 Keras나 Caffe 레이어 타입처럼 모든 타입을 가지진 않지만, 그 갭은 줄어들고 있다...
  • 더욱 편리함. Metal은 항번에 채널 4개의 분할로 데이터를 구성하는데(이미지가 MTLTextrue 오브젝트에의해 돌아오기 때문에),  그것때문에 MPSImage으로 작업하는것은 항상 좀 이상하다. 그러나 이제 MPSImage는 데이터를 읽고 쓰는 메소드를 가지므로 한결 편해질것이다.또다른 편리함은 레이어에 batch normalization 파라미터를 설정하게 해주는 새로운 메소드를 가지는 것이다. 이 말은 더이상 여러분이 컨볼루션 레이어 가중치에 batch normalization을 접지 않아도 MPS가 알아서 다 해줄것이라는 의미다. 매우 편리하다!


  • 성능 개선. 기존에 있던 커널들이 더 빨라졌다. 이 소식은 항상 좋다.
  • 그래프 API. 내 생각에는 이것이 큰 소식이다. 모든 레이어와 (임시의) 이미지를 직접 생성하는것은 항상 성가신다. 이제 Keras에서처럼 그래프를 표현할 수 있다. MPS는 이미지가 얼마나 커져야하는지, 패딩을 어떻게 다뤄야하는지, MPS 커널의 offset을 어떻게 설정할지 등을 자동으로 계산한다. 뒷편에서는 퓨징 레이어(fusing layers)로 그래프까지도 최적화시킬 수 있다.
이제 모든 커널들이 NSSecureCoding으로 시리얼라이즈 가능해보이는데, 이 의미는 그래프를 파일로 저장하여 나중에 복구시킬 수 있다는 의미이다. 그리고 이 그래프로 인터페이스를 사용하면 이제 그냥 한 메소드 호출만 하면 된다. 아직 Core ML만큼 쉽지는 않지만, MPS 사용이 확실히 이전보다 작업이 많이 줄었다.

내가 생각하기에 아직 분명하지 않은 것은 자신만의 컴퓨트 커널을 작성할 수 있는지, 그래프에 이것을 붙일 수 있는지이다. 내 클라이언트 작업에서 나는 전처리 과정에서 종종 필요했었고, Metal Shading Language로 작성된 커스텀 shader를 필요로 했다. 내가 말할 수 있는 부분은, "MPSNNCustomKerneNode" 클래스가 될것 같진 않다. 더 조사할 필요가 있어보인다!

결론: 기계학습을 위한 Metal Performance Shaders은 iOS 11과함께 더 강력해졌지만, 아마 많은 개발자들이 (내부적으로 MPS를 사용해가며) Core ML와 붙여 사용할 수 있다.
주의: 여러분의 앱이 계속 iOS 10을 지원하지 않는한, 새로나온 그래프 API는 내 Forge library를 쓸모없게 만들었다. 곧 예제 앱을 새로나온 그래프 API로 포팅할 것이고, 그것에대한 세부적인 내용을 블로그에 포스팅할 예정이다.

남은것들
발표된 것 중에 다른 부분이다.
  • Accelerate: Accelerate 프레임워크에서 BNNS는 기능적 업데이트가 크게 일어나진 않았다. 결국 Softmax 레이어가 나왔지만, MPS가 얻은 새로운 레이어 타입은 없었다. 아마 맞을것이다. 딥 뉴럴 네트워크를위한 CPU 사용은 어쨌든 좋은 아이디어가 아닌것같다. 이 말은, 나는 Accelerate를 사랑하고, 이것으로 많은 즐거움이 있었다. 그리고 이번년도에 나는 스파스 메트릭스를 더 지원했었는데, 꽤 멋졌다.
  • 자연어 처리(Natural Language Processing): Core ML은 이미지만을 위한게 아니라 텍스트를 포함한 수많은 종류의 데이터를 다룰 수 있다. 이 API에는 NSLinguisticTagger 클래스를 사용하는데, 얼마간 사용해봤지만 iOS 11이 나오면서 더욱 효과적게 되었다. 이제 NSLinguisticTagger는 언어 식별, 토큰화, part-of-speech tagging, lemmatization, Named Entity Recognition을 한다.
나는 NLP에 많은 경험이 없으므로 다른 NLP 프레임워크에대해 어떤식으로 stack up 되었는지 말할순 없을것 같지만, NSLinguisticTagger는 보기에 꽤 강력해 보인다. 여러분의 앱에 NLP를 넣고 싶으면 이 API로 시작하기에 좋아보인다.

이 모든게 좋은 소식인가?
애플이 개발자를위해 이 모든 새로운 툴을 제공하는것은 훌륭한 일이지만, 애플의 많은 API에는 중요한 "문제"가 있다.
  1. 오픈소스가 아니다
  2. 제한을 가진다
  3. 새로운 OS가 배포될때만 업데이트를 한다
이 세가지가 함게 있으면 애플의 API는 항상 다른 툴들에비해 뒤떨어질것이다. 만약 Keras가 멋진 새로운 레이어 타입을 추가하면 애플이 그 프레임워크와 OS를 업데이트 하기 전까지 Core ML로는 사용할 수 없을 것이다.

그리고 API의 어떤 부분은 여러분이 원하는대로 동작하지 않을때, 내부로 들어가서 고칠 수 없다. 여러분은 이것으로 작업해야하거나(항상 가능하진 않다) 다음 OS 배포까지 기다려야한다(모든 사용자들이 업그레이드하도록 해야한다).

물론 나는 애플이 비밀 소스를 줄거라 생각하진 않지만, 많은 다른 기계학습 툴킷들이 오픈소스니 Core ML도 오픈소스로 만드는 건 어떨까?🙏

애플이 이것을 아는것은 아마 빠른 시일안에 일어나진 않을 것이지만, 여러분의 앱에 기계학습을 넣기로 했을때는 적어도 위의 내용들을 마음에 담아주자.

더 읽을거리...



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

으로 보내주시면 됩니다.



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

,
제목: 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

,
제목: 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

,
제목: Isolating tasks in Swift, or how to create a testable networking layer.

최근 몇년동안 더욱더 멋져지고있는 iOS 아키텍처가 많다. 모두 유효하고 부분적으로 장단점을 가진다. 이들은 모두 같은 것을 다룬다. 바로 프레젠테이션으로부터 비지니스 로직을 분리하는 것이다. 오늘 나는 여러분이 다음 프로젝트의 아키텍처에 적용시킬 수 있지만 어떤 아키텍처를 사용하더라해도 쓸 수 있는, 간단한 개념을 적어보려 한다.

꽤 일반적인 네트워크 레이어
내 관점을 설명하기 위해, 나는 먼저 보통 네트워크 레이어를 어떻게 구현하는지에대해 이야기할 것이다.

나는 각기 다른 여러 네트워크 레이어를 보아왔다. 대부분 NetworkManager, ConnectionManager 이런 모습이다. 앱에서 한 클래스안에 있는 모든 API 호출을 담고있다. 이게 유효하고 동작할지라도, 소프트웨어 설계에서 핵심 개념인 단일책임(Single Resoponsibility)은 실패한 것이다.

ConnectionManager에는 좋다고 생각되는 책임들을 너무 많이 담고있다. 게다가 종종 싱글톤으로 구현된다. 그리고 나는 싱글톤이 필연적으로 나쁘다고 말하진 않겠지만, 싱글톤은 의존성으로서 주입될 수 없고 테스팅에 쉽게 목(mock)이 될 수도 없다.

네트워크 레이어는 일반적으로 싱글톤으로 구현된다네트워크 레이어는 일반적으로 싱글톤으로 구현된다


이것은 매우 자주있는 방법이다. 나는 이것을 MVVM나 MVP 아키텍처에서도 보았다.

다른 방법
데이터 접근 레이어는 다른 방법으로 구현될 수 있다. 네트워크 패칭에서 그 프로세스를 표현해보자.

네트워크 호출에 포함된 단계네트워크 호출에 포함된 단계


이 방법을 넣어 세단계로 네트워크 호출을 함축한다.
  1. 요청 만들기: 요청 만들기에서는 URL, method, 파라미터(URL 경로든 http 바디에든), HTTP 헤더를 설정한다.
  2. 요청 디스패치하기: 이것은 매우매우 중요한 단계이다. 이전단계에서 만들어지고 설정된 이 요청은 반드시 URLSession이나 이걸 덮는 레이어(예를들면 Alamofire)를 사용하여 디스패치 되야한다.
  3. 응답을 받고 파싱하기: 이 부분은 이전 두 단계와 분리되어 구현되야할 중요한 단계이다. 이것은 JSON이나 XML 응답을 검증하고 유효한 Entity(혹은 Model)에 파싱하는 곳이다.

여러분의 아키텍처가 깔끔해지고 테스트하기 좋아지길 원한다면, 이 세단계는 다른 오브젝트에서 구현되어야한다.

네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함네트워크 레이어는 적어도 세 컴포넌트를 사용하여 구현되어야함


  1. Request: Request 오브젝트는 네트워크 요청을 구성하는데 필요한 모든 것들을 가진다. 이 구조체(혹은 클래스)는 하나의 네트워크 요청을 구성하고있는 책임을 갖는다. 그리고 한 네트워크 요청에 한 Request 오브젝트굉장히 중요하다.
  2. NetworkDispatcher: NetworkDispatcherRequest를 받아 Response를 반환하는 역할의 오브젝트이다. 이것은 프로토콜로 구현될 수 있다. 구체적인 클래스(혹은 구조체)가 아닌 프로토콜로 코드를 짤 수 있지만, 절때 싱글톤으로 구현하지 말아야한다. 그렇게 한다면, 이 NetworkDispatcherMockNetworkDispatcher과 대체될 수 잇고, 이 목은 실제 네트워크 요청을 날리지 않는 대신에 JSON 파일로부터 응답을 받아준다. 이것이 자연스럽게 테스트하기 좋은 아키텍처를 만든다.
  3. NetworkTask: NetworkTaskTask라는 제네릭 클래스의 자식클래스이다. 이 Task(좀 있다가 더 설명할것이지만)는 비동기적으로든 동기적으로든 Input 타입을 받아서 Output 타입을 반환하는 책임을 가지는 제네릭 클래스이다. TaskRxSwiftReactiveCocoaHydraMicrofuturesFOTask나 아니면 간단하게 클로저를 이용해서 구현할 수 있다. 여러분에 달려있다. 여기서 중요한 부분은 세부적인 구현이 아니라 개념이다.

요청 만들기
RequestURLRequest를 만드는데 필요한 모든 구성에대한 책임을 가진 오브젝트이다.

네트워크 요청에대한 예시는 다음과같이 생겼을 것이다.
//
//  Request.swift
//
//  Created by Fernando Ortiz on 2/12/17.
//

import Foundation

enum HTTPMethod: String {
    case get, post, put, patch, delete
}

protocol Request {
    var path        : String            { get }
    var method      : HTTPMethod        { get }
    var bodyParams  : [String: Any]?    { get }
    var headers    : [String: String]? { get }
}

extension Request {
    var method      : HTTPMethod        { return .get }
    var bodyParams  : [String: Any]?    { return nil }
    var headers    : [String: String]? { return nil }
}
여기서 중요한 부분은 Request가 분리된 오브젝트로 구현되었다는 점이다. 물론 Moya promotes처럼 열거형으로 구현될 수도 있는데, 여러분이 원하는 것에 달렸다. 나는 개인적으로 객체지향 스타일을 선호하고, BaseRequest 클래스와 AuthenticatedRequest같은 자식클래스나 GetAllUsersRequest, LoginRequest같은 세부적인 요청을 구현하길 선호한다.



NetworkDispatcher 구현하기
NetworkDispatcher는 네트워크 요청을 디스패치하는 책임을 가지는 컴포넌트이다.

주의: 여기서부터 내 예제에는 RxSwift를 사용할 것이지만, 여러분은 여러분이 좋아하는 방법으로 구현하면 된다.
//
//  NetworkDispatcher.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//  Copyright © 2017 Fernando Martín Ortiz. All rights reserved.
//

import Foundation
import RxSwift

protocol NetworkDispatcher {
    func execute(request: Request) -> Observable<Any>
}
NetworkDispatcherRequest 오브젝트를 디스패치하고 응답을 반환하는 단일 책임을 가진다.
"
구체적인 구현대신 이 프로토콜을 사용하는것에서 멋진 점은 프로토콜 기반 구현은 쉽게 교체가능하게 만들어준다. MockNeoworkDispatcher은 실제 "네트워크" 작업을 실항하지는 한고 대신 JSON 파일에서 응답을 반환해주도록 하여 더욱 테스트하기 쉽게 만들어준다.

Task 고립시키기
Task는 하나의 로직 오퍼레이션을 실행시키는 책임의 간단한 오브젝트이다. 뒷단에서 사용자를 받아오거나, 로그인하기, 사용자 등록하기등이 있을 것이다. Task는 동기적으로나 비동기적으로 일어날 수 있지만, 클라이언트단에서 투명해야한다. 나는 편리한 추상화인 RxSwift의 Observable을 사용하길 좋아하는데, Promise, Signal, Future, 혹은 간단한 콜백이 충분할 수 있다.

Task의 간단한 구현은 아래처럼 될 수 있다.
//
//  Task.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class Task<Input, Output> {
    func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
나는 객체지향 스타일을 사용했지만, 연관 타입이나 타입 erasure같은 것도 좋은 방법으로 사용할 수 있다. 이것도 잘 동작할 것이다. 내가 이런 객체지향 스타일을 좋아하는 이유는 덜 조잡하고 구현하기 단순해 보이기 때문이다.

모든 TaskInput 타입과 Output 타입이라는 두 제네릭 파라미터를 필요로한다. TaskInput 오브젝트를 받아 Output을 반환하는 일을 포함한 작업을 수행하는데, Observable처럼 이것을 추상화하여 사용할 것이다.

Task를 네트워크 작업을 실행시키기위해 특별하게 만들어줘야한다.
//
//  NetworkTask.swift
//
//  Created by Fernando Ortiz on 2/11/17.
//

import Foundation
import RxSwift

class NetworkTask<Input: Request, Output>: Task<Input, Output> {
    let dispatcher: NetworkDispatcher

    init(dispatcher: NetworkDispatcher) {
        self.dispatcher = dispatcher
    }

    override func perform(_ element: Input) -> Observable<Output> {
        fatalError("This must be implemented in subclasses")
    }
}
위에서 볼 수 있듯 ,NetworkTask는 두가지 제네릭 타입이 필요한데, InputOutput이다. Input이 반드시 Request 오브젝트이여야한다는 것은 당연한 일이다. NetworkTaskNetworkDispatcher로만 인스턴트화되어야 하므로 테스트하고 싶을때 MockNetworkDispatcher를 쉽게 보낼 수 있다.

아키텍처 검토하기
이 방법으로 비지니스 로직을 구현하면 복잡성을 설명하거나 테스트 용이함을 증가시키지 않고 결합력을 줄이는데 도움이 된다.

이 방법은 아래처럼 다이어그램으로 표현될 수 있다.

Task 기반 네트워크 레이어Task 기반 네트워크 레이어


결론
분리된 오브젝트에서 비지니스 로직 오퍼레이션을 고립시키는 것은 더욱 테스트하기 좋은 아키텍처로 만들기 때문에 좋은 방법이다. 복잡성을 줄여주고, 여러분이 사용하는 아키텍처를 전반적으로 독립시킨다. 이것은 ViewModel, Presenter, Interactor, Store 혹은 프레젠테이션 로직에서 비지니스 로직을 분리하는데 사용하기위한 어떤것 뒤에서든 사용될 수 있다.

이것이 나만큼 도움이 되었길 바란다. 뭔가 의심스러운 점이 있거나, 더 좋은 방법을 안다면 커멘트를 남겨달라.



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

으로 보내주시면 됩니다.



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

,
제목: WWDC 2017 Viewing Guide

다른 년도, 다른 WWDC 그리고 더욱 많은 세션들을 이해하기위해서 준비했다. 이 글에서는 이번년도 가장 흥미있는 세션을 위한 나의 가이드를 공유한다.

기계학습, 드레그 앤 드롭, Xcode9
제일 먼저 Platform state of the union이 좋다.
  • Platforms State of the Union 아직 이 세션을 보지 않았다면, 여기에는 iOS, macOS, tvOS, watchOS의 전반적인 변화들을 발표한다.

이것들은 넘겨보지 말자
개인적으로 내가 가장 좋아하는 것들인데, 여러분은 그냥 넘겨볼 수도 있겠지만 시간을 투자해보는 것도 의미있는 일이 될 것이다.

앱 프레임워크
수많은 좋은 세션들이 여기에 있다. 나는 watchOS, tvOS, macOS 세션까지 넣지는 않았다. 여기에 내가 뽑은 리스트이다.
  • Session 201 What’s New in Cocoa Touch WWDC의 스타같은 팀, Eliza Block와 Josh Shaffer이 돌아와 Cocoa Touch의 바뀐점을 요약해준다.
  • Session 204 Updating Your App for iOS 11 통합된 서치바, 리프레시 컨트롤, 오토레이아웃 지원과함께 새로나온 큰 타이틀 네비게이션 바(large title Navigation bar)를 다룬다. 새로운 레이아웃 마진(layout margin)은 뷰컨트롤러의 루트뷰 세팅에대한 가이드를 하고(이제 시스템 최솟값을 바꿀 수 있다) 세이프 영역(safe area)는 상단, 하단 레이아웃 가이드를 대신한다. 또한 뷰컨트롤러는 더이상 스크롤뷰의 contentInset으로 더러워지지 않는다. 테이블뷰 셀은 이제 디폴트로 스스로 크기변경이 가능하고 더 쉽게 스와이프 액션을 넣을 수 있다.
  • Session 206 Introducing Password AutoFill for Apps 여러분의 웹 서비스에 로그인 기능이 있다면 쉽게 구현 가능해 보인다. 여러분의 사용자는 iOS 키체인에 증명서를 저장하게 된다.
  • Session 210 What’s New in Core Data 이번년도의 커다란 코어데이터 뉴스는 Core Spotlight 통합과 Persistent History Tracking을 지원한다는 점이다. 후자는 영속 저장소를 갱신하고있는 여러 컨텍스트와 여러 익스텐션의 이슈를 대상으로 한다. 포그라운드로 돌아오는 앱은 일부로 다시 불러오게 하지 않고서 변경사항들을 패치할 수 있다.
  • Session 214 What’s New in SiriKit 아마 예상한것처럼 극적인 변화는 아닐 것이나, 리스트와 노트를 위한 새로운 계획을 받았고 비주얼 (QR) 코드를 보여주는 것도 곧 나올 것이다.
  • Session 219 Modern User Interaction on iOS 애플에서는 이것을 지향하지는 않을 수 있지만, 여러분은 이제 컨트롤센터, 노티피케이션, 액션을 넘은 슬라이드가 발생하기 전에 스크린의 모서리에서 일어나는 시스템 스와이프를 가로챌 수 있다.
  • Session 225 What’s New in Safari View Controller 여러분의 앱에 더 매칭시키기위해 바(bar)와 틴트 칼라(tint color)를 커스터마이징 한다. 드레그 앤 드롭에대한 지원을 탑재한다. 이제 각 앱은 사파리 브라우저와 별개로된 고유의 쿠키 저장소(jar)를 얻었다. 취소나 닫기를 위해 완료(done) 버튼을 변경하고, 공유 시트로부터 활성화를 배제한다.
  • Session 230 Advanced Animations with UIKit 프로퍼티 애니메이터를 사용하는 것에대한 좋은 요약이고, CALayer의 cornerRadius 속성은 이제 애니메이션 가능한 점에대한 좋은 팁이다.
  • Session 235 Building Visually Rich User Experiences 세션 230를 이은 코어 애니메이션의 좋은 팁과 트릭이다.
  • Session 237 What’s New in MapKit 지도에서 여러분의 데이터를 표현할 수 있는 새로운 맵 타입. 새로운 MKMarkerAnnotationView는 주석들이 겹칠때 이것을 묶어준다.
  • Session 241 Introducing PDFKit on the iOS CoreGraphics 위에 만들었지만 현대의 스위프트와 Objective-C API를 지원한다. PDF 문서를 더 쉽게 보고 주석을 달기위해 만들어졌다.
  • Session 242 The Keys to a Better Text Input Experience 높이를 변경하거나 커스텀 인풋 뷰를 넣은 키보드를 다루는 여러 방법을 소개한다. 사용자가 선택한 키보드가 어떤것인지 앱이 기억하게 만들 수 있다. iOS는 똑똑하게도 자동으로 따옴표와 대쉬를 바꿔준다.
  • 개발바들을위해 애플은 동적 타입을 지원하여 계속해서 밀고있다. iOS11에서 큰 변화로는, 모든 스타일이 이제 여분의 큰 접근성 크기로 커진다(예전에는 바디 스타일만 가능했다). 또한 애플은 UIFontMetrics를 사용하여 커스텀 폰트도 쉽게 지원할 수 있게 만들었다. 어셋(asset) 카탈로그에서 PDF vector asset은 이제 벡터 데이터를 저장할 수 있게 설정할 수 있다.

드래그 앤 드롭
이번년도의 커다란 UIKit 증진사항은 앱간의 드래그 앤 드롭을 지원한다는 점이다.

여러분이 시간이 있다면 아래 세션은 더 깊은 내용을 담고 있다.
자연어 처리(NLP, Natural Language Processing), 기계 학습(ML, Machine Learning), 증강 현슬(AR, Augmented Reality)
애플이 밀고 있는 이번년도의 또다른 큰 테마는, 우리 앱에서 NLP, ML, AR을 쉽게 사용할 수 있게 만들고 있다는 점이다. 또한 애플은 개인정보, 속도, 낮은 지연율에 대한 이점을 강조했는데, 클라우드 기반 서비스 없이 기기에서 가능하기 때문이다.

스위프트
스위프트 에볼루션 과정이 공개로 됨으로서 새로운 스위프트4를 보기위해 WWDC까지 기다릴 필요가 없어졌다. 그럼에도 잘 정리해 놓았다.

개발 
Xcode9는 스위프트를 크게 돕기위해 리팩토링 지원에대한 업데이트를 보고 있는것 같다.

디자인과 접근성
애플이 계속해서 개선하고 강조하는 부분으로, 모두를 위한 앱 디자인과 제작에서 필요한 점을 볼 수 있다.
  • Session 110 Convenience for You is Independence for me Todd Stabelfeldt (aka the Quadfather)는 8살때부터 사지마비가 왔는데, 접근성 제작으로 다른 앱 디자인을 만들어 그의 발표에서 영감을 얻자. 만약 여러분 앱의 접근가능함에 의심이 된다면 그가 손으로 사용하는 스위치 컨트롤 시연을 보자 (가끔 혀를 쓰기도 한다).
  • Session 215 What’s New in Accessibility iOS11 사용자를 위해 나온 일반적인 접근성 기능 몇개가 크게 개선되었다. 여러분은 이제 텍스트로 시리와 인터렉트할 수 있다. 모든 시스템은 동적 타입을 위해 감사(audit)할수 있게 되는데, 이제 모든 스타일에 최고로 큰 접근성 크기를 사용할 수 있다(여러분의 UI에서 테스트해보라!). 이제 VoiceOver은 이미지 안에 있는 텍스트를 인지해서 말할 수 있고, 컨테이너 타입(container type) 지원이 개선되었다. 마지막으로 당신이 드래그 앤 드롭 지원을 고려하고 있다면 접근성을 고려하는 것도 잊지 말자.
  • Session 803 Designing Sound Highly entertaining talk on using sound including how some of the familiar Apple system sounds are created.

보안과 네트워킹

앱 스토어
여러분이 앱내구매(IAP, In App Purchase)를 사용한다면 앱스토어에서 바뀌게될 부분을 주목하자. 아래 두 세션을 추천하고 싶다.
  • 몇몇 멋진 개선이다: 올해 후반기에 TestFlight 제한이 10,000명 사용자로 올라갈 것이다. 더이상 iTunes Connect에서 앱 아이콘을 관리할 필요가 없다. 앱 부제목과 프로모셔널 텍스트 필드가 있다. 20 API까지 올렸다. 단계별 배포(phased release).

미디어
요점은 MusicKit이 Apple Music API에 접근할 수 있게 된 것과 Photos API가 바뀐 부분이다. 이미지 피커(image picker)는 과정에서 사라졌으며, 따라서 사용자는 허가를 묻지 않고 이미지를 선택할 수 있다.



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

으로 보내주시면 됩니다.


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

,
제목: Vapor and its callback and throwing stacks

이 글은 스위프트로 쓰여진 서버 프레임워크, Vapor에 관한 이야기이다. 나는 iOS 개발자이며, 스위프트를 사랑하기때문에 내 개인 블로그를 이것으로 바꾸려고 하였다. 그렇게 하면서 내가 발견한 몇몇가지 특징들(미들웨어, 콜백체인, 에러핸들링)을 소개해주고 싶었다. 우리는 계층을 만들고 기능을 확장하고 프로젝트를 더 구조화하기위해 프로토콜과 익스텐션 사용을 할 것이다.

다소 간단한 서버로 시작해보자.
let drop = Droplet()  // 1



try? addProvider(VaporPostgreSQL.Provider.self) // 2

drop.preparations += Model.self // 3


drop.middleware.insert(RedirectMiddleWare(), at: 0) // 4

drop.middleware.append(GzipMiddleWare()) // 5

drop.middleware.append(HeadersMiddleware())

drop.get("/test", handler: {request in

    print("4")
    return try JSON(node: "This is a test page.")
})
Droplet 설정

droplet을 만듦으로서 시작하고(1), 데이터베이스 프로바이더를 추가하며(2) 함께 사용될 Model 엔티티를 추가한다. 그런다음 우리의 미들웨어를 추가한다. 미들웨어의 순서는 보통 상관으므로 append하면 된다. 그러나 가끔 구체적인 요구나 미들웨어중 하나를 가장 먼저 실행해야하는 경우가 있을 수 있다. 이 경우는 insert하여 스택의 제일 첫번째에 넣을 수 있다(4). 그리하여 내부에 다른 것들 이전에 첫번째것이 호출된다.

이제 미들웨어로가서, 이것이 어떻게 동작하는지 그리고 왜 이렇게 추가했는지 보자. Vapor는 Middleware라는 프로토콜을 제공하는데, 이것은 그냥 하나의 메소드를 필요로 한다.
func respond(to request: Request, chainingTo next: Responder) throws -> Response
미들웨어의 유일한 메소드


이 메소드를 통해 미들웨어는 요청을 받고, 적절하게 수정하여(혹은 그대로두어), 그것을 다음 응답자(next responder)에 보낸다. 이 응답자는 또다른 미들웨어일 수도 있고 엔드포인트의 핸들러일 수도 있다. 이 응답자는 응답을 반환하고, 적절하게 수정하여(혹은 그대로두어), 직접 응답을 생성하고 반환한다. 아래 예제에서 그것에대한 각각의 상황을 확인할 수 있다.

먼저 스택에서 RedirectMiddleware이다.
struct RedirectMiddleware: Middleware {
    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        print("1")
        guard request.uri.scheme != "https" else { // 1

            let response = try next.respond(to:request) // 2

            print("7")
            return response // 3

        }

        let uri = uriWithSecureScheme

        print("alternate")
        return Response(redirect:uri.description) // 4

    }
}
RedirectMiddleware

이 미들웨어의 유일한 목표는 현재 요청이 안전한지 확인하는 것이다(1). 안전하지 않으면 요청 URIhttps 스킴을 넣어 새로운 응답을 만들어 반환한다. 만약 안전하면 다음 응답자에게 보내고(2), 끝난다. 우리에게 반환된것이 어떤것이든 반환한다(3). 모든 요청이 안전하고싶기 때문에 모든 다른 미들웨어를 통해 요청이 통과될 필요가 없다. 그러므로 0번째 인덱스에 위치시킨다.

다음 미들웨어는 HeaderMiddleware이다.
struct HeadersMiddleware: Middleware {

    funcrespond(torequest:Request,chainingTonext:Responder)throws->Response{
        print("2")
        let response=try next.respond(to:request) // 1

        print("6")

        response.headers["Cache-Control"] = "public, max-age=86400" // 2


        // Disable the embedding of the site in an external one.

        response.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
        response.headers["X-Frame-Options"]  ="DENY"

        return response// 3

    }

}
HeadersMiddleware

이것은 요청을 바로 다음 응답자에게 보내고(1), 응답이 리턴된 후에 여기에 몇 헤더를 설정하고(2), 반환한다(3).

이 스택의 다음으로 다음 응답자인 GzipMiddleware이다.
struct GzipMiddleware: Middleware {
    func respond(torequest: Request, chainingTo next: Responder) throws->Response {
        print("3")
        let response = try next.respond(to: request) // 1

        print("5")

        response.body = gzippedBody // 2

        response.headers["Content-Encoding"] = "gzip"// 3

        response.headers["Content-Length"] = gzippedBody.length

        returnresponse // 4

    }

}
GZipMiddleware

여기서도 바로 다음 리스폰더에게 응답을 보내고(1), 몇몇 헤더를 추가한다(3). 그러나 다른점은 이것을 리턴하기 전에(4) 바디를 바꾼다.

내부적으로 미들웨어는 어떻게 동작할까? 기본적으로 우리가 이것을 사용한 것처럼 된다. DropletResoonder 프로토콜을 따르는데, 이것은 그냥 한 메소드를 가지고 있다.
func respond(to request: Request) throws -> Response
Droplet의 응답 메소드

Middleware 프로토콜과 비교해보면, 이것은 아무거나 연결(chain)하여 호출될 수 없고 그 구현에서 모든 미들웨어를 연결한다.
extension Droplet: Responder {

    public func respond(to request: Request) throws -> Response {

        [...]

        print("0")
        let mainResponder = middleware.chain(to: routerResponder) // 1

        var response: Response


        do {
            response = try mainResponder.respond(to: request) // 2

        }
        catch {
            return Response(status: .internalServerError, headers: [:], body: "Error message".bytes)// 3

        }

        print("10")
        returnresponse// 4

    }

}
Droplet의 respond 메소드 구현

모든 미들웨어를 연결하여 만들어진 응답자(1)를 볼 수 있다(아직 호출되진 않았다). 그리고 요청의 바깥에 응답을 만드는데 사용되며(2), 모든게 괜찮으면 리턴될 것이다(4). 만약 실패하면 새로운 응답이 만들어지고 반환된다(3). 여기에는 에러메시지와 상태코드가 담겨있다. 미들웨어는 어떻게 연결되고(1) 호출될까? Collection을 익스텐션하고 Responder를 상속하여 이런 한 목표는 클로저를 잡아두고 호출하는 것이다.
extension Request {
    public struct Handler: Responder {
        public typealias Closure = (Request) throws -> Response


        private let closure: Closure


        public init(_c losure: @escaping Closure) {
            self.closure = closure // 1

        }

        public func respond(to request: Request) throws -> Response {
            return try closure(request) // 2

        }
    }
}


extension Collection where Iterator.Element == Middleware {

    func chain(to responder: Responder) -> Responder {
        return reversed().reduce(responder) { nextResponder, nextMiddleware in // 3

            return Request.Handler { request in

                return try nextMiddleware.respond(to: request, chainingTo: nextResponder) // 4

            }
        }
    }

}
Request와 Collection을 익스텐션하기

chain 메소드는 모든 미들웨어를 뒤집고(3) 각 단계에 새로운 Handler를 생성하고 반환한다. 이것은 요청과 현재 응답을 보냄으로서 앞에서 말한 Middleware 프로토콜의 메소드를 호출한다. 이렇게하여 우리의 미들웨어 스택(쉽게 설명하기위해 줄인말)을 기억한다면 [Redirect, Headers, Gzip]는 아래처럼 chain메소드를 통해 갈것이다.
reverse ->
[Gzip, Headers, Redirect] ->
create and return a Handler, that in its closure calls Gzip's respond(to: request, chainingTo: mainResponder) ->
create and return a Handler, that in its closure calls Headers' respond(to: request, chainingTo: gzipResponder) ->
create and return a Handler, that in its closure calls Redirect's respond(to: request, chainingTo: headersResponder) ->
return the Redirect Handler
미들웨어의 클로저를 연결하기

Dropletrespond 메소드는 반환된 Handlerclosure가 호출된 곳에 위치한다(2). 이것은 다음 그리고 다음을 호출할 것이다. 마지막으로 호출될 것은 get 메소드에서 전달된 Handler 클로저 이고, 이것은 응답/에러를 다시 연결한 처음의 것이다.
drop.get("/test", handler: { request in

    print("4")
    return try JSON(node: "This is a test page.")
})
get의 핸들러

마지막으로 모두 합치고 그 chain을 따라가보자. 안전한 요청은 이런식으로 보내진다.
create the main responder ("0") ->
RequestMiddleware ("1") ->
Other, internal middleware ->
HeadersMiddleware ("2") ->
GZipMiddleware ("3") ->
MiscMiddleware1 ->
MiscMiddleware2
안전한 요청 chain

그리고 응답은 이런식으로 보내진다.
get's handler ("4") ->
GZipMiddleware ("5") ->
HeadersMiddleware ("6") ->
Other, internal middleware ->
RequestMiddleware ("7") ->
response is sent to client ("10")
응답 chain

안전하지않은 요청은 아래처럼 약간 긴 경로를 따라간다.
get("/test") ->
RequestMiddleware ("1") ->
RequestMiddleware ("alternate") ->
start over ->
RequestMiddleware ("1") ->
[continue with the rest of the secure path]
안전하지 않은 요청 chain

여러분도 보았듯, 미들웨어의 주된 장점은 특화된 클래스로 더 작게 쪼개어 코드를 모듈화 시킬 수 있다는 점이다. 각각이 보통 하나의 작은 목적을 제공하는 덕분에, 더 쉬워지고 더 표현력있고 테스트/변경/제고하기 더 쉬워질 수 있다. 이것은 서로 전혀 모르며 며,이것들은 모두 체인의 위 아래에서 무슨일이 일어나는지 모른체 독립적으로 요청/응답이 동작한다.

체인을 넘기는 것은 정확히 같은 경로를 따르며, 굉장히 직관적이다. 만약 무엇이든 에러를 던졌는데 처리하지 않았다면 헨들러가 발견될때까지 요청/응답 체인에 돌려 보내거나 제네릭 에러 메시지와 함께 만들어진 Response에 내부 캐치(catch) 블럭(extension Droplet: Responder 예제코드에 있는 3)에 도달하게 될 것이다.

어떻게 에럴를 처리할 수 있는지 한번 보자. 먼저, 가장 직관적인 그 자리에서 처리하는 방법이다.
extension String: Error { } // 1


drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        returnJSON(answer) // 2

    }
    catch {
       return JSON("Answer couldn't be computed.") // 3

    }
})

drop.get("/pass-along", handler: { request in

   let answer = try findAnswer(in: request) // 4

   return JSON(answer) // 5

})


func findAnswer(for request: Request) throws->String {
   guard let answer = request.extract("answer") as? String else {
       throw "Answer parameter is not present." // 6

   }

  guard computingFinishedFast else {
       throw "Computing took too long to finish."
   }

   return answer
}
문자열 던지기

StringError를 따르게 하여(1) , 연관 값으로 만든 열거형을 만들어서 Error를 따르게 할 수 있으면 문자열을 던질 수 있다.

handle 엔드포인트 경우에, findAnswer에서 잘 처리했다면 answer로 만들어낸 JSON을 반환한다(2). 뭔가 문제가 있다면 에러를 캐치하고 에러로 만들어낸 JSON을 반환한다.

pass-along 엔드포인트의 경우,  모든것이 잘 처리되었다면 같은 시나리오대로 (5)를 적용하고, 에러가 던져졌다면 여기서 처리하지 않으므로 앞에서 언급한대로 콜백체인으로 보내질 것이다.

앞에서 본 것 처럼, findAnswer 메소드의 사용할 다른 엔드포인트를 추가하면 계속계속 에러를 다뤄야 할 것이다.

Vapor의 문서에서 추천해주는 다른 방법으로는, 모든 에러를 처리해주는 에러 처리 미들웨어를 만드는 것이다. AppError 엔티티를 만들고 findAnswer 메소드를 바꿈으로서 시작해보자.
enum AppError: Error {
    case argumentNotPresent(name: String)
    case computingTimedOut
}

drop.get("/handle", handler: { request in

    do {
        let answer = try findAnswer(in: request)
        return JSON(answer) // 1

    }
    catch {
        return JSON("Answer couldn't be computed.") // 2

    }
})

drop.get("/pass-along", handler: { request in

    let answer = try findAnswer(in: request) // 3

    return JSON(answer) // 4

})


func findAnswer(for request: Request) throws -> String {
    guard let answer = request.extract("answer") as? String else {
        throw AppError.argumentNotPresent(name:"answer") // 5

    }

    guard computingFinishedFast else {
        throw AppError.computingTimedOut // 6

    }

    return answer
}
커스텀 에러 던지기

여전히 그 자리에서 에러를 다룰 수 있지만(1)(2), 에러를 다루기위한 미들웨어를 추가할 것이기 때문에 이것들을 함께 던져(3)(4) 어디로 갈지 정할 수 있다. 이제 findAnswer메소드는 기대하는 파라미터를 위해 연관 값과함께 AppError를 던지며(5), 타임아웃을 위한 에러도 던진다(6).

이미 알고 있듯, 한 메소드만 구현하면 되는데, 아래에서 어떻게 하는지 확인해보자.
struct ErrorHandlingMiddleware: Middleware {

    func respond(to request: Request, chainingTo next: Responder) throws -> Response {
        do {
            return try next.respond(to: request) // 1

        }
        catch AppError.argumentNotPresent(let name) { // 2

            throw Abort.custom( // 3

                status: .badRequest,
                message: "Argument \(name) was not found." // 4

            )
        }
        catch AppError.computingTimedOut { // 5

              throw Abort.custom(
                status: .requestTimeout, // 6

                message: "Computing an answer has timed out."
            )
         }
         catch { // 7

                return Response( // 8

                    status: .serverError,
                    message: "Something unexpected happened."
                )
          }
    }
}
ErrorHandlingMiddleware

우리가 해야할 일은 do-catch 블럭 안에서 다음 응답자에 요청을 보내주는 일만 하면 된다. 만약 모든 처리가 잘 되었다면(1), 요청은 get 헨들러에 도달할 것이고 응답은 우리에게 돌아온다(1). 그리고 체인을 따라 간 것을 반환할 수 있다(1).

findAnswer 메소드에서 뭔가 잘못되었다면 우리 스스로 캐치할 수 있게 만들수 있고 특정의 Abort 에러들을 만들어서 던질 수 있다(3). Abort는 Vapor에서 제공하는 열거형일 수도 있고 Response를 생성해서 반환할 수도 있다(8).

연관 값과함께 에러를 캐치하는 것은 우리에게 어떤 유연함을 제공해 주는데, 보이지 않는 파라미터를 추출해내거나(4) 다른 파라미터에 같은 에러를 던질 수 있는 가능성을 열어준다.
func findMeaningOfLife(for request: Request) throws -> Int {
    guard let answer = request.extract("meaningOfLife") as? Int else {
        throw AppError.argumentNotPresent(name: "meaningOfLife")
    }

    return 42
}
연관 값과 함께 에러 던지기

이제 적절한 상태와 함께(6) 개별적인 타임아웃 에러를 다룰 수 있게 되었다(5). 그리고 serverError를 반환하는 곳에 나머지 모든 것을 위한 catch 콜백도 가질 수 있다. 이 serverError는 제네릭 메시지이다.

마지막으로 droplet에  우리의 새 미들웨어를 추가한다.
// [...]
drop.middleware.append(ErrorHandlingMiddleware())
// [...]
에러 처리 미들웨어 추가하기

한가지만 더, 미들웨어의 괜찮은 기능을 소개하고 싶다. 서버당 설정(per-server configuration)이다. ErrorHandlingMiddleware를 제품판(production)에서만 동작하게 하고 싶다고 가정하자. 어떤 이유로 우리앱에서 부분적으로 크래쉬를 내고 싶다고 하자. 우리가 지금까지 봐온 방식대로 미들웨어를 붙이는 것 대신에, droplets은 설정파일을 제공하며, 아래와같이 사용할 수 있다.

먼저, 미들웨어(middleware)로 붙이는 것 대신 컨피겨레이블(configurable)로 미들에웨를 추가하자.
// [...]
// drop.middleware.append(ErrorHandlingMiddleware()) -> replaced with:
drop.addConfigurable(middleware: ErrorHandlingMiddleware(), name: "error-handling")
// [...]
컨피겨레이블 미들웨어 추가하기

Config/production/droplet.jsonConfig/staging/droplet.json 파일서 적절한 키를 추가하자.
// production/droplet.json
{
   ...
    "middleware": {
        "server": [
            ...
            "error-handling", // 1
            ...
        ],
        "client": [
            ...
        ]
    },
    ...
}

// staging/droplet.json
{
    ...
    "middleware": {
        "server": [
            ... // 2
        ],
        "client": [
            ...
        ]
    },
    ...
}
droplet.json

이제 앱 실행때 ErrorHandlingMiddleware는 제품판 서버의 미들웨어에 추가될 것이지만(1), (2) 단계에서는 아니다. 서버와 클라이언트 두가지 다에 미들웨어를 추가할 수 있고, Config/server-type/droplet.json에도 추가할 수 있다. 또한 그 배열의 순서에 따라 된다.
이 글의 끝에 도달하고 있는 것처럼 스위프트는 서버 세팅을 위한 실용적인 솔루션임을 볼 수 있다. 우리의 요청/응답을 수정하기 쉽게 하기 위해 프로토콜은 Middleware를 정의하게 해주고, 체인된 스택에 핸들러를 추가하는 다양한 방법을 제공한다. 또한 기본 에러 처리를 위해 문자열을 던지는 용도로 익스텐션 하는 것 뿐만 아니라, 프로젝트의 더 나은 구조를 만들기 위해 메인 스트럭쳐에서 Request.Handler를 분리해내는 것도 하였다.

서버와 클라이언트에서 둘 다 스위프트를 사용하면, 중복을 피하면서 모델, 기능, 핼퍼/유틸리티를 공유할 수 있고,  서버와 클라이언트를 전체적인 하나로 봄으로서 모든것을 더 쉽게 만들 수 있다.(역자: 이 문장은 조금 더 의논해볼 필요가 있는 것 같습니다.)

Vapor 그 자체로서, 프레임워크로서 선택인가?(As for Vapor itself, as the framework of choice?) 이 에 따르면 최고도 아니고 최악도 아니다. Vapor는 Express.js와 Sinatra와 비슷한 성격을 제공하며(이것들은 이전에 내 블로그에 쓰인 두 방법이었다.), 내 요구에 잘 들어 맞았다(블로그는 너무 많은 요청이 없으며, API를 필요로 하지 않는다).


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

으로 보내주시면 됩니다.



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

,
제목: What's new in Swift 4.0

This article was translated with permission from the English original, What's new in Swift 4.0? on Hacking with Swift .

새로운 스위프트4의 새로운 점을 배우기위한 실제 예제 코드: 새로운 인코딩과 디코딩, 똑똑해진 키패스(keypaths), 다열 문자열(multi-line string) 등이 있습니다!

스위프트4.0이 모두의 인기있는 앱 개발 언어로 새로 배포되었고, 간단하고 안전한 코드를 작성할 수 있는 다양한 기능들을 소개했다. 스위프트 3.0이 발표되었을때 그 극적인 변화에 비하면 즐겁게 받아드릴 수 있을 것인데, 실제로 대부분 변화는 현재 존재하는 스위프트 코드와 완전히 호환이 되게 만든 것이다. 따라서 여러분이 약간의 변경사항을 원한다면, 그렇게 오래 걸리지 않을 것이다.

주의: 스위프트4는 아직 활발히 개발중이다. 나는 여기서 몇가지 유용한 새 기능을 골랐고, 이것들은 현재 모두 구현되었으며 시도해볼 수 있다. 마지막 배포가 있기 전의 그 달에 더 많은 기능이 나올 것임을 기억하라.

이 글이 마음에 들었다면, 아래의 것들도 흥미있을 수 있을 것이다.

스위프트스러운 인코딩과 디코딩
값 타입이 멋지다는 사실은 알고있지만, NSCoding과같은 Objective-C API와 인터렉트하기는 힘들다는 것도 알고있다. 둘을 이어주는 레이어를 만들거나 클래스에 넣어 사용해야한다. 두가지 방법 다 좋지만은 않다. 더욱 나쁜것은, 클래스에 넣어 변경할지라도 직접 인코딩과 디코딩 메소드를 작성해야하며, 이것은 고통스러운 에러를 야기할 수 있다.

스위프트4는 특수한 코드 없이 커스텀 데이터 타입을 시리얼라이즈하고 디-시리얼라이즈하게해주는 Codable 프로토콜을 소개했다. 여러분의 값 타입 손실에대해 더이상 걱정하지 않아도 된다. 더 나은점은 어떻게 데이터를 시리얼라이즈 할지 정할 수 있다는 것이다. 기존의 프로퍼티 리스트 양식을 사용할 수도 있고 JSON도 가능하다.

여러분이 읽고 있는 것이 맞는 말이다: 스위프트4는 특별한 코드 없이 여러분의 커스텀 데이터 타입을 JSON으로 시리얼라이즈 해준다.

이 얼마나 아름다운 광경인지 지켜보자. 먼저, 여기에 커스텀 데이터 타입이 있고, 그 인스턴스가 있다.
struct Language: Codable {
   var name: String
   var version: Int
}

let swift = Language(name: "Swift", version: 4)
let php = Language(name: "PHP", version: 7)
let perl = Language(name: "Perl", version: 6)
Language 구조체에 Codable 프로토콜을 따르게 한 것을 볼 수 있을 것이다. 이 간단한 추가사항만으로 아래처럼 JSON의 Data 표현으로 변환할 수 있다.
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(swift) {
   // save encoded somewhere
}
스위프트는 자동으로 여러분의 데이터 타입 안에있는 모든 프로퍼티들을 인코딩할 것이다. 여러분은 딱히 할 일이 없다.

이제 여러분도 나와같이 오랫동안 NSCoding을 사용해왔다면 뭔가 의심스러울 것이다. 정말로 제대로 동작할까? 그리고 이게 동작한다는 것을 어떻게 확신할 수 있을까? 음, Data 오브젝트를 문자열로 만드는 코드를 추가해서 한번 출력해보자. 그리고 다시 Language 인스턴스로 디코딩 시켜, 읽을 수 있는지 보자.
if let encoded = try? encoder.encode(swift) {
   if let json = String(data: encoded, encoding: .utf8) {
       print(json)
   }

   let decoder = JSONDecoder()
   if let decoded = try? decoder.decode(Language.self, from: encoded) {
       print(decoded.name)
   }
}
디코딩이 타입캐스팅을 필요로 하지 않는다는 점을 인지하자. 첫번째 파라미터로 데이터 타입 이름을 주면, 스위프트는 여기서 리턴 타입을 추론한다.

JSONEncoder와 그 프로퍼티 리스트에 짝인 PropertyListEncoder는 이 부분이 어떻게 동작할지 수많은 커스터마이징 옵션을 가진다. 빽빽한(compact) JSON을 원하는지, 출력하지 좋은(pretty-printed) JSON을 원하는지, ISO8601 데이터를 원하는지, 유닉스 epoch 데이터를 원하는지, 바이너리 프로퍼티 리스트를 사용하고 싶은지 XML을 사용하고 싶은지 정할 수 있다. 다른 옵션들에대한 더 많은 정보를 원한다면  the Swift Evolution proposal for this new feature을 확인해보자.

다열 문자열 리터럴
스위프트에서 다열 문자열로 쓰려면 항상 문자열 안에 \n을 써서 개행을 했었어야 했다. 코드상에서 보기에 좋아보이지 않지만 사용자에게는 올바르게 보여진다. 다행히 스위프트4에서 새로 소개된 다열 문자열 리터럴 문법은 문자열 보간법같은 기능도 쓸 수 있는 채로, 자유롭게 개행을 할 수 있게하고 이스케이핑 없이 따옴표를 사용할 수 있다.

문자열 리터럴을 시작하기 위해서는 쌍따옴표를 3개(""") 적고 리턴키를 누른다. 그 다음 여러분이 원하는 만큼 문자열을 쳐내는데, 변수나 개행도 가능하며, 문자열이 끝날때는 리턴키를 누르고 쌍따옴표를 세개 치면 된다.

문자열 리터럴에는 두가지 중요한 규칙이 있기 때문에 리턴키를 누르는 것에대해 이야기하고 싶다. """으로 문자열을 열때, 여러분의 문자열 내용은 반드시 새로운 라인에서 시작해야한다. 그리고 """로 다열 문자열을 끝낼때는 새로운 줄에서 """을 쳐야한다.

여기에 예시가 있다.
let longString = """
When you write a string that spans multiple
lines make sure you start its content on a
line all of its own, and end it with three
quotes also on a line of their own.
Multi-line strings also let you write "quote marks"
freely inside your strings, which is great!
"""
이것은 정의에서 몇몇 개행이 들어있는 새로운 문자열을 만든 것이다. 훨씬 읽고 쓰기 쉽다.

더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

키-밸류 코딩을위한 키패스 증진(Improved keypaths for key-value coding)
Objective-C에서 사랑했던 기능 중 하나는 직접 접근하는 것이 아닌 동적으로 프로퍼티를 참조하는 능력이다. 이것은 "주어진 오브젝트 X에 내가 읽고 싶은 프로퍼티가 있어"라고 말할 수 있다. 이런 참조 방법을 키패스(keypath)라 부르고 이것은 직접 프로퍼티에 접근하는 것과 구별되는데, 여기서는 실제로 값을 읽고 쓰는 것이 아니라 슬며시 감춰놨다가 나중에 사용하는 것이다.

여러분이 아직 키패스를 사용해보지 않았다면 기존의 스위프트 메소드를 이용해서 어떤식으로 동작했는지 보여주겠다. Starship이라는 구조체를 하나 정의하고 Crew라는 구조체를 정의하자. 그리고 각각의 인스턴스를 하나씩 만든다.
// an example struct
struct Crew {
   var name: String
   var rank: String
}

// another example struct, this time with a method
struct Starship {
   var name: String
   var maxWarp: Double
   var captain: Crew

   func goToMaximumWarp() {
       print("\(name) is now travelling at warp \(maxWarp)")
   }
}

// create instances of those two structs
let janeway = Crew(name: "Kathryn Janeway", rank: "Captain")
let voyager = Starship(name: "Voyager", maxWarp: 9.975, captain: janeway)

// grab a reference to the goToMaximumWarp() method
let enterWarp = voyager.goToMaximumWarp

// call that reference
enterWarp()
스위프트에서는 함수가 일급 타입이므로 마지막 두 줄에서 goToMaximumWarp() 메소드를 enterWarp으로 참조하게 만들 수 있다. 그리고 나중에 우리가 원할때 언제든지 사용할 수 있다. 여기서 문제는 이와같은 것을 프로퍼티에는 못한다는 것이다. "불가피한 상황이 생겼을때 확인할 수 있도록 captain의 이름 프로퍼티를 참조하는 것을 만들어라"고 말할 수 없다. 스위프트는 단지 직접 프로퍼티를 읽어서 원래의 값을 얻어내가 때문이다.

이것은 키패스로 고칠 수 있는데, 우리의 enterWarp() 코드같은 프러퍼티를 참조하며 호출되지 않은 것(uninvoked references to properties)이다. 이제 참조를 호출하면 현재 값을 얻어낸다. 그러나 나중에 참조를 호출하면 나중에 값을 얻어낸다. 프로퍼티의 수를 통해 파헤쳐볼 수 있고, 스위프트는 올바른 타입을 받나냈는지 확인하기위해 타입 추론을 이용한다.

스위프트 에볼루션 커뮤니티는 이 키패스 문법이 올바른지에대해 꽤 오랫동안 토론해왔었는데, 이것이 스위프트 코드와 시각적으로 달라 보일 필요가 있었기 때문이다. 결국 백슬래시를 사용한 문법이 되었다. \Starship.name, \Starship.maxWarp, \Starship.captain.name. 이 두가지를 변수에 할당하여 여러분이 원하는 언제나 어떤 Starship 인스턴스에서도 사용할 수 있다. 예제를 보자.
let nameKeyPath = \Starship.name
let maxWarpKeyPath = \Starship.maxWarp
let captainName = \Starship.captain.name

let starshipName = voyager[keyPath: nameKeyPath]
let starshipMaxWarp = voyager[keyPath: maxWarpKeyPath]
let starshipCaptain = voyager[keyPath: captainName]
스위프트는 타입 추론이 가능하기 때문에 starshipName 문자열과 starshipMaxWarp 더블형을 만들 것이다. 이 예제의 세번째처럼 프로퍼티의 프로퍼티라도 스위프트는 역시 올바르게 이해할 것이다.

나중에는 런타임중에도 배열 인덱스를 접근할 수 있거나 문자열로부터 키패스를 만들어낼 수 있는 계획이 있다. 더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

약간의 광고
If you're enjoying this article, you might like my free Natural Swift video. It gives you 75 minutes of hands-on coding that teaches functional programming, protocol-oriented programming, and value types, and you can download it for free with no obligation or catches – just click here.

And now back to your regularly scheduled broadcast…

딕셔너리 기능 증진
스위프트4에서 한가지 더 흥미로운 프로퍼절은 딕셔너리를 더욱 강력하게 만들고 특정 상황에 여러분이 예상한대로 동작하게 만드는 기능이었다.

간단한 예제로 시작해보자. 스위프트3에서 딕셔너리를 필터링 하는 것은 새로운 딕셔너리를 반환하지 았다. 대신 키/값 래이블과 함께 튜플 배열을 반환했었다. 예제이다.
let cities = ["Shanghai": 24_256_800, "Karachi": 23_500_000, "Beijing": 21_516_000, "Seoul": 9_995_000];
let massiveCities = cities.filter { $0.value > 10_000_000 }
이후에는 더이상 딕셔너리가 아니므로 massiveCities["Shanghai"]로 읽을 수 없다. 대신 massiveCities[0].value를 사용해야했는데, 별로 좋아보이지 않는다.

스위프트4부터는 더욱 여러분이 생각한대로 동작한다. 여기서는 새로운 딕셔너리를 반환해준다. 명백하게 이것은 튜플-배열 리턴 타입에 의존하고있는 현재 코드를 변경해야 할 것이다.

비슷하게, 딕셔너리에서 map() 메소드도 많은 사람들이 원하던 방식대로 되지 않았었다. 전달되면 키-값 튜플을 받고, 배열에 추가하기위해 한 값을 반환할 수 있다. 예제를 보자.
let populations = cities.map { $0.value * 2 }
이것은 스위프트4에서 바뀌지 않았지만, mapValues()라 불리는 새 메소드가 추가되었다. 이것은 더욱더 유용할 것으로 보이는데, 값을 변경하고 기존의 키를 이용해 딕셔너리에 다시 값을 넣어둘 수 있게 해준다.

예를들어, 이 코드는 모든 도시 인구를 100만으로 나누고 문자열로 만든다. 그리고 다시 Shanghai, Karachi, Seoul의 같은 키로 새로운 딕셔너리에 넣는다.
let roundedCities = cities.mapValues { "\($0 / 1_000_000) million people" }
(여기서 여러분은 실수로 중복할 수도 있기 때문에 딕셔너리 키를 맵핑시키는게 안전하지 않다고 생각할 수 있다.)

쉽게, 내가 좋아하는 새로운 딕셔너리 추가사항은 grouping 생성자이다. 이것은 시퀀스를 여러분이 원하는대로 그룹화한 시퀀스의 딕셔너리로 만들어준다. 계속해서 cities 예제로가서, 도시 이름의 배열을 얻어내기위해 cities.keys를 사용할 수 있으며, 첫글자로 그룹화한다.
let groupedCities = Dictionary(grouping: cities.keys) { $0.characters.first! }
print(groupedCities)
// ["B": ["Beijing"], "S": ["Shanghai", "Seoul"], "K": ["Karachi"]]
대신 아래처럼 그 이름의 길이에따라 도시들을 그룹화 시킬 수 있다.
let groupedCities = Dictionary(grouping: cities.keys) { $0.count }
print(groupedCities)
// [5: ["Seoul"], 7: ["Karachi", "Beijing"], 8: ["Shanghai"]]
마지막으로, 딕셔너리 키에 접근할 수 있고 키에 해당하는 값이 없다면 디폴트 값을 제공할 수 있다.
let person = ["name": "Taylor", "city": "Nashville"]
let name = person["name", default: "Anonymous"]
이제 경험이 좀 있는 개발자들은 nil-coalescing으로 사용하는게 더 났다고 주장할 것이다. 나도 동의한다. 현재 버전의 스위프트것을 사용하는 것 대신에 아래처럼도 쓸 수 있다.
let name = person["name"] ?? "Anonymous"
그러나 그냥 읽는데는 문제가 없지만, 딕셔너리 값을 수정할때는 동작하지 않는다. 적절하게 딕셔너리 값을 수정할 수 없는데, 그 키로 접근하면 옵셔널을 반환하기 때문이다. 키가 존재하지 않을 수도 있다. 스위프트4의 디폴트 딕셔너리 값으로 여러분은 더욱 적합한 코드를 작성할 수 있다. 아래처럼 말이다.
var favoriteTVShows = ["Red Dwarf", "Blackadder", "Fawlty Towers", "Red Dwarf"]
var favoriteCounts = [String: Int]()

for show in favoriteTVShows {
   favoriteCounts[show, default: 0] += 1
}
이 코드는 favoriteTVShows에 있는 모든 문자열을 돌면서, favoriteCounts라는 딕셔너리를 사용하여 각 항목이 나타날때마다 카운팅을 한다. 코드 한줄에서 딕셔너리를 수정할 수 있는데, 딕셔너리에는 항상 값을 가질거라는 것을 알기 때문이다. 디폴트값의 0이든 이전에 카운팅되었던 것에따라 더 높은 숫자든.

더 많은 정보를 원한다면 the Swift Evolution proposal for these new features을 확인해보자.

문자열은 다시 컬랙션이다!
작은 변화지만 많은 사람들을 행복하게 만든다. 문자열이 다시 컬랙션이 되었다. 이 의미는, 이것을 뒤집거나, 문자를 하나씩 돌거나, map()하거나 flatMap()하는 등이 가능하다. 예제를 보자.
let quote = "It is a truth universally acknowledged that new Swift versions bring new features."
let reversed = quote.reversed()

for letter in quote {
   print(letter)
}
이 변경은 String Manifesto라 불리는 다양한 개정사항의 부분에 소개되어 있다.

한쪽만의 범위(One-sided ranges)
가장 최신은 아니지만 최근에, 스위프트4는 파이썬같은 한쪽만 컬랙션 자르기를 소개했다. 한쪽이 빠지면 자동으로 컬랙션의 시작이나 끝을 추론한다. 이 변경은 현재 코드에 영향을 주지 않는다. 현재 연산자에 새로운 부분이기 때문에 잠재적인 손상을 걱정하지 않아도 된다.

아래에 예제가 있다.
let characters = ["Dr Horrible", "Captain Hammer", "Penny", "Bad Horse", "Moist"]
let bigParts = characters[..<3]
let smallParts = characters[3...]
print(bigParts)
// ["Dr Horrible", "Captain Hammer", "Penny"]
print(smallParts)
// ["Bad Horse", "Moist"]
더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

아직 더 남은게 있다...
스위프트4를 탑재한 Xcode의 첫번째 배포판은 6월에 iOS11, tvOS11, watchOS4, macOS와함께 나올것으로 보인다(역자: 이미 나왔습니다). 지금까지 우리가 본 것은 특히 명확하게 이미 나오기로 약속되었고, 팀은 가능한 추가할 수 있도록 스위프트4를 만들려고 노력하고 있다. 주로 기존에 있는 것을 고치거나 수정하지 않도록 새로운 기능을 추가하는 것이 업그레이드하기 편하기 해줄 수 있고, 고맙게도 이 언어를 위한 새로운 안정성의 시작의 신호가 있다.

스위프트 에볼루션이 때론 혼돈속에 빠지기도 했지만(접근 수준같은 부분), 스위프트4는 애플의 커뮤니티 방법을 다시 검증했다. 나는 위에 몇몇 스위프트 에볼루션을 링크 걸어두었는데, 각각의 것들은 커뮤니티 여론의 도움으로 광범위하게 토론되었다. 애플 엔지니어가 변경을 강행하지 않고 분별력있게 하였으며, 무엇이 이미 스마트하고 엘레강트한 언어인지 다듬기위해 고려했다.

한가지 지연된 기능은 ABI 호환성인데, 이것은 개발자들에게 컴파일된 라이브러리를 배포할 수 있게 해줄 것이다. 오늘 스위프트에 몇가지 핵심 기능이 구현되지 않고 남아있다. 다행히도 스위프트5가 되기 전에 만나볼 수 있을 것이다...


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

으로 보내주시면 됩니다.




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

,
제목: What's New in Swift 4

주의: 이 튜토리얼은 Xcode9 beta 1에 들어있는 스위프트 4버전을 사용함.

스위프트4는 애플이 2017년 가을에 낼 가장 최신의 메이저 배포이다. 그 메인 초점은 스위프트3 코드와 소스 호환성 제공이며, ABI 안정성에 맞춰 작업하고 있다. 이 글에서는 스위프트가 바뀌면서 여러분의 코드에 중요하게 영향을 줄 것을 짚어 줄 것이다. 그리고 이제 시작해보자!


Swift 4Swift 4

시작하며
스위프트4는 Xcode9에 포함되있다. 여러분은 애플의 developer portal에서 최신버전의 Xcode9를 다운받을 수 있다(반드시 활성화된 개발자 계정이 있어야한다). 각 Xcode beta는 배포될 시점의 가장 최신의 스위프트4를 탑재할 것이다.

읽어내려가다보면 [SE-xxxx] 양식의 링크를 볼 수 있을 것이다. 이 링크들은 관련된 스위프트 에볼루션 프로포절로 연결해준다. 어떤 주제든 더 배우고 싶으면 이 링크를 확인해보자.

나는 여러분이 스위프트4의 각 기능을 시도해보길 추천하며 플레이그라운드에서 돌려보길 바란다. 그리하여 지식을 여러분 머릿속에 확립시킬 수 있고 각 주제마다 더 깊게 들어가볼 수 있게 해줄 것이다. 각 예제를 고쳐보고 추가해보면서 가지고 놀아보자. 잘 즐겨보길 바란다!

노트: 이 글은 각 Xcode beta에대해 갱신될 것이다. 여러분이 다른 스위프트 스넵샷을 사용하면 여기 코드가 동작할것이라는 보장은 할 수 없다.

스위프트4로 마이그레이션하기
스위프트3에서 4로 마이그레이션하는 것은 2.2에서 3으로 가는 것보다 덜 힘들 것이다. 보통 많은 변화가 추가되고 개별적으로 건드릴 필요가 없다. 그덕에 스위프트 마이그레이션 툴은 주요 변화만 처리해 줄 것이다.

Xcode9는 동시에 스위프트4와 스위프트3.2안에 스위프트3도 가능하게 지원한다. 여러분 프로젝트에서 각 타겟은 스위프트3.2나 스위프트4가 될 수 있고, 필요에따라 조각조각 마이그레이션 할 수 있다. 그래도 스위프트3.2로 변환하는 것은 완전히 자유롭진 않다. 새로운 SDK에 맞추기위해 여러분의 코드 부분을 업데이트 해야할 것이다. 그리고 스위프트는 아직 ABI 안정이 되지 않았기 때문에 Xcode9로 여러분의 의존성들을 다시 컴파일 해야할 것이다.

스위프트4로 마이그레이션할 준비가 되었다면 Xcode는 다시한번 마이그레이션 툴로 여러분을 도와줄 것이다. Xcode에서 Edit/Convert/To Current Swift Syntax...로 가서 변환 툴을 실행시킨다.

변환하고 싶은 타겟을 선택한 후, Xcode는 Objective-C 추론에대한 설정을 물어볼 것이다. 추론을 제한하여 바이너리 크기를 줄이는 추천 옵션을 선택하자(이 토픽에대해 더 알고 싶으면 아래에 Limiting @objc Inference를 확인해보자).

여러분의 코드가 의도한대로 바뀌었는지 잘 이해하기위해 우리는 제일 먼저 스위프트4의 API 변경사항들을 다룰 것이다.

API 변경사항
스위프트4에서 소개된 추가사항에 들어기보기 전에, 기존의 API를 변경하거나 증진시킨것이 무엇이 있는지 먼저 살펴보자.

문자열(String)
스위프트4에서 String은 상당히 많은 사랑을 받고 있다. 이 프로포절은 많은 변경사항을 가지며, 큰것부터 하나씩 보자. [SE-0163]

여러분은 이 부분에서 옛날 기분이 들것이다. 스위프트가 2.0버전으로 돌아와서 다시 컬랙션이 되었다. String에서 characters 배열이 필요하다는 점을 없앴다. 이제 String 오브젝트를 직접 이터레이트(iterate) 할 수 있다.
let galaxy = "Milky Way 🐮"
for char in galaxy {
  print(char)
}

YesYes


String으로 논리적인 이터레이트만 가능한게 아니라, SequenceCollection의 모든 기능을 사용할 수 있다.
galaxy.count       // 11

galaxy.isEmpty     // false

galaxy.dropFirst() // "ilky Way 🐮"

String(galaxy.reversed()) // "🐮 yaW ykliM"


// Filter out any none ASCII characters
galaxy.filter { char in
  let isASCII = char.unicodeScalars.reduce(true, { $0 && $1.isASCII })
  return isASCII
} // "Milky Way "
위의 ASCII 예제는 Character의 작은 개선사항을 설명해준다. 이제 Character에서 직접 UnicodeScalarView에 접근할 수 있다. 이전에는 새 String을 만들어야 했었다 [SE-0178].

다른 추가사항은 StringProtocol이다. 이전에 String에 정의되있던 많은 기능들이 이 프로토콜에 정의되있다. 이렇게 변경하게된 이유는 자르기(slice) 작업을 개선하기 위함이다. 스위프트4는 String에서 서브시퀀스를 참조하기위해 Substring 타입을 추가했다.

StringSubstring은 둘 다 StringProtocol에 주어진 기능을 대부분 비슷하게 구현하였다.
// Grab a subsequence of String
let endIndex = galaxy.index(galaxy.startIndex, offsetBy: 3)
var milkSubstring = galaxy[galaxy.startIndex...endIndex]   // "Milk"

type(of: milkSubstring)   // Substring.Type


// Concatenate a String onto a Substring
milkSubstring += "🥛"     // "Milk🥛"


// Create a String from a Substring
let milkString = String(milkSubstring) // "Milk🥛"
또다른 멋진 개선사항은 String이 grapheme cluster를 해석하는 방식이다. 이 해결방안은 유니코드9 각색한 것으로부터 나왔다. 이전에는 유니코드 문자들이 여러 코드 포인트로 만들어져있었는데, 이때문에 count는 1로 합산되었다. 피부톤을 선택하고 이모티콘을 넣으면 이런 일이 발생했다. 아래에 몇가지 예제가 있는데, 예전의 동작방식과 이후의 동작방식을 보여준다.
"👩💻".count // Now: 1, Before: 2

"👍🏽".count // Now: 1, Before: 2

"👨❤️💋👨".count // Now: 1, Before, 4
이것은 String Manifesto에 언급된 변화들중 일부분이다. 나중에는 여러분이 예상했던대로 이것에대한 모든 동기와 제안된 솔루션에대해 읽을 수 있다.

딕서녀리와 셋(Dictionary and Set)
Collection 타입이 가버린다면(As far as Collection types go), SetDictionary은 항상 직관적이지 않았다. 운좋게도 스위프트 팀은 여기에 애정을 가져주었다 [SE-0165].

생성자 기반의 시퀀스
그것들 중 첫번째는 키-값 쌍(튜플)의 시퀀스에서 딕셔너리를 만들 수 있게 해주었다는 점이다.
let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B", "Barnard's Star", "Wolf 359"]
let nearestStarDistances = [4.24, 4.37, 4.37, 5.96, 7.78]

// Dictionary from sequence of keys-values
let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances))
// ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Barnard's Star": 5.96]

중복 키 해결법
이제 여러분이 원하는 방식으로 중복키로 딕셔너리를 초기화할 수 있게 되었다. 이렇게하여 이 문제에대해 별 말 없이도 키-값 쌍을 덮어쓰는 것을 막을 수 있다.
// Random vote of people's favorite stars
let favoriteStarVotes = ["Alpha Centauri A", "Wolf 359", "Alpha Centauri A", "Barnard's Star"]

// Merging keys with closure for conflicts
let mergedKeysAndValues = Dictionary(zip(favoriteStarVotes, repeatElement(1, count: favoriteStarVotes.count)), uniquingKeysWith: +) // ["Barnard's Star": 1, "Alpha Centauri A": 2, "Wolf 359": 1]
위 코드에서는 충돌하는 두 값을 더하여 중복 키 문제를 해결하기위해 축약된 +와 함께 zip을 사용했다.

노트: zip이 생소하다면 애플의 Swift Documentation를 빠르게 읽어보자.

필터링
이제 DictionarySet 모두 필터하였을때 원래 타입의 새 오브젝트를 결과로 할 수 있게 되었다.
// Filtering results into dictionary rather than array of tuples
let closeStars = starDistanceDict.filter { $0.value < 5.0 }
closeStars // Dictionary: ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Alpha Centauri B": 4.37]

딕셔너리 맵핑
Dictionary는 그 값을 바로 맵핑할 수 있는 유용한 메소드를 얻어냈다.
// Mapping values directly resulting in a dictionary
let mappedCloseStars = closeStars.mapValues { "\($0)" }
mappedCloseStars // ["Proxima Centauri": "4.24", "Alpha Centauri A": "4.37", "Alpha Centauri B": "4.37"]

딕셔너리의 디폴트 값
일반적으로 Dictionary의 값에 접근할 때, 값이 nil인 경우 디폴트 값을 지정하기위해 nil-coalescing 연산자를 사용한다. 스위프트4에서는 그 줄에서 변경하여 더 깔끔하고 멋진 방법으로 되었다.
// Subscript with a default value
let siriusDistance = mappedCloseStars["Wolf 359", default: "unknown"] // "unknown"


// Subscript with a default value used for mutating
var starWordsCount: [String: Int] = [:]
for starName in nearestStarNames {
  let numWords = starName.split(separator: " ").count
  starWordsCount[starName, default: 0] += numWords // Amazing

}
starWordsCount // ["Wolf 359": 2, "Alpha Centauri B": 3, "Proxima Centauri": 2, "Alpha Centauri A": 3, "Barnard's Star": 2]
이전에 이 타입을 변경하려면 커다란 if let문으로 감싸야 했다. 스위프트4는 한줄에 된다!

딕서녀리 그룹핑
또다른 멋진 추가사항은 Dictionary를 Sequence로부터 초기화할 수 있는 기능과 대괄호로 그룹화할 수 있는 기능이다.
// Grouping sequences by computed key
let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }

// ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]
이렇게하면 데이터를 특정 패턴으로 그룹화할때 편하게 다가올 것이다.

용량 잡아놓기(Reserving Capacity)
SequenceDictionary 둘 다 이제 명시적으로 용량을 정해놓을 수 있다.
// Improved Set/Dictionary capacity reservation
starWordsCount.capacity  // 6

starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity

starWordsCount.capacity // 24
이 타입에서 재할당은 부담되는 작업일 수 있다. 저장되는데 필요한 데이터가 얼마인지 알때 reserveCapacity(_:)을 사용하여 퍼포먼스를 쉽게 개선할 수 있다.

이것은 엄청난 양의 정보이니, 그 타입들에대해 명확하게 확인해보고 추가적으로 여러분의 코드에 사용할 방법이 있는지 찾아보자.

private 접근 수정자
스위프트3애서 그렇게 반갑지 않던 부분은 fileprivate 였을 것이다. 이론적으로는 좋지만, 실제로는 사용하기 혼란스러울 수 있었다. private의 목표는 맴버 자신에서만 사용하는 것이었는데, fileprivate은 같은 파일안에 맴버들사이에서 접근을 공유하려는 경우에는 거의 사용하지 않는다.

이 이슈는 스위프트가 익스텐션을 사용하여 논리적인 그룹으로 코드를 쪼개도록 지향하기 때문에 생긴다. 익스텐션은 원래 맴버가 선언된 범위 밖에서 사용할때가 많다. 그렇기때문에 fileprivate에대한 확장이 필요하게 되었다.

스위프트4는 한 타입과 다른 익스텐션 사이에 같은 접근 제어 범위를 공유하여 원래 의도를 이해한다. 이것은 오직 같은 소스파일 안에서만 잡아둔다 [SE-0169].

struct SpaceCraft {
  private let warpCode: String

  init(warpCode: String) {
   self.warpCode = warpCode
  }
}

extension SpaceCraft {
  func goToWarpSpeed(warpCode: String) {
   if warpCode == self.warpCode { // Error in Swift 3 unless warpCode is fileprivate

     print("Do it Scotty!")
   }
  }
}

let enterprise = SpaceCraft(warpCode: "KirkIsCool")
//enterprise.warpCode  // error: 'warpCode' is inaccessible due to 'private' protection level
enterprise.goToWarpSpeed(warpCode: "KirkIsCool") // "Do it Scotty!"
그리하여 fileprivate는 코드 구조를 위한 임시목적이라기보단 의도한 목적에 맞게 사용할 수 있다.

API 추가사항들
이제 스위프트4의 반짝반짝 빛나는 새 기능을 살펴보자. 이 변경사항들은 현재 코드를 망가뜨리진 않고 간단하게 추가할 수 있다.

아키브와 시리얼라이즈


지금까지 스위프트는, 커스텀타입을 시리얼라이즈와 아키브하기위해서 여러노력을 했었어야 했다. class 타입의 경우 NSObject를 상속하고 NSCoding 프로토콜을 구형해야했다.

structenum같은 값 타입은 꼼수처럼 NSObjectNSCoding을 확장한 보조 오브젝트를 만들어야 했다.

스위프트4는 세가지 타입 모두 시리얼라이즈 할 수 있게하여 문제를 해결해 주었다 [SE-0166].

struct CuriosityLog: Codable {
  enum Discovery: String, Codable {
   case rock, water, martian
  }

  var sol: Int
  var discoveries: [Discovery]
}

// Create a log entry for Mars sol 42
let logSol42 = CuriosityLog(sol: 42, discoveries: [.rock, .rock, .rock, .rock])
이 예제에서 볼 수 있듯, EncodableDecodable을 위해 Codable 프로토콜만 구현하면 된다. 모든 프로퍼티가 Codable이면 그 프로토콜은 컴파일러예의햬 자동으로 생성된다.

실제로 오브젝트를 인코드하기 위해, 이것을 인코더에 보내야 할 것이다. 스위프트 인코더는 스위프트4에서 활발하게 구현되고있다. 각각의 다른 스킴에따라 오브젝트를 인코딩할 수 있다 [SE-0167]. (주의: 이 제안중 일부는 아직 개발중에 있다)

let jsonEncoder = JSONEncoder() // One currently available encoder


// Encode the data
let jsonData = try jsonEncoder.encode(logSol42)
// Create a String from the data
let jsonString = String(data: jsonData, encoding: .utf8) // "{"sol":42,"discoveries":["rock","rock","rock","rock"]}"
한 오브젝트를 받아서 자동으로 JSON 오브젝트로 인코딩 시킨다. 속성들을 확인하기위해 JSONEncoder를 커스터마이징된 아웃풋으로 만든다.

이 프로세스의 마지막 과정은 데이터를 구체적인 오브젝트로 디코딩하는 것이다.
let jsonDecoder = JSONDecoder() // Pair decoder to JSONEncoder


// Attempt to decode the data to a CuriosityLog object
let decodedLog = try jsonDecoder.decode(CuriosityLog.self, from: jsonData)
decodedLog.sol         // 42

decodedLog.discoveries // [rock, rock, rock, rock]
스위프트4에서의 인코딩과 디코딩은 @objc 프로토콜의 오버헤드나 한계에 의존하지 않고서 스위프트로부터 기대되는 타입 세이프티를 취할 수 있다.

키-값 코딩
지금까지 스위프트에서 함수는 클로저이기 때문에 함수를 호출하지 않은채 참조하여 붙잡아둘 수 있었다. 그렇지만 프로퍼티가 데이터를 잡아두어 실제로 접근하고 있지 않는한 함수를 프로퍼티에 참조하여 잡아둘 수 없었다.

스위프트4에서 아주 흥미로운 추가사항은 인스턴스의 값에서도 get/set으로 타입의 키패스를 참조할 수 있다는 점이다.
struct Lightsaber {
  enum Color {
   case blue, green, red
  }
  let color: Color
}

class ForceUser {
  var name: String
  var lightsaber: Lightsaber
  var master: ForceUser?

  init(name: String, lightsaber: Lightsaber, master: ForceUser? = nil) {
   self.name = name
   self.lightsaber = lightsaber
   self.master = master
  }
}

let sidious = ForceUser(name: "Darth Sidious", lightsaber: Lightsaber(color: .red))
let obiwan = ForceUser(name: "Obi-Wan Kenobi", lightsaber: Lightsaber(color: .blue))
let anakin = ForceUser(name: "Anakin Skywalker", lightsaber: Lightsaber(color: .blue), master: obiwan)
이것은 몇 ForceUser 인스턴스를 그 name, lightsaber, master로 설정하여 생성하는 코드이다. 키패스를 만들기위해서는 필요한 프로퍼티 앞에 백슬래시(\)만 넣어주면 된다.
// Create reference to the ForceUser.name key path
let nameKeyPath = \ForceUser.name

// Access the value from key path on instance
let obiwanName = obiwan[keyPath: nameKeyPath]  // "Obi-Wan Kenobi"
이 예제에서는 ForceUsername 프로퍼티 키패스를 생성했다. 그리고 keyPath라는 새로운 서브스크립트에 이 키패스를 담아서 사용한다. 이제 디폴트에의해 이 서브스크립트는 모든 타입에서 사용할 수 있다.

아래에는 하위 오브젝트 예제, 프로퍼티 설정, 키패스 참조 만들기에대한 여러가지 키패스 사용법 예시가 있다.
// Use keypath directly inline and to drill down to sub objects
let anakinSaberColor = anakin[keyPath: \ForceUser.lightsaber.color]  // blue


// Access a property on the object returned by key path
let masterKeyPath = \ForceUser.master
let anakinMasterName = anakin[keyPath: masterKeyPath]?.name  // "Obi-Wan Kenobi"


// Change Anakin to the dark side using key path as a setter
anakin[keyPath: masterKeyPath] = sidiousan
akin.master?.name // Darth Sidious


// Note: not currently working, but works in some situations
// Append a key path to an existing path
//let masterNameKeyPath = masterKeyPath.appending(path: \ForceUser.name)
//anakin[keyPath: masterKeyPath] // "Darth Sidious"
스위프트의 키패스에서 장점은 강타입이라는 점이다! 더이상 Objective-C의 문자열 스타일로 지저분할 필요가 없다!

다열 문자열 리터럴(Multi-line String Literals)
많은 프로그래밍 언어에서는 일반적인 기능인 다열 문자열 리터럴이다. 스위프트4에서 이것을 추가했으며 세개의 쌍따옴표로 텍스트를 감싸는 형태의 문법이다 [SE-0168].

let star = "⭐️"let introString = """
  A long time ago in a galaxy far,
  far away....

  You could write multi-lined strings
  without "escaping" single quotes.

  The indentation of the closing quotes
      below deside where the text line
  begins.

  You can even dynamically add values
  from properties: \(star)
  """

print(introString) // prints the string exactly as written above with the value of star
XML/JSON을 만들떄나 UI에 보여줄 긴 양식의 텍스트를 만들때 매우 유용하다.

한쪽만의 범위(One-Sided Ranges)
장황한 표현을 줄이고 가독성을 개선하기위해 표준 라이브러리는 한쪽만 표현된 범위로 시작과 끝을 추론할 수 있다 [SE-0172].

한 인덱스에서 시작이나 끝 인덱스까지 범위의 컬랙션을 만들어보면 유용하다는 점을 바로 인지할 수 있다.
// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..<planets.endIndex]

let firstThree = planets[..<4]          // Before: planets[planets.startIndex..<4]
보이는바와 같이, 한쪽만의 범위는 명시적으로 시작이나 끝 인덱스를 지정해줘야하는 필요를 줄여준다.

무한 시퀀스
시작이나 끝 인덱스가 셀 수 없는 타입일때 무한 Sequence를 정의할 도 있다.
// Infinite range: 1...infinity
var numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (8, "Neptune")]

planets.append("Pluto")
numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (9, "Pluto")]

패턴 매칭
한쪽만의 범위를 사용할 때 또다른 좋은 사용 방법은 패턴 매칭이다.
// Pattern matching

func temperature(planetNumber: Int) {
  switch planetNumber {
  case ...2: // anything less than or equal to 2

   print("Too hot")
  case 4...: // anything greater than or equal to 4

   print("Too cold")
  default:
   print("Justtttt right")
 }
}

temperature(planetNumber: 3) // Earth

제네릭 서브스크립트
서브스크립트는 데이터 타입의 접근성을 직관적이게 만드는데 중요한 역할을 한다. 이런 유용함을 증진하기 위해 서브스크립트는 이제 제네릭으로 만들 수 있다 [SE-0148].

struct GenericDictionary<Key: Hashable, Value> {
  private var data: [Key: Value]

  init(data: [Key: Value]) {
   self.data = data
  }

  subscript<T>(key: Key) -> T? {
   return data[key] as? T
  }
}
이 예제에서는 리턴 타입이 제네릭이다. 그리하여 아래처럼 제네릭 서브스크립트를 사용할 수도 있다.
// Dictionary of type: [String: Any]
var earthData = GenericDictionary(data: ["name": "Earth", "population": 7500000000, "moons": 1])

// Automatically infers return type without "as? String"
let name: String? = earthData["name"]

// Automatically infers return type without "as? Int"
let population: Int? = earthData["population"]
리턴 타입을 제네릭으로 하는것 뿐만 아니라 실제 서브스크립트 타입을 제네릭으로 할 수도 있다.
extension GenericDictionary {
  subscript<Keys: Sequence>(keys: Keys) -> [Value] where Keys.Iterator.Element == Key {
   var values: [Value] = []
   for key in keys {
     if let value = data[key] {
       values.append(value)
     }
   }
   return values
  }
}

// Array subscript value
let nameAndMoons = earthData[["moons", "name"]]        // [1, "Earth"]

// Set subscript value
let nameAndMoons2 = earthData[Set(["moons", "name"])]  // [1, "Earth"]
이 예제에서는 두가지 다른 Sequence 타입(ArraySet)을 넣어 각각 값의 배열을 내뱉는다.

잡다한 것
여기까지는 스위프트4에서 큼직한 변화들을 다뤄봤다. 이제는 비교적 작은 부분들을 빠르게 볼 것이다.

MutableCollection.swapAt(_: _:)
MutableCollection은 이제 변경가능한 swapAt(_:_:) 메소드를 가진다. 이 메소드는 이름대로 동작하는데, 주어진 인덱스에 값을 스왑한다 [SE-0173].

// Very basic bubble sort with an in-place swap
func bubbleSort<T: Comparable>(_ array: [T]) -> [T] {
  var sortedArray = array
  for i in 0..<sortedArray.count - 1 {
   for j in 1..<sortedArray.count {
     if sortedArray[j-1] > sortedArray[j] {
       sortedArray.swapAt(j-1, j) // New MutableCollection method

     }
   }
  }
  return sortedArray
}

bubbleSort([4, 3, 2, 1, 0]) // [0, 1, 2, 3, 4]

연관 타입 제약(Associated Type Constraints)
이제 where을 사용하여 연관타입에 제약을 둘 수 있다 [SE-0142].

protocol MyProtocol {
  associatedtype Element
  associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element
}
프로토콜 제약을 사용하여 많은 associatedtype 선언이 그 값을 제약할 수 있다.

클래스와 프로토콜의 존재
결국 Objective-C에서 스위프트로 만들어진 이 기능은 한 타입이 클래스를 따를 수 있을 뿐만 아니라 여러 프로토콜도 따르면서 정의할 수 있는 기능이다 [SE-0156].

protocol MyProtocol { }
class View { }
class ViewSubclass: View, MyProtocol { }

class MyClass {
  var delegate: (View & MyProtocol)?
}

let myClass = MyClass()
//myClass.delegate = View() // error: cannot assign value of type 'View' to type '(View & MyProtocol)?'
myClass.delegate = ViewSubclass()

@objc 추론을 제한하기
스위프트 API를 Objective-C에서 쓸 수 있게 만들기 위해서는 @objc라는 컴파일러 속성을 사용한다. 대부분 스위프트 컴파일러는 여러분을 위해 추론해준다. 이 추론에서 커다란 세가지 이슈는 다음과 같다.
  1. 바이너리 크기를 상당하게 증가시킬 가능성을 가진다.
  2. @objc가 추론될 때 알고있는 것이 명확하지 않다.
  3. 무심결에 Objective-C selector 충돌을 만들어낼 가능성을 증가시킨다.
스위프트4는 @objc 추론을 제한하여 이 문제를 해결하였다 [SE-0160]. 이 말은 Objective-C의 모든 동적 디스패치 능력을 원할때는 명시적으로 @objc를 사용해야 함을 의미한다.

이런 변화를 만들어야하는 몇가지 예제는 다음과 같다. private 메소드, dynacmic 선언, NSObject 자식 클래스의 메소드.

NSNumber 연결하기
NSNumber와 스위프트의 넘버 사이에는 여러 답답한 동작들이 존재해왔는데, 이 언어에 너무 오랫동안 몰해왔다. 다행히 스위프트4는 이 버그를 잡았다 [SE-0170].

아래에는 그 동작들의 예제를 설명해준다.
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231
스위프트3에서 일어나는 이상한 이 동작은 넘버가 오버플로우되면 0에서 시작하게된다. 이 예제에선 999 % 2^8 = 231으로 나온다.

스위프트4는 숫자가 담겨진 타입안에 세이프하게 표현될 수 있을 때만 값을 반환하는 옵셔널으로 만들었다.

스위프트 패키지 매니저
지난 몇달동안 스위프트 패키지 매니저에 여러 업데이트가 있었는데, 가장 큰 변화는 다음과 같다.
  • 브랜치나 커밋 해시에서 의존성을 제공한다(Sourcing dependencies from a branch or commit hash)
  • 더 많은 수용할 수 있는 패키지 버전의 조작(More control of acceptable package versions)
  • 더 일반화하여 해결한 패턴으로 직관적이지 않은 핀닝 명령을 교체()
  • 컴펄레이션에 사용될 스위프트 버전 정의 기능(Ability to define the Swift version used for compilation)
  • 각 타겟마다 소스파일의 위치를 지정함(Specify the location of source files for each target)
이것들은 모두 SPM이 되어야할 모습의 큰 발자국이다.SPM은 아직 긴 여정이 남았지만 프로포절에 활발하게 남아서 윤곽을 잡는 일을 도와줄 수 있다.

최근에 다뤄지는 프로포절의 개괄적인 내용을 보려면 Swift 4 Package Manager Update을 확인해보자.

아직 진행중인 것
이 글을 쓰고 있는 시점에서 15가지 수용된 프로포절이 큐에 있다. 무엇이 어떻게 되가고 있는지 슬쩍 보고 싶으면 Swift Evolution Proposals에 들어가서 Accepted로 필터링해보자.

이제 모두 하나하나 따라가보는것 보단, Xcode9의 베타버전에서 업데이트 된 것을 포스팅 할 것이다.

여기서 어디로 까?
스위프트 언어는 지난 몇년간 매우 커지고 성숙해왔다. 프로포절 과정과 커뮤니티 관계는 이 파이프라인을 따라가면 쉽게 변경사항을 확인할 수 있게 만들어왔다. 또한 누구나 쉽게 직접 에볼루션에 영향을 줄 수 있게 해놓았다.쉽게

스위프트4에서 이런 변화들로, 우리는 마침내 ABI 안정성의 모퉁이에 위치하고 있다. 스위프트 버전을 업그레이드하는데 좀 덜 고통스러워지고 있다. 빌드 퍼포먼스와 툴의 기능은 광대하게 개선되고 있다. 애플의 생태계 바깥에서 사용하는 스위프트는 더욱 실용적이게 되어가고 있다. 그리고 생각하건데, 우리는 아마 오직 몇개만 직관적인 구현에서 벗어나는 String의 완전한 재작성일 것이다(And to think, we're probably only a few full rewrites of String away from an intuitive implementation). ;]

스위프트에 더 많은 것들이 들어왔다. 모든 변경사항들이 어떻게 되가는지 최신 정보를 유지하기 위해서, 아래 자료를 확인하면 된다.
스위프트4에대한 여러분의 생각은 어떤가? 여러분이 제일 좋아하는 변경사항은 무엇인가? 아직도 이 언어 바깥에서 미련이 있는 기능은 무엇인가? 여기서 다루지 않은 새롭고 멋진 무언가를 발견하였는가? 아래 주석에 알려주기 바란다!


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

으로 보내주시면 됩니다.



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

,
제목: Protocols and MVVM in Swift to avoid repetition

우리가 Viable을 최신 iOS 앱의 토대를 만들어갈때, 이전 iOS 앱으로부터 배우려 했다. 우리는 2가지 목표를 정했다.
  • Massive View Controller(MVC를 비꼬는 약자) 증후군 피하기
  • 가능한 적은 중복
초기에 디자인팀이 만든 Viable 화면에는 수많은 비슷한 화면이었다. 아래에 간단하게만든 예시를 한번 보자. 두 화면은 모두 상단에 UILabel이 있고 검색 결과를 보여주는 UITableView가 있다. 각각의 결과에대한 UITableViewCell도 매우 비슷했다. 이들은 다소 레이아웃을 공유했고 데이터만 달랐다.


Viable은 화면에 표시되는 6가지 타입의 데이터가 있었으며, 각 타입마다 새로운 뷰 컨트롤러를 만들어서 코드 중복이 많았다. 그리하여 우리는 6개의 데이터 타입을 모두 표시할 수 있는 SearchResultsViewcController를 만들었다.
데이터 타입에따라 다르게 렌더링하기위해 제일 처음 떠오른 방법으로는, tableView:cellForRowAtIndexPath:에 거대한 if/else문이었는데, 코드 규모가 잘 정연되지 못했고 결국 길고 못난 메소드가 되버렸다.

MVVM와 프로토콜을 사용하여 해결하기
테일러 구이던(Taylor Guidon)은 MVVM(Model-View-ViewModel) 패턴에대한 입문의 글을 포스팅했는데, 여기서 확인할 수 있다. 이 글은 그 요약 버전인데, 데모 프로젝트에 적용한 것을 깃헙에서 확인할 수 있다.

모델(Models)
모델 그룹에서의 모델은 데이터를 담고 있는다. 우리는 DomainModelProductModel을 가지는데, 둘 다 구조체이다. DomainModel은 이름(name)과 그 상태 도메인을 가질것이고, ProductModel은 제품이름(product name), 제품평점(product rating), 제품로고(product logo), 제품가격(product price)을 가진다.
struct Product {
    var name: String

    var rating: Double

    var price: Double?
}

뷰모델(View Models)
모든 데이터 모델은 해당되는 뷰모델을 가진다. 그 말은, 우리 예제에서는 DomainViewModelProductViewModel을 가진다는 뜻이다. 뷰모델은 모델로부터 데이터를 받아서 사용자에게 보여주기전에 뷰에 적용시킨다. 예를들어 ProductViewModel4.99 가격의 부동소수점을 받아서 $4.99라 읽히는 문자열로 변형한다.
class ProductViewModel: CellRepresentable {
    var product: Product

    var rowHeight: CGFloat = 80

    var price: String {
        guard let price = product.price else {
            return "free"
        }

        return "$\(price)"
    }

    init(product: Product) {
        self.product = product
    }

    func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
        // Dequeue a cell

        let cell = tableView.dequeueReusableCell(withIdentifier: "ProductCell", for: indexPath) as! ProductTableViewCell


        // Pass ourselves (the view model) to setup the cell

        cell.setup(vm: self)

        // Return the cell

        return cell
    }
}

뷰(Views)
우리 예제에서 뷰는 두가지 UITableViewCell이다. DomainTableViewCellProductTableViewCell를 가진다. 레이아웃은 앱의 스토리보드에 만들어놓았따. 두 클래스 모두 간단한데, 뷰모델을 인자로 받는 setup 메소드 하나만 가지고 있다. 뷰모델은 셀에 정보를 옮길때 사용되는데, 예를들자면 읽을 수 있는 가격($4.99)을 받아서 UILabel의 테스트 프로퍼티에 할당한다.
class ProductTableViewCell: UITableViewCell {
    func setup(vm: ProductViewModel) {
        self.textLabel?.text = vm.product.name
        self.detailTextLabel?.text = vm.price
    }
}

합쳐보기
3가지 큰 기둥을 만들었으니 합쳐보자. 뷰 컨트롤러와 뷰모델을 합치기위해 프로토콜을 사용할 것이다. 프로토콜은 이것을 따르는 클래스나 구조체가 어떤 변수와 메소드를 가질지 정의한다. 계약서를 생각해보자. 여러분이 X라는 프로토콜을 따르고 싶다면, 여기에 명시된 모든것을 구현해야한다. 간결하게 만들기위해 한 프로퍼티와 한 메소드만 넣어놨다. DomainViewModelProductViewModel 둘 다 이 프로토콜을 따른다.
protocol CellRepresentable {
    var rowHeight: CGFloat { get }
    func cellInstance(_ tableView: UITableView, indexPath: IndexPath) -> UITableViewCell

}
스위프트에서 프로토콜은 일급 객체(first class citizen)이므로 SearchResultsViewController 파일은 화면에 표시할때 필요한 뷰모델 배열을 가진다. [DomainViewModel]()이나 [ProductViewModel]()처럼 배열을 초기화하는것 대신, 프로토콜을 사용하여 뷰모델을 담아둘 수 있다. var data = [CellRepresentable](). DomainViewModelProductViewModelCellRepresentable을 따르기 때문에 배열은 둘 다 담아둘 수 있다.

이제 배열에 있는 모든 요소를 CellRepresentable을 따르게하여 UITableViewCell을 반환하는 cellInstance(_ tableView: UITableView, indexPath: IndexPath) 메소드를 가진다고 확신하게 만들자. 고맙게도 tableView:cellForRowAtIndexPath:cellInstance 메소드만 호출하면 된다.
extension SearchresultsViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return data[indexPath.row].cellInstance(tableView, indexPath: indexPath)
    }
}

extension SearchresultsViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return data[indexPath.row].rowHeight
    }
}
이게 전부다. 우리는 다양한 셀의 다양한 열 높이로 표시해주는 작은 뷰컨트롤러를 가지게 되었다! ISL의 깃헙 페이지에서 데모 프로젝트를 확인해볼 수 있다. 제안이나 질문이 있다면 주저하지말고 @thomasdegry에 트윗해달라.



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

으로 보내주시면 됩니다.


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

,
제목: Optimizing your Swift Codebase with Attirbutes

편집자의 노트: 우리 커뮤니티 블로그 포스트 시리즈는 buddybuild 유저와 훌륭한 모바일 개발자들에의해 작성되었습니다. 이 포스트는 Jordan Morgan에의해 작성되었는데, Buffer의 iOS 개발자이다.

스위프트 + 속성들
모든 iOS 개발자들이 모두 여기에 있을거라 생각한다: 우리는 그냥 프로그래밍을 고르거나 어떤것이 이색적인 새로운 언어를 배우기 시작했다. 그리고 몇 코드를 만날 수 있다. 우리가 이해할 수 없을 수도 있지만, 이것이 동작한다고 생각한다. 따라서 그것에대해 좋은 확신을 가지고 계속하면 나아지지 않는다.

이 과정은 정확히 내가 어떻게 내 코드베이스에 스위프트 속성을 넣는것을 시작할수 있는지에 관한 이야기이다. 스위프트는 탄탄하고 다양한 속성들을 지원하는데, 깃헙 저장소를 검색해볼때 우리가 모르는 한두개를 볼 수 있을지도 모르겠다. 나의 경험과 비슷하다면 나중에 구글에 적어두면 당신만의 방법이 된다.

왜 스위프트 속성들을 이용할까?
속성은 우리 코드베이스의 코드 질을 희생시키지 않은채 효율성을 증진시키는데 도움을 준다. 그 코드는 더 읽기 쉬워지고, 컴파일하기 쉬워지고, 궁극적으로 유지보수하기 쉽고 사용하기에 더 안전하게 된다.

개인적인 프로젝트에서나 Buffer에서나 효율성은 항상 나의 iOS 개발의 중심에 있다. 이 포스트는 스위프트 개발자로서 내 효율성을 증진시킬 수 있는 몇가지 스위프트 속성들을 열거한다. 안으로 들어가보자...

기초
수많은 정의를 보기 전에, 속성이 정확하게 무엇인지 빠르게 짚고 넘어가야한다고 생각한다. 그리고 매우, 모든 스위프트의 속성은 타입이나 선언에대한 더맣은 정보를 준다. 이 정보는 어떤것이 메모리에서 어떻게 다뤄질지 컴파일러 경고로부터 모든것을 지시할 수 있다.

어떻게 생겼든, 모두 "@" 표시를 앞에 붙인다. 또한, 속성 선언은 파라미터로 감싸진것 안에 인자를 받을 수 있다.

여기 간단한 것이 있다.
@attributeName

//Or with arguments…
@attributeName(arguments)
아래 몇가지 예시를 보자.

속성들
@available(args) 속성
스위프트 속성중에 맥가이버칼은 @available()이다. 강력한만큼 유연하고, 매번 API를 관리하고 배포하는 입장이라면 반드시 필요한 것임을 발견할 수 있다. 이것으로는 API 네이밍이 바뀌었다는 것을 지시할 수 있고, 가능한 플랫폼을 지시하는 등을 할 수 있다.

블로그 포스트를 올리는 은유적인 API로부터 나온 오브젝트를 생각해보자.
class BasicPost {}
우리 API를 쓰는 사람들은 오랫동안 BasicPost 클래스를 즐겨 사용한다. 그러나 기술 블로그 포스트를 표현하는 오브젝트에서 더 원하는 몇몇 요청을 처리한다고 생각해보자. 여러분이 읽고 있는 것과 대체로 같다. 그러니 버전1.2에서 이것을 소개하였다.
class TechnicalPost {}
이제 우리 문서를 완성하기위해서,  이것이 존재한다는것을 알리기위해 @available() 이점을 취함으로서 API 사용자에게 정보를 제공하고 코드는 분별력있게된다.
@available(*, introduced: 1.2)
class TechnicalPost {}
이 특정 속성은 몇몇 이자를 받을 수 있지만, 첫번째 인자는 항상 의도하는 플랫폼을 나타낸다. 나머지 인자들은 그 순서에따라 공급될수 있게 지원한다.

와일드카드의 이점을 얻을 수도 있다. 이 경우, 와일드카드는 첫번째 인자로 들어가있는 별표이다. 이것은 사용하는 모든 플랫폼의 API에서 소통한다. 이 클래스는 버전1.2에서 처음 소개되었다(두번째 인자가 설명하는 바).

깔끔하지만, 꽤 길다. 고맙게도 더 단축된 문법으로 초점을 맞출 수 있다.
@available(iOS 10.0, macOS 10.12)
class TechnicalPost {}
더 낫다! 이 속성에대해 잘 알지 못하더라도, 이제 우리가 만들어낸 API는 이 클래스가 사용될때 명확하게 볼 수 있다.

그러나 그대로 두면 컴파일타임 에러가 나올 것이다. 왜일까?

애플은 새로운 플랫폼을 소개하는 경향이 있는데, 우리는 우리 코드에 이것을 설명해야한다. 그러기위해서 이 코드는 제공된 플랫폼과 다른 잠재적인 미래의 플랫폼에 사용가능하다는 마지막 인자로서 와일드카드를 넣는다.
@available(iOS 10.0, macOS 10.12, *)
class TechnicalPost {}
이 방법은 여러분의 코드가 다음 애플의 계획에대해 이미 준비해 놓았다는 뜻이다. For now though, 애플은 오늘날에 존재하는 각 플랫폼을 표현한 것들을 나열하여 제공하고 있따.
  • iOS
  • iOSApplicationExtension
  • macOS
  • macOSApplicationExtension
  • watchOS
  • watchOSApplicationExtension
  • tvOS
  • tvOSApplicationExtension

다음 속성으로 가보기 전에, 다른 공통성을 생각해보자. 최근의 변경에서, 우리 API가 쓰지않게 되었고, 세상에있는 iOS 개발자들은 기술 블로그 포스트를 표현하는 사랑스러운 약간의 JSON으로 제공하도록 책임을 맡게되었다.

이처럼, 원래 클래스는 더이상 필요없게 되었다. 그리고 디프리케이트시킬 시간이 된 것이다.
@available(*, deprecated: 1.3)
class BasicPost {}
한 스위프트에서 모든 플랫폼에서 BasicPost를 디프리케이스 시켜, 와일드카드 인자가 유용해질 수 있게 명확해졌다.

게다가 이것으로 계속하면서 조금씩 리팩토링 하고 싶으면, API 네이밍 변경사항을 알도록 제공해야 할것이다. 내가 애플로부터 발견한 기술의 예의는, ㅈ사용자들이 사용하기 더 쉽게 만들기 위해 사용불가능한 인자와 타입에일리어스로 짝을 지을 수 있다는 것이다.
//From an earlier API version
class BasicPost {}

//From a new API version, where we renamed it for whatever reason
class BaseTechnicalPost {}

@available(*, unavailable, renamed: “BaseTechnicalPost”)
typealias BasicPost = BaseTechnicalPost
개인적으로 이것을 좋아하는데, 효율성 관점에서나 비용적 관점에서, 가장 명확한 코드는 항상 최고의 코드이다.

이 속성은 워닝, 에러 등을 연결하여 보다 트릭적인 메시지를 제공한다(여기서 '트릭적인'은 옛 코드를 인자로 지정하는 것을 의미함).

@discardableResult 속성
성숙하고 물려받은 코드베이스에서 작업하고 있다면, 몇몇 함수가 약간 장황하게 되있는것이 놀랍지 않을 수 있다.

아마 90년대 초반부터 만들어진 기능이 지금까지 계속 추가되고 바뀐 코드를 물려받았으며 기능 가체에 결함이 없었을 것이다. (여기엔 소프트웨어가 켜질때 일어나는 124가지의 중요한 작업들이 있을 수 있다. 데이터베이스 접근, 케스 설정, 프로세스에서 몇몇 사인을 초기화하기 - 시나리오는 끝도 없다)

그 전까지, 당신이 돌아와서 필요한 리팩토링을 할 수 있게 프로덕트 매니저를 설득하고 있다.

그렇다. 그런가?! 그렇다.
let someUnusedVarBecauseIHaveToCallThisOldInsaneFunction = anOldInsaneFunction()
그러나 이상한 커플링 이유를 위해 이 함수를 실행하게 되버려서 clang은 이것들을 더 컴파일할 것이다. 이제 우리는 이것을 위해 보여주기위해 사용하지 않는 변수도 가진다. 아래로 내려가면서 이야기해보자.

필요 없을것같은 함수의 결과를 컴파일러에게 말하여 @discardableResult 속성이 도움이 될 수 있다. 컴파일타임에 경고도 없애준다.
@discardableResult func anOldInsaneFunction() -> String{
   //Bunch of business logic occurs
   return “”
}
이제 불러진 함수를 호출한 저 코드는 과거의 소프트웨어 공학적 실수처럼만 남아있을 것이다. 그러나 에러 없이 되고 있을 것이다.

명확성을 추가하기위해 간단하게 _를 할당하여 좀 더 명확한 상태로 만들 수 있다.
_ = anOldInsaneFunction()
때론 소프트웨어 개발에선 우리가 직접 제어하거나 고칠 수 없는 함수나 아키텍처가 있으며, 이 속성이 그런 상황을 조금이라도 더 낫게 해준다.

@autoclosure 속성
@autoclosure는 여러분의 코드베이스에 손쉽게 명확성을 추가할 수 있는 또다른 속성이다. 이것은 인자로 공급된 클로저를 자동으로 감싼다. 클로저가 아무 인자도 스스로 받으면 이것으로 감싸인 표현식의 실제 값을 반환할 것이다.

보기에 좀 혼란스러울 수 있는데, 한번 만나보면 쉽게 이해된다. 고수준 관점에서, 자동으로 클로저로 만들기위해 표현식을 얻는 능력에대해 이야기하고 있다. 여러분이 만약 프로젝트에 유닛테스트를 넣으려 해보았다면, 이미 이 속성들을 몇번 만나보았을지도 모르겠다.

아래처럼 클래스에 간단한 테스트를 넣고 싶다고 가정해보자.
class Programmer  {
   var pay:Int
   init(withPay pay:Int)
   {
       self.pay = pay
   }

   func applyRaise(by amount:Int)
   {
       self.pay += amount
   }
}

class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000
       let expectedSalaryPostRaise = devsPay + raiseAmount

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(expectedSalaryPostRaise, aDev.pay, "Unexpected salary after raise was applied.")
    }
}
XCAssertEqual의 앞에 두 파라미터는 둘다 클로저인데, 이 클로저는 제네릭 표현식으로 받는다. 함수의 시그니처가 약간 위협적으로 보이지만, 처음 두 파라미터는 @autoclosure의 이점을 취하였다는 것을 인지하자.
func XCTAssertEqual<T>(_ expression1: @autoclosure () throws -> T?, _ expression2: @autoclosure () throws -> T?, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line) where T : Equatable
@autoclosure 속성이 제공되었기 때문에, 함수를 호출하는 것이 꽤 가독성 좋고 우리에게도 쉬워졌다. 클로저를 값처럼 무언가를 간단하게 전달하거나(이전 예제에서 했던 것처럼) 약간의 로직을 더 전달할 수 있고, 각각은 가볍게 사용할 수 있다.
class ProgrammerTests: XCTestCase
{
   func testPayRaise()
   {
       let devsPay = 50000
       let raiseAmount = 25000

       let aDev = Programmer(withPay: devsPay)
       aDev.applyRaise(by: raiseAmount)

       XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
   }
}
첫번째 인자가 제공될때, 클로저라기보단 추가적인 연산처럼 읽힌다.
XCTAssertEqual(aDev.pay + raiseAmount, 750000, "Unexpected salary after raise was applied.")
반대로 @autoclosure 속성이 없을때는 어떻게 생겼을지 보자.
XCTAssertEqual({
     return aDev.pay + raiseAmount
,}, {
   return 75000}
, "Unexpected salary after raise was applied.")
여러분도 볼 수 있듯, (문법대로) 완전한 클로저를 전달한다. 읽고 쓰기에 좀 많은 편이다. 하나가 마지막 인자로 후행 클로저(trailing closure)를 사용할 수 없으면, 복합적인 문제가 될 수 있다.

@autoclosure를 사용하면 그 안에 랩핑된 실제값을 반환한다. 아마 자동으로 파라미터가 클로저가 된다고까지 말할 수 있으므로... @autoclosure이다!

이 코드는 상속적으로도 지연된다. 실제 클로저가 결국 무거운 작업을 하고 있거나 의도치않은 사이트 이팩트를 낳았다면 추가적인 이점이 있다. 제공된 코드는 안에 감싸인 클로저가 되기 전까지 절때 실행되지 않는다.

더 나아가서, 여러분의 최근 iOS 작업물의 어디에서 보았을까? assert()는 어떨까?
struct Programmer
{
   var isSenior:Bool
   var appsShipped:Int
}

let aSeniorDev = Programmer(isSenior: true, appsShipped: 13)

assert(aSeniorDev.isSenior, “This dev isn’t a senior!”)
제공된 첫번째 인자는 @autoclosure를 사용한다. 그렇지 않았다면 이렇게 호출해야 했을 것이다.
assert({
     return aSeniorDev.isSenior
}, {
    return “This dev isn’t a senior!”
})
@autoclosure를 사용하면, 코드는 작성할때 좀 더 쉬워지고, 즐겁게 코드를 읽게 만드는 경험을 제공한다고도 말할 수 있겠다.

그리고 assert() 시그니처가 어떻게 생겼는지 궁금하다면, 이렇게 생겼을 것이다.
func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = default, file: StaticString = file, line: UInt = line)
우리는 이 시그니처에서 두가지 파라미터로부터 벗어나면, 여기에 할당된 디폴트 값이 있기 때문에 예제에서 이것들을 생략한다. 여러분도 알다시피 그렇지 않을까?

여러 속성사용하기
각 속성은 그들이 가지고 있는 것으로 도와줄 수 있는 이점을 가지지만, 어떤 시나리오에서는 이것을 같이 사용하는 것도 도움이 된다.

예를들어, @escaping 속성을 보자. 이 @escaping 속성은 클로저에 들어온 것이 보냈던 함수보다 오래 살 수 있게 해준다.
//A property on a view controller
var onFakeCompletions:[()->()] = []

func fakeNetworkOp(_ completion:@escaping ()->())

{
   //Network stuff happens
   //The closure is appended to an external array outside of the function's scope. This implies it could be invoked outside of the function - i.e., it could "escape" it
   onFakeCompletions.append(completion)
}
이것을 고려하여 같은 파라미티터에 @escaping@autoclosure를 둘다 쓸 수 있다. 예제로 인사부(H.R.)를 생각해보자.  예를들어 임금 인상을위해 직급이 "Senio"인 개발자이면서 적어도 세개의 앱을 출시한 개발자가 누구인지 알려주는 인사부(H.R.)를 생각해보자. 이 개발자는 이력적인 이유로 각각의 평가를 계속 추적해야한다(As an example, let's imagine H.R. let us know that any developer who is both a "Senior" in title and has shipped at least three apps is due for a raise, but we also need to keep track of each evaluation for historical purposes).
class Programmer
{
   var previousPayRaiseEvaluations:[()->Bool] = []
   var isSenior:Bool = false
   var appsShipped:Int = 0

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true
aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1

   func evaluatePayRaise(withAccolades raiseEvaluation:@escaping @autoclosure ()->Bool)
   {
       if raiseEvaluation()
       {
           //Give them a raise, and then save it to their records
           previousPayRaiseEvaluations.append(raiseEvaluation)
       }
   }
}

let aProgrammer = Programmer()
aProgrammer.isSenior = true

aProgrammer.appsShipped = 4

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //0

aProgrammer.evaluatePayRaise(withAccolades: aProgrammer.isSenior && aProgrammer.appsShipped > 3)

print("Past pay raise evaluations: \(aProgrammer.previousPayRaiseEvaluations.count)") //1
확실히 속성들을 함께 "채이닝(chaining)"하는 것을 금지하지 않는다. 그리고 그 상황을 위해 호출할때 꽤 매끄럽게 동작한다.

마지막 생각
속성은 항상 내 코드에서 사용에대해 특별히 열정적으로 해왔던 것이다. 나는 명확하고 정확하고 간단한 방법으로 강력하게 무거운것을 들어올리는 아이디어를 좋아한다. 그리고 이것이 정말로 속성이 하는 일이다. 물론 스위프트와 Objective-C 프로젝트 어디에나 있는 @objc 속성같은 알만한 가치가 있는 것들도 있다.

이것을 고려하여, 한 인자는 속성이 nicety 보다는 necessity에 더 가깝게 만들어질 수 있다. 끝으로 개발자로서 이 모든것들은 여러분의 작업 플로우의 최적화에대한 것이다. 여러분의 코드베이스나 다른 곳에 이렇게 만들어라. 속성은 이런 최적화를 달성하기위한 하나의 방법에 불가하다.

앱이 완성되고나면, buddybuild 같은 서비스를 사용하여 여러분의 지속적 통합(CI, Countinuous Integration)과 배포 과정같은 부담되는 일을 자동화할 수 있겠다.

Buffer에서는 이미 우리 개발 작업 플로우를 최적화하기위한 방법을 항상 찾아보고있고, 이것들은 우리를 도와줄 수 있는 방법 중 우리가 찾은 것이다.


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

으로 보내주시면 됩니다.



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

,
제목: On Comments

코드를 작성할때 가장 큰 적은 복잡성이다. 추상화 수준(level of abstraction)을 유지하고, 코드베이스에서 작업하는 개발자들은 그 개념으로 동작하게 하는 것은 큰 소프트웨어 프로젝트에서 필수적이다.

주석는 복잡성을 유지보수하는 도구이기도하지만, 때로는 돕는게 아니라 미묘하게 코드베이스를 상하게 만들기도 한다.

주석의 내 견해는 두가지로부터 온다. 1) 주석은 컴파일하지 않는다. 2) 주석은 연하게 문법 하이라이트된다. 이것은 컴파일 되지 않고 시아에서 흐릿하게 보이기 때문에 코드에 변경사항이 생길때 무시되기 쉽다. 코드는 변경했는데 주석은 바뀌지 않는다면, 코드의 내용을 정확하게 반영하지 않은 주석과 함께해야한다.

이상적인 세상에서는 코드 리뷰때 이것을 발견할 수도 있지만, 실제로는 절때 발견하지 못할때도 있다. 코드 리뷰 툴이 문법 하이라이트 기능을 가지고 있더라도 주석은 그 문맥에서 백그라운드로 사라지게된다. 또한, 코드 리뷰는 변경된 부분의 주변만 보여주기 때문에, 오래된 주석은 그 자체가 변경될 쯤에만 발견될 수 있을 것이다. 만약 메소드의 전제조건을 변경하여 한 라인을 바꿨다면, 리뷰어는 주석을 보지 못한채로 바꿔라고 말하지 못할 것이다.

일반적인 주석을 피하는 몇가지 방법이 있다. 그 중 몇개는 a blog post by an old manager of mine에서 다루고, 나머지는 Andrew에서 나온 것인데, 루비에서 나온 것이므로 다이나믹 타입 언어에대한 이야기여서, 적용시킬 수 없다.

  1. 이름을 잘 정하자! 주석을 피하는 첫번째 방법은 다음과 같다. 한 단어 이름, 추상적인 이름, 모호한 이름을 피하자. 여러분이 할 수 있는 만큼 더 정확하게 이름을 정할수록 주석을 달 필요가 줄어든다.
  2. 메소드에 전제조건이 있으면 유효하지 않은 값이 들어올때 앱이 크래쉬내는 assertion를 넣자(항상 그러지는 말고 디버깅때만!). 만약 양수지만 0이아닌 정수만 받는다면 코드에 precondition(int>0)으로 작성할 수 있다.
  3. 런타인 assertion보다 나은 것이 컴파일타임의 것이다. 만약 메소드가 비어있지 않는 배열만 받는다면 precondition(!array.isEmpty)를 쓸 수 있다. 그러나 절때 빈 배열이 되지 않는 타입을 사용하는 것도 하나의 방법이다. 여러분의 API의 사용자들은 이제 절때 파라미터에 빈 배열을 보낼 수 없을 것이다.같은 성질에서, 두 이름을 case를 가진 열거형으로 표현된 것보다 더 잘 표현된 불(bool) 파리미터를 가져본 적이 있는가? 다른 열거형처럼 여러분의 옵셔널이 더 잘 표현되는가?  여러분의 의도를 네이밍에 드러내라.
  4. 임시 코드, 프로토타입 코드에 표시를 하자. 나는 종종 이 함수가 이상적으로 짜지지 않았으면 hack_를 표시한다. 스위프트에서 메소드의 언더바는 내 코드베이스를 좋지않게 만들어 괴롭히므로 보기에 거슬린다.그리고 이것을 상기시켜 고치게 만든다. 우리는 최근에 shouldReallyBeInTheCoordinator_를 접두에 붙인 함수를 만들었는데, 코드리뷰를 받아야야할때 코드가 올바른 클래스에 있지 않았기 때문이다. 올바르지 않은 코드가 거슬리게 생긴다면, 코드베이스 요구와 여러분의 감정이 드러맞게 된다. 또다른 좋은 접두에는 perf_temp_ 같은 것들이 있다.
  5. Mark Sands에 의하면, ID를 버그 추적기에서 메소드 이름으로 인코드 할 수 있고, 이것은 스택 트레이스(stack traces)에서 나타날 것이다. UIKit은 몇 케이스에서 레이더 넘버(Radar Number)를 참조한다. 이것은 현실이다.
    -[UIViewController _hackFor11408026_beginAppearanceTransition:animated:]
  6. 함수의 이름이 "왜" 이것인지 설명하는 것을 두려워하지마라. updateFrameOnNextTickBecauseAutoLayoutHasntCompletedYet(frame: CGRect)처럼 함수를 만들 수 있다. 컴파일러는 메소드 길이를 신경쓰지 않으며 코드는 작성하는 수보다 읽히는 횟수가 더 많다. 주석은 단지 단어들이며 메소드 이름이다. 그 코드베이스의 미래 유지보수 담당자는 여러분의 장황한 설명에 감사할 것이다.
  7. TODO(date: Date, message: String) 같은 보조함수(helper function)을 만들어라. 여기서 TODO는 어떤 날짜를 정하지 않으면 에러를 출력한다. (혹은 디버깅에서 크래쉬를 내는게 더 나을 수도 있다) Jordan Rose의 또다른 예제이다.
  8. 테스트에 어떤 알고리즘 요구사항을 인코드하라. 만약 위의 모든것이 실패하고, 특정 문제를 풀기에 precondition, 타입, 메소드이름에 의존할 수 없다면, 테스트를 작성하라. 이것은 특히 엣지 케이스에 좋다. 누군가 코드를 다시 작성했는데, 테스트에 실패한다면 새로운 코드안에 다뤄줘야할 케스이가 있다는 사실을 알게될 것이다.
기억하기: 받아드리기 힘든 코드를 작성하거나 주석이 없는 코드는 변명할 수 없다! 주석을 스킵한 코드는 반드시 명확해야한다. 결국에는 나를 위한 주석이다. 다음 프로그래머에게 내 의도를 표현할 수 있는 또다른 방법을 찾을 수 있다면 주석을 달지 않을 것이다.

더 읽을거리


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

으로 보내주시면 됩니다.



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

,
제목: Swift: UserDefaults Protocol


스위프트3은 언어뿐만아니라 우리 코드베이스까지도 쓰나미같은 변경이 생겼는데, 이 글을 읽는 몇몇은 아직도 마이그레이션과 투쟁하고 있을지 모르겠다. 그러나 이런 모든 변경에도 stringly typed의 Foundation으로된 몇몇 API들이 남을 것인데, 이는 꽤 괜찮아 보이지만... 그렇지 않을 수도 있다.

이것은 일종의 애증관계인데, API에서 문자열의 유연성은 '애'이나, 그들이 가져오는 상속적인 이유때문에 이것을 사용해야 하는 것을 '증'한다. 신경쓰지 않으면 위험하게 작업하고 있는 것과 동일하다.

Foundation 프레임워크를 만드는 사람들은, 우리가 의도한대로 정확하게 미리 정의할 수 없게 해놓아서 우리는 stringly typed API를 쓸 수 있게 되었다. 그래서 그들의 모든 지혜로움, 능력, 지식으로 개발자로서 무한한 가능성으로 만들 수 있게 하려고 몇몇 API에서는 문자열을 사용하도록 해놓았다. 이것은 어둠의 비밀의 마법이다. (So in all their wisdom, power and knowledge, they decided to use strings in some of the APIs because of the unlimited possibilities it creates for us as developers. It’s either that or some type of dark arcane magic.)
(역자: stringly 타입의 API로 유연하게 사용할 수 있게 해놓은 장점을 말하는 중입니다.)

UserDefaults
오늘의 주제는 내가 iOS 개발을 하면서 배울때 처음으로 친숙해진 API 중 하나이다. 이것이 익숙하지 않는 사람들을 위해 설명하자면 UserDefaults는 한 이미지나 어플리케이션 세팅같은 작은 정보를 저장하지위한 간단한 영속 데이터 저장소이다. 어떤 사람들은 이것을 "다이어트한 코어데이터"라고 생각하기도하지만, 사람들이 그것의 대체물로 만드려 아무리 노력해도 이것은 견고하지 않다.

Stringly Typed API
UserDefaults.standard.set(true, forKey: “isUserLoggedIn”)
UserDefaults.standard.bool(forKey: "isUserLoggedIn")
일반적으로 앱에서 UserDefaults는 앱의 어디에서라도 값을 간단하게 영속적으로 저장(set)하고, 검색(retrieve)하며, 덮어쓰거나(override), 제거하는(remove) 할 수 있다. 그러나 조심하지 않으면 균일성이나 문맥이 없는채로 해볼 순 있겠지만 오타를 칠 가능성이 높아질 것이다. 이 포스팅에서는 UserDefaults의 일반적인 특징을 변형하여 커스터마이징 할 것이다.

상수를 이용하기
let key = "isUserLoggedIn"

UserDefaults.standard.set(true, forKey: key)

UserDefaults.standard.bool(forKey: key)
이런 이상한 트릭을 따라하면 일시적으로는 더 나은 코드를 작성할 수 있을 것이다. 문자열을 한번 이상 쓴다면 상수로 바꿔서 쓰는 규칙을 적용시켜보자. 아마 나에게 고마워 할지도 모르겠다.

그룹 상수
struct Constants {
    let isUserLoggedIn = "isUserLoggedIn"
}
...
UserDefaults.standard
   .set(true, forKey: Constants().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants().isUserLoggedIn)
균일성을 유지하기에 더 도움이 되는 방법은, 한곳에 중요한 디폴트 상수를 모아놓는 것이다. 그래서 여기에 디폴트를 저장하고 참조할 수 있는 Constants 구조체를 만들었다.

또다른 좋은 팁에는, 디폴트로 작업할 때 특히 프로퍼티 이름에 그 값을 반영해놓는 것이다. 이렇게하면 코드를 단순화 시켜주고 전반적인 속성을 더 균일화시켜줄 것이다. 프로퍼티 이름을 복사하고 문자열 안에 붙여넣으면 타이핑을 줄일 수 있을 것이다.
let isUserLoggedIn = "isUserLoggedIn"

문맥 추가하기
struct Constants {
    struct Account

        let isUserLoggedIn = "isUserLoggedIn"
    }
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account().isUserLoggedIn)
단지 Constants 구조체를 가지는 것만으로도 괜찮겠지만, 코드를 작성할 때 문맥을 제공해야함을 잊어서는 안된다. 여러분 자신을 포함한)함께 작업할 누군가에게 더 읽기 좋은 코드를 만들어야함을 목표로 하는것이 좋다.
Constants().token // Huh?
token의 의미가 무엇일까? 문맥에서 네임스페이스가 없기때문에, 이 코드베이스에 익숙하지 않은 누군가(혹은 미래의 이 코드를 관리하는 사람)가 token이 무엇인지 알아내려할때 고생할 것이다.
Constants.Authentication().token // better

초기화 피하기
struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }

    private init() { }
}
절때로 우리가 의도하지도 않았고 우리 Constants 구조체를 초기화시키고 싶지 않기 때문에, 생성자는 private로 선언되어야한다. 이거은 좀 더 예방적인 단계이지만 계속 추진하고 있는 방법이다. 최소한 적어도 static만 원할때도 실수로 인스턴스 프로퍼티를 선언하는 것을 막아줄 것이다. 그러나 static에 관해 말하자면... 다음을 보자.

static 변수들
struct Constants {
    struct Account
        static let isUserLoggedIn = "isUserLoggedIn"
    }
    ...
}
...

UserDefaults.standard
  .set(true, forKey: Constants.Account.isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account.isUserLoggedIn)
키에 접근할때마다 주의해야하는 것이 있는데, 접근할때마다 이것이 속한 구조체를 초기화해야할 수 있다. 그러지말고 static 선언을 사용하면 한번만 초기화한다.

구조체를 저장 타입으로 정했기 때문에 class대신 static을 사용한다. 스위프트 컴파일러 법에 따르면 구조체는 class 프로퍼티 정의를 사용할 수 없다고 한다. 또한 class 프로퍼티에 static 선언을 사용하면 그 프로퍼티는 final class로 선언한 것과 같다.
final class name: String

static name: String
// final class == static

열거형 케이스로 더 적게 타이핑하기
enum Constants {
    enum Account : String {
        case isUserLoggedIn
    }
    ...
}
...

UserDefaults.standard
    .set(true, forKey: Constants.Account.isUserLoggedIn.rawValue)
UserDefaults.standard
    .bool(forKey: Constants.Account.isUserLoggedIn.rawValue)
이 포스트의 초반부에서 말했듯, 균일성을 위해 프로퍼티는 그 값을 반영해야한다고 했었다. 여기에 static let 대신 enum case를 써서 그 과정을 자동화하여 한걸을 더 나가볼 것이다.

여러분도 인지했듯, 우리는 String을 따르는 Account열거형을 만들었는데, 이것은 RawRepresentable 프로토콜을 따른다. 이렇게 한 이유는, case를 위한 rawValue를 제공하지 않으려면 디폴트로 케이스가 반영될 것이기 때문에 이 작업을 한다. 우리가 해야할 타이핑이나 복사/붙여넣기를 줄이면 더 편해질 것이다.
// Constants.Account.isUserLoggedIn.rawValue == "isUserLoggedIn"

위에는 지금까지 UserDefaults로 꽤 괜찮은 것들을 달성했지만, 멋진것을 했다고 하기엔 좀 부족해 보인다. 가장 큰 문제는 문자열을 입고 있을지라도 여전히 stringly typed API로 작업하고 있어서 여전히 우리 프로젝트에 문제가 생길 수 있다는 점이다.

우리는 주어진 것으로만 작업할 수 있다는 마음을 가진다. 스위프트는 아주 많이 멋진 언어이고, 우리가 배워왔던, 그리고 Objective-C를 작성해가면서 알고 있는 많은 것들을 도전해볼 수 있다. 이 API에 문법 슈거를 만들어보자.

API 목표
UserDefaults.standard.set(true, forKey: .isUserLoggedIn)
// APIGoals
남은 이야기에서는 일반적인 populus 대신, UserDefaults와 소통할때 더 괜찮게 작업할 수 있는 API를 우리 필요에 맞게 만들어보려 할 것이다. and what better way than to do so than making extensions with protocols.

BoolUserDefaultable
protocol BoolUserDefaultable {
    associatedType BoolDefaultKey : RawRepresentable
}
불리언 UserDefaults를 위한 프로토콜을 만들면서 시작해보자. 변수나 함수가 없는 간단한 프로토콜이다. 그러나 RawRepresentable을 따르는 BoolDefaultKey라는 associatedType을 지원하는데 왜 이렇게 했는지는 바로 다음에 이해할 수 있을 것이다.

익스텐션
extension BoolUserDefaultable
    where BoolDefaultKey.RawValue == String { ... }
만약 Crusty's Laws 프로토콜을 따르려는 계획이라면 프로토콜 익스텐션을 선언할 수 있다. 그러나 associatedTyperawValueString 타입이라는 익스텐션에만 제약하는 where절을 적용시켰다.
모든 프로토콜로, 동등하고 해당되는 프로토콜 익스텐션이 있다 - Crusty's Third Law
With every protocol, there is an equal and corresponding protocol extension

UserDefaults 세터
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
}
그렇다. 이것은 표준 UserDefaults를 감싼 간단한 API이다. 이렇게 하는 이유는 Key-Path로된 문자열을 보내는것 보다 간략한 enum case를 보내는게 가독성면에서 더 좋기 때문이다.
UserDefaults.set(false,
    forKey: Aint.Nobody.Got.Time.For.this.rawValue)

프로토콜 따르기
extension UserDefaults : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
}
우리는 BoolDefaultable을 따르게 하기 위해 UserDefaults를 익스텐션하고 RawRepresentable (String)을 따르는 BoolDefaultKey라는 연관타입을 지원했다.
// Setter

UserDefaults.set(true, forKey: .isUserLoggedIn)

// Getter

UserDefaults.bool(forKey: .isUserLoggedIn)
다시 말하자면, 작업하는 표준에 우리것을 정의하는 것 대신 우리가 지원한 API로 도전하는 중이다. UserDefaults를 익스텐션하면서 우리 API와함께 문맥을 잃어버리기 때문이다. 만약 .isUserLoggedIn 말고 다른 키 였다면 무엇과 관련되었는지 이해할 수 있었을까?
UserDefaults.set(true, forKey: .isAccepted)
// Huh? isAccepted for what?
이 키는 매우 모호해서, 모든 범주의 어떤것이든 될 수 있다. 이것처럼 보이지 않더라도 문맥을 제공하는 것은 항상 유익할 것이다.
필요하지만 가지지 못한것 보다는, 필요없더라도 가지고 있는 편이 낫다.
어렵게 생각하지 말자. 문맥을 추가하는 것은 쉬운 일이다. 간단하게 키를 위한 네임스페이스를 만든다. 이 경우, isUserLoggedIn 키가 있는 곳인 Account 네임스페이스를 만들었다.
struct Account : BoolUserDefaultable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn

    }
    ...
}
...
Account.set(true, forKey: .isUserLoggedIn)

충돌
let account = Account.BoolDefaultKey.isUserLoggedIn.rawValue
let default = UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue
// account == default
// "isUserLoggedIn" == "isUserLoggedIn"
같은 프로토콜을 따르고 같은 키 케이스를 제공하는 서로다른 두 타입을 가지는 것이 가능하다. 이걸 출시하기 전까지 해결하지 못하면 분명 이것이 새벽에 우리를 깨우는 버그가 될것이다. 다른 값을 바꾸는 키를 가지는 위험을 안고 갈 수 없다. 그러니 우리 키를 네임스페이스한 것으로 만들자.

네임스페이스로 만들기
protocol KeyNamespaceable { }
물론 우리는 스위프트 개발자니까 프로토콜을 만든다. 프로토콜은 우리가 직면한 문제를 풀때 제일 먼저 하게되는 시도일 것이다. 만약 프로토콜이 초콜릿 소스라면, 심지어 스테이크에까지도 어디든지 올려놓을 수 있다(역자: 왜하필 초콜릿 소스에 비유를 했는지.. 다목적 소스라면 역시 굴소스 아닌가요?). 이것이 우리가 프로토콜을 만들어가며 개발하는게 얼마나 좋은지 보여준다.
extension KeyNamespaceable {
    static func namespace<T>(_ key: T) -> String

    where T: RawRepresentable {
          return "\(Self.self).\(key.rawValue)"
    }
}
이 간단한 함수는 두 오젝트를 합친 문자열 보간법을 쓰고, 그 사이에 마침표로 구분했다. 클래스의 이름과 그 키의 rawValue이다. 이 함수는 RawRepresentable을 따르면 key 인자로 받을 수 있게 제네릭을 인자로 받도록 해놓았다.
// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = namespace(key)

    UserDefaults.standard.set(value, forKey: key)
}

static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = namespace(key)

    return UserDefaults.standard.bool(forKey: key)
}
...

let account = namespace(Account.BoolDefaultKey.isUserLoggedIn)

let default = namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)


// account != default

// "Account.isUserLoggedIn" != "UserDefaults.isUserLoggedIn"

문맥
우리가 이 프로토콜을 만들었기 때문에, UserDefaults API 사용으로부터 해방된 느낌을 받고, 아마 프로토콜의 힘에 취했을 것이다. 이렇게하여 우리은 키를 우리가 원하는 곳으로 옮겨 문맥을 만듦으로서 코드를 읽을때 이해할 수 있게 되었다.
Account.set(true, forKey: .isUserLoggedIn)
그러나 API가 완전히 이해되지 않게 문맥을 잃어버리기도 했다. 처음 보면 이 코드가, 불리언을 영속적으로 저장시키는지 아니면 UserDefaults에 넣는지에대한 아무런 정보도 주지 않는다. 따라서 모든 사이클을 보여주기위해 UserDefaults를 익스텐션하여 우리의 디폴트 타입을 그 안에 넣을 것이다.
extension UserDefaults {
    struct Account : BoolUserDefaultable { ... }
}
...

UserDefaults.Account.set(true, forKey: .isUserLoggedIn)
UserDefaults.Account.bool(forKey: .isUserLoggedIn)


NatashaTheRobot에게 감사의 말을 전한다. 9월에 try! Swift NYC에서 발표할 기회를 얻었었다. 내 발표가 녹화되어 Realm에서 이것을 남겨두었고 Speaker Deck에 슬라이드 자료가 있으니 확인해보자. 발표를 한 이례로 몇가지 배운점을 이 글에 반영했으며, 샘플코드는 Gist나 Playground
에 있다.


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

으로 보내주시면 됩니다.



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

,
제목: 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

,

제목: 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

,
제목: 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

,
제목: Keeping XCTest in sync on Linux

갱신이력:
2017.03.30 이 이슈에대해 프로세스를 추적하는 버그를 참조한 꼬릿말 추가
2017.03.30 코드 생성을 사용하는 솔루션을 대신해주는 appendix 를 추가
2017.03.31 생성된 파일의 타겟 패스를 명세하는 Sourcery 양식을 수정. 이것은 수공업의 리네이밍 단계를 절약해준다.

스위프트는 크로스-플랫폼이지만, 애플 플랫폼과 다른 OS에서 다르게 동작하는데, 주로 두가지 이유가 꼽힌다.
  • Objective-C 런타은 애플 플랫폼에서만 가능하다.
  • Foundation과 다른 core library들은 애플OS가 아닌 것의 구현이 따로 되어있다. 이 의미는 Foundation API는 macOS/iOS와 리눅스에서 다른 결과를 만들수도 있고, 혹은 그냥 아직 완전히 구현되지 않았을 수도 있다.
그러므로 라이브러리를 어떤 애플 플랫폼의 특정 기능에 의존하지 않게 짜면, macOS/iOS 그리고 리눅스의 코드를 테스트하기 좋은 전략이 된다.

리눅스에서 테스트 디스커버리
어떤 유닛 테스트 프레임워크는 실행해야 테스트를 찾을수 있게 해놓았따. 애플 플랫폼에는 XCTest 프레임워크가 있는데, 이것은 모든 테스트 수트와 테스트 타겟에있는 메소드를 돌기위해 Objective-C 런타임을 사용한다. Objective-C 런타임은 리눅스에서 사용할 수 없고 스위프트 런타임은 최근에 동등한 기능이 부족하기 때문에, 리눅스의 XCTest는 실행하고자하는 테스트 목록을 명시저으로 제공하도록 개발자들에게 요구한다.

allTests 프로퍼티
이 방식으로 실행하는 방법(스위프트 패키지 매니저에서 만들어진 컨밴션)은 여러분의 각 XCTestCase 서브클래스에 allTests라는 이름의 프로퍼티를 추가하는 방법이다. 이것은 테스트 함수와 그 이름들의 배열을 반환한다. 예를들어 한 테스트를 가지고있는 클래스는 아래처럼 생겼을 것이다.
// Tests/BananaKitTests/BananaTests.swift
import XCTest
import BananaKit
class BananaTests: XCTestCase {
   static var allTests = [
       ("testYellowBananaIsRipe", testYellowBananaIsRipe),
   ]
   func testYellowBananaIsRipe() {
       let banana = Banana(color: .yellow)
       XCTAssertTrue(banana.isRipe)
   }
}

LinuxMain.swift
이 패키지 매니저는 LinuxMain.swift라는 이름의 또다른 파일을 만드는데, 이것은 애플 플랫폼이 아닌 곳에서 동작시키는 테스트 실행자처럼 행동한다. 여기에는 XCTMain(_ :)을 호출하는데 이것은 모든 테스트 수트들의 리스트가 있다.
// Tests/LinuxMain.swift
import XCTest
@testable import BananaKitTests

XCTMain([
   testCase(BananaTests.allTests),
])

수작업의 유지보수는 잊어버리기 쉽다
이 방법은 두곳에서 수작업의 유지보수가 필요하기 때문에 분명 이상적이지 않다.
  1. 새로운 테스트를 추가할때마나 그 클래스의 allTests를 반드시 추가해야한다.
  2. 새로운 테스트 수트를 생성할때마다, LinuxMain.swift에서 XCTMain 호출을 반드시 추가해야한다.
이 두가지 단계 모두 잊어버리기 쉽다. Even worse, 불가피하게 그중 하나를 잊어버릴때, 뭔가 잘못되었음이 조금도 명확하지 않다. 테스트들이 리눅스에서는 통과할 것이고, macOS와 리눅스에서 실행되는 테스트 수를 손수 비교하지 않으면, 몇몇 테스트가 리눅스에서는 돌아가지 않는다는 점을 인지하지 못할수도 있다.

나에게는 이런일이 자주 발생했으므로 이것에대해 뭔가 조처를 취하기로 했다.

리눅스 테스트를 빠트리는 것에대해 보호하기
유지보수 단계중 잊어버릴때 자동으로 테스트 수트를 실패하게 만드는 매커니즘을 만들어보자. 각 XCTesetCase 클래스(와 그들의 allTest 배열)마다 아래 테스트를 추가할 것이다.
class BananaTests: XCTestCase {
   static var allTests = [
       ("testLinuxTestSuiteIncludesAllTests",
        testLinuxTestSuiteIncludesAllTests),
       // Your other tests here...
   ]

  func testLinuxTestSuiteIncludesAllTests() {
       if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
           let thisClass = type(of: self)
           let linuxCount = thisClass.allTests.count
           let darwinCount = Int(thisClass
               .defaultTestSuite().testCaseCount)
           XCTAssertEqual(linuxCount, darwinCount,
               "\(darwinCount - linuxCount) tests are missing from allTests")
       endif
   }
   // Your other tests here...
}
이 테스트는 Objective-C에의해 발견된 테스트 수와 allTest 배열에 항목 수를 비교하여 두 수가 일치하지 않음을 발견하면 실패를 띄워버릴 것이다. 정확히 우리가 원하던 것이다.

(Obj-C 런타임에서의 의존성이라는 의미는 애플 플랫폼에서만 동작하는 테스트라는 뜻이다. 리눅스에서는 컴파일되지 않을 것이고, #if os(macOS) ... 블럭으로 감싼 이유가 바로 그것이다)

allTests에 테스트를 추가하는 것을 잊어버렸을때 실패한 테스트
이것을 테스트 하기위해 다른 테스트를 추가하자, 이 테스트는 allTests 갱신을 깜빡한 경우이다.
import XCTest
import BananaKit

class BananaTests: XCTestCase {
   static var allTests = [
       ("testLinuxTestSuiteIncludesAllTests",
        testLinuxTestSuiteIncludesAllTests),
       ("testYellowBananaIsRipe", testYellowBananaIsRipe),
       // testGreenBananaIsNotRipe is missing!
   ]

   // ...

   func testGreenBananaIsNotRipe() {
       let banana = Banana(color: .green)
       XCTAssertFalse(banana.isRipe)
   }
}
이 테스트을 macOS에서 돌리면 우리의 보호 테스트가 실패할 것이다.

allTests 배열에 테스트를 추가하는 것을 까먹었기 때문에, 보호 테스트가 실패하고있다allTests 배열에 테스트를 추가하는 것을 까먹었기 때문에, 보호 테스트가 실패하고있다


나는 이것을 매우 좋아한다. 분명히, 모든 테스트마다 allTests 배열에 담고 싶을때만 동작할것이다. 즉 위에서 한것처럼 조건부의 컴파일에서 어떤 다윈- 혹은 리눅스 테스트를 감싸야할 것이다. 나는 이것이 많은 코드베이스의 한계를 만족시킬 것이라 믿는다.

LinuxMain.swift를 보호하기
다른 플랫폼은 어떤지 LinuxMain.swift가 완료되었다고 검증할 수 있을까? 이것은 좀 더 힘들다. LinuxMain.swift는 실제 테스트 타겟의 부분이 아니므로(될 수 없다), XCTMain으로 전달되는 내용은 쉽게 확인할 수 없다.

내가본 유일한 솔루션은 Run Script 빌드 단계를 테스트 타겟에 추가하고, LinuxMain.swift에 스크립트 파싱 코드를 넣어, 테스트 타겟에서 테스트 수트의 수에 배열의 항목을 비교한다. 아직 시도해보진 않았지만, 꽤 복잡하게 느껴진다.

업데이트: 코드 생성을 사용한 솔루션으로 appendix를 사용할 수도 있다.

결론
새 테스트에도 불구하고, 여전히 잠재적으로 잊어버릴 수 있는 두가지 때문에 완벽해보이진 않는다. 새로운 XCTestCase 클래스를 생성할때마다 반드시 아래를 행해야한다.
  1. 새 클래스에 testLinuxTestSuiteIncludesAllTests 테스트를 복사하여 붙여넣어야한다.
  2. LinuxMain.swift를 갱신해야한다.
그럼에도 불구하고, 새로운 테스트라는게 가장 일반적인 케이스(현재 테스트 수트를 새로운 테스트를 추가하고는 allTests 배열을 갱신하는 것을 까먹는 케이스)를 커버하므로 현재 상태 보단 상당히 낫다고 생각한다.

스위프트의 반영 능력이 더 강력해지는것을 기다릴 수 없다. 이런 모든것이 불필요하게 되면 좋겠다.


Appendix: Sourcery로 코드 생성
최근에 반복되는 주제로 되는 것처럼 보이는 것에서, Krzysztof Zabłocki가 짚어주었는데, 그의 훌륭한 코드 생성 툴인 Sourcery로 리눅스 테스트 기반을 유지보수할 수도 있다. 이것은 훌륭한 대체물이고 꽤 쉽게 세팅할 수 있다. 아래에 그 방법을 설명해 놓았다.

1. Sourcery 설치하기. 나는 스위프트 패키지 매니저 의존성으로 이것을 추가하는 것에서 동작하지 않았는데(빌드 실패), 스위프트3.1이 새로 나오면서 관련된 일시적인 문제가 아닐까 생각이 된다. 결국 가장 최신 배포을 다운받아서 바이너리를 직접 실행시켰다.

2. LinuxMain.stencil이라는 이름의 파일을 생성하는데, 아래 자료를 함께 넣는다. 여러분의 프로젝트에서 편한 곳에 저장한다. 나는 sourcery/의 하위 폴더에 넣었다.
// sourcery:file:Tests/LinuxMain.swift
import XCTest
{{ argument.testimports }}

{% for type in types.classes|based:"XCTestCase" %}
{% if not type.annotations.disableTests %}extension {{ type.name }} {
  static var allTests = [
  {% for method in type.methods %}{% if method.parameters.count == 0 and method.shortName|hasPrefix:"test" %}  ("{{ method.shortName }}", {{ method.shortName }}),  {% endif %}{% endfor %}]}
{% endif %}{% endfor %}

XCTMain([
{% for type in types.classes|based:"XCTestCase" %}{% if not type.annotations.disableTests %}  testCase({{ type.name }}.allTests),
{% endif %}{% endfor %}])
// sourcery:end
이것은 Ilya Puchka가 작성한 양식을 기반으로 하였다. 나는 그냥 처음과 끝에 //sourcery:... 표시를 추가했다. 이것은 생성된 파일의 경로를 결정한다(Sourcery 0.5.9가 필요함).여러분도 볼 수 있듯, 이것은 양식화 언어(templating language)와 스위프트 코드를 합쳐놓았다. Sourcerey를 호출할때, 여러분의 소스코드에서 타입들을 파싱하여 여러분이 보내온 양식에 맞춰 코드를 생성하는데 쓰일 것이다. 예를들어 {% for type in types.classes|based: "XCTesetCase" %}로 시작하는 루프는 XCTestCase를 상속하는 모든 클래스를 돌면서 allTests 프로퍼티를 담은 익스텐션을 생성할 것이다.

3. 여러분의 테스트 클래스에 있는 allTests 정의를 제거하기. 우리는 다음 단계에서 Sourcery로 이것들을 생성할 것이다. 이미 testLinuxTestSuitIncludesAllTests 메소드를 추가했었다면 이것도 제거하거나 여기서부터 떠나게 만들자. 영향을 주진 않고 여전히 이슈를 찾고 있을 것이다, 예를들어, 테스트를 추가하고나서 Sourcery를 실행시키지 않았을때, 더이상 반드시 엄격하게 하진 않을 것이다.

4. 프로젝트 폴더에서 Sourcery를 실행하기.
$ sourcery --sources Tests/ \
   --templates sourcery/LinuxMain.stencil \
   --args testimports='@testable import BananaKitTests'
Scanning sources...
Found 1 types.
Loading templates...
Loaded 1 templates.Generating code...
Finished.
Processing time 0.0301569700241089 seconds
이것은 현재 있던 Tests/LinuxMain.swift 파일을 생성된 코드로 덮어쓸 것이다.
// Generated using Sourcery 0.5.9 — https://github.com/krzysztofzablocki/Sourcery

// DO NOT EDIT

import XCTest@testable
import BananaKitTests

extension BananaTests {
    static var allTests = [
          ("testYellowBananaIsRipe", testYellowBananaIsRipe),
          ("testGreenBananaIsNotRipe", testGreenBananaIsNotRipe),
    ]
}

XCTMain([
     testCase(BananaTests.allTests),
])
이 작은 예제에선 두 테스트와함께 한 클래스만 있지만, 여러 테스트 클래스에도 역시 동작할 것이다.

5. 마지막(선택적인) 단계에는 더이상 생성된 빌드에서 생겨난 파일이 필요없으므로 그것을 제거한다.
$ rm LinuxMain.generated.swift
그리고 이게 다다. 모든 빌드에 실행되는 스크립트를 위해 두가지 터미널 명령(Sourcery를 호출하고 파일을 제거하는)을 추가하고, 여러분의 리눅스 테스트들은 이제 항상 최신으로 유지될것이다. 매우 멋지다!



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

으로 보내주시면 됩니다.



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

,
제목: 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

,
제목: 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

,
제목: 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

,
제목: 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

,
제목: 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

,
제목: Pretty much every way to assign optionals

Non-옵셔널을 저장하거나 옵셔널을 옵셔널에 저장하기

기본적인 것이다. 로켓과학처럼 복잡한 것이 아니다.
optItem = 5 // optItem is now .some(5)
optItem = optValue // optItem is whatever optValue is
요약
사용빈도: 모든 경우
터무니없는 접근법인가: 전혀 그렇지 않다.

옵셔널을 non-옵셔널에 저장하기
많은 함수와 메소드들이 옵셔널 값을 반환한다. throw와 함께 try?를 사용할때도 옵셔널 값을 반환한다. 여러분은 종종 그 결과를 non-옵셔널 변수 혹은 프로퍼티로 담아둬야 할 때가 있을 것이다.

이렇게하기위해 nil을 테스트하고 어떤 non-nil 결과의 언랩핑된 결과를 저장한다. 아래에 몇가지 방법이 있다.

조건부의 바인딩
if let을 사용하여 조건부로 옵셔널을 바인딩하고 대입을 실행할 수 있다.
if let optItem = optItem { 
    item = optItem // if optItem is non-nil
}
if let과 완전히 동일하지만 그래도 여러분이 원한다면 if case도 사용할 수 있다.
// Sugared optional
if case let optItem? = optItem { 
    item = optItem // if optItem is non-nil
}

// External let
if case let .some(optItem) = optItem {
    item = optItem // if optItem is non-nil
}

// Internal let
if case .some(let optItem) = optItem { 
    item = optItem // if optItem is non-nil 
}

Nil coalescing
만일의 대비의 값으로 nil coalescing을 사용할 수 있다.
item = optItem ?? fallbackValue
만일의 대비의 값이 필요없다면 원래 값을 사용해도 된다.
item = optItem ?? item
약간의 주의. 컴파일러가 "자신에 자신을 할당하는" 경우를 최적화하는지 잘 모르겠다. 그것을 확인하고 할당한다면 if let 방법보다 덜 효율적일 것이다.

또한 이 Itemnon-nil 일때만 갱신한다는 의도가 추가적인 if let을 쓰는게 깔끔하다고 생각하므로 효율면이나 가독성면에서 이것을 추천하진 않는다.

요약
사용빈도: 종종 사용한다
터무니없는 접근법인가: 전혀 아니다

optItemnon-nil일때만 옵셔널을 갱신하기
이번에는 현재 옵셔널이 nil일때 갱신을 스킵하는 시나리오에대해 설명하겠다. 이러한 경우 nil은 "이 옵셔널을 건드리지 마시오"라는 의미이다. 나는 이런 시나리오가 일어나지 않게 생각했다.

명확한 해결 방법이다.
if optItem != nil { optItem = newValue }
? 표시를 사용한 완전히 이상한 벙법이다.
optItem? = nonOptionalValue
?의 사용에서 rhs는 반드시 non-옵셔널이어야하고 컴파일 시점에 보장된다. 스위프트 언어의 특징중에 다소 모호한 것이다(이것을 짚어준 Joe Groff에게 감사하다).

혹은 "non-nil 수신자를 위한 테스트" 대입을 위해 이렇게 할 수 있다(바보같지만).
if let optValue = optValue {
    optItem? = optValue
}
이 예제는, rhs? 대입은 non-옵셔널이여야한다. 조건부로 옵셔널을 바인딩하는 것은 ?을 쓸 수 있게 해준다. Madalin Sava가 아래의 간단한 대안을 알려주었다. (이 섹션의 모든 것이 그렇듯) 절약 면에서는 높은 점수지만 명료하지 않은 결과에대해서는 낮은 점수를 받는다.
optItem? = optValue ?? optItem!
요약
사용빈도: 절대 사용하지 말라
터무니없는 접근법인가: 확실히 그렇다

optItemnil일때만 옵셔널을 갱신하기
이번에는 "대부분 사용하는, 한번만 세팅하기"로 설명할 수 있겠다. 옵셔널이 non-nil 값으로 한번 대입하게 되면, 그것은 다시 덮어쓰이지 않게 만든다. 대입을 하기 전에 nil을 확인하여 가장 간단하게 할 수 있다.
if optItem == nil { optItem = newValue }

혹은 오퍼레이터로 불태워도 된다. 아마 이렇게 하고 싶진 않을 것이다.

infix operator =?? : AssignmentPrecedence

// "fill the nil" operator
public func =??<T>(target: inout T?, newValue: T?) {
    if target == nil { target = newValue }
}
optItem =?? newValue

이것도 한번 보자: SE-0024

요약
사용빈도: 나는 이렇게 사용하진 않으나, 다시 대입되는 것을 막고 싶을때 유용할 것이다. 이것이 묵시적으로 언랩핑된 옵셔널의 미친 버전의 종류이나, 그것을 다시는 바꾸지 않는다고 보장함을 테스트하는 곳과, (IVO의) 모든 일련의 변화가 non-nil 값이여야하는 곳이 아닌..
터무니없는 접근법인가: 터무니없지는 않으나 일반적이지도 않다.

새로운 값이 non-nil일때만 옵셔널을 갱신하기
이 시나리오는 기본적으로 묵시적인 언랩핑된 옵셔널을 따라한 것이지만 세이프티가 추가되고 IVO 크레쉬가 없다. 항상 non-nil에대한 테스트를 하기때문에 그 값을 검증하여 한번 세팅하면 옵셔널이 다시는 nil을 반환하지 않을 것이다.

한계는 non-nil의 새 값을 갱신하고 nil 대입을 버린다.
if let newValue = newValue { optItem = newValue }

혹은 이렇게(nil-값을 위해 다시 대입하는 행위를 한다는게 낭비처럼 느껴지지 않는가?)
optItem = newValue ?? optItem
혹은 연산자로 불태워도 된다. 마찬가지로 이렇게 하고 싶지는 않을 것이다.
infix operator =? : AssignmentPrecedence

// "assign non-nil values" operator
public func =?<T>(target: inout T, newValue: T?) {
    if let newValue = newValue {
        target = unwrapped
    }
}
이것도 한번 보자 : Swift Evolution

요약
사용빈도: 내가 사용하진 않는다만 non-IVO 옵셔널을 사용하고 싶은 사람들이 원할 수도 있을 것 같다.
터무니없는 접근법인가: 그렇진 않으나 일반적이지도 않다.


'Swift와 iOS > Swift Basic' 카테고리의 다른 글

[번역]No-contiguous raw value enumeration  (372) 2017.05.13

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

,

최근에 Brennam Stehling는 내가 완전히 모르고 있었던 환상적인 Swift의 기능 하나를 알려주었다. 여러분은 각 case마다 자동으로 값이 증가하는 raw값의 열거형을 만들 수 있을 것이다.
enum MyEnumeration: Int {
   case one = 1, two, three, four
}

MyEnumeration.three.rawValue // 3
그리고 여러분은 손수 값을 지정한 raw 값 열거형을 만들 수도 있다.
enum MyEnumeration: Int {
    case one = 1, three = 3, five = 5
}
그런데 나는 이 두가지를 합쳐서 사용할 수 있다는 것을 몰랐었다!(아마 아래처럼 표준-기반 값들에는 그렇게 하지 않을것 같지만..)
enum HTTPStatusCode: Int {
    // 100 Informational
    case continue = 100
    case switchingProtocols
    case processing
    // 200 Success
    case OK = 200
    case created
    case accepted
    case nonAuthoritativeInformation
}

HTTPStatusCode.accepted.rawValue // 202
그래도 멋지지 않는가?

나는 아마 그 위치(예를들면 "1에서 시작")와함께 값으로 접근할 수 있게 지정하고, 아래에 있는 값들은 확정되지 않은 의미를 가진다. Kristina Thai는 다음과같이 언급했다. 의미있는 값을 생략하는 것은 가독성면이나 열람시에 도움이 되지 않을것이라고.



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

으로 보내주시면 됩니다.

'Swift와 iOS > Swift Basic' 카테고리의 다른 글

[번역]옵셔널을 대입하는 여러가지 방법들  (0) 2017.05.13

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

,
제목: Refactoring singleton usage in Swift

더 명료하고, 더 모듈화시키며, 더 테스트용이한 코드베이스를 위한 팁

소프트웨어 개발에서 싱글톤널리 권장되지 않고 눈쌀을 찌푸리게 만든다. 이것을 테스트하는 것은 어렵거나 불가능하고, 묵시적으로 다른 클래스에서 사용하면 여러분의 코드베이스는 헝클어져버린다. 또한 이것은 코드의 재사용도 어렵게 만든다. 오랫동안 싱글톤은 전역 변수나 가변 상태의 변형에 지나지 않다고 생각해왔다. 적어도 많은 사람들은 이 방법이 나쁜 방법이라는 것 정도는 인지하고 있다. 그러나 때때로 싱글톤은 피할수 없는, 필요한 독이기도 하다. 이것을 어떻게 깔끔하고 모듈화되고 테스트용이하게 우리 코드에 집어넣을 수 있을까?

싱글톤은 어디에나 있다
애플 플랫폼에서 싱글톤은 Cocoa와 Cocoa Touch 프레임워크 어디에나 있다. UIApplication.shared, FileManager.default, NotificationCenter.default, UserDefaults.standard, URLSession.shared 등이 있다. 또한 이 디자인 패턴은 Cocoa Core Competencies 가이드의 한 섹션으로 나와있다.

여러분이 묵시적으로 저 싱글톤(혹은 여러분의 싱글톤)을 참조할때 여러분의 코드를 변경하는데 드는 노력이 증가할 것이다. 싱글톤을 사용하는 클래스에서 싱글톤을 변경하거나 목(mock) 할 수 있는 방법이 없기 때문에 여려분의 코드를 테스트하기 어려워지거나 불가능해진다. 아래는 iOS 앱에서 일반적으로 볼 수 있는 것이다.
class MyViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let currentUser = CurrentUserManager.shared.user
        if currentUser != nil {
            // do something with current user
        }

        let mySetting = UserDefaults.standard.bool(forKey: "mySetting")
        if mySetting {
            // do something with setting
        }

        URLSession.shared.dataTask(with: URL(string: "http://someResource")!) { (data, response, error) in
            // handle response
        }
    }
}
이것이 묵시적인 참조이다(클래스 안에서 직접 싱글톤을 사용한다). 더 나아지게 할 수 있는데, 스위프트로 가볍고 쉬우며 의존성을 줄이는 방법이 있다. 또한 스위프트로 우아하게 만들 수도 있다.

의존성 주입
짧게 말해, 답은 의존성 주입이다. 이 원리는 여러분의 함수와 클래스를 모든 입력이 명시적으로 되게 설계하는 방법이다. 위의 코드를 의존성 주입을 사용하여 리팩토링 한다면 아래처럼 생겼을 것이다.
class MyViewController: UIViewController {

    let userManager: CurrentUserManager
    let defaults: UserDefaults
    let urlSession: URLSession

    init(userManager: CurrentUserManager, defaults: UserDefaults, urlSession: URLSession) {
        self.userManager = userManager
        self.defaults = defaults
        self.urlSession = urlSession
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        let currentUser = userManager.user
        if currentUser != nil {
            // do something with current user
        }

        let mySetting = defaults.bool(forKey: "mySetting")
        if mySetting {
            // do something with setting
        }

        urlSession.dataTask(with: URL(string: "http://someResource")!) { (data, response, error) in
            // handle response
        }
    }
}
이 클래스는 이제 더이상 모든 싱글톤에 묵시적으로 의존하지 않는다. 이것은 명시적으로 CurrentUserManager, UserDefault, URLSession에 의존한다. 그러나 이런 의존성들이 이것들이 싱글톤이라는 것을 이 클래스는 전혀 모른다. 이런 세부사항들은 기능이 바뀌지 않은채 문제가 없다. 뷰컨트롤러는 단이 이 오브젝트의 인스턴스가 존재한다는 사실만 알고있고, 호출 시점에서 싱글톤을 담아 보낼 수 있다. 다시 말하지만, 이런 세부사항은 이번 수업의 관점과는 관련이 없다.
let controller = MyViewController(userManager: .shared, defaults: .standard, urlSession: .shared)

present(controller, animated: true, completion: nil)
프로 팁: 여기서 스위프트 타입 추론이 일어난다. URLSession.shared라고 쓰는 것 대신에 .shared라고 쓸 수 있다.

만약 다른 userDefaults를 줘야한다면(예를들어 App Groups과 데이터를 공유해야한다면) 바꾸기 쉽다. 사실 이 클래스에서는 아무것도 바뀌지 않아야한다. UserDefualts.standard를 보내는 것 대신에 UserDefaults(suitName: "com.myApp")을 보내야한다.

게다가 이제 유닛테스트에서 이 클래스의 가짜나 목(mock)을 보낼 수 있다. 실제 스위프트에서 목 하는 것은 불가능하지는 않으나 더 편한 workarounds가 있다. 이것은 여러분의 코드를 구성하고 싶은대로 할 수 있다. CurrentUserManager라는 프로토콜을 쓸 수 있는데, 이것은 테스트에서 "목"할 수 있다. 또한 테스트를 위한 가짜 UserDefault에 제공할 수 있고 URLSession을 옵셔널로 만들어 테스트에선 nil을 넣으면 된다.

리팩토링 지옥
이 방법에 열중하여 이제 기술적 빚을 안고있는 여러분의 코드베이스를 해방시키고 싶을 것이다. 의존성 주입은 이상적이고 더 순수한 객체 모델을 제공하지만, 종종 달성하기 어려울때가 있다. 게다가 처음 코드를 짤 때 이것을 수용하도록 설계하지 않을 것이다.

위에 우리가 리팩토링한 것은 이제 더 모듈화되고 테스트용이하다. 그러나 여기에는 현실적인 문제가 있다. MyViewController의 생성자는 빈(init())것에 익숙한데 이제 3개의 파라미터를 받아야한다. 모든 호출시점이 바뀌게 되는 것이다. 이것을 구성하기위해 깔끔하고 적절한 방법은, 계층 위아래로 혹은 이전 뷰컨트롤러에서 지금 뷰컨트롤러까지 인스턴스를 보내도록 만드는 방법이다. 이것은 객체 그래프의 루트에서부터 모든 자식까지 데이터를 보내야 한다는 뜻이다. iOS에서는 특히 뷰컨트롤러에서 뷰컨트롤러로 데이터를 보내는 것이 머리아픈일이다. 특히 다른 사람에게 넘겨받은 코드베이스는 갑자기 많이 바뀌는 구현에서 애를 먹을 것이다. 그러면 대부분 클래스들(특히 뷰컨트롤러)의 생성자는 바뀌어야한다. 이런 바뀜은 앱 전체를 일괄적으로 리팩토링해야하는데 당신이 인지하고 있기 어려운 범주가 되버린다. 모든 코드를 고칠 수도 있겠지만 아니면 다른 클래스들은 여전히 묵시적으로 싱글톤을 참조하게 두고 몇개만 의존성 주입으로 바꾸는 것이다. 그래도 이런 부조화는 훗날에 문제를 야기할 수도 있다.

따라서 이런 리팩토링은 복잡하고, 크고, 넘겨받은 코드베이스에는 알맞지 않다. 이런 이유로 리팩토링을 하지 말고 이런 기술적 부채와 함께 살아갈것인지도 의논해보아야한다. 그러다가 몇달, 몇년뒤 멀티 사용자 기능을 지원해야할때, 계정을 바꿀때 CurrentUserManager가 동작하지 않을 수 있다. 이것은 어떻게 해결할 것인가?

여러분이 어떤 프로젝트를 시작할때부터는 클래스 설계의 첫 단계부터 이런 종류의 변경을 수용할 수 있게 만드는 방법이 있을 것이다.

디폴트 파라미터 값들
스위프트에서 내가 좋아하는 기능 중 하나는 디폴트 파라미터 값들이다. 이것은 놀랍도록 유용하고 여러분 코드에 엄청난 유연성을 가져다준다. 디폴트 파라미터로, 의존성 주입이라는 토끼구멍으로 들어가지 않고, 또 여러분의 코드베이스에서 엄청난 복잡성도 만들지 않고서, 위에서 말한 문제를 해결할 수 있다. 아마 여러분의 앱은 한명의 유저만을 가질 것이니, 위와같으 의존성 주입의 구현이 불필요하게 과하다.

싱글톤을 디폴트 파라미터로하여 사용할 수 있다.
class MyViewController: UIViewController {

    init(userManager: CurrentUserManager = .shared, defaults: UserDefaults = .standard, urlSession: URLSession = .shared) {
        self.userManager = userManager
        self.defaults = defaults
        self.urlSession = urlSession
        super.init(nibName: nil, bundle: nil)
    }
}
이제 호출 시점을 고칠 필요가 없다. 그러나 클래스 그 자체 안에서는 수많은 다양한 것들이 있다. 이제 의존성 주입을 사용하고 더이상 싱글톤을 참조하지 않는다.
let controller = MyViewController()

present(controller, animated: true, completion: nil)
이러한 변경으로 어떤 이득을 취할 수 있을까? 모든 호출 지점을 바꾸지 않고 이 패턴을 사용하기 위해 모든 클래스를 리팩토링할 수 있다. 의미로나 기능적으로나 바뀐것은 없다. 그러나 여러분의 클래스들은 이제 의존성 주입을 사용한다. 이것들은 단지 내부적으로 인스턴스를 사용하고 있다. 이것을 위에서 설명한 것처럼 테스트할 수 있고 유연하게 모듈화된 API를 유지보수 할 수 있다. (그래도 모든 퍼블릭 인터페이스는 바뀌지 않는다는 점) 본질적으로는 아무것도 바뀌지 않은채 계속 코드베이스에서 작업할 수 있을 것이다.

커스텀을 전달받는 떄가 오면, non-싱글톤 파라미터로 어떤 클래스도 변경하지 않고서 해결할 수 있다. 오직 호출 지점만 바꾸면 된다. 게다가 완전한 의존성 주입을 구현하여 계층의 위에서 애래로 모든 의존성마다 전달해가려면 그냥 드폴트 파라미터를 지우고 그 위에서 의존성으로 전달하면 된다.

필요에따라 어떤 디폴트값의 opt-in 혹은 opt-out도 할 수 있다. 아래 예제에서는 커스텀 UserDefaults를 제공하지만, CurrentUserManagerURLSession을 위해 디폴트 파라미터를 가지고 있는다.
let appGroupDefaults = UserDefaults(suiteName: "com.myApp")!

let controller = MyViewController(defaults: appGroupDefaults)

present(controller, animated: true, completion: nil)

결론
스위프트는 적은 노력으로 "partial" 종류의 의존성 주입을 만든다. 여러분의 클래스에 드폴트값으로 새 프로퍼티와 생성자 파라미터를 추가하여, 코드를 모듈화시키고 테스트하기 좋게 만들 수 있다(리팩토링에 빠지지 않고 완전한 의존성 주입을 만들 필요 없이 가능하다). 만약 프로젝트 시작 시점에 클래스를 이렇게 설계하면 코딩하면서 궁지에 몰리는 일이 더 적어질 것이다(그리고 여러분이 궁지에 몰리더라도 쉽게 빠져나올 수 있을 것이다).

여러분은 이 예제를 넘어 클래스, 구조체, 열거형, 함수 등 코드 전반에 이 개념과 설계를 적용시켜 볼 수 있다. 스위프트의 모든 함수는 디폴트 파라미터 값을 받을 수 있다. 나중에 어떤게 바뀔 수 있을지 생각해봄으로서, 적은 노력으로 변경할 수 있는 타입이나 함수를 만들어낼 수 있을 것이다.

좋은 소프트웨어를 만드는 것과 설계하는 것은 원하는 것을 쉽게 바꿀 수 있지만, 모든것을 바꾸진 않아도 되는 코드를 짠다는 의미이다. 이것이 의존성 주입 뒤에 있는 그 이유이며 스위프트의 디폴트 파라미터가 이것을 빠르고 쉽고 우아하게 해결할 수 있게 도와줄 것이다.


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

으로 보내주시면 됩니다.



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

,

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

옵셔널은 멋지다. 이제까지 나는 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

,
제목: System Level Breakpoints in Swift


모든 훌륭한 소프트웨어 개발자들은 결국 훌륭한 소프트웨어 디버거가 되야한다. 디버깅은 크게는 브레이크포인트의 설정으로 이루어지고, 실행시간동안 앱의 임의의 상태 지점을 관찰하기위해 브레이크 포인트를 걸어 놓은 곳에 가본다. 큼직하게는 두가지 종류의 브레이크 포인트가 있다. 하나는 당신의 코드에 설정한 것이고, 다른 하나는 다른사람의 코드에 설정한 것이다.

여러분의 코드에 브레이크 포인트를 설정하는것은 간단하다. 그냥 Xcode 프로젝트의 소스코드 라인을 찾아서, 관련된 라인 옆에 홈 안의 공간을 탭하면 된다.



그러나 시스템 API에대해 브레이크 포인트를 설정하고 싶거나 소스코드를 가지고 있지 않은 라이브러리 안에 구현된 메소드에 브레이크 포인트를 설정하고 싶은 경우는 어떨까? 예를들어, 레이아웃 버그를 잡아야한다고 할때, UIView에서 애플의 고유의 내부 layoutSubviews 메소드 호출을 관찰하는데 도움이 될 수 있다. 역사적으로 Objective-C 개발자들에게는 이것이 큰 문제가 아니다. 어떤 메소드를 심볼릭하게 표현하기위해 그리고 그것을 브레이크하기위해, Xcode의 lldb 콘솔(View -> Debug Area -> Activate Console)로 들어가서 이 이름을 지정해가면서 브레이크 포인트를 지정하면 된다는 것을 안다. lldb에서 "b"라는 단축 명령어는 우리가 입력한 것으로부터 전체 이름과 매칭되는 것을 찾는 정규식이다.

(lldb) b -[UIView layoutSubviews]
Breakpoint 3: where = UIKit`-[UIView(Hierarchy) layoutSubviews], address = 0x000000010c02f642(lldb)

lldb 콘솔에서 겁을 주거나, 현재 디버그 세션보다 더 길게 붙이는 브레이크 포인트를 원하면, Xcode의 내장된 심볼릭 브레이크 포인트 인터페이스(Debug -> Breakpoints -> Create Symbolic Breakpoint)를 사용하여 같은 결과를 달성할 수 있다.


사실, 여러분의 iOS 앱에 브레이크 포인트를 걸고 앱을 돌리면 애플의 layoutSubviews 메소드에서 브레이크 포인트 안으로 실행시켜볼것이라고 생각한다 lldb 콘솔로 돌아와서 메시지를 받은 오브젝트를 확인한다.

(lldb) po $arg1
<UIClassicWindow: 0x7f8e7dd06660; frame = (0 0; 414 736); userInteractionEnabled = NO; gestureRecognizers = <NSArray: 0x60000004b7c0>; layer = <UIWindowLayer: 0x600000024260>>

이제 계속해서 심블에 브레이크를 걸어보자. 그리고 또다시 그렇게 하자. lldb 콘솔에 "po $arg1"이라고 입력하여 매번 타겟을 확인하라. 유별난 버그를 추적하는동안 이런 종류의 분석을 시행하는게 얼마나 유용한지 상상할 수 있을 것이다.

그러나 우리 플랫폼을 이제 시작한 가여운 스위프트 프로그래머들이나 스위프트 문법에만 열관하는 사람들은 어떻게할까? 애플의 문서를 읽어보거나, "-[UIView layoutSubviews]"를 해석하는게 불가능한 사람들은 "UIView.layoutSubviews"이 완전히 눈에 거슬릴 뿐만 아니라, 스위프트에게는 옳은 것일까?

불행히도 "UIView.layoutSubviews"라고 브레이크포인트를 설정하면 동작하지 않는다.

(lldb) b UIView.layoutSubviews
Breakpoint 3: no locations (pending).WARNING:  Unable to resolve breakpoint to any actual locations.
(lldb)

이것이 실패하는 이유는, UIView에 스위프트로 구현된 layoutSubviews라는 메소드가 없기 때문이다. 이것은 모두 Objective-C로 구현되있다. 사실 아주 많은 스위프트에서 쓸 수 있는 Objective-C 메소드들이 Objective-C 메시지 전송으로 바로 전달하도록 컴파일된다. 스위프트 파일에 "UIView().layoutIfNeeded()"같은 것을 입력하면, 컴파일은 될것이며, layoutIfNeeded라는 스위프트 메소드가 없기때문에 호출해도 아무일도 일어나지 않을 것이다.

이것이 스위프트로 맵핑된 모든 Cocoa 타입들에게 해당되지는 않는다. 예를들어, 모든 "Data.write(to:options:)" 호출에 브레이크를 걸고 싶다고 생각하자. 아마 "Data.write"에 브레이크 포인트를 걸어서 동작하길 원할것이다.

(lldb) b Data.write
Breakpoint 11: where = libswiftFoundation.dylib`Foundation.Data.write (to : Foundation.URL, options : __ObjC.NSData.WritingOptions) throws -> (), address = 0x00000001044edf10

그리고 된다! 이건 어떨까? 실제로 이것만 아니다. 이것은 -[NSData writeToURL:options:error:]의 길에서 libswiftFoundation을 통해 전달하는 모든 호출에 브레이크가 걸릴 것인데, Objective-C 구현을 호출하는것에 직접 잡히지는 않을 것이다. 메소드에서 모든 호출을 잡기 위해서는, 저수준(Objective-C 메소드)에서 브레이크포인트를 설정할 필요가 있다.

따라서 규칙으로, iOS나 Mac 플랫폼에서 더 좋은 디버거를 원하는 스위프트 프로그래머들은 Objective-C의것과 동일하게 스위프트 메소드를 매핑할 수 있는 능력이 필요하다. UIView.layoutSubviews같은 메소드의경우, 바로 "-[UIView layoutSubviews]"로 맵핑하지만, 많은 메소드의경우 지금처럼 간단하게 될것이다.

스위프트로 맵핑된 메소드 이름을 Objective-C로 다시 매핑하기위해서는, 많은 Foundation 클래스들이 NS 접두를 뺐다는 것을 인식하고, 스위프트 API 가이드라인을 적용하기위해 메소드 시그니처를 다시 작성한 영향이다. 예를들어 순수한 스위프트 프로그래머들은 "Data.write(to:options)"의 저수준 구현으로 브레이크 포인트를 설정하기 때문에 쉽게 유추하지 못할것이다. "NS" 접두를 붙이고, URL 파라미터를 명시적으로 넣고, 이상한 에러 파라미터를 추가한다. 이것은 보기에 옛날의 나쁜 시기에 까탈스러운 백발의 노인이 실패를 전달하는데 사용하는 것 같아 보인다.

(lldb) b -[NSData writeToURL:options:error:]
Breakpoint 13: where = Foundation`-[NSData(NSData) writeToURL:options:error:], address = 0x00000001018328c3

성공했다!

이런 Obejctive-C 메시지 시그니처와 API 규약들에대한 추가적인 지식을 개발하게 만드는 생각은 여러분을 힘겹게 만든다는 점에서, 나는 여러분의 다음 과제를 통해 얻을 약간의 수정된것을 제공한다(I offer a little hack that will likely get you through your next challenge). API가 이런 원리중 하나를 사용햐여 작성되었으면, 함수의 스위프트 이름은 거의 Objective-C 메소드 이름의 부분집합이다. 아마도 lldb의 정규식 일치 기능을 활용하여 브레이크포인트를 설정하고싶은 메소드를 0으로 만들 수 있다.

(lldb) break set -s Foundation -r Data.*write
Breakpoint 17: 8 locations.

이제 "break list"를 입력하고 lldb가 표시한 매칭 수를 보자. 그것들 중에는 스위프트로 된 (libswiftFoundation으로 이루어진) 여러 메소드가 있으나, 질문에서 타겟 메소드를 찾아야할 것이다. 사실, 여러분이 브레이크를 걸고싶은 다른 저수준의 Objective-C 메소드도 찾아봐야할 것이다.

리스트를 더 잘 관리하기위해, 주어진 Objective-C 프레임워크안에 타겟 메소드가 있다는 지식을 주고, 이름으로 특정 공유된 라이브러리에 제한된 매칭을위한 "-s" 플래그를 넣는다.

(lldb) break set -s Foundation -r Data.*write
Breakpoint 17: 8 locations.

이 브레이크포인트 사이에서 NSPageData에서 몇 거짓이 있지만, 그 목록은 모두 더 관리할 수 있다. 하나의 브레이크포인트 "17"은 하위-숫자로 식별된 매치의 모든것을 가지고 있는다. 여러분의 방법으로 브레이크포인트의 목록을 걸러내도 좋다.

(lldb) break disable 17.6 17.7 17.8
3 breakpoints disabled.
(lldb) c

Objective-C를 스위프트에 맵핑하는 애플의 방식은 스위프트 개발자를위해 더 즐거운 프로그래밍 경험을 함께 만들게 해주지만, 그 세부적인 구현을 이해하지 못하고 있거나 어떻게 동작하는지에대한 이해가 부족하게되면 엄청난 혼란으로 빠질 수 있다. 나는 이 글이 여러분의 스위프트 앱을 디버깅하는데 필요한 도구가 되었으면 좋겠고, 불가피하게 사용하는 Objective-C 코드를 더 효율적이게 썼으면 좋겠다.

Update: 나는 두가지 관련된 버그를 넣었다: Radar #31115822 스위프트 메소드 포맷에서 자동으로 매핑한 것을 Obejctive-C 메소드로 돌아오도록 해야한다, 그리고 Radar #31115942 간결한 스위프트 메소드 시그니처를 만드는것에대해 lldb를 더 직관적으로 만들도록 해야한다.



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

으로 보내주시면 됩니다.



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

,
제목: 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

,