본문 바로가기

Swift

Protocol(1)

사용하는 이유

1. 추상화의 방법 중 하나로 사용 가능

다른 언어(예: 자바)에서 추상화를 위한 방법 중 하나로 Interface라는 것을 구현하고 그것을 implements해서 사용했다면 Swift에서는 추상화를 위한 방법 중 하나로 Protocol을 사용합니다.

 

2. First Calss Citizen으로서 사용의 편리함 + 부가적인 장점

또한, Swift에서는 First Class Citizen이라고 해서 Protocol을 하나의 타입처럼 사용할 수 있습니다.

이렇게 사용할 수 있는 것은 아래 내용에서도 다루겠지만 Protocol의 편리함을 느끼게 해줍니다.

더보기

First Class Citizen은 아래와 같은 특성을 갖고 있습니다.

 

1. Property에 저장할 수 있다.
2. Method Parameter로 전달할 수 있다.
3. Method에서 Type을 return하듯 return 할 수 있다.

 

3. 타입 구현 시 코드의 중복을 최소화

예를 들어 고양이, 개라는 타입이 있다고 가정해보겠습니다.

두 타입은 포유류로써 가져야 되는 특징과 기능들이 있을 것입니다.

이때, 두 타입에만 특징과 기능들을 각 각 적는다고 하면 그렇게 큰 부담과 효율성에 대한 고민을 하지 않을 수도 있을텐데요.

두 타입을 비롯해서 많은 포유류 중에서 다른 타입에 대한 정의가 더 많이 필요하게 된다면 공통으로 가져야되는 특성(Property)과 기능(Method)들에 대해 반복적으로 작성해야되는 상황이 발생할 수 있습니다. 

 

Protocol은 이런 상황을 해소해줍니다.

 

4. 클래스 상속을 대체하면서도 클래스 다중 상속과 같은 기능

Swift에서 클래스 상속은 단일 상속만 가능합니다.

반면, 프로토콜은 기본적으로 클래스와 같이 구현 내용이 있지 않지만, 클래스와 같이 구현 내용이 있는 것처럼 만들 수가 있고, 다중으로 Conforming이 가능하기 때문에 클래스와 같이 상속이 필요하면서, 다중 상속이 필요한 상황일 때 Protocol로 이런 Needs를 충족시킬 수 있습니다.

더보기

프로토콜은 Inheritance와 다른 표현인 Conforming이라는 표현을 사용하고, 한국말로는 채택, 채용, 따르다, 준수하다라는 표현을 사용하거나 듣기도 했지만 원어가 좀 더 정확한 표현이 될 것 같아 Conforming이라는 원어로 표현했습니다.

 

개념

 

1. 프로토콜 Syntax 

공식 문서에 나오는 Protocol의 문법은 다음과 같이 CustomType(Structure, Class, Enum)을 작성할 때와 비슷합니다.

protocol SomeProtocol {
	//protocol definition
}

 

2. 다중 Conforming

Protocol을 '사용하는 이유'에 대해서 언급했던 다중 Conforming이라는 것은 다음과 같이 타입 옆에 ':'와 함께 프로토콜 이름을 적어주면 됩니다.

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure member(property, method)
}

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class member(property, method)
}

class Enum: FirstProtocol, AnotherProtocol {
	// enum definition
}
더보기

클래스의 경우 SuperClass를 먼저 적고 프로토콜들을 뒤에 적었는데 이 순서는 정해진 룰이기 때문에 따르시면 됩니다. 다르게 작성할 경우 컴파일 에러가 발생합니다.

 

If a class has a superclass, list the superclass name before any protocols it adopts, followed by a comma:

 

3. Property Requirements

1) Intance Property

Property Requirements는 다음과 같이 작성할 수 있습니다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

 

문서에도 적혀있지만 Property Requirements는 몇 가지 룰이 있습니다.

  • var 키워드로 작성
  • gettable, settable property는 뒤에 { get set }으로 작성
  • gettable은 { get }으로 작성
