티스토리 뷰

iOS

[Swift] 프로토콜 (Protocol)

Peppo 2022. 1. 16. 17:46
728x90

 

요즘 ModernRIBs를 공부하면서 protocol이 굉장히 많이 쓰이길래 

이거부터 알아야 코드가 읽힐것 같아 공부를 해봅니다 :) 

가봅시닷

 


프로토콜 (Protocol)

프로토콜은 메소드, 프로퍼티, 다른 작업 또는 기능의 부분에 맞는 요구사항을 청사진을 정의합니다.

 

인터페이스이며, 구현은 하지 않습니다. 정의만 합니다.

구현은 해당 프로토콜을 채택한 곳에서 합니다.

 

여기서 채택 이란 이겁니다. 

 

알게 모르게 많이 써왔던건데 오늘 자세하게 공부해봅시다!

 


 

프로토콜 문법 (Protocol Syntax)

 

 

프로토콜은 클래스, 구조체, 열거형과 유사한 방법으로 정의 합니다. 

protocol SomeProtocol {
	// protocol definition goes here.
}

 

프로토콜을 채택하는 방법

- 구조체, 클래스, 열거형의 이름에 콜론 ( : ) 을하고 해당 프로토콜 이름을 붙여주시면 됩니다.

(아래 예시는 다중프로토콜)

struct SomeStructure: FirstProtocol, AnotherProtocol {
	// structure definition goes here
}

 

클래스가 슈퍼클래스(상위클래스)를 가지면, 프로토콜 앞에 슈퍼클래스를 먼저 입력합니다.

class SomeClass: SomeSuperclass, FirstProtoco, AnotherProtocol {
	// class definition goes here
}

 


프로퍼티 요구사항 (Property Requirements)

 

프로토콜에서는 프로퍼티가 저장 프로퍼티인지, 연산 프로퍼티인지 지정하지 않습니다.

필요한 프로퍼티 이름과 타입만 지정하며, 각 프로퍼티가  gettable 인지 (gettable, settable) 인지 지정합니다.

 

프로퍼티 요구사항은 항상 변수(var)로 선언되어야 하며, 

gettable과 settable 프로퍼티들은 { get set }을 타입선언 뒤에 나타내고, gettable 프로퍼티는  { get } 으로 나타냅니다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }   // 읽기/쓰기
    var onlyGettalbe: Int { get }         // 읽기
}

 

타입 프로퍼티 역시 프로토콜 안에서 정의 할 수 있습니다. 

물론 static 키워드를 붙여주셔야 합니다.

protocol AnotherProtocol {
	static var someTypeProperty: Int { get set }
}

 

아래는 싱글 인스턴스 프로퍼티 요구사항이 있는 프로토콜의 예 입니다.

protocol FullyNamed { 
    var fullName: String { get }
}

프로토콜 내용을 풀어보자면 

  • FullNamed 프로토콜이 String 타입의 읽기전용인 변수 fullName을 요구한다.

아래는 FullyNamed 프로토콜을 채택하고, 준수하는 간단한 구조체 예시 입니다.

protocol FullyNamed { 
    var fullName: String { get }
}

struct Person: FullyNamed {
    var fullName: String
}

let john = Person(fullName: "John Appleseed")
  • Person이라는 구조체는 특정사람 이름을 나타냄
  • FullyNamed 프로토콜을 적용
  • 각 Person 인스턴스는 fullName 이라는 단일 저장 프로퍼티를 가지며 문자열 타입.

 

아래는 좀 더 복잡한 클래스에서 FullyNamed 프로퍼티에 연관되고 적용됩니다.

protocol FullyNamed { 
    var fullName: String { get }
}

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")

위 코드를 해석해보자면

 

이 Starship 클래스는 연산 읽기 프로퍼티로서, fullName 프로퍼티 요구사항을 구현합니다. 

 

 

각 Starship 클래스 인스턴스는 name 을 항상 갖고 있어야 하고, prefix는 있어도 없어도 됩니다. (optional)

 

 

fullName 프로퍼티는 prefix 값이 있으면 값을 사용하고, name의 앞부분에 붙입니다.

 

 

 

 


 

메소드 요구사항 (Method Requirements)

프로토콜은 인스턴스 메소드타입 메소드를 명시할 수 있습니다. 

메소드의 body (중괄호 { } )부분은 사용할 수 없으며, 
메소드의 매개변수에는 기본값을 지정 할 수 없습니다.

 

