본문 바로가기

학습 노트/Swift (2021)

140 ~ 151. Protocol (프로토콜)

Protocol (프로토콜)

형식에서 공통으로 제공하는 멤버 목록이다.
프로토콜 내에는 멤버들이 선언되어 있지만 실제 구현은 포함되지 않는 대신 클래스나 구조체 등이 이를 구현한다.
이를 'Adopting Protocol(프로토콜을 채용한다.)'라고 한다.
프로토콜을 채용한 형식은 프로토콜에 선언된 멤버를 모두 구현해야만 한다.
따라서 프로토콜 내에 선언된 멤버들을 'Requirements(요구사항)'이라고 한다.

Syntax

protocol ProtocolName {
    propertyRequirements
    methodRequirements
    initilaizerRequirements
    subscriptRequirements

}

protocol ProtocolNameProtocol, ... {

}

 

Defining Protocol (프로토콜 선언)

프로토콜은 protocol 키워드와 UpperCamelCase 형식의 이름으로 선언한다.
내부엔 멤버 선언이 위치한다.
또한, 다중 상속을 지원한다.

protocol Some {
	func doit()
}

Some 프로토콜에 doit 메소드를 선언했다.

Adopting Protocol (프로토콜 채용)

Syntax

enum TypeName: ProtoclName, ... {

}

struct TypeName: ProtoclName, ... {

}

class TypeName: SuperClass, ProtoclName, ... {

}

 

 

enum과 struct는 형식 지정과 비슷하게 진행하면 되지만, 상속을 지원하는 클래스는 조금 다르다.
클래스가 상속받는 슈퍼클래스가 있고, 프로토콜을 채용하면 슈퍼클래스의 이름이 프로토콜보다 앞에 자리한다.

struct Size: Some {
    
}
결과

//error

여기까지 진행하면 에러가 발생한다.
이는 프로토콜에 선언되어 있는 요구사항을 구현하지 않았다는 의미이다.

struct Size: Some {
	func doit() {
	
	}
}

위와 같이 함수의 선언 부만 동일하게 하고, 구현부는 자유롭게 구성 가능하다.

Class-Only Protocol

Syntax

protocol ProtocolName: AnyObject {
}

 

위와 같이 AnyObject 속성을 추가하면 클래스 전용 프로토콜로,
구조체나 열거형에서는 해당 프로토콜을 채용할 수 없게 된다.

protocol Some {
	func doit()
}

protocol Some4Class: AnyObject, Some {

}

struct Value: Some4Class {

}
결과

//error

위와 같이 AnyObject 속성을 가지고, Some을 상속받는 Some4Class를 생성했다.
따라서 Some4Class는 클래스 전용 프로토콜이기 때문에 구조체인 Value에서는 이를 채용할 수 없다.

protocol Some {
	func doit()
}

protocol Some4Class: AnyObject, Some {

}

class Object: Some4Class {

}
결과

//error

채용한 Some4Class 프로토콜에는 멤버가 선언되어 있지 않지만, 다른 Some 프로토콜을 상속받는 프로토콜이기 때문에 Object 클래스는 이를 반드시 구현해야 한다.

protocol Some {
	func doit()
}

protocol Some4Class: AnyObject, Some {

}

class Object: Some4Class {
	func doit() {
	
	}
}

에러가 사라지고 채용 조건을 만족해 에러가 사라졌다.

 

Property Requirements (속성 선언)

Syntax

protocol ProtocolName {
    var nameType { get set }
    static var nameType { get set }
}

 

프로토콜의 속성 선언은 항상 var 키워드를 사용해야 한다.
var 키워드는 속성의 가변성과는 관련이 없다.
프로토콜 속성의 가변성은 이후에 '{ }' 사이에 작성되는 get과 set이 관여하게 된다.
get과 set이 모두 존재하면 읽기와 쓰기가 모두 가능하도록 채용하는 형식에서 구현해야 한다.
하지만 get만 존재하면 읽기만 구현하거나 읽기와 쓰기 모두 가능하도록 구현해도 된다.

형식 속성으로 선언할 때는 var 앞에 static 키워드를 추가한다.

protocol Figure {
	var name: String { get }
}

struct Rectangle: Figure {
	let name = "tangle"
}

Figure 프로토콜에는 이름이 name이고 자료형이 String인 속성이 선언되어 있다.
또한, get을 가지고 있으므로 이후 구현할 때 읽기만 구현하거나 읽기와 쓰기 모두 구현해도 된다.
따라서 Figure 프로토콜을 채용하는 Rectangle 구조체에서는 읽기만 가능한 let으로 이를 구현해도 문제가 없다.

protocol Figure {
	var name: String { get }
}

struct Circle: Figure {
	var name = "angle"
}

따라서 변수건 실수건 상관없다.

protocol Figure {
	var name: String { get set}
}

struct Circle: Figure {
	var name = "angle"
}

struct Octo: Figure {
	var name: String {
		return "pus"
	}
}
결과

//error

Figure 프로토콜의 name 속성은 이제 get과 set을 모두 가진다.
따라서 읽기만 구현해서는 조건을 만족시키지 못하게 된다는 의미이다.
Octo 구조체의 name은 읽기 전용 계산 속성이다. 따라서 getter와 setter를 모두 구형해야 조건을 만족시킬 수 있다.

protocol Figure {
	var name: String { get set}
}

struct Octo: Figure {
	var name: String {
		get {
			return "pus"
		}
		set {
		
		}
	}
}

조건을 만족시키기 대문에 에러가 사라진다.

protocol Figure {
	static var name: String { get set}
}

static 키워드를 추가하면 채용할 때 모두 형식 속성으로 작성해야 한다.

protocol Figure {
	static var name: String { get set}
}

struct Rectangle: Figure {
	static var name = "tangle"
}

struct Circle: Figure {
	static var name = "angle"
}

struct Octo: Figure {
	static var name: String {
		get {
			return "pus"
		}
		set {
			
		}
	}
}

따라서 채용하는 모든 형식들에서 static 키워드를 작성해야 한다.

protocol Figure {
	static var name: String { get set}
}

