iOS/Swift

[iOS] 프로토콜 (Protocol )

듀IT 2021. 12. 19. 15:49

Swift에서 프로토콜은 Java, C#과 같은 객체지향 언어의 인터페이스와 거의 같은 개념이다.

프로토콜에 대해 알아보도록 하자.

프로토콜


프로토콜은 클래스나 구조체의 설계도로, 클래스나 구조체가 구현해야 할 메소드와 프로퍼티 등으로 구성된다.

 

프로토콜 사용


  • 이벤트 발생시 콜백 메소드 호출을 위한 Delegate pattern을 구현할 때 프로토콜을 사용한다.
  • 프로토콜은 클래스나 구조체의 명세만 정의하고 기능을 구현하지 않는다
  • 프로토콜의 명세만 노출하고 구현체의 메소드나 프로퍼티를 은닉한다
  • 클래스, 구조체 구현시 프로토콜을 이용하여 공통된 메소드, 속성을 정의할 수 있다. 
  • 하나의 타입으로 사용할 수 있다. 
    • 따라서 메소드의 파라미터, 리턴 타입으로 사용될 수 있으며
    • 상수 변수의 타입으로도 사용할 수 있다. 
  • 하나의 타입으로 사용될 수 있지만, 프로토콜 자체는 인스턴스화 할 수 없다.

프로토콜과 추상 클래스


  •  공통점
    • 그 자체로 인스턴스를 생성할 수 없다. 구현체를 통해서만 인스턴스를 생성할 수 있다.
    • 특정 기능에 대한 추상화, 명세를 제공한다.
  • 차이점
    • 추상클래스는 상속을 통해서 추상 메소드를 구현한다. 구현체는 추상클래스를 '상속' 받으므로, 추상클래스의 속성 및 기능을 이어 받고 기능을 확장할 수 있다. 따라서 구현체는 추상클래스와 밀접한 속성을 가진다. (종적인 개념)
    • 프로토콜은 '구현'하는 것이다. 프로토콜은 대상 클래스 전체에 대한 책임을 지지 않으며, 몇가지 기능만을 담당한다. 또한 구현체와 밀접한 속성을 가지지 않아도 된다. (횡적인 개념) 

프로토콜의 정의


protocol 프로토콜명 {
  구현해야할 프로토콜 명세
}

 

프로토콜을 구현할 수 있는 타입은 다음과 같다

  • 클래스 (Class)
  • 구조체 (struct)
  • 열거형 (Enum)
  • 익스텐션 (extension)
struct/class/enum/extension 구현체 이름 : 프로토콜 이름 {

}

프로토콜의 프로퍼티


프로토콜에는 프로퍼티의 타입, 이름, 변수, read-only 여부만을 정의할 수 있다. 

초기값을 정의할 수 없고, 저장 프로퍼티인지 연산 프로퍼티인지 구분하지 않는다. 

 

protocol Person {
    var name : String { get  set}
    var age : Int { get set }
    var description : String { get }
}

get, set 키워드를 이용하여 읽기 전용여부를 설정한다. 

get, set 모두 선언되어 있으면 읽기/쓰기 모두 가능한 프로퍼티이다.

get 만 선언되어 있으면 읽기 전용 프로퍼티이다. 

 

연산 프로퍼티를 프로토콜에 정의할 땐 get만 사용하거나 get set 모두 사용하여 설정할 수 있다.

그러나 프로토콜에 저장 프로퍼티를 선언할 때는 반드시 get, set 모두 선언하여야 한다. 

 

프로토콜을 다음과 같이 구현할 수 있다.

class Dew : Person {
    
    var name: String // 저장 프로퍼티
    var age : Int = 99 // 저장 프로퍼티
    var description: String { // 연산 프로퍼티
        return "Name: \(name), Age: \(age)"
    }
    
    init() {
        name = "dew"
    }
}

 

중요한 건, 프로토콜을 구현한 구현체는 프로토콜에 정의된 모든 프로퍼티를 구현해야한다. 일부 누락하면 오류가 발생한다. 

프로토콜 메소드


프로토콜의 메소드는 타입, 이름, 파라미터 타입 및 이름, 리턴 타입만 정의한다. 구현은 하지 않는다. 

protocol Person {
    var name : String { get  set}
    var age : Int { get set }
    var description : String { get }
    
    func execute()
    func eat(food : String)
    func introduceSelf() -> String
    
}

 

