[Swift] 프로토콜 지향 프로그램 - 1 (프로토콜이란)

728x90

 

Protocol-Oriented-Programming을 알아볼텐데 먼저 Protocol의 개념에 대해 알아보도록 하겠습니다.

 

 

프로토콜이란


프로토콜 정의

스위프트2를 소개하면서 세계 최초의 프로토콜지향 프로그래밍 언어라고 애플은 발표했다.
이런 이유로 프로토콜이 프로토콜지향 프로그램잉의 전부일 것이라고 생각할 수도 있으나, 이는 잘못된 생각이다. 프로토콜지향 프로그래밍은 단지 프로토콜뿐만이 아니라 더 많은 것을 포함하고 있습니다.

 

타 언어의 인터페이스와 유사하지만, 프로토콜은 대부분의 객체지향 언어에 있는 인터페이스보다 실제로는 훨씬 더 강력합니다.

그럼 프로토콜의 기초부터 시작해볼까요.

 

프로토콜 정의

프로토콜 정의를 위해서는 protocol키워드를 사용한다.

protocol MyProtocol {
	//프로토콜 정의
}
  • 프로토콜의 프로퍼티
    프로토콜은 프로토콜을 따르는 타입에 명시된 이름과 타입을 갖는 특정 프로퍼티를 제공할 것을 요구할 수 있다.
    프로토콜에서 프로퍼티를 정의할 때에는 get  set 키워드를 사용해 프로퍼티가 읽기 전용 프로퍼티인지 읽기 쓰기 프로퍼티인지를 반드시 명시 해줘야한다.
protocol Seungjin {
    var name: String { get set } // 읽기 쓰기
    var walk: String { get } // 읽기 전용
}

name, walk 프로퍼티를 정의했으므로, 이 프로토콜을 따르는 모든 타입은 이 두 프로퍼티를 반드시 구현해야한다.

 

struct MyStruct: Seungjin {
    var name: String
    var walk: String
}

static키워드를 사용하면 정적 프로퍼티를 정의하는 것도 가능합니다.

static var typeProperty: String { get }
  • 프로토콜의 메소드
    메소드는 중괄호와 메서드의 구현이 없다는 점을 제외하고는 클래스와 구조체에서 정의했던 것과 동일합니다.
    또, static키워드를 사용함으로써 이러한 메소드가 인스턴스 메소드나 타입 메소드가 되도록 정의할 수 있습니다.
protocol Seungjin {
    var name: String { get set }
    var walk: String { get }

    func getName() -> String
}

이제 Seungjin 프로토콜은 name, walk의 프로퍼티와 getName() 메소드를 요구합니다.

 

구조체의 경우 메소드가 메소드 자신이 속해 있는 인스턴스를 변경하고자 의도하는 경우에는 반드시 메소드 정의부 앞부분에 mutating키워드를 추가해야만 합니다.
클래스 타입의 경우 mutating키워드를 입력할 필요가 없고 오로지 값타입(구조체, 열거형)타입에만 사용됩니다.

  • 프로토콜의 선택 사항
    프로토콜이 선택 가능한 요구사항을 정의하기를 바라는 경우가 있습니다.
    즉, 프로퍼티 채택시 전체 프로퍼티나 메소드를 전부 구현하지 않고 구현해야만 하는 프로퍼티나 메소드를 설정 할 수 있습니다.

 

그러기 위해 먼저 @objc키워드를 프로토콜 앞부분에 위치 시킵니다.

오직 클래스만이 @objc속성을 사용하는 프로토콜을 채택할 수 있습니다.
구조체와 열거형은 @objc프로토콜을 채택할 수 없습니다.

 

@objc라는 키워드와 optional키워드를 사용하면 프로퍼티나 메소드가 선택적으로 구현하다는 것을 의미합니다.

@objc protocol Phone {
    var phoneNumber: String { get set }
    @objc optional var emailAddress: String { get set }
    func dialNumber()
    @objc optional func getEamil()
}

class Myst: Phone {
    var phoneNumber: String
    
    func dialNumber() {
        
    }
}
  • 프로토콜의 상속
    프로토콜은 한 개 이상의 프로토콜로 부터 요구 사항을 상속받을 수 있으며, 요구 사항을 추가할 수도 있습니다.

 

Person프로토콜 생성

protocol Seungjin {
    var name: String { get set }
    var walk: String { get }

    func getName() -> String
}

