본문 바로가기
Swift 공식 가이드/Swift 3

Protocols (1)

by 토끼찌짐 2017. 3. 23.

Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Protocols 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.



들어가며

프로토콜은 특정 task나 functionality를 위한 메서드, 프로퍼티, 그 외 요구사항들의 청사진을 정의한다. 클래스, 구조체, ENUM은 프로토콜을 적용하여 그 요구사항들의 실제 구현을 제공한다. 어떤 타입이 특정 프로토콜의 요구사항을 만족시키면, 그 타입이 프로토콜을 따른다conform고 말한다.

필요 시 프로토콜을 확장하여 그것을 따르는 타입에게 도움이 되는 기능을 제공하거나 할 수도 있다.



문법

프로토콜의 정의 문법은 클래스, 구조체, ENUM과 매우 유사하다.

protocol SomeProtocol {
    // 프로토콜 정의가 여기에 온다
}

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 구조체 정의가 여기에 온다
    // 이 구조체는 FirstProtocol과 AnotherProtocol을 따르고 있다
}

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 클래스 정의가 여기에 온다
    // 이 클래스는 SomeSuperClass를 상속받는다
    // 이 클래스는 FirstProtocol, AnotherProtocol을 따르고 있다
    // 슈퍼클래스가 존재할 경우 프로토콜보다 먼저 나오도록 위치시킨다
}



프로퍼티 요구사항

프로토콜은 자신을 따르는 타입(conforming type)에게 "특정 이름과 타입을 가진 프로퍼티를 제공해야 한다"고 요구할 수 있다.

  • 종류 : 인스턴트 프로퍼티, 타입 프로퍼티

  • 프로퍼티가 stored인지 computed인지는 명시하지 않는다.

  • 프로퍼티의 이름과 타입을 명시해야 한다.

  • 프로퍼티가 읽기/쓰기 속성인지 읽기 전용인지 명시해야 한다.

  • 프로토콜이 읽기/쓰기용 프로퍼티를 요구한다면 -> 읽기/쓰기가 가능한 프로퍼티로만 요구사항을 만족시킬 수 있다. (읽기전용 프로퍼티로는 안 된다는 말)

  • 프로토콜이 읽기전용 프로퍼티를 요구한다면 -> 어떤 종류의 프로퍼티라도 요구사항을 만족시킬 수 있다. (읽기/쓰기용 프로퍼티로 구현해도 된다는 말)

  • 프로퍼티 요구사항은 항상 변수로 선언되어야 한다. (var 키워드)

  • 타입 프로퍼티 요구사항 앞에는 static 키워드를 붙인다. 클래스가 이 프로토콜을 따를 때에는 그 타입 프로퍼티 앞에 class 또는 static 키워드를 붙이도록 한다. (오버라이드 되게 하려면 class 키워드 쓰기)

protocol SomeProtocol {
    var mustBeSettable: Int { get set } // 읽기/쓰기용
    var doseNotNeedToBeSettable: Int { get } // 읽기전용
}

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set } // 타입 프로퍼티
}


예제를 살펴보자.

// 인스턴스 프로퍼티 요구사항 1개만 가진 프로토콜
protocol FullyNamed {
    var fullName: String { get }
}

// 프로토콜을 따르는 간단한 구조체
struct Person: FullyNamed {
    // stored 프로퍼티로 fullName 프로퍼티를 제공한다
    var fullName: String
}

let john = Person(fullName: "John Appleseed")

// 프로토콜을 따르는 조금 복잡한 클래스
class Starship: FullyNamed {
    var prefix: String?
    var name: String
    
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    
    // computed 프로퍼티로 fullName 프로퍼티를 제공한다
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}

var ncc1701 = Starship(name: "Enterprise", prefix: "USS")



메서드 요구사항

프로토콜은 자신을 따르는 타입(conforming type)에게 "특정한 메서드를 제공해야 한다"고 요구할 수 있다.

  • 종류 : 인스턴스 메서드, 타입 메서드

  • 메서드 요구사항은 프로토콜 정의부에서 일반적인 메서드와 같은 방식으로 작성하면 된다. 단, 대괄호와 메서드 본문은 뺀다. 예제를 보면 무슨 말인지 쉽게 이해된다.

  • 가변 파라미터Variadic parameters도 허용된다.

  • 파라미터의 디폴트 값을 명시할 수 없다.

  • 타입 메서드 요구사항 앞에는 static 키워드를 붙인다. 클래스가 이 프로토콜을 따를 때에는 그 타입 메서드 앞에 class 또는 static 키워드를 붙이도록 한다. (오버라이드 되게 하려면 class 쓰기)

protocol SomeProtocol {
    static func someTypeMethod()
}


예제를 살펴보자.

// 인스턴스 메서드 요구사항 1개만 가진 프로토콜 (난수생성 프로토콜)
protocol RandomNumberGenerator {
    func random() -> Double
}

// 프로토콜을 따르는 클래스
class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    
    func random() -> Double {
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}

let generator = LinearCongruentialGenerator()
print("랜덤넘버: \(generator.random())")
// Prints "랜덤넘버: 0.37464991998171"
print("한 번 더 랜덤넘버: \(generator.random())")
// Prints "한 번 더 랜덤넘버: 0.729023776863283"



Mutating 메서드 요구사항

값 타입(구조체, ENUM 등)의 인스턴스 메서드 안에서 자신(인스턴스)을 수정modify, mutate할 필요가 있다면 메서드 앞에 mutating 키워드를 붙였었다. 이 룰은 프로토콜 정의부에서도 똑같이 적용된다.