protocol SomeProtocol {
	var num: Int { get }
}

class TestClass: SomeProtocol {
	var num: Int = 0 //문제 없음
}

여기서 실제 테스트를 해봤을 때는 gettable일 때 { get } 으로 작성을 하라고 하는 룰이 다른 룰들과 달리{ get }만 적었는데도 set이 가능하다는 점이 특이했습니다. 

더보기

var로 작성해야한다는 룰은 let으로 적을 경우 바로 compile error가 뜨는데 말이죠🙉

 

근데 특이점은 더 있었습니다.

 

위의 코드에서 SomeProtocol을 채택한 TestClass에서 var를 let으로 바꾸고 다음과 같이 set을 해도 문제가 없었고,

protocol SomeProtocol {
	var num: Int { get }
}

class TestClass: SomeProtocol {
	let num: Int = 0 //문제 없음
}

 

오히려 Protocol의 num을 { get set }으로 바꿔주면 위의 코드에서 let키워드로 시작하는 set코드에서 compile error가 발생했습니다.🙉

 

정리해봤을 때 다음과 같습니다.

  • Property Requirements에 { get }만 써도 get, set이 가능하다.
  • Property Requirements에 { get }만 썼을 경우에는 Protocol을 Conforming한 CustomType은 Property Requirements를 구현 시 let키워드를 사용해도 get set에 문제가 없다.
  • Property Requirements에 { get set }을 쓰면 Protocol을 Conforming한 CustomType은 Property Requirements를 구현 시 var 키워드만 사용해야한다.(let 키워드를 사용하면 compile error발생)

 

2) Type Property

Type Property의 경우 Requirements로 작성 시 Instance Property의 Requirements를 작성할 떄와 동일하게 작성을 하고 CustomType에서 Type Property를 선언할 때와 동일하게 static키워드를 사용해주면 된다고 합니다. 다음과 같습니다.

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

 

만약 'class 상속과 overriding을 위해 static을 class 키워드로 작성해야되겠다'싶어서 class 키워드를 사용하면 다음과 같은 메러 메시지를 마주하게 됩니다.

네. Protocol Requirements를 작성할 때는 추가적으로 신경써줘야되는 부분이 있는 것 같습니다.

 

더보기

class 상속과 override와 관련된 부분인 것 같은데 이 부분은 class 상속과 관련된 부분을 한번 다뤄 보면 좋을 것 같아 해당 글에서 추가적으로 다뤄보도록 하겠습니다.

 

4. Method Requirements

프로토콜에서 메서드 선언은 메서드 헤드만 작성합니다. 다음과 같습니다.

protocol Togglable {
    mutating func toggle()
}

이 메서드에 mutating키워드는 반드시 필요할까요?

 

이는 어떤 타입이 해당 프로토콜을 Conform하느냐에 따라 다릅니다.

 

mutating키워드는 값 타입인 struct, enum타입에서 인스턴스의 상태 변화가 필요할 때(예: 값이 바뀌는) 구현해야만 하는 룰인데요. 이와 같은 용도로 생각하시면 됩니다.

 

class에서는 mutating키워드가 없어도 되기 때문에 class가 conform할 protocol이라면 사실 mutating키워드가 없어도 되는데, struct와 class가 둘다 conform할 수도 있기 때문에 범용적이기 위해 mutating을 붙이라고 하는 것 같습니다.

 

실제로 mutating키워드를 붙이고 난뒤 다음과 같이 class에서 conform후 fix it을 통해 자동 완성을 하면 다음과 같이 mutating없이 자동 완성 됩니다. 

protocol SomeProtocol {
	var num: Int { get }
	mutating func test()
}

class TestClass: SomeProtocol {
	let num: Int = 0
	func test() {
		//body
	}
}

 

 