class Octo: Figure {
	static var name: String {
		get {
			return "pus"
		}
		set {
			
		}
	}
}

단, 위와 같이 클래스에서 프로토콜의 형식 멤버를 채용할 때 동일하게 static 키워드를 사용하면
해당 속성은 서브클래스로 상속은 가능하되, 오버 라이딩은 지원하지 않게 된다.
이 때는 아래와 같인 class로 키워드를 바꿔 사용할 수 있는데, 이렇게 되면 상속과 오버라이딩 모두 가능하게 된다.

protocol Figure {
	static var name: String { get set}
}

class Octo: Figure {
	class var name: String {
		get {
			return "pus"
		}
		set {
			
		}
	}
}

 

Method Requirements (메소드 선언)

Syntax

protocol ProtocolName {
    func name(param) -> ReturnType
    static func name(param) -> ReturnType
    mutating func name(param) -> ReturnType
}

 

프로토콜에서 메소드 선언은 메소드 헤드까지만 작성하면 된다.
타입 메소드를 선언할 때는 static 키워드를 활용하고, 만약 이를 변경해야 할 필요가 있다면 mutatig 키워드를 사용해도 좋다.
원래의 mutating과는 다르게 값 형식 전용이 아닌 메소드에서 값을 변경할 수 있어야 한다는 의미이다.

protocol Some {
	func reset()
}

class Size: Some {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}
}

이것이 간단한 프로토콜 선언과 채용이다.
Size에서 채용한 reset 메소드의 이름, 파라미터, 반환값이 모두 동일하다.
이는 위의 세가지만 동일하면 메소드 바디 자체에는 관여하지 않는다는 뜻이다.

protocol Some {
	func reset()
}

struct Size: Some {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}
}
결과

//error

잘 작동하던 코드를 구조체로 바꾸면 에러가 발생한다.
이는 참조 형식이던 클래스에서 값 형식인 구조체로 바꾸면서 생기는 문제로,
값 형식의 형식에서 속성 값을 바꾸려면 mutating 키워드가 필요하다.

protocol Some {
	mutating func reset()
}

struct Size: Some {
	var width = 0.0
	var height = 0.0
	
	mutating func reset() {
		width = 0.0
		height = 0.0
	}
}

따라서 프로토콜의 선언과 구조체의 채용에서 동일하게 mutating 키워드를 추가하면 에러는 사라진다.
단, 프로토콜에서 mutating을 사용했다고 속성에서 반드시 mutating을 사용해야 하는 것은 아니다.

protocol Some {
	mutating func reset()
}

class Size: Some {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}
}

다시 구조체에서 클래스로 바꿔 보았다.
클래스는 참조 형식을 사용하므로 mutating 키워드가 필요 없다.
따라서 채용 시에 mutating 키워드를 삭제했고, 코드는 문제없이 동작한다.

protocol Some {
	static func reset()
}

class Size: Some {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}

	static func reset() {
	}
}

이번엔 타입 메소드를 선언 후 사용했다.
같은 이름의 메소드가 두개이지만 오버로딩 규칙에 의해 인스턴스 메소드와 타입 메소드 각각 하나씩 구현할 수 있는 것이다.
따라서 새로 생성한 타입메소드는 서브클래스에 상속될 수 있지만 오버 라이딩은 불가능하다.
속성과 마찬가지로 static대신 class 키워드를 사용해 오버 라이딩을 허용할 수 있다.

 

Initializer Requirements (생성자 선언)

Syntax

protocol ProtocolName {
    init(param)
    init?(param)
    init!(param)
}

 

프로토콜의 생성자도 메소드와 마찬가지로 생성자의 헤드만 구현하며, failable 생성자의 사용도 자유롭다.

protocol Figue {
	var name: String { get }
	init(name: String)
}

struct Some: Figue {
	var name: String
}

Figue 프로토콜의 내부에는 name 속성과 이를 초기화하는 생성자가 포함되어있다.
이 프로토콜을 채용하는 구조체 Some에는 속성 name만 채용하고 있으나 어째서인지 에러는 발생하지 않는다.
구조체 Some에는 속성만 선언되어 있고, 이에 해당하는 별도의 생성자가 존재하지 않는다.
따라서 memberwise 생성자가 기본 제공되고, 이 memberwise 생성자는 속성의 이름과 자료형, 아규먼트 이름이 같기 때문에 자동으로 이를 충족시킨다.

protocol Figue {
	var name: String { get }
	init(n: String)
}

struct Some: Figue {
	var name: String
}
결과

//error

만약 위와 같이 프로토콜에서 선언한 생성자의 파라미터나 아규먼트 이름이 달라지면 구조체의 memberwise 생성자와 형식이 다르게 되므로 새로 구현해야 한다.

protocol Figue {
	var name: String { get }
	init(n: String)
}

struct Some: Figue {
	var name: String
	
	init(n: String) {
		name = n
	}
}

에러가 사라졌다.

생성자를 클래스에서 사용할 때는 required 생성자를 사용해야 한다.
상속을 지원하기 때문에 상속을 고려해야 하고, 상속받는 모든 클래스에서 프로토콜의 요구사항을 만족시켜야 한다.

protocol Figue {
	var name: String { get }
	init(n: String)
}

class Thing: Figue {
	var name: String
	
	init(n: String) {
		name = n
	}
}

단, 클래스에서 유일하게 상속을 신경 쓰지 않아도 되는 final 클래스는 reguired 키워드가 없어도 요구사항을 충족하게 된다.

protocol Figue {
	var name: String { get }
	init(n: String)
}

class Thing: Figue {
	var name: String
	
	required init(n: String) {
		name = n
	}
}

class Oval: Thing, Figue {

}
결과

//error

Oval 클래스는 Thing을 상속받고, Figue를 채용하고 있다.
Thing 클래스는 Figue를 채용하고 있다.
따라서 Oval은 Figue를 중복으로 채용하고 있는 셈이 된다.
따라서 에러가 발생한다.

protocol Figue {
	var name: String { get }
	init(n: String)
}

class Thing: Figue {
	var name: String
	
	required init(n: String) {
		name = n
	}
}

class Oval: Thing {
	var gen: Int
	