protocol Person: Seungjin {
    var age: Int { get set }
}

struct Student: Person {
    
    var name: String
    var walk: String
    var age: Int
    
    func getName() -> String {
        return name
    }
}

Person 프로토콜에 정의된 요구 사항뿐만 아니라 Seungjin 프로토콜에 정의된 요구 사항 역시 반드시 구현해야 합니다.

 

 

  • 프로토콜 컴포지션(composition)

        -> 프로토콜 컴포지션은 여러 프로토콜을 채용할 수 있게 해줍니다.

protocol ProtocolThree: ProtocolOne, ProtocolTwo {
    
}

프로토콜 컴포지션은 모든 요구 사항을 단일 프로토콜이나 단일 클래스에서 상속하지 않고, 요구 사항을 여러 작은 컴포넌트로 나눌 수 있게 해줍니다.
이렇게 작은 컴포넌트로 나누게 되면, 필요로 하는 요구사항이 아닌 것을 포함하는 비대한 타입을 생성하는것을 피하게 해준다는 의미입니다.

이 클래스 구조는 운동 선수라는 기본 클래스를 갖습니다.
그런 다음 아마추어, 프로인 두개의 서브클래스를 갖습니다.
이 구조를 자세히 보면 아마추어, 프로 두 클래스 모두는 하위 클래스로 별도의 축구선수와 야구선수를 가집니다. 즉, 이러한 구조는 클래스간에 수많은 중복 코드를 갖게 요구할 것 입니다.

 

하나의 슈퍼클래스로부터 모든 기능을 상속받는 서브클래스를 갖는 대신, 프로토콜 컴포지션을 사용하면 타입에서 믹스앤매치가 가능한 프로토콜 컬렉션을 갖게 됩니다.

 

  • 프로토콜을 타입으로 사용하기
  •  
  •         -> 프로토콜을 함수의 매개변수나 반환 타입으로 사용할 수 있습니다.

 

 

protocol Seungjin {
    var name: String { get set }
    var walk: String { get }

    func getName() -> String
}

func updateSeungjin(person: Seungjin) -> Seungjin {
    var newSeungjin: Seungjin
    
    // 코드 작성
    
    return newSeungjin
}

컬렉션 타입으로도 저장 가능합니다.

var personArr = [Seungjin]()
var personDic = [String: Seungjin]()
  • 프로토콜과 연관타입
    연관 타입은 프로토콜 내에서 타입을 대신해 사용할 수 있는 플레이스홀더명을 제공한다.
    연관 타입에서 사용되는 실제 타입은 프로토콜이 채택되기 전까지는 정의되지 않는다.
    즉, 프로토콜을 채택하는 타입이 정확한 타입을 정할 것이다.
protocol Queue {
    associatedtype QueueType
    mutating func addItem(item: QueueType)
    mutating func getItem() -> QueueType?
    func count() -> Int
}

이 프로토콜에서는 QueueType이라는 이름의 연관 타입을 정의 했습니다.
이 연관타입으로 addItem의 매개변수 타입, getItem의 반환타입으로 두번 사용하고 있습니다.
즉, Queue프로토콜을 구현하는 모든 타입은 QueueType 플레이스홀더를 위해 사용할 타입을 반드시 명시해야만 하며, 프로토콜이 QueueType 플레이스홀더를 사용하는 경우 해당 타입의 아이템만이 사용됨을 보장해야만 합니다.

 

Queue프로토콜을 채택한 IntQueue에 대해 살펴 보겠습니다.

protocol Queue {
    associatedtype QueueType
    mutating func addItem(item: QueueType)
    mutating func getItem() -> QueueType?
    func count() -> Int
}

struct IntQueue: Queue {
    var items = [Int]()
    mutating func addItem(item: Int) {
        items.append(item)
    }
    
    mutating func getItem() -> Int? {
        if items.count > 0 {
            return items.remove(at: 0)
        } else {
            return nil
        }
    }
    
    func count() -> Int {
        return items.count
    }
}

addItem(), getItem() 모두 연관 타입 대신 Int형 타입을 사용하고 있습니다.

 

지금 까지 프로토콜에 대한 전반적인 개념에 대해 살펴 보았습니다.
다음엔 델리게이션패턴을 구현하기 위해 프로토콜을 사용하는 방법을 알아 보겠습니다.

 

참고

스위프트4 프로토콜지향 프로그래밍 3/e
에이콘