예제를 보자.

// mutating 메서드 요구사항 1개만 가진 프로토콜
protocol Togglable {
    mutating func toggle()
}

// 프로토콜을 따르는 ENUM
enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case off:
            self = on
        case on:
            self = off
        }
    }
}

var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch 는 이제 on 상태

<note> 프로토콜의 요구사항으로 mutating 인스턴스 메서드를 선언했더라도, 클래스가 이 프로토콜을 따를 경우에는 메서드 앞에 mutating 키워드를 쓰지말라. mutating 키워드는 오직 값 타입(구조체, ENUM)을 위한 것이다.



이니셜라이저 요구사항

프로토콜은 자신을 따르는 타입(conforming type)에게 "특정 이니셜라이저를 제공해야 한다"고 요구할 수 있다.

이니셜라이저 요구사항은 프로토콜 정의부에서 일반적인 이니셜라이저와 같은 방식으로 작성하면 된다. 단, 대괄호와 이니셜라이저 본문은 뺀다.

protocol SomeProtocol {
    init(someParameter: Int)
}


<이니셜라이저 요구사항을 클래스에서 구현하기>

프로토콜에 정의된 이니셜라이저를 클래스에서 구현할 수 있다. designated 이니셜라이저 또는 convenience 이니셜라이저로 구현할 수 있는데, 두 경우 모두 반드시 required 수식어를 붙여야 한다.

protocol SomeProtocol {
    init(someParameter: Int)
}

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 이니셜라이저 구현이 여기에
    }
} 

required 수식어를 붙임으로써 이 클래스를 상속받은 서브클래스들 역시 프로토콜에 정의된 이니셜라이저를 구현함을 보장받을 수 있다. (* 클래스의 이니셜라이저 앞에 required 수식어를 붙이면 모든 서브클래스는 해당 이니셜라이저를 구현해야 한다. 자세한 것은 Initialization 포스팅 3편에서 다뤘다. 보러가기)

<note> final 수식어가 붙은 클래스의 경우에는 required 수식어를 붙일 필요가 없다. final 클래스는 어차피 서브클래싱이 불가능하기 때문이다.

만약 서브클래스가 슈퍼클래스로부터 designated 이니셜라이저를 오버라이드하고, 그 이니셜라이저가 자신이 따르고 있는 프로토콜의 이니셜라이저와 형태가 일치한다면, required와 override 수식어를 둘 다 이니셜라이저 구현에 붙인다. 예제를 보자.

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance;
    // "override" from SomeSuperClass
    required override init() {
        
    }
}


<Failable 이니셜라이저 요구사항>

프로토콜은 자신을 따르는 타입(conforming type)에게 "failable 이니셜라이저(init?)를 제공해야 한다"고 요구할 수 있다.

  • failable 이니셜라이저 요구사항은 -> failable 이니셜라이저 또는 nonfailable 이니셜라이저(= 일반 이니셜라이저)로 만족시킬 수 있다.  (* failable 이니셜라이저를 서브클래스에서는 nonfailable 이니셜라이저로 오버라이드할 수 있는 것과 같다. 이에 대해서는 Initialization 포스팅 3편에서 다뤘다. 보러가기)

  • nonfailable 이니셜라이저(=일반 이니셜라이저) 요구사항은 ->  nonfailable 이니셜라이저(일반 이니셜라이저)나 암시적 언랩핑 failable 이니셜라이저(init!)로 만족시킬 수 있다.



타입으로서의 프로토콜

프로토콜 그 자체는 어떤 기능도 구현하고 있지 않다. 그러나 모든 프로토콜은 코드 상에서 "타입"과 같은 역할을 할 수 있다.


즉, 프로토콜을 타입으로 사용할 수 있다. (fully-fledged type)

  • 함수, 메서드, 이니셜라이저에서 파라미터 타입이나 리턴 타입으로 쓰일 수 있다.

  • 상수, 변수, 프로퍼티의 타입으로 쓰일 수 있다.

  • 배열, 딕셔너리, 그 외 컨테이너 타입에 들어있는 아이템 타입으로 쓰일 수 있다.


<note> 프로토콜도 타입으로 쓰일 수 있으니, 프로토콜의 이름은 대문자로 시작하라. (Swift에서 타입의 이름은 대문자로 시작함, Int, String, Double 등)


그럼 프로토콜을 타입으로 쓰고 있는 예제를 보자.

protocol RandomNumberGenerator {
    func random() -> Double
}

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    
    func random() -> Double {
        lastRandom = ((lastRandom * a + c).truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}

class Dice {
    let sides: Int
    let generator: RandomNumberGenerator
    
    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }
    
    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("주사위 : \(d6.roll())")
}
// 주사위 : 3
// 주사위 : 5
// 주사위 : 4
// 주사위 : 5
// 주사위 : 4


objective-c에서는 id<프로토콜이름>으로 프로토콜 타입을 표현해야 했는데, 이제 프로토콜이름으로 바로 타입처럼 쓸 수 있게 되었다. 상속이 아니라 프로토콜로 문제를 해결하기가 더 편해진 것이다.

이번 편에서는 프로토콜의 간단한 사항들을 알아보았다. 다음 2편에서는 Delegation부터 시작해서 나머지 사항들에 대해 알아보자.


2편을 작성하였습니다. http://wlaxhrl.tistory.com/58

'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Protocols (3)  (2) 2017.03.25
Protocols (2)  (0) 2017.03.25
Extensions  (1) 2017.03.23
Nested Types  (0) 2017.03.23
Type Casting  (0) 2017.03.23