	init() {
		gen = 0
		super.init(n: "Oval gen")
	}
}
결과

//error

언뜻 보면 채용 조건을 만족시킨 것 같지만 생성자의 구조가 프로토콜의 선언과 다르다.
따라서 프로토콜에서 선언한 생성자를 구현해야 할 필요가 있다.

protocol Figue {
	var name: String { get }
	init(n: String)
}

class Thing: Figue {
	var name: String
	
	required init(n: String) {
		name = n
	}
}

class Oval: Thing {
	var gen: Int
	
	init() {
		gen = 0
		super.init(n: "Oval gen")
	}
	
	required convenience init(n: String) {
		self.init()
	}
}

반드시 지정 생성자를 사용해 구현할 필요는 없다. 다만 채용 시에도 required 키워드가 필요하다는 것에 주의하자.

protocol Newgen {
	init(white: Double)
}

struct This: Newgen {
	init?(white: Double) {

	
	}
}
결과

//error

새로운 프로토콜인 Newgen은 nonfailable 생성자의 형식이다.
해당 프로토콜을 채용하는 This 구조체에서의 생성자 구현은 failable 생성자로 구현되어 있는데,
이것도 컴파일러 입장에선 말이 안 되는 상황이다.
failable 생성자에서 반환되는 값은 optional This 인스턴스이다.
하지만 프로토콜에서 요구하는 값은 nonoptional 인스턴스이다.

protocol Newgen {
	init(white: Double)
}

struct This: Newgen {
	init!(white: Double) {
	
	
	}
}

이렇게 강제 failable 생성자로 바꾸면 에러는 사라진다.
단, 초기화에 실패할 경우 런타임 오류가 발생한다.

protocol Newgen {
	init?(white: Double)
}

struct This: Newgen {
	init(white: Double) {
	
	
	}
}

이번엔 프로토콜의 생성자를 failable 생성자로 바꿨다.
이러한 경우에는 nonfailable 생성자로 구현해도, failable 생성자로 구현해도 무관하다.

 

Subscript Requirements (서브스크립트 선언)

Syntax

protocol ProtocolName {
    subscript(param) -> ReturnType { get set }
}

 

역시 바디를 제외한 헤드 부분만 작성하며, 속성과 같이 get과 set 키워드로 가변성을 지정한다.

protocol List {
	subscript(id: Int) -> Int {get}
}

struct Store: List {
	subscript(id: Int) -> Int {
		return 0
	}
}

구현하고 호출하는 데에 키워드를 제외하면 크게 다를 점이 없다.

protocol List {
	subscript(id: Int) -> Int {get set}
}

struct Store: List {
	subscript(id: Int) -> Int {
		get {
			return 0
		}
		set {
			
		}
	}
}

만약 set을 추가했다면 읽기와 쓰기 모두 가능하도록 구현해야만 한다.
위와 같이 setter와 getter를 구분해 구현한다.
이 또한 속성과 마찬가지로 get이 선언되어 있어도 반드시 읽기만 지원해야 하는 것은 아니다.
프로토콜의 채용 기준은 최소한을 제공한다. 따라서 get이 선언되어 있더라도 위와 같이 getter와 setter를 사용하는 건 여전히 가능하다.

 

Protocol Types (프로토콜 타입)

프로토콜은 First-class Citizen이다.
따라서 변수나 상수를 선언할 때 자료형으로 사용하거나, 파라미터로 전달할 수 있으며, 반환형으로도 사용 가능하다.

protocol Resettable {
	func reset()
}

class Size: Resettable {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}
}

let s = Size()
type(of: s)

let r: Resettable = Size()
type(of: r)
결과

__lldb_expr_1.Size.Type
__lldb_expr_1.Size.Type

프로토콜은 자료형으로 사용할 수 있다. 따라서 Resettable을 채용하고 있는 Size를 Resettable 형식으로 생성할 수 있다.
이는 클래스 상속의 업 캐스팅과 비슷한데, 업 캐스팅에서 해당 클래스에 선언된 멤버로 접근이 제한되는 것과 같이 프로토콜에 선언된 멤버로만 접근이 제한된다.
즉, Size 전체가 생성되어 있기는 하지만 실제로 사용할 수 있는 건 Resettable에 선언되어 있는 reset 메서드이다.

이러한 특징은 상속과 비슷하지만, 상속과는 구별되는 특징이다.
따라서 '상속을 지원하지 않는' 값 형식에서도 프로토콜을 사용하면 상속과 비슷한 패턴을 구현할 수 있다.

Protocol Conformance (프로토콜 적합성)

프로토콜 적합성은 특정 형식이 해당 프로토콜을 채용하고 있느냐를 타입 캐스팅 연산자를 통해 확인한다.
또한, 다운 캐스팅과 유사한 특징을 가지고 있다.

Syntax

특정 인스턴스가 해당 프로토콜을 채용하고 있는가?
instance is  ProtocolName

특정 인스턴스를 해당 프로토콜로 캐스팅하거나 프로토콜 형식으로 저장되어 있는 형식을 실제 형식으로 캐스팅한다.
instance as ProtocolName
instance as? ProtocolName
instance as! ProtocolName

 

 

좌변의 인스턴스가 우변의 프로토콜을 채용하고 있는지 확인하고, true와 false를 반환한다.

is

protocol Resettable {
	func reset()
}

class Size: Resettable {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}
}

let s  = Size()

//-----

s is Resettable
s is ExpressibleByNilLiteral
결과

true
false

인스턴스 s의 형식인 Size는 Resettable을 채용하고 있다.
따라서 true가 반환된다.
하지만 ExpressibleByNilLiteral는 채용하고 있지 않기 때문에 false를 반환한다.

as

protocol Resettable {
	func reset()
}

class Size: Resettable {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}
}

let r = Size() as Resettable

생성되는 것은 Size 인스턴스이지만 저장되는 것은 Resettable 형식이다.

r as? Size

반대로 as를 사용하면 다시 Size 형식으로 되돌릴 수 있다.

