이 포스팅은 Swift 시리즈 20 편 중 9 번째 글 입니다.

  • Part 1 - 01: Optional, Any, AnyObject, nil
  • Part 2 - 02: struct, class, enum
  • Part 3 - 03: Closure
  • Part 4 - 04: Property
  • Part 5 - 05: 상속, 생성자, 소멸자
  • Part 6 - 06: 옵셔널 체이닝과 nil 병합
  • Part 7 - 07: 타입 캐스팅
  • Part 8 - 08: assert, guard
  • Part 9 - This Post
  • Part 10 - 10: Extention
  • Part 11 - 11: 오류처리
  • Part 12 - 12: 고차함수
  • Part 13 - 13: ARC(Automatic Reference Counting)
  • Part 14 - 14: Access control, Access modifier
  • Part 15 - 15: Generics
  • Part 16 - 16: Optional에 대한 깊은 이해
  • Part 17 - 17: Lazy Variables
  • Part 18 - 18: Enumeration
  • Part 19 - 19: Initialization
  • Part 20 - 20: Concurrency
▼ 목록 보기

프로토콜

  • 특정 역할을 수행하기 위한 메서드, 프로퍼티, 요구사항 등의 청사진을 정의
  • 구조체, 클래스, 열거형은 프로토콜을 채택해서 특정 기능을 수행하기 위한 프로토콜의 요구사항을 구현할 수 있음
  • 어떤 프로토콜의 요구사항을 모두 따르는 타입은 프로토콜을 준수한다. 고 표현한다.
  • 프로토콜의 요구사항을 충족시키려면 프로토콜이 제시하는 청사진의 기능을 모두 구현해야 함
  • 기능을 정의하고 제시할 뿐, 스스로 기능을 구현하지는 않음

정의

protocol 프로토콜 이름 {
    // define
}

구현

  • 프로퍼티 요구는 항상 var 키워드를 사용함
  • get은 읽기만 가능해도 상관 없음을 의미
  • get, set을 모두 명시하면 읽기, 쓰기 모두 가능한 프로퍼티여야 한다.
protocol Talkable {
    // 프로퍼티 요구
    var topic:String { get set }
    var language: string { get }
    
    // 메서드 요구
    func talk()
    
    // 생성자 요구
    init(topic: String, language: String)
}

프로토콜 채택 및 준수

  • 채택 : 해당 프로토콜에서 요구하는 요구사항을 구현하겠다.
  • 준수 : 채택한 프로토콜이 제시하는 청사진의 기능을 모두 구현하는 것

이렇게 프로토콜이 제시하는 요구사항을 모두 구현해야 한다. 만약 그렇지 않을 경우, 해당 구조체가 프로토콜을 준수하지 않았다는 경고가 뜨게 된다.

struct Person: Talkable { // Person 구조체는 Talkable 프로토콜을 채택
    // 프로퍼티의 요구사항을 준수
    var topic: String
    var language: String
    
    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다.")
    }
    
    init(topic: String, language: String) {
        self.topic = topic
        self.language = language
    }
}

그런데 프로토콜의 요구가 읽기전용이라면 (get) 기본 연산 프로퍼티로 대체할 수 있다.

struct Person: Talkable { // Person 구조체는 Talkable 프로토콜을 채택
    // 프로퍼티의 요구사항을 준수
    var topic: String
    var language: String { get { return "한국어" } } // 현재 읽기만 선언되어 있으므로 읽기만 되도록 연산 프로퍼티로 구현해도 됨
//    var language: String { return "한국어" } // 축약한 방법
    
    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다.")
    }
    
    init(topic: String, language: String) {
        self.topic = topic
    }
}

물론 읽기, 쓰기 프로퍼티 역시 연산 프로퍼티로 대체가 가능하다.

struct Person: Talkable { // Person 구조체는 Talkable 프로토콜을 채택
    // 프로퍼티의 요구사항을 준수
    var subject: String = ""
    var topic: String {
        get {
            return self.subject
        }
        set {
            self.topic = newValue
        }
    }
    var language: String { get { return "한국어" } } // 현재 읽기만 선언되어 있으므로 읽기만 되도록 연산 프로퍼티로 구현해도 됨
    
    func talk() {
        print("\(topic)에 대해 \(language)로 말합니다.")
    }
    
    init(topic: String, language: String) {
        self.topic = topic
    }
}

이렇게 다양한 방법으로 해석, 구현할 수 있다. 상황에 따라 굉장히 유연함.

상속

  • 프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항을 확장할 수 있음
  • 프로토콜 상속은 클래스의 상속과 비슷하지만 다중상속이 가능
protocol Readable {
    /* define */
}
protocol Readable {
    func read()
}

protocol Writeable {
    func write()
}

protocol ReadSpeakable: Readable {
    func speak()
}

protocol ReadWriteSpeakable: Readable, Writeable { // 다중 상속을 받고 다시 확장
    func speak()
}

