본문 바로가기

Maggie's Books

[Jon Hoffman] Swift 4 : 프로토콜지향 프로그래밍 3/e (1)

5월의 책

|   스위프트 4 : 확장성 있는 iOS 프로그래밍을 위한 프로토콜지향 프로그래밍 3/e

 

 

 

| 이 책에서 다루는 내용

  • 객체지향 프로그래밍과 프로토콜지향 프로그래밍 간의 차이점
  • 스위프트에서 제공하는 여러 타입의 비교 및 위험 사항
  • 제네릭과 제네릭 프로그래밍에 대한 집중 탐구
  • 커스텀 타입에서 Copy-on-write를 구현하는 방법
  • 프로토콜 우선 애플리케이션 설계 및 타입 구현

 

 

iOS를 시작할 때 지겹도록 들었던 객체지향.. 

그래서 그런지 이 책을 추천받았을 때 궁금해졌던 '프로토콜지향'이라는 키워드.

객체지향이나 프로토콜지향이나 공통적으로는 더 나은 개발 환경으로의 확장성을 염두하고 있다고 생각한다. 물론 4장에서 이야기하는 제네릭 함수를 사용하면 중복 코드를 제거한(훨씬 간단한) 해결책을 제공하기도 하지만.. 

 

오늘은 3장 확장편에서 나에게 필요한 부분만 기록해 두고,

시간이 허락한다면 4장 제네릭 내용도 정리해보겠다.

 


 

 

|  3장에서 다루는 내용

 

   1. 구조체와 클래스, 그리고 열거형을 확장하는 방법

   2. 프로토콜을 확장하는 방법

   3. 실제 예제에서 확장을 사용하는 방법 

 

이미 존재하는 타입에 다음과 같은 아이템을 추가(확장)할 수 있다.

  • Computed property
  • Instance methods, Type methods
  • Convenience initializers
  • Subscripts

→  but, 단점은 확장한 타입의 기능을 override할 수 없다는 점이다. 확장은 기능을 추가하게 설계됐으며, 타입의 기능을 변경하는 의미로 설계되지는 않았다. 확장에서 할 수 없는 또 다른 것은 저장 프로퍼티를 추가할 수 없다는 것이다. 그러나 연산 프로퍼티(Computed property)는 추가할 수 있다.

 

... 대부분의 객체지향 프로그래밍 언어에서 이미 존재하는 클래스에 기능을 추가하고자 할 경우 일반적으로 기능을 추가하고자 하는 클래스를 서브클래싱하게 된다. 그런 다음 새로운 서브클래스에 새로운 기능을 추가한다. 이 방법의 문제점은 원본 클래스에 실제로 기능을 추가하지 않는다는 점이다. 그러므로 추가적인 기능이 필요한 원본 클래스의 모든 인스턴스를 새로운 서브클래스의 인스턴스로 변경해야만 한다. 

 

... 또 다른 문제점은 참조 타입(클래스)만 서브클래싱이 가능하다는 점이다. 이는 구조체나 열거형과 같은 값 타입은 서브클래싱을 할 수 없다는 것을 의미한다. 애플은 애플리케이션에서 참조 타입보다 값 타입을 선호하기를 권고하고 있으며, 이는 애플의 권고안을 받아들일 경우(받아들여야 한다) 대다수의 커스텀 타입을 서브클래싱할 수 없다는 것을 의미한다.

 

 

|  구조체와 열거형, 클래스와 같은 타입을 확장하는 방법

 

| 확장 정의

extension String {
// 추가할 기능은 여기에 위치시킨다.
}

 

이 코드는 스위프트 표준 라이브러리에 있는 문자열 타입에 확장을 추가했다. ... 일반적으로는 타입 자체에 직접 기능을 추가하는 편이 더 낫다. 커스텀 타입을 위한 모든 코드가 함께 있으면 유지가 더 쉽기 때문.

 