형식을 프로토콜 형식으로 캐스팅할 때는 런타임, 컴파일 타임 캐스팅 모두 사용할 수 있다.
하지만 이렇게 캐스팅한 형식을 원래대로 되돌리기 위해서는 런타임 캐스팅밖에 사용할 수 있다.

Collections of Protocol Types

위에서 언급했던

 

이러한 특징은 상속과 비슷하지만, 상속과는 구별되는 특징이다.
따라서 '상속을 지원하지 않는' 값 형식에서도 프로토콜을 사용하면 상속과 비슷한 패턴을 구현할 수 있다.

 

을 구현해 본다.

protocol Figure {
	func draw()
}

struct Triangle: Figure {
	func draw() {
		print("draw triangle")
	}
}

class Rectangle: Figure {
	func draw() {
		print("draw rect")
	}
}

struct Circle: Figure {
	var radius = 0.0

	func draw() {
		print("draw circle")
	}
}

let t = Triangle()
let r = Rectangle()
let c = Circle()

draw 메서드를 가진 Figure 프로토콜,
Figure 프로토콜을 채용하고 있는 구조체 Triangle, Circle과 클래스 Rectangle 그리고 각각의 인스턴스 t, r, c가 선언되어 있다.

protocol Figure {
	func draw()
}

struct Triangle: Figure {
	func draw() {
		print("draw triangle")
	}
}

class Rectangle: Figure {
	func draw() {
		print("draw rect")
	}
}

struct Circle: Figure {
	var radius = 0.0
	
	func draw() {
		print("draw circle")
	}
}

let t = Triangle()
let r = Rectangle()
let c = Circle()

let list = [t, r, c]
결과

//error

이들은 모두 형식이 다르고, 값 형식과 참조 형식이 혼용되어 있기 때문에 배열에 저장할 수 없다.
하지만 모두 동일하게 Figue 프로토콜을 채용하고 있다.

protocol Figure {
	func draw()
}

struct Triangle: Figure {
	func draw() {
		print("draw triangle")
	}
}

class Rectangle: Figure {
	func draw() {
		print("draw rect")
	}
}

struct Circle: Figure {
	var radius = 0.0

	func draw() {
		print("draw circle")
	}
}

let t = Triangle()
let r = Rectangle()
let c = Circle()

let list: [Figure] = [t, r, c]

따라서 위와 같이 배열의 형식을 Figure로 지정해 주면 배열에 저장할 수 있게 된다.
이때, 형식에 상관없이 자동으로 캐스팅되어 저장된다.

protocol Figure {
	func draw()
}

struct Triangle: Figure {
	func draw() {
		print("draw triangle")
	}
}

class Rectangle: Figure {
	func draw() {
		print("draw rect")
	}
}

struct Circle: Figure {
	var radius = 0.0

	func draw() {
		print("draw circle")
	}
}

let t = Triangle()
let r = Rectangle()
let c = Circle()

let list: [Figure] = [t, r, c]

for item in list {
	item.draw()
	item.radius
}
결과

//error

Figure에서 선언되어 있는 draw 메소드에 접근하는 것은 문제없다.
하지만 해당 프로토콜을 채용하고 있는 속성에서 선언한 인스턴스에 접근하려 할 때 문제가 생기는데,
이때 프로토콜 적합성을 사용한다.

protocol Figure {
	func draw()
}

struct Triangle: Figure {
	func draw() {
		print("draw triangle")
	}
}

class Rectangle: Figure {
	func draw() {
		print("draw rect")
	}
}

struct Circle: Figure {
	var radius = 0.0

	func draw() {
		print("draw circle")
	}
}

let t = Triangle()
let r = Rectangle()
let c = Circle()

let list: [Figure] = [t, r, c]

for item in list {
	item.draw()
	
	if let c = item as? Circle {
		c.radius
	}
}

이렇게 상속을 지원하지 않는 값 형식과는 상관없이 업 캐스팅, 다운 캐스팅과 비슷한 방식을 구현할 수 있다.

 

Protocol composition (프로토콜 병합)

하나의 형식은 다수의 프로토콜을 채용할 수 있다.
또한, 프로토콜을 채용한 형식의 인스턴스는 프로토콜의 형태로 저장될 수 있다.

protocol A {
	func reset()
}

protocol B {
	func printValue()
}

class Size: A, B {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}

	func printValue() {
		print(width, height)
	}
}

class Circle: A {
	var radius = 0.0

	func reset() {
		radius = 0.0
	}
}

class Oval: Circle {

}

let r: A = Size()
let p: B = Size()

프로토콜 A와 B,
두 프로토콜을 모두 채용하는 클래스 Size와 A만 채용하는 Circle 클래스, 그리고 Circle 클래스를 다시 상속받는 Oval 클래스가 있다.

두 가지 프로토콜을 채용한 Size는 A 프로토콜 형식으로도, B 프로토콜 형식으로도 저장할 수 있다.
즉,

let r: A = Size()

에서 지정된 프로토콜 A는 A 프로토콜을 채용한 어떤 형식이건 저장할 수 있다는 일종의 조건이다.

그렇다면 조건을 더 걸 수는 없을까?
이때 사용하는 것이 Protocol Composition(프로토콜 병합)이다.

Syntax

Protocol & Protocol & ...

 

이렇게 '&'로 프로토콜을 나열하면 해당 프로토콜은 병합한 '임시 프로토콜'이 생성된다.

protocol A {
	func reset()
}

protocol B {
	func printValue()
}

class Size: A, B {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}

	func printValue() {
		print(width, height)
	}
}

class Circle: A {
	var radius = 0.0
	
	func reset() {
		radius = 0.0
	}
}

class Oval: Circle {

}

let r: A & B = Size()
let p: A & B = Circle()
결과

//error

A와 B 프로토콜을 모두 채용하고 있는 Size는 r에 저장될 수 있지만,
A만 채용하고 있는 Circle은 p에 저장될 수 없다.

Syntax

Class & Protocol & ...

 

클래스와 프로토콜이 혼용된 경우도 존재한다.
같은 문법에 클래스를 포함하게 되면 모든 서브클래스를 저장할 수 있게 된다.

protocol Resettable {
	func reset()
}

protocol Printable {
	func printValue()
}

