프로토콜에 대해 이해하게 된 것들을 정리한 글입니다.
객체 지향 프로그래밍에서는 하나하나의 개별적인 내용들들 공통된 개념으로 묶어 추상화하고, 그렇게 만들어지는 객체를 바탕으로 소통하는 방법론을 지향하고 있습니다. 이때 어떻게 객체를 만들것인지를 위한 청사진으로 활용되는 것이 클래스(또는 구조체)라고 할 수 있죠.
사람을 예로 들어보겠습니다. 모든 사람은 개별적인 존재입니다. 눈, 코, 입, 팔, 다리 등 굉장히 많은 것들을 속성으로 갖는다고 볼 수 있죠. 하지만 프로그래밍 세계에서의 사람은 이 모든 속성을 반드시 가져야 하는 것은 아닙니다.
사람, 그리고 옷을 다루는 프로그램을 만들기 위해 청사진을 그린다고 가정해 보겠습니다.
enum 사이즈 {
case 스몰, 미디엄, 라지, 엑스라지
}
struct 사람 {
var 상체: 옷
var 하체: 옷
}
이 가상의 프로그램은 우선 사람을 상체와 하체라는 프로퍼티를 갖는 아주 간단한 구조체로 그려냅니다. 일반적으로 우리가 잘 아는 객체의 청사진이라고 할 수 있죠. 그렇다면 오늘 말하려고 하는 프로토콜은 과연 또 무엇일까요?
A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality.
공식 문서는 프로토콜을 특정한 작업이나 기능에 적합한 메소드, 속성 그리고 기타 요구사항을 위한 청사진을 정의하는 것이라고 소개합니다. 앞서 객체의 청사진이 곧 클래스 또는 구조체라고 말한 바 있죠. 클래스가 큰 단위를 위한 청사진이라면, 프로토콜은 클래스 내부의 프로퍼티 또는 메소드를 위한 청사진입니다.
이번에는 여러가지 옷을 프로토콜을 이용해 간단하게 구현해 보겠습니다. 사이즈를갖는
이라는 프로토콜을 우선 구현해보도록 하죠.
enum 상_하의 {
case 상의, 하의
}
protocol 사이즈를갖는 {
var 크기: 사이즈 { get set }
var 상_하의여부: 상_하의 { get }
}
사이즈를갖는
이라는 이름으로 프로토콜을 하나 구현했습니다. 상, 하의 여부를 표시하기 위해 별도의 열거형도 하나 만들었습니다. 이 프로토콜을 준수하는 모든 객체는 크기
와 상_하의여부
라는 프로퍼티를 구현해 주어야 합니다.
class 옷: 사이즈를갖는 {
var 크기: 사이즈
let 상_하의여부: 상_하의
init(크기: 사이즈, 상_하의여부: 상_하의) {
self.크기 = 크기
self.상_하의여부 = 상_하의여부
}
}
옷
클래스가 사이즈를갖는
프로토콜을 준수하도록 만들었습니다. 이제 옷을 상속받는 모든 클래스는 자동으로 사이즈를갖는
프로토콜을 준수하게 됩니다. 아래는 그 예시입니다.
class 히트텍바지: 옷 {
var 설명: String = "언제 어디서나 편안하고 따뜻하게"
init(크기: 사이즈) {
super.init(크기: 크기, 상_하의여부: .하의)
}
}
let 미디엄사이즈바지 = 히트텍바지(크기: .미디엄)
let 라지사이즈바지 = 히트텍바지(크기: .라지)
dump(미디엄사이즈바지)
/* ▿ 히트텍바지 #0
▿ super: 옷
- 크기: 사이즈.미디엄
- 상_하의여부: 상_하의.하의
- 설명: "언제 어디서나 편안하고 따뜻하게"
*/
옷을 상속받은 히트텍바지
는 기본값을 갖는 설명
이라는 별도의 프로퍼티를 가지고 있습니다. 이니셜라이저를 구현해줄 때 하의
로 기본값을 설정했기 때문에, 사이즈만 입력해주면 서로 다른 히트텍바지 인스턴스를 여러 개 만들어줄 수 있죠.
- 프로토콜로 프로퍼티에 대한 준수사항을 만들 때는 반드시
var
를 사용해야 합니다.- 프로토콜을 준수하는 쪽(여기에서는 옷)에서는 필요에 따라
let
을 쓸 수 있습니다.- { get }, 또는 { get, set } 으로 Gettable 인지 Gettable and Settable 인지를 표시해주어야 합니다.
- 각각의 클래스 또는 구조체는 프로토콜에서 요구하는 것 외에 별도의 속성을 가질 수 있습니다. (e.g. 히트텍바지의
설명
속성)
사이즈를갖는
이라는 프로토콜이 속성만 다루고 있어서 메서드 프로토콜도 하나 구현해 보겠습니다. 입을수있는
이라는 이름으로 만들어 볼까요?
protocol 입을수있는 {
mutating func 입는다(옷: 옷) -> Void
}
사람 구조체가 이 프로토콜을 준수하도록 바꿔보겠습니다.
struct 사람: 입을수있는 {
var 상체: 옷
var 하체: 옷
mutating func 입는다(옷: 옷) {
switch 옷.상_하의여부 {
case .상의:
self.하체 = 옷
case .하의:
self.상체 = 옷
}
}
}
혹시 무언가 이상한 점이 있지 않나요? 분명히 사람 구조체는 입을수있는
프로토콜을 준수하고 있습니다. 하지만 상의일 때 하체에, 하의일때 상체에 옷을 넣어주고 있죠. 이는 프로토콜에서 정의해준 메서드의 모양만 따라갈 수 있다면, 실제 구현된 메서드 내부에서 무엇을 하더라도 관여하지 않기 때문입니다. 입을수있는
프로토콜은 그저, 입는다
라는 모양의 메서드가 존재하는지, 그 메서드가 옷이라는 매개변수 이름으로 옷
클래스를 전달받는지를 확인할 뿐인 것이죠.
- 프로토콜로 메서드에 대한 준수사항을 요구할 때에는, {} 또는 함수 바디를 제외하면 일반적으로 함수를 정의하는 방법으로 구현하면 됩니다.
- 프로토콜은 준수사항 외에 함수가 무엇을 하는지에 대해 관여하지 않습니다.
- 메서드가 구조체(또는 열거형)의 값을 변경하는 경우 프로토콜에도
mutating
키워드를 붙여줘야 합니다. (mutating
키워드가 있더라도 클래스에서는 사용할 필요가 없습니다)
스위프트에서는 프로토콜을 활용해 위임을 구현할 수 있다고 하는데요. 다음 글에서는 공식문서의 내용을 바탕으로 위임에 대해 알아가보도록 하겠습니다.