.. 소스를 갖고 있는 프레임워크에 기능을 추가하는 경우에도 프레임워크 자체의 소스를 변경하는 것보다는 확장을 사용해 기능을 추가하는 편이 낫다. 프레임워크 내부에 있는 코드에 기능을 추가하는 경우 새로운 버전의 프레임워크를 받게 되면 수정했던 사항을 덮어쓰기 때문이다. == 확장한 코드는 프레임워크에 속해있는 파일 내부에 있지 않기 때문에 새로운 버전의 프레임워크가 확장을 덮어쓰지는 않을 것이다.

extension String {
	func getFirstChar() -> Character? {
		guard characters.count > 0 else {
        	return nil
        }
        return self[startIndex]
    }
}

 

위의 예시는 표준 스위프트 타입에 기능을 추가하는 방법으로,

String 타입을 확장해 문자열의 첫 번째 단어나 문자열이 비어 있는 경우에는 nil을 가진 옵셔널 값을 반환하는 메소드를 추가했다. 

var myString = "This is a test"
print(myString.getFirstChar())

 

(위의 코드는 문자 T를 콘솔에 출력할 것이다. )

 

다음으로 String 확장에 범위 연산자(range operator)를 인자로 받고,

범위 연산자에 의해 정의된 문자로 이루어진 서브 스트링(sub string)을 반환하는 스크립트를 추가했다.

extension String {
	func getFireChar() -> Character? {
    	guard characters.count > 0 else {
        	return nil        
        }
        return self[startIndex]
    }
    subscript (r: CountableClosedRange<Int>) -> String {
    	get {
        	let start = index(self.startIndex, offsetBy:r.lowerBound)
            let end = index(self.startIndex, offsetBy:r.upperBound)
            return substring(with: start..<end)
        }
    }
}

 

이름 있는 타입으로 구현됐기 때문에 다른 타입과 마찬가지로 확장할 수 있다. 한 예로 정수를 제곱한 값을 반환하는 메소드를 추가하기 위해 Int 타입을 확장하고자 할 경우 다음과 같이 확장을 사용할 수 있다.

extension Int {
	func squared() -> Int {
    	return self * self
    }
}

 

그리고 다음 코드에서 볼 수 있듯이 정수를 제곱한 값을 얻기 위해 앞서 생성한 확장을 사용할 수 있다. 

print(21.squared())

 

 

| 프로토콜 확장 

 

... 프로토콜지향 프로그래밍과 GamePlayKit 같은 프레임워크는 프로토콜 확장에 대한 의존도가 높다. 프로토콜 확장이 없이 프로토콜을 따르는 타입의 그룹에 특정 기능을 추가할 경우 각각의 타입에 기능을 개별적으로 추가해야만 할 것이다. 참조 타입(클래스)을 사용하고 있었다면 클래스 계층을 생성할 수도 있었겠지만 ... 값 타입에서는 클래스 계층을 사용할 수 없다. (또 나온다) 애플은 개발자들이 참조 타입보다는 값 타입을 선호할 것을 명시하고 있으며, 프로토콜 확장을 사용하면 공통적인 기능을 모든 타입에 기능을 구현할 필요 없이 특정 프로토콜을 따르는 값 타입은 물론 참조 타입에 추가할 수 있는 능력을 갖추게 된다고 명시하고 있다. 

 

.. 스위프트 표준 라이브러리는 Collection이라는 이름의 프로토콜을 제공한다. 

http://swiftdoc.org/nightly/protocol/Collection/

 

이 프로토콜은 Squence 프로토콜을 상속하고 있으며,

Dictionary와 Array 같은 모든 스위프트 표준 컬렉션 타입이 이 프로토콜을 채용하고 있다. 

 

Collection 프로토콜을 따르는 모든 타입에 기능을 추가한다고 생각해보자.

컬렉션 안에 있는 아이템을 섞거나 인덱스 번호가 짝수인 아이템만 반환한다. 다음 코드에서 볼 수 있듯이 Collection 프로토콜을 확장함으로써 이 기능을 쉽게 추가할 수 있다.

extension Collection {
	func evenElements() -> [Iterator.Element] {
    	var index = starIndex
        var result = [Iterator.Element] = []
        var i = 0
        repeat {
        	if i % 2 == 0 {
            	result.append(self[index])
            }
            index = self.index(after: index)
            i += 1
        } while (index != endIndex)
        return result
    }
    