class Size: Resettable, Printable {
	var width = 0.0
	var height = 0.0
	
	func reset() {
		width = 0.0
		height = 0.0
	}
	
	func printValue() {
		print(width, height)
	}
}

class Circle: Resettable {
	var radius = 0.0

	func reset() {
		radius = 0.0
	}
}

class Oval: Circle {

}

//-----

var j: Circle & Resettable = Circle()
j  = Oval()
결과

//error

Circle 클래스는 Resettable 프로토콜을 채용하고 있다.
또한 Circle 클래스가 자료형에 지정되어 있지만 본인이므로 저장하는데 문제가 없다.

또한 Oval 클래스도 Resettable 프로토콜을 채용하고 있는 Circle 클래스를 상속받고 있으므로 , 저장하는데 문제가 없다.

 

Optional Requirements (선택적 속성 선언)

Optional 형식을 의미하는 것이 아닌 '선택적'을 의미하는 Optional이다.

protocol A {
	var strokeWidth: Double { get set }
	var strokeColor: UIColor { get set }
	func draw()
	func reset()
}

프로토콜 A에는 두 개의 속성과 두개의 메서드가 존재한다.
프로토콜 A를 채용하기 위해서는 네 개의 멤버를 모두 구현해야 한다.

Syntax

@objc protocol ProtocolName {
    @objc optional requirements
}

 

각각은 위에서부터 ObjectC Attribute, Optional Modifier라고 부른다.
스위프트의 대부분의 Attribute(어트리뷰트)는 대부분 '@'으로 시작하고, 선언이나 형식에 부가적인 어트리뷰트를 추가할 때 사용한다.
오브젝트 C 어트리뷰트는 스위프트의 코드를 오브젝트 C에서 사용할 수 있도록 하는 어트리뷰트이다.
Optional modifier는 선택적 멤버를 선언할 때 사용한다.

선택적 멤버를 추가하고 싶다면 프로토콜과 멤버의 앞에 @objc 어트리뷰트가 반드시 추가되어야 한다.

@objc protocol Drawable {
	@objc optional var strokeWidth: Double { get set }
	@objc optional var strokeColor: UIColor { get set }
	func draw()
	@objc optional func reset()
}

이렇게 objc 어트리뷰트를 선언한 프로토콜을 objc 프로토콜이라고 부르기도 한다.
이러한 프로토콜을 클래스 전용이다.
이전에 anyobject 프로토콜을 상속하면 클래스 프로토콜이 된다고 정리했다.
objc 프로토콜을 anyobject 프로토콜을 자동으로 상속받는다.
따라서 클래스 전용이 된다.

@objc protocol Drawable {
	@objc optional var strokeWidth: Double { get set }
	@objc optional var strokeColor: UIColor { get set }
	func draw()
	@objc optional func reset()
}

class Test: Drawable {
	func draw() {
	
	}
}

let t: Drawable = Test()
t.draw()

Test 클래스는 Drawable 프로토콜의 필수 조건인 draw 메소드만 구현했고, 에러는 발생하지 않는 것을 확인할 수 있다.
또한 인스턴스에 프로토콜형으로 저장했을 때 선언된 메소드에도 자연스럽게 접근 가능하다.
하지만 선택적 멤버에 접근하려면 옵셔널 체이닝이 필요하다.

@objc protocol Drawable {
	@objc optional var strokeWidth: Double { get set }
	@objc optional var strokeColor: UIColor { get set }
	func draw()
	@objc optional func reset()
}

class Test: Drawable {
	func draw() {
	
	}
}

let t: Drawable = Test()
t.strokeWidth
t.strokeColor
결과

nil
nil

옵셔널 멤버는 채용하는 형식에서 구현이 될 수도, 안 될 수도 있다.
따라서 구현이 안 된 상태에서 접근을 시도한다면 이는 존재하지 않는 멤버에 접근하게 되는 것이므로 Optional 형식을 가진다.
따라서 위 코드에서 구현하지 않은 strokeWidth와 strokeColor에 접근할 시 반환되는 nil은.
해당 멤버에 nil이 저장되어 있다는 의미와 동시에 형식에 존재하지 않는다는 의미이기도 하다.

@objc protocol Drawable {
	@objc optional var strokeWidth: Double { get set }
	@objc optional var strokeColor: UIColor { get set }
	func draw()
	@objc optional func reset()
}

class Test: Drawable {
	func draw() {
	
	}
}

let t: Drawable = Test()
t.reset()
결과

//error

선택적 메소드 또한 Optional 형식으로 대체된다.
따라서 Optional 체이닝을 사용하여 접근해야 정상적인 작동을 보장할 수 있다.

@objc protocol Drawable {
	@objc optional var strokeWidth: Double { get set }
	@objc optional var strokeColor: UIColor { get set }
	func draw()
	@objc optional func reset()
}

class Test: Drawable {
	func draw() {
	
	}
}

let t: Drawable = Test()
t.reset?()
결과

nil

 

Protocol Extension (프로토콜 익스텐션)

프로토콜 또한 형식이기 때문에 익스텐션을 사용할 수 있다.
프로토콜에 익스텐션을 추가하면 프로토콜을 채용한 모든 형식에 기멤버를 제공할 수 있다.
문법상 프로토콜에 구현을 추가하지만 실제로는 프로토콜을 채용하는 형식에 구현이 추가된다.
코드의 중복을 최소화하면서 프로토콜의 요구사항을 충족시킬 수 있다는 장점이 있다.

protocol Figure {
	var name: String { get }
	func draw()
}

extension Figure {
	func draw() {
		print("A")
	}
}

struct A: Figure {
	var name = ""
}

let a = A()
a.draw()
결과

A

프로토콜 Figure의 채용 조건은 속성 name과 메소드 darw지만,
이를 실제로 채용하고 있는 구조체 A에는 속성 name 밖에 구현되어 있지 않다.
하지만 오류가 발생하지 않는데, 이것이 '문법상 프로토콜에 구현을 추가하지만 실제로는 프로토콜을 채용하는 형식에 구현이 추가된다.'의 의미이다.

protocol Figure {
	var name: String { get }
	func draw()
}

