티스토리 뷰

swift/문법

22. Swift 문법 - Protocol 1

DevBee 2020. 10. 26. 14:40

1. Protocol Syntax

Protocol 을 간단하게 정리하면 형식에서 공통으로 제공하는 멤버 목록을 말합니다. Protocol 에는 형식에서 구현해야 하는 멤버가 선언되어 있으며 실제 구현은 Protocol 에 포함되지 않습니다. 대신 class 나 structure 같은 타입들이 실제 Protocol 의 멤버들을 구현하게 됩니다. 이것을 "프로토콜을 따른다"고 표현하거나 "프로토콜을 채용한다"고 표현합니다. 프로토콜을 채용한 형식은 반드시 프로토콜에 선언되어 있는 필수 멤버를 모두 구현해야 합니다. 그래서 이런 멤버들을 요구사항이라고 부르기도 합니다.

 

프로토콜을 선언하는 문법은 다음과 같습니다.

protocol ProtocolName {
    propertyRequirements
    methodRequirements
    initializerRequirements
    subscriptREquirements
}

// Protocol 은 상속을 지원합니다. Protocol 은 다중 상속을 지원합니다.
protocol ProtocolName: Protocol, ... {

}

 

간단한 프로토콜을 선언해 보겠습니다.

protocol Something {
    func doSomething()
}

 

프로토콜을 채용하는 문법은 다음과 같습니다. 

// Adopting Protocols

enum TypeName: ProtocolName, ... {

}

struct TypeName: ProtocolName, ... {

}

class TypeName: SuperClass, ProtocolName, ... {

}

 

위에서 선언한 Something 프로토콜을 채용하는 간단한 코드를 살펴보겠습니다.

struct Size: Something {
    // Protocol 채용 시 헤더만 일치시키면 되고 바디는 자유롭게 구현하면 됩니다.
    func doSomething() {
        print("doSomething")
    }
}

 

기본적으로 프로토콜을 채용하는 형식에는 제한이 없지만 AnyObject 프로토콜을 상속하면 클래스 전용 프로토콜로 선언됩니다. 이렇게 하면 구조체나 열거형에서는 해당 프로토콜을 채용할 수 없게 됩니다.

// 문법
protocol ProtocolName: AnyObject {

}

// 예제
protocol SomethingObject: AnyObject, Something {
}

// 에러
// struct Value: SomethingObject {
//
// }

class Object: SomethingObject {
    // SomethingObject Protocol 에는 구현해야할 메소드가 없지만
    // SomethingObject 가 Something Protocol 을 채용하고 있기 때문에
    // Something Protocol 의 doSomething 메소드를 구현해야 합니다.
    func doSomething() {
        
    }
}

 

2. Property Requirements

프로토콜에서 속성을 선언하는 문법을 살펴보겠습니다.

protocol ProtocolName {
    var name: Type { get set }
    static var name: Type { get, set }
}

 

프로토콜에서 변수는 항상 var 키워드로 선언합니다. 프로토콜에 포함된 var 키워드는 가변성과는 아무 관련이 없습니다. 단지 선언하는 멤버가 속성이라는 것을 나타내는 키워드로 사용됩니다.

{} 사이에 오는 get, set 키워드가 가변성을 결정합니다. 두 키워드가 모두 포함되어 있다면 형식에서 읽기와 쓰기가 가능한 속성을 선언해야 합니다. 반대로 get 키워드만 포함되어 있는 경우에는 형식에서 읽기 전용 속성으로 선언해도 되고 읽기, 쓰기가 모두 가능한 속성으로 선언해도 문제가 없습니다.

 

먼저 인스턴스 변수를 프로토콜에 선언하고 해당 프로토콜을 채용한 예제를 살펴보겠습니다.

protocol Figure {
    var name: String { get }
}

struct Rectangle: Figure {
    let name = "Rect"
}

struct Triangle: Figure {
    var name = "Triangle"
}

struct Circle: Figure {
    var name: String {
        return "Circle"
    }
}

 

Figure 프로토콜에서 name 속성이 get 키워드만 가지도록 선언되었기 때문에 Figure 프로토콜을 채용한 다른 형식에서 name 속성을 읽기만 가능한 속성으로 구현하거나 읽기, 쓰기가 모두 가능한 속성으로 구현할 수 있습니다. 하지만 Figure 프로토콜의 name 속성 선언 시 get, set 키워드를 모두 가지고 있다면 반드시 읽기, 쓰기가 모두 가능한 속성으로 구현하여야 합니다.

protocol Figure {
    var name: String { get set }
}

struct Rectangle: Figure {
    var name = "Rect"
}