    func shuffle() -> [Iterator.Element] {
    	return sorted(){ left, right in
        	return arc4random() < arc4random()
        }
    }
}

 

... (프로토콜을 확장하기 위해서는 extension 키워드를 사용하며, 뒤따라서 확장하고자 하는 프로토콜의 이름이 오게 된다. 그런 다음 추가하고자 하는 기능을 중괄호 사이에 위치시킨다.) 이제 Collection 프로토콜을 따른는 모든 타입은 evenElements() 함수와 shuffle() 함수를 받게 될 것이다. 다음 코드에서 추가한 함수를 배열(array)과 어떻게 함께 사용할 수 있는 지 보자. 

var origArray = [1,2,3,4,5,6,7,8,9,10]

var newArray = origArray.evenElements()
var ranArray = origArray.shuffle()

 

 newArray 배열의 요소는 1, 3, 5, 7, 9까지가 될텐데 이는 이 요소들의 인덱스 번호가 짝수이기 때문이다.(요소의 값이 아닌 인덱스 번호를 찾고 있다.) ranArray 배열은 origArry와 요소는 같지만, 요소의 순서가 섞여 있을 것이다.

 

프로토콜 확장은 개별적으로 각각의 타입에 코드를 추가할 필요 없이 타입의 그룹에 기능을 추가하기에 훌륭하지만, 확장하고자 하는 프로토콜을 따르는 타입이 무엇인지 아는 것이 중요하다. 앞 예에서는 Collection 프로토콜을 따르는 모든 타입에 evenElements() 메소드와 shuffle() 메소드를 추가함으로써 Collection 프로토콜을 확장했다. Dictionary 타입은 이 프로토콜을 따르는 타입 중 하나이지만 순서가 없는 컬렉션이므로 evenElements() 메소드는 생각했던 대로 작동하지 않을 것이다.

var origDict = [1:"One", 2:"Two", 3:"Three", 4:"Four"]
var returnElements = origDict.evenElements()
for item in returnElements {
	print(item)
}

 

Dictionary 타입은 특정 순서로 아이템을 저장하는 것을 보장하지 않기에

코드에서는 두 아이템 중 어느 아이템도 출력될 수 있다.

(2, "Two")
(1, "One")

 

... 원본 컬렉션이 Dictionary 타입이기 때문에 returnElements 인스턴스도 Dictionary 타입이 될 것이라고 기대할 수 있겠지만, 실제로는 Array 타입의 인스턴스라는 점이 혼란을 불러일으킬 수 있다. 즉, 프로토콜을  확장하는 경우 추가하는 기능이 프로토콜을 따르는 모든 타입에서 예상대로 작동하는 것을 보장하도록 주의를 기울여야 한다. shuffle() 메소드와 evenElements() 메소드의 경우 Collection 프로토콜보다는 Array 타입을 확장해 기능을 추가하는 것이 더 나을지도 모른다. 또한 정의된 기능을 전달받을 수 있는 타입을 제한하는 제약을 확장에 추가할 수도 있다. 

 

타입이 프로토콜 확장에서 정의한 기능을 전달받기 위해서는 프로토콜 확장에서 정의한 모든 제약을 만족해야만 한다. 제약은 확장하는 프로토콜 이름 뒤에 where 키워드를 사용해 추가한다. 다음 코드에서는 Collection 확장에 제약을 어떻게 추가하는 지 보여준다. 

extension Collection where Self: ExpressibleBytArrayLiteral {
 // 확장 코드는 여기에 위치한다.
}

 

추가하는 지 보여준다. 이 코드의 Collection 프로토콜 확장에서는 ExpressibleBytArrayLiteral 프로토콜을 따르는 타입만 확장에서 정의한 기능을 전달받을 수 있다. Dictionary 타입은 ExpressibleBytArrayLiteral 프로토콜을 따르지 않기 때문에 이 프로토콜 확장에서 정의한 기능을 전달받지 못한다.