Swift

[Swift] Protocol(프로토콜)이란?

Hatchling.dev 2023. 6. 13. 19:01

(편의상 편한 말투로 작성하는 점 이해 부탁드립니다.😅)

(부정확한 정보가 있을 수 있습니다. 지적 환영🤩)

 

오늘은 Protocol(프로토콜)에 대해서 알아보겠습니다~

이전 포스팅인 구조체, 클래스, 열거형에서 계속 등장했던 프로토콜이란 무엇인가.... 간단하게 설명하자면 특정 역할을 위해 프로퍼티, 메서드, 기타 요구사항 등의 청사진이라고 생각하시면 됩니다! 그럼 이 말은 무엇이냐.. 어떤 구조체, 클래스, 열거형이 프로토콜을 채택하면 해당 프로토콜에서 요구하는 프로퍼티, 메서드 등을 실제 구현해야 합니다. 프로토콜은 실제 구현체가 아닌 단순 역할을 정의하는 역할을 담당한다고 생각하시면 이해가 쉬울까요(?)😅

아직 이해가 되지 않는 분들을 위해 간단한 예시와 코드를 통해 설명해보도록 하겠습니다!

 

예시

아래 코드는 자동차와 오토바이 구조체를 생성하는 예제입니다. 자동차와 오토바이는 바퀴를 가지고 있다는 공통점과 앞으로 가는 공통 기능을 가지고 있지만 뒤로 가는 기능은 자동차에만 있습니다. 이렇게 공통으로 가져야하는 프로퍼티와 기능을 프로토콜로 미리 정의하고 해당 프로토콜의 기능을 가져야 하는 인스턴스가 채택한다면 꼭 필요한 프로퍼티와 기능들을 구현하도록 도와주는 기능을 하는거죠!!

만약 Drive 프로토콜을 채택하고 실제 구현체를 작성하지 않으면 "Type 'Car' does not conform to protocol 'Drive'"라는 에러가 발생합니다.

참고로 프로토콜을 채택하기 위해서는 인스턴스 이름 옆에 :(콜론)을 붙인 뒤에 프로토콜 이름을 써주시면 됩니다. 하지만 슈퍼클래스를 가지는 클래스가 프로토콜을 채택할 때는 슈퍼클래스 뒤에 프로토콜을 채택하면 됩니다. 또, 상속은 하나만 가능하지만 프로토콜은 여러개를 채택할 수 있습니다.

protocol Drive {
    var wheelCount: Int { get set }
    func go()
}

struct Car: Drive {
    var wheelCount: Int = 4
	
    func go() {
        print("자동차가 앞으로 갑니다.")
    }
	
    func back() {
        print("자동차가 뒤로 갑니다.")
    }
}

struct Bike: Drive {
    var wheelCount: Int = 2
	
    func go() {
        print("오토바이가 앞으로 갑니다.")
    }
}

또, 프로토콜은 아래 코드처럼 저장프로퍼티, 연산프로퍼티 모두 정의할 수 있습니다. 다만 저장프로퍼티인지 연산프로퍼티인지를 프로토콜에서 정의하는 것이 아닌 해당 프로토콜의 실제 구현체 부분에서 정의할 수 있습니다. 또한 프로토콜에선 프로퍼티가 항상 var로 선언되어야 하며 let으로 선언하고 싶다면 프로토콜에서 해당 프로퍼티를 get-only로 정의하고 실제 구현체에서 let으로 선언할 수 있습니다.

protocol Drive {
    var wheelCount: Int { get }
    var name: String { get set }
    var info: String { get }
    func go()
}

struct Car: Drive {
    let wheelCount: Int = 4
    var name: String
    var info: String {
        return "\(name)의 바퀴 개수는 \(wheelCount)개 입니다."
    }
	
    func go() {
        print("자동차가 앞으로 갑니다.")
    }
}

var myCar = Car(name: "sonata")
print(myCar.info)    // sonata의 바퀴 개수는 4개 입니다.

이번에는 프로토콜에서 프로퍼티를 옵셔널로 사용하는 방법입니다. 프로토콜에선 옵셔널 타입의 프로퍼티 뿐만 아니라 해당 프로토콜을 채택하는 인스턴스가 해당 프로퍼티를 선언할지 선언하지 않을지에 대한 옵셔널도 가능합니다. 하지만 아래 코드처럼 @objc 프로토콜로 선언하기 때문에 클래스만 채택할 수 있습니다. (클래스 포스트에서 설명했으니 이유도 아시겠죠?!)

@objc protocol Drive {
    var wheelCount: Int { get }
    @objc optional var name: String { get set }
    func go()
}

class Car: Drive {
    let wheelCount: Int = 4
	
    func go() {
        print("자동차가 앞으로 갑니다.")
    }
}

이번에는 기능을 담당하는 메서드 사용방법입니다. 기본적인 사용방법은 위 예시를 참고해주시고 이번에는 구조체와 열거형에서 사용가능한 mutating 키워드입니다. 프로토콜에서도 mutating을 사용할 수 있으며 프로토콜에서 mutating으로 선언되었어도 클래스에서는 mutating 키워드를 생략해도 됩니다. (구조체와 열거형은 Value Type, 클래스는 Reference Type이기 때문!!)

protocol Drive {
    var name: String { get set }
    mutating func changeName(newName: String)
}

struct Car: Drive {
    var name: String
	
    mutating func changeName(newName newNmae: String) {
        name = newNmae
    }
}

var myCar = Car(name: "sonata")
print(myCar.name)	// sonata
myCar.name = "avante"
print(myCar.name)	// avante

자 이제 마지막으로 이니셜라이저에 대해 알아보겠습니다. 프로토콜에서도 이니셜라이저 요구사항을 정의할 수 있습니다. 다만 클래스라면 designated init이나 convenience init으로 정의할 수 있으며 required 키워드가 필수이고 구조체나 열거형에서는 쓰지 않아도 됩니다.

그렇다면 왜 클래스에서는 required 키워드가 필수일까요?? 클래스는 상속이 가능하기 때문입니다. required 키워드는 서브클래스에서 이니셜라이저 구현을 보장받을 수 있도록 합니다.

protocol Drive {
    init(price: Int)
}

class Car: Drive {
    required init(price: Int) {
        print("해당 차량의 가격은 \(price)원 입니다.")
    }
}

var myCar = Car(price: 1000)    // 해당 차량의 가격은 1000원 입니다.

 

오늘은 프로토콜에 대해 알아보았습니다!! 간단하게 요약하자면 인스턴스의 요구사항을 정의할 수 있도록 도와주는 역할을 한다고 정리할 수 있겠네요! Swift는 POP(Protocol Oriented Programing: 프로토콜 지향 프로그래밍)이기도 하고 구조체와도 굉장히 잘 어울려서 잘 이해하고 있다면 개발할 때도 많은 도움이 될 것 같습니다.😀

다음에는 Extension(익스텐션)에 대해 알아보겠습니다.