[iOS] Protocol

강치우·2024년 4월 10일
0

수입푸드

목록 보기
10/13
post-thumbnail
post-custom-banner

오늘은 뭔가 아직도 밍숭맹숭하게 넘어가버린 프로토콜에 대해서 다시 한번 짚고 넘어가볼 생각이에요.

프로토콜(protocol)이란?

애플에서는 프로토콜을 이렇게 정의해요

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. 
프로토콜은 특정 작업이나 기능에 적합한 메서드, 속성, 그 외 필수요소의 청사진을 정의한다.

그리고 프로토콜은 일급 객체이며, 기본적으로 규약, 협약이라는 의미를 가져요.

저는 솔직히 저 말이 아직도 잘 이해는 안되지만 저는 프로토콜을 약속 이라고 이해하고 넘어갔어요.


여기서 일급 객체가 뭐냐?

일급 객체

일급객체란? (쉽게 말해 변수나 상수처럼 사용 가능하다는 의미)
- 변수에 할당 가능
- 함수의 파라미터로 전달 가능
- 반환값으로 사용 가능

규약과 협약은 '~을 해야한다' 또는 '~에 대한 정의'가 되어있고, 실제 실행은 해당 규약과 협약을 맺은 공동체(행위의 주체)가 한다.

프로토콜도 동일하게, 단순히 무엇을 해야하는지, 무엇을 정의해야하는지에 대한 요구사항만을 담고, 구현은 프로토콜을 채택한 클래스, 구조체, 열거형에서 한다.


여기서 요구사항은 청사진을 의미해요.

'뭔 소리야?' 싶을 수 있는데, 예제 코드를 보며 이해해 보자구요


protocol Music {
    // 프로토콜은 초기화하지 않음
    func playPiano(title: String, time: Int)
    func sing(title: String)
    
    var name: String { get }
}

예제를 보면, 프로토콜은 초기화하지 않은 변수와 실행구문이 없는 함수를 선언하고, 상수 let을 사용하지 않죠?

왜냐하면, 프로토콜은 메서드나 변수의 이름과 타입만 정하기 때문에

프로토콜 내 변수는 Computed Property(계산 속성)만 가능하며, Computed Property는 실행할 때 초기화하기 때문에, 값이 변경된다라는 의미로 해석되기 때문이에요.

만약 let으로 선언하게 된다면, 친절하게도 상수가 아닌 변수로 선언하라고 컴파일러가 강제해줄거에요.


요약하면, 프로토콜은

메서드나 속성 등에 대한 요구사항(청사진)을 정의하는 일급객체라고 할 수 있다.


프로토콜 확장(Protocol Extension)

아니 프로토콜을 확장까지 해야해요?
왜 이렇게 까지 해야하나요? 라고 물어본다면

코드 중복 감소 - 공통 구현을 프로토콜 확장에 넣어 다양한 타입들이 재사용할 수 있습니다.
기본 구현 제공 - 프로토콜 요구 사항에 대한 기본 구현을 제공하여, 개별 타입에서 이를 다시 구현할 필요가 없게 합니다.
조건부 기능 확장 - 특정 조건을 만족하는 타입에 대해서만 기능을 확장할 수 있습니다.

이런 이유로 프로토콜 확장을 사용하는 이유를 간단하게 알아봤고 다시 돌아가서,

extension은 타입을 확장합니다.

프로토콜 역시 타입이기 때문에 extension을 활용하여 확장할 수 있어요.

프로토콜에는 정의만 한다고 하였는데 extension을 통해 구현을 추가한다는 것이 조금 어색할 수도 있겠죠?

정리하자면 문법적으로는 프로토콜의 구현을 추가하지만 실제로는 프로토콜을 채용한 타입의 구현이 추가된다고 보면 돼요.

코드의 중복을 줄이면서 프로토콜을 확장시킬 수 있다는 장점이 있어요.

예제를 통해 이해해보자구요.