extension Figure {
	func draw() {
		print("A")
	}
}

struct A: Figure {
	var name = ""
	
	func draw() {
		print("second")
	}
}

let a = A()
a.draw()
결과

second

이번엔 구조체 A에서 다시 draw 메소드를 구현했다.
하지만 에러는 발생하지 않는데,
이는 형식에서 직접 구현한 멤버가 프로토콜 익스텐션에서 구현한 멤버보다 우선순위가 높기 때문이다.

이번엔 멤버를 추가할 형식을 제한해본다.

protocol Figure {
	var name: String { get }
	func draw()
}

extension Figure where Self: Equatable {
	func draw() {
		print("A")
	}
}

struct A: Figure {
	var name = ""
	
	func draw() {
		print("second")
	}
}

위 코드에서 Self는 프로토콜을 채용한 형식을 나타낸다.
다시 말해 Equatable 프로토콜을 채용한 형식이라면 where 절은 참을 반환한다.
이후에는 Equatable 프로토콜과 Figure 프로토콜을 전부 채용해야 해당 멤버를 추가한다.

protocol Figure {
	var name: String { get }
	func draw()
}

extension Figure where Self: Equatable {
	func draw() {
		print("A")
	}
}

struct A: Figure {
	var name = ""
}
결과

//error

구조체 A는 Figure 형식을 채용하고 있지만 Equatable 형식을 채용하고 있지는 않다.
따라서 익스텐션에 구현된 draw 메소드를 사용할 수 없으며, Figure 프로토콜의 채용 조건을 만족시키기 위해 새로운 draw 메소드를 구현해야 한다.

 

Equatable Protocol

Euatable 프로토콜은 동일성을 비교할 때 반드시 구현해야 하는 프로토콜이다.
기본적으로 제공하는 Int, Double 등의 타입들은 이미 해당 프로토콜을 구현하고 있다.
그래서 '=='와 '!='로 동일성을 비교할 수 있다.

enum Gender {
	case female
	case male
}

Gender.female == Gender.male
결과

false

위와 같이 연관 값을 가지고 있지 않는 열거형을 선언하면 컴파일러가 Equatable 구현을 자동으로 추가한다.

struct MySize {
	let width: Double
	let height: Double
}

enum VideoInterface {
	case dvi(width: Int, height: Int)
	case hdmi(width: Int, height: Int, version: Double, audioEnabled: Bool)
	case displayPort(size: CGSize)
}

let a = VideoInterface.hdmi(width: 2560, height: 1440, version: 2.0, audioEnabled: true)
let b = VideoInterface.displayPort(size: CGSize(width: 3840, height: 2160))

a == b
결과

//error

이번에 선언한 열거형은 서로 다른 연관값을 가지고 있고, 형식이 모두 기본으로 제공되는 형식이다.
이런 경우에도 Equatable 구현을 자동으로 추가한다.
하지만 '=='연산을 사용했을 때 오류가 발생하는데, 이렇게 연관 값을 가지고 있는 경우에는 다음과 같이 Equatable 프로토콜을 채용해 줘야 한다.

enum VideoInterface: Equatable {
	case dvi(width: Int, height: Int)
	case hdmi(width: Int, height: Int, version: Double, audioEnabled: Bool)
	case displayPort(size: CGSize)
}

let a = VideoInterface.hdmi(width: 2560, height: 1440, version: 2.0, audioEnabled: true)
let b = VideoInterface.displayPort(size: CGSize(width: 3840, height: 2160))

a == b

따라서 다음과 같이 Equatable을 채용하지 않은 구조체를 연관 값으로 사용하게 되면 자동으로 구현하지 않고, 에러가 발생하게 된다.

struct MySize {
	let width: Double
	let height: Double
}

enum VideoInterface: Equatable {
	case dvi(width: Int, height: Int)
	case hdmi(width: Int, height: Int, version: Double, audioEnabled: Bool)
	case displayPort(size: MySize)
}
결과

//error

이런 경우엔 직접 구현을 추가해야 할 필요가 있다.

Equatable for Structures

struct Person {
	let name: String
	let age: Int
}

let a = Person(name: "Steve", age: 50)
let b = Person(name: "Paul", age: 27)

a == b
결과

//error

비교 대상인 Person 구조체는 Equatable 프로토콜을 채용하지 않아 비교를 할 수 없다.
따라서 Equatable 프로토콜만 채용해 주면 비교 연산 사용이 가능해진다.

struct Person: Equatable {
	let name: String
	let age: Int
}

let a = Person(name: "Steve", age: 50)
let b = Person(name: "Paul", age: 27)

a == b
결과

false

구조체에서도 마찬가지로 구조체에 포함된 모든 형식이 Equatable을 구현한 형식으로 선언되어 있다면,
Equatable 프로토콜을 채용하는 것만으로도 컴파일러에서 자동으로 구현을 추가한다.

Comparable for Classes

class Person: Equatable {
	let name: String
	let age: Int
	
	init(name: String, age: Int) {
		self.name = name
		self.age = age
	}
}
결과

//error

클래스는 위의 두 경우와 다르게 Equatable의 채용만으로 해결되지 않는다.
클래스에서는 직점 멤버를 작성해야 한다.

public protocol Equatable {

	/// Returns a Boolean value indicating whether two values are equal.
	///
	/// Equality is the inverse of inequality. For any values `a` and `b`,
	/// `a == b` implies that `a != b` is `false`.
	///
	/// - Parameters:
	///   - lhs: A value to compare.
	///   - rhs: Another value to compare.
	static func == (lhs: Self, rhs: Self) -> Bool
}

Equatable 프로토콜의 선언 부이다.
안을 보면 '==' 연산 메소드가 타입메소드로 선언되어 있다.
따라서 Equatable 프로토콜을 채용하는 클래스는 해당 부분을 구현해야만 한다.

class Person {
	let name: String
	let age: Int
	
	init(name: String, age: Int) {
		self.name = name
		self.age = age
	}
}

extension Person: Equatable {
	static func == (lhs: Person, rhs: Person) -> Bool {
		return lhs.name == rhs.name && lhs.age == rhs.age
	}
}