타입 프로퍼티를 프로토콜에 정의한 경우 , 타입메소드엔 항상 static 키워드를 붙입니다.

클래스에서 구현할 땐, 타입 메소드 앞에 class 또는 static 키워드를 붙여줍니다.

protocol SomeProtocol {
    static func someTypeMethod()
}

 

아래는 싱글 인스턴스 메소드 요구사항에 프로토콜을 정의하는 예제 입니다.

protocol RandomNumberGenerator {
    func random() -> Double
}

 

RandomNumberGenerator를 채택 하고 준수 하는 클래스, 구조체, 열거형에 random이라는 메소드를 요구(준수) 하고 

호출 될 때 마다 Double 값을 return 합니다.

 

protocol  RandomNumberGenerator {
    func random() -> Double
}
class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 15.0
    let a = 30.0
    let c = 20.0
    func random() -> Double {
        lastRandom = (lastRandom + m + a)
        return lastRandom - c 
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 67.0"
print("And another one: \(generator.random())")
// Prints "And another one: 112.0"

위 코드를 설명하자면

  • LinearCongruentialGenerator 클래스가 RandomNumberGenerator 프로토콜을 채택 
  • 변수 lastRandom 과 상수 m, a, c 등 여러 저장 프로퍼티를 선언했고 각각 기본값이 있으므로 초기화(init)은 안해도 됩니다.
  • RandomNumberGenerator을 채택했으니 이 프로토콜에서 요구한 random() 메소드도 준수 해줍니다.  

 

변경 가능한 메소드 요구사항 (Mutating Method Requirements)

가끔 인스턴스에 속한 메소드를 수정할 때가 있습니다.

Value Type (값타입)의 메소드, 즉 구조체, 열거형에 속한 메소드일 경우 func 앞에 mutating을 붙여 

해당 메소드가 속한 인스턴스와 인스턴스내에 있는 모든 프로퍼티를 수정할 수 있게 해줍니다. 

 

아래 코드는 mutating 메소드를 선언한 프로토콜의 예 입니다.

protocol Togglable {
    mutating func toggle()
}

 

구조체, 열거형에서 Togglable을 채택하면, toggle() 메소드를 변경할거라는 뜻이죠.

 

아래 예제를 볼게요.

protocol Togglable {
    mutating func toggle()
}

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() // on
lightSwitch.toggle() // off
print(lightSwitch)   // off

 

 


초기자 요구사항 (Initializer Requirements)

 

프로토콜에서 필수로 구현해야하는 이니셜라이저를 지정할 수 있습니다.

protocol SomeProtocol {
    init(someParameter: Int)
}

 

클래스에서 프로토콜 필수 이니셜라이저의 구현 (Class Implementation of Protocol Initializer Requirements)

 

선언한 프로토콜을 준수하는 Class는 프로토콜에서 요구하는 이니셜라이저 요구사항을 구현할 수 있는데, 

이 경우엔 required 를 써줘야 합니다. 

protocol SomeProtocol {
    init(someParameter: Int)
}

class SomeClass: SomeProtocol {          // SomeProtocol 채택
    required init(someParameter: Int) {  // SomeProtocol의 init을 준수
        // initializer implementation goes here 
    }
}

⚠️ Class의 경우에만 사용을 합니다.

구조체인 경우는 required를 사용할 필요가 없습니다. 

NOTE

final로 선언되면 서브클래싱 되지 않기 때문에,
클래스 타입에서 final로 선언된 것에는 required를 표시하지 않아도 됩니다.

 

final 을 잠깐 짚고 넘어가자면, 

말그대로 마지막입니다. 메소드, 프로퍼티, 클래스 등에 붙일 수 있으며, 

서브클래스화 할 수 없어요.

상속안된다는거죠 . 

 

 

다음으로, 

하위클래스가 상위클래스의 지정된 이니셜라이저(init)를 재정의(override)하고,

프로토콜에서 요구하는 이니셜라이저를 구현하는 경우에

이니셜라이저를 required override init() 으로 표시합니다. 

 

아래 예제를 보시죠 ! 

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
       // SomeProtocol과 일치시키기 위함 "required" 사용
       // SomeSuperClass 로 부터의 "override" 사용
    required override init() {
       // initializer implementation goes here
    }
}

 

 


 

프로토콜 내용이 꽤 기네요.

 

이후에는 타입프로토콜, 위임, 확장형 프로토콜 등 정리해보겠습니다.

728x90