struct Triangle: Figure {
    var name = "Triangle"
}

struct Circle: Figure {
    var name: String {
        get {
            return "Circle"
        }
        set {
            
        }
    }
}

 

이번에는 타입 변수를 프로토콜에 선언해보겠습니다. 프로토콜에서 static 키워드로 선언된 타입 변수들은 해당 프로토콜을 채용한 형식에서도 반드시 static 키워드를 붙여서 구현하여야 합니다.

protocol Figure {
    static var name: String { get set }
}

struct Rectangle: Figure {
    static var name = "Rect"
}

struct Triangle: Figure {
    static var name = "Triangle"
}

struct Circle: Figure {
    static var name: String {
        get {
            return "Circle"
        }
        set {
            
        }
    }
}

 

static 키워드로 선언된 속성은 sub class 로 상속되지만 overriding 은 불가능합니다. overriding 을 허용하려면 static 키워드 대신 class 키워드로 선언해야 합니다.

class Circle: Figure {
    class var name: String {
        get {
            return "Circle"
        }
        set {
            
        }
    }
}

 

3. Method Requirements

프로토콜에서 메소드를 선언하는 문법을 살펴보겠습니다.

protocol ProtocolName {
    func name(param) -> ReturnType
    static func name(param) -> ReturnType
    // 해당 프로토콜을 값 형식이 채용할 수 있고, 메소드 내부에서 속성을 변경해야 한다면 mutating 키워드를 추가하여 선언합니다.
    // 프로토콜에서 mutating 은 메소드에서 값 형식을 변경할 수 있다는 것을 의미할 뿐입니다.
    // 따라서 값 형식 뿐만 아니라 참조 형식에서도 문제없이 채용할 수 있습니다.
    mutating func name(param) -> ReturnType
}

 

먼저 인스턴스 메소드를 선언하고 채용하는 간단한 예를 살펴보겠습니다.

protocol Resettable {
    func reset()
}

class Size: Resettable {
    var width = 0.0
    var height = 0.0
    
    // 메소드 이름, 파라미터 형식, returnType 만 일치한다면 메소드의 body 는 자유롭게 구현이 가능합니다.
    func reset() {
        width = 0.0
        height = 0.0
    }
}

 

위 예제에서 Size 클래스를 구조체로 변경하는 경우 에러가 발생합니다. 값 형식의 인스턴스는 메소드에서 속성을 변경하고자 할 때 메소드 앞에 mutating 키워드를 추가해야 하기 때문입니다. 따라서 Size 구조체의 reset 메소드 앞에 mutating 키워드를 붙이면 해당 메소드는 프로토콜에 선언되지 않은 메소드로 인식되어 구조체 자체에 에러가 발생합니다. 따라서 프로토콜의 reset 메소드 선언 앞에도 mutating 키워드를 붙여주어야 합니다. 이렇게 mutating 키워드를 붙인 메소드라도 클래스에서는 mutating 을 제거하고 구현할 수 있습니다.

protocol Resettable {
    mutating func reset()
}

class Size: Resettable {
    var width = 0.0
    var height = 0.0
    
    // class 에서는 mutating 키워드를 추가하지 않아도 메소드에서 속성을 자유롭게 변경할 수 있습니다.
    func reset() {
        width = 0.0
        height = 0.0
    }
}

struct SizeValue: Resettable {
    var width = 0.0
    var height = 0.0
    
    // 값 형식의 인스턴스 메소드에서 속성을 변경하려면 mutating 키워드를 추가해야 합니다.
    mutating func reset() {
        width = 0.0
        height = 0.0
    }
}

 

다음으로 타입 메소드를 선언하고 채용하는 예를 살펴보겠습니다.

protocol Resettable {
    mutating func reset()
    static func reset()
}

class Size: Resettable {
    var width = 0.0
    var height = 0.0
    
    func reset() {
        width = 0.0
        height = 0.0
    }
    
    // overloading 규칙에 따라 이름이 같은 메소드를 인스턴스 메소드와 타입 메소드를 구현할 수 있습니다.
    // 이 메소드는 sub class 상속되지만 overriding 은 불가능합니다.
    // overriding 을 가능하게 하려면 static 키워드를 class 키워드로 변경하면 됩니다.
    static func reset() {
        
    }
}

struct SizeValue: Resettable {
    var width = 0.0
    var height = 0.0
    
    mutating func reset() {
        width = 0.0
        height = 0.0
    }
    
    static func reset() {
        
    }
}

 

4. Initializer Requirements

프로토콜에서 생성자를 선언하는 문법을 살펴보겠습니다.