let a = Person(name: "Steve", age: 50)
let b = Person(name: "Paul", age: 27)

a == b
결과

false

클래스 내부에 구현해도 상관없지만 보통은 위와 같이 익스텐션으로 구현한다.
구현하고 나면 에러도 사라지고, 클래스 형태의 인스턴스의 비교도 정상적으로 사용할 수 있다.
이때, '=='연산자 하나만 구현해도 이에 반대되는 '!=' 연산자는 자동으로 구현된다.

주의점

Equatable을 구현할 때는 인스턴스를 명확히 구분할 수 있도록 구현해야 한다.

extension Person: Equatable {
	static func == (lhs: Person, rhs: Person) -> Bool {
		return lhs.name == rhs.name && lhs.age == rhs.age
	}
}

위에서 구현한 Equatable을 보면 인스턴스나 한 가지 속성만 비교하는 것이 아니라 모든 속성을 비교하고 있다.
예상에서 벗어난 결과를 방지하기 위해 되도록이면 모든 속성을 비교하도록 구현해야 한다.

이후 세 가지 조건을 검증해 봐야 한다.

  1. a == a
    동일한 인스턴스를 비교하면 항상 true가 반환되어야 한다.
  2. a == b와 b == a의 결과가 항상 같아야 한다.
  3. a == b와 b == c의 결과가 true라면 a == c의 결과도 true여야 한다.

 

Hashable Protocol

hash는 단방향 암호화 기법이다.
어떠한 값을 고정된 길이의 문자열로 바꾸거나, 고유한 정수 값으로 바꾸는 것이다.
해당 작업을 해싱이라고 하고, 여러 해쉬 함수가 존재한다.
해쉬 함수의 결과물이 동일하다면 두 값을 동일하다고 할 수 있다.
따라서 해쉬는 전달된 값의 무결성을 체크하거나 사용자 인증을 구현할 때 활용한다.

swift에서는 dictionary에서 key를 구현하는 데 사용하거나, set에 저장할 수 있는 타입을 구현할 때 사용한다.
이러한 타입들을 반드시 Hashable 프로토콜을 구현해야 한다.
swift에서 제공하는 기본 타입들은 모두 해당 프로토콜을 구현하고 있고, 따라서 dictionary의 key로 사용하거나 set에 바로 저장할 수 있다.

장점

값의 유일성을 보장하고, 검색 속도가 빠르다.

열거형의 Hashable

enum ServiceType {
	case onlineCourse
	case offlineCamp
}

let types: [ServiceType: String]
let typeSet: Set = [ServiceType.onlineCourse]

위와 같이 딕셔너리의 키로 사용할 수 있고, 셋에도 저장할 수 있다.

enum VideoInterface {
	case dvi(width: Int, height: Int)
	case hdmi(width: Int, height: Int, version: Double, audioEnabled: Bool)
	case displayPort(size: CGSize)
}

이번엔 열거형에 연관 값이 존재한다.
이 때는 hashable의 구현 조건이 달라진다.

enum VideoInterface {
	case dvi(width: Int, height: Int)
	case hdmi(width: Int, height: Int, version: Double, audioEnabled: Bool)
	case displayPort(size: CGSize)
}

let a: [VideoInterface: String]
결과

//error

이번엔 위의 경우와 동일하게 코드를 작성해도 에러가 발생한다.
이유는 해당 열거형이 hashable 프로토콜을 채용하지 않았다는 것으로,
이는 셋도 동일하다.

enum VideoInterface: Hashable {
	case dvi(width: Int, height: Int)
	case hdmi(width: Int, height: Int, version: Double, audioEnabled: Bool)
	case displayPort(size: CGSize)
}
결과

//error

이번엔 해당 프로토콜을 구현하지 않았다는 오류가 발생한다.
자동으로 hashable이 구현되길 원한다면 연관 값들의 자료형들이 모두 hashable 프로토콜을 구현하고 있어야 한다.
VedieoInterface 열거형의 연관 값들이 대부분 그러하지만 CGSize는 그렇지 않다.
이러한 경우에는 직접 구현을 추가해 줘야 한다.

구조체의 Hashable

struct Person: Hashable {
	let name: String
	let age: Int
}

let set: Set = [Person(name: "A", age: 1)]
결과

{{name "A", age 1}}

위와 같이 모든 속성의 자료형이 Hashable을 구현하고 있고, 구조체에서 Hashable을 채용하면,
프로토콜 구현이 자동으로 제공된다.

클래스의 Hashable

클래스는 프로토콜 구현을 자동으로 제공하지 않는다.
따라서 직접 구현해야 할 필요가 있다.

public protocol Hashable : Equatable {

	/// The hash value.
	///
	/// Hash values are not guaranteed to be equal across different executions of
	/// your program. Do not save hash values to use during a future execution.
	///
	/// - Important: `hashValue` is deprecated as a `Hashable` requirement. To
	///   conform to `Hashable`, implement the `hash(into:)` requirement instead.
	var hashValue: Int { get }
	
	/// Hashes the essential components of this value by feeding them into the
	/// given hasher.
	///
	/// Implement this method to conform to the `Hashable` protocol. The
	/// components used for hashing must be the same as the components compared
	/// in your type's `==` operator implementation. Call `hasher.combine(_:)`
	/// with each of these components.
	///
	/// - Important: Never call `finalize()` on `hasher`. Doing so may become a
	///   compile-time error in the future.
	///
	/// - Parameter hasher: The hasher to use when combining the components
	///   of this instance.
	func hash(into hasher: inout Hasher)
}

Hashable 프로토콜은 Equatable 프로토콜을 상속하고 있다.
따라서 Equaltable 프로토콜도 함께 구현해야만 한다.

var hashValue: Int { get }
func hash(into hasher: inout Hasher)

프로토콜 안에는 위와 같이 두 가지의 속성이 선언돼 있는데,
이 중 위의 속성은 더 이상 사용되지 않는 속성이다.
따라서 hash(into:) 메소드만 구현하면 된다.

class Person {
	let name: String
	let age: Int
	
	init() {
		name = "Jane Doe"
		age = 0
	}
}