예상대로 struct의 경우 fix it을 통해 자동 완성을 하면 다음과 같이 mutating키워드가 붙어서 자동 완성됩니다.

struct TestClass2: SomeProtocol {
	let num: Int = 0
	mutating func test() {
		//body
	}
}
더보기

메서드 명을 통해 자동 완성을 하면 mutating키워드는 자동으로 붙지 않습니다.

필요시 mutating키워드를 붙여줘야 합니다.

 

5. Initializer requirements

생성자도 일종의 메서드이기 때문에 메서드와 동일하게 다음과 같이 헤드만 작성을 해줍니다.

protocol SomeProtocol {
    init(num: Int)
}

 

알아둬야할 것이 있습니다.

이 프로토콜을 구현하는 CustomType이 struct냐 class이냐에 따라 생성자 구현에 다양성이 발생합니다.

 

1) struct

예를들어, struct에는 기본 생성자(멤버와이즈)가 있기 때문에 만약 프로토콜을 conform하려는 struct의 property와 프로토콜의 생성자 파라미터의 이름이 같다면 다음과 같이 생성자를 구현하지 않아도 문제가 되지 않습니다.

protocol SomeProtocol {
	var num: Int { get }
	init(num: Int)
}

struct TestClass2: SomeProtocol {
	let num: Int
}

하지만 만약 생성자 requirements의 파라미터 이름과 그 프로토콜을 채택하려는 struct의 property 이름이 다르다면 다음과 같이 conform하지 않았을 때 발생하는 에러가 발생합니다. 


2) class

클래스는 이와 다르게 기본 생성자가 없기 때문에 protocol confrom 후 fix it을 통해 자동 완성을 하든, init키워드로 생성자 생성을 자동완성하든 다음과 같이 생성자 앞에 required키워드가 붙어 생성됩니다.

class TestClass: SomeProtocol {
	let num: Int
	
	required init(num: Int) {
		self.num = num
	}
}

 

required 키워드를 지울 경우 compile error가 발생합니다.

더보기

required가 붙는 것은 상속과 관련이 있는 부분으로 상속에서 같이 다뤄보도록 하겠습니다.

 

3) final class

클래스에서는 required키워드가 붙지 않으면 compile error가 발생했지만 final키워드가 class앞에 붙을 경우 init키워드로 자동 완성을 하든, fix it을 통해 자동 완성을 하든 required키워드가 붙지 않고, required키워드가 붙지 않아도 다음과 같이 compile error가 발생하지 않습니다.

final class TestClass: SomeProtocol {
	let num: Int
	
	init(num: Int) {
		self.num = num
	}
}

 

4) Failable initializer

failable initializer가 requirements일 경우 protocol conform 후 구현하는 타입에서 init(), init?(), init!() 다음과 같이 모두 문제 없습니다.

class TestClass: SomeProtocol {
	let num: Int
	
	required init(num: Int) {
		self.num = num
	}
	
//	required init?(num: Int) {
//		self.num = num
//	}

//	required init!(num: Int) {
//		self.num = num
//	}
}
더보기

여기에서 class가 final class라면 required키워드는 없어도 괜찮습니다.

 

이와 같이 어떤 타입이 protocol을 conform하느냐에 따라 생성자의 구현이 달라지는 부분,  failable initializer에 따라 달라지는 부분이 있으니 알고 가면 좋을 것 같습니다.

 

공부하고 글을 정리해서 작성하는데에도 제가 글을 잘 쓰고 싶은 욕심이 있어서 그런지, 정리해야할 내용이 많아서 그런지 글이 너무 길어지는 것 같아서 여기서 한번 끊고 넘어가겠습니다.🙂

 

참고

공식 문서

Protocol

 

 

 
 
 
 

'Swift' 카테고리의 다른 글

배열 요소에 안전하게 접근하는 방법(How to access array's element safely)  (0) 2024.03.15
Generics  (0) 2023.08.13