프로토콜을 구현할 때는 프로토콜에 정의된 메소드명과 외부 파라미터 이름은 항상 일치해야한다. 

class Dew : Person {
   
    var name: String
    var age : Int = 99
    var description: String {
        return "Name: \(name), Age: \(age)"
    }
    
    init() {
        name = "dew"
    }
    
    func execute() {
        print("Execute Hello")
    }
    
    func eat(food: String) {
        print("Eat \(food)")
    }
    
    func introduceSelf() -> String {
        return "Hi I'm Dew"
    }
    
    
}

프로토콜에서 mutating 키워드


reference type인 클래스와는 달리, 구조체와 열거형(enum)은 value-type이므로 메소드가 프로퍼티를 조작하는 경우 mutating 키워드를 메소드 앞에 붙여야 한다. 

 

프로토콜에서도 동일하다. 만약 프로토콜의 구현체가 메소드 내부에서 프로토콜에 정의된 프로퍼티를 조작하는 경우, 프로토콜 선언시 메소드 앞에 mutating 키워드를 붙여야 한다. 만약 mutating 키워드가 선언되지 않은 경우, 구현체의 메소드 내에서 프로토콜의 프로퍼티를 조작할 수 없다.

 

protocol Person {
    mutating func eat(food: String)
}

struct Dew : Person {
    var eatingFood: String
    
    mutating func eat(food: String) {
        eatingFood = food
    }
}

 

프로토콜에 메소드 정의시 mutating을 붙여도, 실제 구현체의 메소드가 프로토콜의 프로퍼티를 조작하지 않을 경우 구현체에서는 mutating 키워드를 생략해도 상관 없다.

 

** 구조체와 열거형일 때만 mutating 키워드를 붙이면 된다. 클래스는 붙일 필요가 없다!! 

프로토콜에서 Static 키워드


프로토콜에서 타입 메소드와 타입 프로퍼티를 정의하기 위해서는 앞에 static 키워드를 붙이면 된다. 

protocol Person {
    
    static func printClassName()
    static var className: String { get <#set#> }
}

struct Dew : Person {
    static var className: String = "Dew"
    
    static func printClassName() {
        print(className)
    }
    
}

프로토콜과 초기화 메소드


프로토콜에서 초기화 메소드인  init()을 정의할 수 있다.

  1. 프로토콜에 정의된 초기화 메소드를 구현시, 정의된 매개변수명과 동일하게 구현해야한다.
  2. 프로토콜에 정의된 초기화 메소드가 struct에서 기본적으로 제공하는 멤버와이즈 메소드일지라도 직접 구현해야 한다.
  3. 프로토콜에 정의된 초기화 메소드를 클래스에서 구현시, required 키워드를 붙여야 한다. (초기화 메소드일 때만! 다른 프로퍼티에는 required를 붙이지 않는다)
protocol Person {
    init(name: String)
}

struct Dew : Person {
    var name: String
    init(name: String){
        self.name = name
    }
}

class Dew2: Person {
    var name: String
    required init(name: String) {
        self.name = name
    }
}

만약 어떤 클래스가 초기화 메소드 init()을 정의한 프로토콜과 클래스를 상속 받는다면, override, required 키워드 모두 붙여야 한다.

즉 어떤 클래스가 상속을 통해 초기화 메소드를 물려받았다 하더라도 구현해야 할 프로토콜 명세에 동일한 초기화 메소드가 선언되어 있다면 이를 다시 구현해야 한다는 것이다. 

protocol Init {
    init()
}

class ParentClass {
    init() { }
}

class ChildClass: ParentClass, Init {
    override required init() {
        
    }
}

두개의 프로토콜을 상속받는 객체


한 객체가 두개의 프로토콜을 구현할 수 있다. 

protocol A {
    func doA()
}

protocol B {
    func doB()
}

class AandB: A, B {
    
    func doA() {
        
    }
    
    func doB() {
        
    }
    
    func desc() {
        print("Implement A And B Protocol")
    }
}

var a: A&B = AandB()
a.doA()
a.doB()

이때 객체 a는 doA(), doB() 메소드만 호출할 수 있다. desc()는 호출할 수 없다.

a의 타입은 A&B으로, A 프로토콜과 B 프로토콜 모두를 포함한 객체 타입이라는 의미이다.  

 

 

참고) 꼼꼼한 재은씨의 Swift 문법편