extension Person: Hashable {
	static func == (lhs: Person, rhs: Person) -> Bool {
		return lhs.name == rhs.name && lhs.age == rhs.age
	}
	
	func hash(into hasher: inout Hasher) {
		hasher.combine(name)
		hasher.combine(age)
	}
}

복잡한 hashable 알고리즘은 파라미터로 전달되는 Hasher가 전달한다.
따라서 combine 메소드를 사용하여 전달만 해 주면 된다.
이때 되도록이면 속성의 순서에 맞게 전달하며,
모든 속성을 전달할 수 있도록 해야 한다.
또한, combine에 전달되는 속성의 자료형은 반드시 Hashable을 구현하고 있어야 한다.

class Person {
	let name: String
	let age: Int
	
	init() {
		name = "Jane Doe"
		age = 0
	}
}

extension Person: Hashable {
	static func == (lhs: Person, rhs: Person) -> Bool {
		return lhs.name == rhs.name && lhs.age == rhs.age
	}

	func hash(into hasher: inout Hasher) {
		hasher.combine(name)
		hasher.combine(age)
	}
}

let s: Set = [Person()]
let d = [Person.init().name: Person.init().age]
결과

{{name "Jane Doe", age 0}}
["Jane Doe": 0]

hashable까지 구현했다면 정상적으로 딕셔너리와 셋에 저장할 수 있다.

 

Comparable Protocol

Comparable 프로토콜은 값의 크기를 비교하거나 정렬이 필요할 때 반드시 구현해야 하는 프로토콜이다.

Equatable Comparable
==
!=
>
>=
<
<=

Equatable은 equal과 not equal로 동일성을 비교하지만,
Comparable은 비교 연산자로 순서를 비교한다.

swift에서 지원하는 문자열과 정수형, 더블 등의 숫자 타입은 Comparable 프로토콜을 이미 구현해둔 상태이다.
따로 구현하지 않아도 비교를 사용할 수 있다는 의미이다.

열거형

enum Weekday {
	case sunday
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

Weekday.sunday < Weekday.monday
결과

//error

해당 열거형은 임의로 작성했기 때문에 Comparable 프로토콜을 지원하지 않는다.
따라서 별도의 구현 없이는 비교를 할 수도, 정렬을 할 수도 없다.

Equatable과 Hashable과 마찬가지로 채용 선언 만으로 컴파일러가 자동으로 구현을 추가하긴 하지만,
이전 버전부터 지원했던 둘과는 달리 swift 5.3 버전 이상에서야 해당 기능이 추가되었다.
조건을 만족한다면 다음과 같이 채용 선언 만으로 구현은 자동으로 추가될 수 있다.

enum Weekday: Comparable {
	case sunday
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

Weekday.sunday < Weekday.monday
결과

true

연관 값이 없다면 채용 선언 만으로, 연관값이 존재한다면 추가적으로 연관 값의 모든 자료형이 Comparable을 이미 구현한 상태여야 한다.
그 외의 경우라면 직접 구현을 추가해야 한다.

public protocol Comparable : Equatable {

	/// Returns a Boolean value indicating whether the value of the first
	/// argument is less than that of the second argument.
	///
	/// This function is the only requirement of the `Comparable` protocol. The
	/// remainder of the relational operator functions are implemented by the
	/// standard library for any type that conforms to `Comparable`.
	///
	/// - Parameters:
	///   - lhs: A value to compare.
	///   - rhs: Another value to compare.
	static func < (lhs: Self, rhs: Self) -> Bool
	
	/// Returns a Boolean value indicating whether the value of the first
	/// argument is less than or equal to that of the second argument.
	///
	/// - Parameters:
	///   - lhs: A value to compare.
	///   - rhs: Another value to compare.
	static func <= (lhs: Self, rhs: Self) -> Bool
	
	/// Returns a Boolean value indicating whether the value of the first
	/// argument is greater than or equal to that of the second argument.
	///
 	/// - Parameters:
	///   - lhs: A value to compare.
	///   - rhs: Another value to compare.
	static func >= (lhs: Self, rhs: Self) -> Bool
	
	/// Returns a Boolean value indicating whether the value of the first
	/// argument is greater than that of the second argument.
	///
	/// - Parameters:
	///   - lhs: A value to compare.
 	///   - rhs: Another value to compare.
	static func > (lhs: Self, rhs: Self) -> Bool
}

Comparable 프로토콜의 구성이다.
마찬가지로 Equatable 프로토콜을 상속받고 있으며, 네 개의 타입 메소드가 선언되어있다.
이 중 가장 첫 번째인

static func < (lhs: Self, rhs: Self) -> Bool

만 구현하면 나머지 세 개는 자동으로 추가된다.
Comparable을 채용하지 않는다고 가정하고 프로토콜을 추가해 보자.

enum Weekday: Int {
	case sunday
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

extension Weekday: Comparable {
	static func < (lhs: Weekday, rhs: Weekday) -> Bool {
		return lhs.rawValue < rhs.rawValue
	}
}

대상인 Weekday가 원시 값을 가지고 있기 때문에 이들을 비교한 결과를 바로 반환하면 된다.
만약 원시값을 가지지 않는다면 if문 등을 이용해 각각의 케이스를 비교한 결과를 반환해야 한다.
본래는 Equatable을 채용 조건에 포함되지만, 지금 상황에선 원시 값들의 자료형이 모두 Equatable을 지원하는 자료형이므로 자동으로 제공돼 생략할 수 있다.

enum Weekday: Int {
	case sunday
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

extension Weekday: Comparable {
	static func < (lhs: Weekday, rhs: Weekday) -> Bool {
		return lhs.rawValue < rhs.rawValue
	}
}

Weekday.sunday < Weekday.monday
Weekday.sunday > Weekday.monday
Weekday.sunday <= Weekday.monday
Weekday.sunday >= Weekday.monday
결과

true
false
true
false

결과도 동일하게 반환하는 것은 물론,
직접 구현하지 않은 나머지 세 연산자들도 자동으로 추가돼 사용할 수 있다.

 


Log

2021.09.14.
블로그 이전으로 인한 글 옮김 및 수정