protocol ProtocolName {
    init(param)
    init?(param)
    init!(param)
}

 

간단한 예제를 살펴보겠습니다.

protocol Figure {
    var name: String { get }
    init(name: String)
}

struct Rectangle: Figure {
    var name: String
    
    // 이 경우 Memberwise Initializer 로 자동 생성되는 생성자가 
    // Protocol 에 선언된 생성자와 동일하기 때문에  Protocol 의 생성자를 구현하지 않아도 됩니다.
}

 

만약 프로토콜에 선언된 생성자에서 Argument Label 을 변경하는 경우에는 Memberwise Initializer 가 자동 생성되지 않기 때문에 반드시 생성자를 구현해주어야 합니다.

protocol Figure {
    var name: String { get }
    init(n: String)
}

struct Rectangle: Figure {
    var name: String
    
    init(n: String) {
        name = n
    }
}

class Circle: Figure {
    var name: String
    
    // class 는 상속을 고려해야 하고 모든 sub class 에서 프로토콜의 요구사항을 충족시켜야 합니다.
    // 그래서 클래스에서는 Required Inirializer 로 선언해야 합니다.
    required init(n: String) {
        name = n
    }
}

final class Triangle: Figure {
    var name: String
    
    // final class 의 경우 더 이상 상속되지 않기 때문에 상속을 고려할 필요가 없습니다.
    // 그래서 required 키워드 없이도 요구사항을 충족 시킵니다.
    init(n: String) {
        name = n
    }
}

 

이번에는 Circle 클래스를 상속받는 Oval 클래스를 선언해보겠습니다.

// 이미 super class 로부터 Protocol 요구사항을 상속하고 있기 때문에
// 다시 Figure 를 채용하는 것은 중복이기 때문에 문법적으로 허용하지 않습니다.
// class Oval: Circle, Figure {
    
// }

class Oval: Circle {
    var prop: Int
    
    init() {
        prop = 0
        super.init(n: "Oval")
    }
    
    // 요구사항을 충족시키기 위해서 반드시 지정 생성자로 구현해야 하는 것은 아니며
    // 다음과 같이 convenience initializer 로 구현하더라도 요구사항을 충족시킵니다.
    // 다만, 여기에서도 required 키워드가 필요합니다.
    required convenience init(n: String) {
        self.init()
    }
}

 

마지막으로 프로토콜에 선언하는 생성자를 Failable Initializer 로 선언하는 경우에 대해 알아보겠습니다. 프로토콜에서 생성자가 Failable Initializer 로 선언되어 있다면 Failable Initializer 와 Non-Failable Initializer 모두 요구사항을 충족시킵니다. 하지만 프로토콜의 생성자가 Non-Failable Initializer 로 선언되어 있다면 구현 시 Non-Failable Initializer 로 지정하거나 init! 형태의 Failable Initializer 만 사용이 가능합니다.

protocol Grayscale {
    init(white: Double)
}

struct Color: Grayscale {
    // Protocol 생성자에서는 Non Optional 인스턴스를 반환하고
    // 아래 생성자는 Optional 인스턴스를 반환하기 때문에 에러가 발생합니다.
    // init?(white: Double) {
    // }
    
    // 다음과 같은 경우 요구사항은 충족시키지만 초기화에 실패하는 경우 런타임 에러가 발생합니다.
    init!(white: Double) {
        
    }
}

 

5. Subscript Requirements

프로토콜에서 Subscript 를 선언하는 문법을 살펴보겠습니다.

protocol ProtocolName {
    subscript(param) -> ReturnType { get set }
}

 

Sunscript 선언 시 get 키워드는 필수이고 set 키워드만 필요에 따라 생략이 가능합니다.

get 키워드만 사용한 경우에는 형식에서 subscript 구현 시 읽기 전용 subscript 를 구현하여도 되고 읽기, 쓰기가 모두 가능하도록 구현하여도 문제 없습니다.

set 키워드가 선언된 경우에는 반드시 읽기와 쓰기가 가능하도록 구현하여야 합니다.

protocol List {
    subscript(idx: Int) -> Int { get }
}

struct DataStore: List {
	// Protocol 선언 시 get 키워드만 사용하였기 때문에 읽기 전용 subscript 로 구현 가능
    subscript(idx: Int) -> Int {
        return 0
    }
}


protocol List2 {
    subscript(idx: Int) -> Int { get set }
}

struct DataStore2: List2 {
	// Protocol 선언 시 get, set 키워드를 모두 사용하였으므로 get, set block 모두 구현 필요
    subscript(idx: Int) -> Int {
        get {
            return 0
        }
        set {
            
        }
    }
}
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함