protocol SayText {
    var text: String { get set }
    func say()
}

extension SayText {
    func say() {
        print(text)
    }
}

struct A: SayText {
    var text = "프로토콜 확장!"
}

let a = A()
a.say()

구조체 A에서는 프로토콜에 정의된 say()메소드를 구현하지 않았는데 에러가 없어요.
왜냐? 확장을 통하여 say 메서드를 구현해줬기 때문에 프로토콜의 요구사항을 충족시키기 때문이에요.

그러나 아래와 같이 say() 메소드를 구현해주면 확장하여 구현하였던 메서드는 무시돼요.


protocol SayText {
    var text: String { get set }
    func say()
}

extension SayText {
    func say() {
        print(text)
    }
}

struct A: SayText {
    var text = "프로토콜 확장!"
    func say() {  // say 메서드 구현
        print("중복은 가볍게 무시해주지")
    }
}

let a = A()
a.say()

메서드를 직접 구현해도 중복으로 인한 에러는 발생하지 않아요.
타입에서 직접 구현한 멤버가 확장을 통해 구현한 멤버보다 높은우선순위를 가지고 있기 때문이에요.

프로토콜의 확장은 프로토콜을 채택한 모든 멤버를 추가하는데, 멤버를 추가할 타입을 제한할 수도 있어요.


extension SayText where Self: Equatable { // where 키워드로 타입 제한
    func say() {
        print(text)
    }
}

struct A: SayText { // Equatable을 채택하지 않았기 때문에 say 메서드를 직접 구현하지 않으면 에러가 발생한다.
    var text = "프로토콜 확장!"
}

위와 같이 타입을 제한하였는데 프로토콜만 채택하고 확장에서 제한한 타입을 같이 채택하지 않으면 확장에서 구현한 멤버도 추가되지 않아요.

따라서 아래와 같이 제한한 타입을 추가해주면 에러가 해결돼요.


protocol SayText {
    var text: String { get set }
    func say()
}

extension SayText where Self: Equatable { // 확장하면서 타입도 추가하였다.
    func say() {
        print(text)
    }
}

struct A: SayText, Equatable { // Equatable을 추가하여 확장에서 구현한 멤버 say()가 추가되었다.
    var text = "프로토콜 확장!"
}

let a = A()
a.say()

그래서 왜 Swift는 프로토콜 사용을 지향하는데? (= 프로토콜이 왜 필요했을까?)

간단하게 말하면 Swift는 클래스의 단점과 구조체에서의 사용을 위해 프로토콜을 지향하게 되었다고 할 수 있어요.


클래스의 문제점

클래스의 단일 상속 원칙

Swift의 클래스는 일부 다른 언어와는 달리 단일 상속만 가능하며, 당연히 다중 상속은 불가하다.
따라서 여러 클래스의 속성과 메서드를 상속받기 위해서는 클래스 상속 계층이 쌓거나, 구조를 잘 설계해야 한다


Swift의 Class 다중 상속 예시


구조체에서 사용 불가

Swift는 클래스와 구조체를 모두 사용하는데, 구조체는 상속이 불가하다.

상속은 메모리 주소를 참조하는 Reference(참조) 타입만 가능한데 구조체는 Value(값) 타입 이기 때문이다.


프로토콜 타입이 아니라서 상속 불가하다는 에러 발생


이러한 문제들 때문에 Swift에서는 프로토콜 사용을 지향한다! 라는 것만 알아두고 넘어가도 될거 같아요.


마무으리

프로토콜은 Swift 전반에 걸쳐 두루 사용되는 문법 중 하나로, 아주아주 중요하고 자주 쓰이기 때문에, 제대로 이해할수록 좋다고 생각해요.

사실 지금의 정리도 상당부분 축약된 면이 있는데, 실제 사용함에 있어서는 이정도로도 충분하다고 생각되기 때문에 오늘은 20000!

profile
자허블을 좀 더 좋아하긴 합니다.
post-custom-banner

0개의 댓글