struct SomeType: ReadWriteSpeakable { // 채택 -> 준수
    func read() {
        print("Read")
    }
    func write() {
        print("Write")
    }
    func speak(){
        print("Speack")
    }
}
  • 이상태에서 SomeType을 보면 중요한 점이 있다.
  • ReadWriteSpeakable 프로토콜은 현재 speak라는 함수만이 구현해야 한다고 정의되어 있는데
  • 실제로는 3개의 함수를 구현했다.
  • 이는 프로토콜이 상속을 받으면서 상위 클래스의 구현사항 역시 자동으로 구현해야 한다고 정의되어 있기 때문이다.

클래스 상속과 프로토콜

  • 클래스에서 상속과 프로토콜을 동시에 하려면 상속 받을 클래스를 정의하고
  • 그 뒤에 채택할 프로토콜 목록을 작성
class SuperClass: Readable {
    func read() {}
}

class SubClass: SuperClass, Writeable, ReadSpeakable {
    func write() {}
    func speak() {}
}

프로토콜 준수 확인

Any 타입으로 dictionary에서 값을 들고 왔을 떄, 어떤 프로토콜을 준수하는지에 따라 분기를 결정할 수 있다.

someAny = sup
// someAny라는 변수가 Readable 프로토콜을 따르면? (여기서 as를 그냥 쓰지 않는 것은 업캐스팅도 아닐 뿐더러 nil일 수 있기 때문에)
// someAny 인스턴스를 Readable 프로토콜을 준수하는 객체로 캐스팅한 후 함수 호출
if let someReadable: Readable = someAny as? Readable {
    someReadable.read()
}

if let someReadSpeakable: ReadSpeakable = someAny as? ReadSpeakable {
    someReadSpeakable.speak()
} // 해당 프로토콜을 준수하고 있지 않기 때문에 호출되지 않음

someAny = sub

if let someReadable: Readable = someAny as? Readable {
    someReadable.read()
} // 동작

프로토콜과 인터페이스의 차이

굉장히 비슷해보이는 개념이지만 차이점이 분명히 존재한다. 이러한 부분에 대해 정리해보자.

파라미터 기본값 지정 불가

public interface TestInterface
{
    int number = 100;

    int getNumber();
    void setNumber(int num);
}
protocol TestProtocol
{
    var number: Int = 100

    func getNumber() -> Int
    func setNumber(num: Int)
}

이렇게하면 될 것 같지만, swift의 경우 3번째 라인에서 Initial value is not allowed here 라는 에러가 난다.

Protocol의 선택적 구현 (가능한 것일 뿐 자주 안쓰임)

순수 스위프트에 관해서라면 자바에서와 같이 interface(protocol)를 상속받는(채택 하는) 경우 모든 메소드를 다 구현해야 한다. 하지만 스위프트의 경우 @objc optional 키워드를 사용하면 클래스에서 구현하지 않아도 된다.

class TestClass implements TestInterface 
{ 
    private int number; 
    @Override public int getNumber() { return number; } 
    @Override public void setNumber(int num) { 
        this.number = num; 
    } 
}
@objc protocol MathProtocol
{
    func add(a: Int, b: Int)
    @objc optional func subtract(a: Int, b: Int)
}

class MathClass: MathProtocol
{
    internal func add(a: Int, b: Int) {
        print(a + b)
    }
}

자, 그럼 왜 만들어졌냐. 일단 objective-c의 경우 프로토콜을 채택했을 때, 모든 요구사항을 다 구현할 필요가 없었다. 그런데 swift가 나오고 난뒤에 이러한 점을 변경하려고 했다. 즉 모든 구현 사항을 전부 구현하도록. 그런데 이전에 만들어 놓았던 objective-c 코드를 가져와서 사용한다. 뭐 상속 받아서 확장하고 그럴 수 있겠지. 그렇게 된 경우 objective-c에서는 필수로 모든 구현 사항을 구현할 필요가 없지만, 스위프트로 넘어오게 되면서 다 구현해야되는 방향으로 변경된다.

또 이 코드는 objective-c에서도 돌아가도록 만들어줘야 한다. 결국 양쪽 언어에서 사용하는 정의가 다르기 때문에 이러한 점에서 호환이 되도록 해야 하는 것. 이런 부분에서 유연함을 가질 필요가 있기 때문에 사용된다.

static 멤버 변수, 메서드 선언 가능

public interface MathInterface 
{ 
    void add(int a, int b); 
    void subtract(int a, int b); 
    static void round(float a);  // ERROR
}
@objc protocol MathProtocol
{
    func add(a: Int, b: Int)
    @objc optional func subtract(a: Int, b: Int)
    static func round(a: CGFloat)
}

class MathClass: MathProtocol
{
    internal static func round(a: CGFloat) {
        print(round(a: a))
    }
    internal func add(a:Int, b:Int) {
        print(a+b)
    }
}

Reference

출처: https://dongkyprogramming.tistory.com/11 [Dongky’s Programming]](https://dongkyprogramming.tistory.com/11)